RaspberryPi 3 B+ & Windows10 IoT CoreでONNXモデルを使う

#この記事について
マイクロソフトがWindows10でのONNXをサポートを公式に宣言したのが2018年3月。
多くの人々が苦労しているであろう深層学習の実装について現実的な手段の一つとして期待できるのではないでしょうか？

今回はONNXのサポートの具合がどんなもんかを勉強を兼ねて確かめてみました。

#目標
「RaspberryPi3 Model B+ にて WEBカメラの画像 で推論する」ところまでを目標にします。
（ユニバーサルWindowsアプリで作成するので一般のWindows10でも普通に動作します。）

#1.準備
##1-1. PC環境の準備
まずはパソコン側を整えます。

- Visual Studio 2017
- [Windows10 Insider Preview (Build 17744)](https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewiso)
- [Windows10 SDK Insider Preview (Build 17744)](https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewSDK)

MSのサイトからダウンロードですね。

Insider Preview版を使用するので安全のために仮想環境などでやるのが良いと思います。
ちなみに私は面倒だったので無謀にもメイン環境を更新してしまいましたが。まぁ何とかなる。

##1-2. ラズパイの準備
以下の環境で開発します。

- RaspberryPi 3 Model B+
- [Windows IoT Core Technical Preview Build(Build 17744)](https://www.microsoft.com/en-us/software-download/windowsiot)

これもInsider Preview版を使用します。

リンク先の中段くらいに以下の記述があると思うので、そこからファイルをダウンロード
（これ以外のビルドはRaspberryPi3B+に焼いても起動しない）

>Additional Insider Preview downloads
>　RaspberryPi 3B+ Technical Preview Build 17661


ダウンロードしたファイルを実行すると、
インストーラーが少々不親切ですが以下のフォルダにイメージファイル(FFUファイル)が出力されます。
C:\Program Files (x86)\Microsoft IoT\FFU\RaspberryPi2

FFUファイルを「Windows10 IoT Core Dashboard」でSDカードに焼けばOKです。
Windows10 IoT Core Dashboardがない人は[ここから落とす](https://developer.microsoft.com/ja-jp/windows/iot/Downloads)

SDカードを刺して起動するとネットワーク経由のOSアップデートが始まるので完了まで放置します。

##1-3. ONNXモデルを準備
自分で作っても良いし、落としてきても良い
https://github.com/onnx/models
今回は「mobilenet v2」を使用します。

適当に準備すればいいのだが、どうもモデルサイズが大きいとエラー吐いて読めないので、mobilenetとか小さいモデルが推奨。ちなみにalexnetはラズパイNG、ローカルPCはOKだった。


関係無いけど、ONNXって「オニキス」っていうんですね。少し前まで知らなかった。

##1-4. ImageNetのクラスIDラベル名のデータを用意する
上記のgithubページから飛ぶことができます。
リンクも貼っておきます。
https://github.com/onnx/models/blob/master/models/image_classification/synset.txt
こっちも良いかも
https://gist.github.com/yrevar/942d3a0ac09ec9e5eb3a

#2. ユニバーサルWindowsアプリケーションの作成
Windows10 IoT Core用のアプリケーションはユニバーサルWindowsアプリケーション(UWP)として作成します。
ソリューション全体はgithubを参照ください([Github](https://github.com/nkoppe/UWP_ONNX_Demo))

##2-1. プロジェクトの作成
Visual C# → Windowsユニバーサル → 空白のアプリ(ユニバーサルWindows)　を選択します。
ターゲットバージョンと最小バージョンは、今回は「Build 17738」をターゲットにします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/248663/9c68829b-8844-4a65-7864-60b09345ff24.png)

##2-2. モデルの投入
コマンドプロンプトを起動します。
先ほどダウンロードしたモデルファイルがあるディレクトリに移動し、以下のコマンドでモデルのラッパークラスを生成します。

```:コマンドプロンプト
> "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17738.0\x64\mlgen.exe" -i MobileNetv2-1.0.onnx -l CS -n MobileNet -o MobileNetv2-1.0.cs
```
必要に応じてファイル名を変えてください。

すると「MobileNetv2-1.0.cs」が生成されます。
中身は以下のような感じになります。

(2018.10.11追記)
 Windows10 ver1809リリースに伴い、最新のmlgen.exeは以下のフォルダにあります。
「C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x64\mlgen.exe」



```c#:C#
public sealed class Input
{
  public TensorFloat data; // shape(1,3,224,224)
}
    
public sealed class Output
{
  public TensorFloat mobilenetv20_output_flatten0_reshape0; // shape(1,1000)
}

public sealed class Model
{
  private LearningModel model;
  private LearningModelSession session;
  private LearningModelBinding binding;
  public static async Task<Model> CreateFromStreamAsync(IRandomAccessStreamReference stream)
  {
    Model learningModel = new Model();
    learningModel.model = await LearningModel.LoadFromStreamAsync(stream);
    learningModel.session = new LearningModelSession(learningModel.model);
    learningModel.binding = new LearningModelBinding(learningModel.session);
    return learningModel;
  }
  public async Task<Output> EvaluateAsync(Input input)
  {
    binding.Bind("data", input.data);
    var result = await session.EvaluateAsync(binding, "0");
    var output = new Output();
    output.mobilenetv20_output_flatten0_reshape0 = result.Outputs["mobilenetv20_output_flatten0_reshape0"] as TensorFloat;
    return output;
  }
}
```
モデルの入力クラス、出力クラス、モデル本体クラスの３つが生成されました。

<余談>
WindowsMLはWindows10 version 1803から実装されていますが、どうも現在は仕様変更が入っているようです。1803ではTensorFloat型は未実装です。もうすぐリリースのversion 1809以降はTensorFloat型が基本になるんじゃないでしょうか。
<余談終わり>


次に、プロジェクトへモデルを追加します。
モデルファイルをAssetsフォルダへ、ラッパークラスはプロジェクトのルートに置いておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/248663/e1af0059-3bb8-761b-3951-58da0446a076.png)
こんな感じ
この時、モデルファイルのプロパティを以下のように変更しておきます。

 -  ビルドアクション：　コンテンツ
 -  出力ディレクトリにコピー:　新しい場合はコピーする

##2-3. コードを書く
粛々とコーディングします。
3分クッキングのように軽くペタッと貼っていますが、うまくいかなくて苦労しました。
この記事を参考にさせていただきました。
>[3分で分かる！ONNXフォーマットとWindows Machine Learning！](https://qiita.com/ymym3412/items/05a7cecf81309a3f131e)

大事そうなところや苦労したポイントだけピックアップして書きます。
コード全体はgithubを参照ください([Github](https://github.com/nkoppe/UWP_ONNX_Demo))

###モデルを読み込む
作成したラッパークラスの中身を確認し、そのクラス名のインスタンスを作成します。
今回は「Model」というクラスが生成されているので、以下のコードになります。

```c#:C#
try
{
  //モデルをロードする
  Windows.Storage.StorageFile modelFile =
	await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(
						new Uri("ms-appx:///Assets/mobilenet.onnx"));        //AssetsからonnxモデルのStorageFileオブジェクトを取得
  //読み込んだ後メンバ変数へ格納
  ModelGen = await Model.CreateFromStreamAsync(modelFile);  //ModelGenはMainPageクラスのメンバ変数
}
catch (Exception e)
{
  Debug.Print(e.ToString());
}
```
モデルを読み込むタイミングとしては、UIロード完了イベントなどが良いと思います。

###カメラから画像を取得
今回は、スレッドタイマを使用してUIとは別スレッドでWEBカメラから画像を読み込み推論していきます。

スレッドタイマやWEBカメラの詳細な使い方は他の方の作例を参照ください。
ここでは呼び出しだけ記述しておきます。
まずWEBカメラから画像を取得する処理です。

```c#:C#
//カメラの初期化は省略

//画像を取得する
BitmapPixelFormat InputPixelFormat = BitmapPixelFormat.Bgra8;
using (VideoFrame previewFrame = new VideoFrame(InputPixelFormat, 640, 480, BitmapAlphaMode.Ignore))
{
  //フレームを取得
  await this.mediaCapture.GetPreviewFrameAsync(previewFrame);

  if (previewFrame != null)       //フレームを正しく取得できた時
  {
    //画像を変換して推論する処理（後述）
  }
}
```
書き出しはこんな感じ。このスコープの中に処理を実装していきます。

※ちなみに、WebカメラにロジクールC270を使うと怪しい動作をする。大人しく違うWebカメラを使うのが良い。海外でも同じ報告が上がっていたのでどうもUWPのライブラリの問題の気がする。
（ローカルx86/x64環境で実行すると例外を吐いてフレームが取得できない、リモートARM環境は映像がちらつくが一応動く）

###キャプチャ画像をリサイズする
VideoFrameクラスのままでは使えないのでデータ変換をしていきます。
まずはリサイズしつつSoftwareBitmapオブジェクトに変換します

```c#:C#
//ほしい仕様のSoftwareBitmapを作成
SoftwareBitmap bitmapBuffer = new SoftwareBitmap(BitmapPixelFormat.Bgra8, 224, 224, BitmapAlphaMode.Ignore);
        
//SoftwareBitmapでVideoFrameを作成する
VideoFrame buffer = VideoFrame.CreateWithSoftwareBitmap(bitmapBuffer);

//キャプチャしたフレームを作成したVideoFrameへコピーする
await previewFrame.CopyToAsync(buffer);

//SoftwareBitmapを取得する（これはリサイズ済み）
SoftwareBitmap resizedBitmap = buffer.SoftwareBitmap;
```

###1次元のバイト配列に変換する
次に、WriteableBitmapに変換し、さらにバイト配列に変換します。

```c#:C#
//WritableBitmapへ変換する
WriteableBitmap innerBitmap = null;
byte[] buf = null;
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => {
    innerBitmap = new WriteableBitmap(resizedBitmap.PixelWidth, resizedBitmap.PixelHeight);

    resizedBitmap.CopyToBuffer(innerBitmap.PixelBuffer);
    buf = new byte[innerBitmap.PixelBuffer.Length];
    innerBitmap.PixelBuffer.CopyTo(buf);
  });

//バッファへコピーする
//innerBitmap.PixelBuffer.CopyTo(buf);
SoftwareBitmap sb = SoftwareBitmap.CreateCopyFromBuffer(buf.AsBuffer(), BitmapPixelFormat.Bgra8, 224,224, BitmapAlphaMode.Ignore);
```
ここで、わざわざラムダ式で実行しているのは、UIと別スレッド(ex.スレッドタイマなど)で実行することを前提にしているためです。

###データを整形・正規化する
もう一息です。
1次元配列にできましたが、現在のデータには問題があります。

 - アルファチャンネルがある
 - MobileNetの入力段は[3,224,224]の形状 (現時点では[224,224,4])
 - 正規化していない(255で割ってない)

これを力業で直します。OpenCV使ったりするのも良いかもしれません。
ただ、処理がそこまで遅いかというと遅くないので力業も悪くないと思います。

```c#:C#
//画像のアルファチャンネル削除と配列形状変更
byte[] buf2 = ConvertImageaArray(buf);  //自作関数

//正規化しつつfloat配列に変換する
float[] inData = NormalizeImage(buf2);  //自作関数

//----------自作関数は以下の通り------------
// 画像の行列を[*,*,4]からアルファを消して、さらに[3,*,*]の並びに変換する
private byte[] ConvertImageaArray(byte[] src)
{
  //戻り値用の配列を準備
  byte[] res = new byte[(src.Length / 4) * 3];

  int offset_b = 0;
  int offset_g = src.Length / 4;
  int offset_r = src.Length / 2;

  int j = 0;
  for (int i = 0; i < src.Length; i += 4)
  {
    res[offset_b + j] = src[i];
    res[offset_g + j] = src[i + 1];
    res[offset_r + j] = src[i + 2];
    j += 1;
  }
  return res;
}

// 画像の正規化処理
private float[] NormalizeImage(byte[] src)
{
  float[] normalized = new float[src.Length];
  for (int i = 0; i < src.Length; i++)
  {
    normalized[i] = src[i] / (float)255.0;
  }
  return normalized;
}
```
※mobilenet向けの正規化処理、平均値を0にしたり標準偏差で割る必要があるかどうか分からないです。
とりあえず、上記の処理で何となく結果が出るので、ここでは上記の処理だけにしておきます。


###モデルに入力して結果を受け取る
今までつらつらコードを書きましたが、肝心の推論はたったの数行で終わります。

```c#:C#
//入力用のテンソルを作成（Windows.AI.MachineLearning.TensorFloatクラス）
TensorFloat tf =
  TensorFloat.CreateFromArray(new long[] { 1, 3, 224, 224 }, inData);

//入力用のインスタンスを作成
Input indata = new Input();
indata.data = tf;

//AIモデルにデータを渡すと結果の入ったリストが返る
Output output = await ModelGen.EvaluateAsync(modelInput);
```
上記のInput型とOutput型は、モデルファイルのラッパークラスに含まれているモノです。
したがって使用するモデルに応じて名称が変わります。

###推論結果を表示する
今回のコードはUIとは別スレッドで実行することを前提にしているのでDispatcher.RunAsyncを使います。

```c#:C#
//UIスレッドに結果を表示
await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
  //予測結果を表示
  var result_vec = output.mobilenetv20_output_flatten0_reshape0.GetAsVectorView();
  var list = result_vec.ToArray<float>();
  var max1st = list.Max();
  var index1st = Array.IndexOf(list, max1st);     //最大確率のインデックスを取得

  string ans = classList.Classes[index1st].ToString();

  //結果表示
  this.Text_Result_1st.Text = ans + ":" + max1st.ToString();
});
```
結果表示用の「Text_Result_1st」という名前のTextBlockに結果を代入して完了です。
また「classList」のインスタンスは自作の「IDとクラス名のリスト」のクラスです。

#3.仕上げ
最後は以下の手順で完了です。

1. デバッグが終わったらリリースビルドを作成する（自動でRaspberryPiの方に転送される）
2. IoT Dashboardからデバイスポータルを開く
3. 左のメニューから Apps > App maneger に入り、自分がビルドしたAppNameを見つける。
4. StartupのラジオボタンをONにする

以上で、電源投入すると自動でソフトが立ち上がり、延々と推論を繰り返すデバイスができあがります。
起動完了まで3分半くらい？です。遅いわけでは無いですが微妙です。

#4.感想
UWPアプリに関する情報が少なくて以外と苦労しました。
作ってみて改めて感じるのは、Windows10でONNXモデルが扱えるのは大きいです。
ディープラーニング実装のハードルが劇的に下がる気がします。
特に組込機器ではWindows 10 IoT Core/Enterpriseが走ると思うので、C#でソフト作って動作させられます。
現状、ONNXモデルのサポート範囲は広くないかもしれませんが、それでも強力です。

 - SonyのNeural Network ConsoleやMATLABといったローカル環境や、Custom Vision ServiceなどクラウドサービスではGUIベースでモデルを作成・訓練ができ、ONNX出力したり簡単に変換ができるので、フレームワークの使い方を覚える必要がない選択肢が出てくる。
 - OS本体のインストールだけでONNXが扱えて、ライブラリのバージョンが合わないなどのトラブルがない。
 - Linuxのようなインストール後のネット接続は不要(セキュリティの問題で現場に入れた設備がネットに接続不可の場合も多い)
 - リッチなUIを備えたソフトの製作が簡単
 - OSのサポート期間が長い

来年は産業界への深層学習の展開が加速しそうです。
2019年頭には[LATTEPANDA ALPHA](https://www.lattepanda.com/)が日本でも発売するので盛り上がりそうな気がします。
あとは、NVIDIA JetsonにWindows10 IoTが対応してくれるとすごく助かるのですが・・・。そんな日がくるだろうか？

#5.参考資料
以下の記事を参考にさせていただきました。ありがとうございます。

 - Custom Vision Serviceから出力したモデルを使ってWindowsML上で物体認識をする　[その1](https://qiita.com/linyixian/items/1ba75afe7bdf93bd53f8)　[その2](https://qiita.com/linyixian/items/65e979fa3714a6f1b8af)　[その3](Custom Vision Serviceから出力したモデルを使ってWindowsML上で物体認識をする)
 - [3分で分かる！ONNXフォーマットとWindows Machine Learning！](https://qiita.com/ymym3412/items/05a7cecf81309a3f131e)

#関連記事
ONNXモデルを作るにあたり、便利なツールであるMMdnnの簡単な解説を書きました。(2018.09.04)
https://qiita.com/koppe/items/7f85f5411539390c4499
kerasモデルをONNXに変換できました。ONNXの可能性を感じますね。