Windows MLを使ったTiny Yolo V2によるオフライン物体検知のサンプル

# ONNXでのオフライン推論の魅力
Windows MLが登場しオフラインでの機械学習の幅が１つ広がりました。オフラインでの実行の魅力は何より通信のオーバーヘッドがないことです。これは非常に大きく、アプリケーションのUXにかなりインパクトがあります。
一般的にはオンラインの方が推論に必要な計算リソース非常に潤沢です。ですが通信が伴うためそのためのデータ転送等で時間がかかります。また、通信障害が発生するとシステム自体が利用不可になるという自体にもつながります。
反面オフラインはこのような通信部分の制御が全て省略できます。ただし、ローカルマシンのスペックが推論に影響を与えます。
それでも、オフラインでの推論によるメリットは非常に大きいといえると思います。

## Windows ML の状況
最近ようやくWindows 10 October Update(1809)が再開され、新しいWindows MLも利用できるようになりました。Windows ML APIは春のOSアップデートから利用できる学習済みモデルとオフラインで推論のみ実施できるAPIですが、1809から正式なAPIという位置づけになっています。
このため、以前のWindows MLと一部仕様が異なっていることもあり以前のサンプルがあまり使えないことになりそうです。

今回はHoloLensに組み込む前にUWPアプリケーションとしてTiny Yolo V2のONNXを利用したオフライン推論を実現しその実装方法を備忘録として残します。

## 今回の開発環境
今回は以下の開発環境で実施しています。開発機に関しては1803で開発しています（検証時は1809が下りてきていなかったため）。最終的にはHoloLensの3Dアプリケーションに仕立てるつもりですが、まずは基本的な部分がうまく動作するように2Dアプリケーションを作成しました。

* 開発機
    * Windows 10 Pro 1803  
    * Windows 10 SDK 1809(10.0.17763.0)
    * Visual Studio 2017
* 実行環境
    * Windows 10(OSバージョン 1809)
    * HoloLens (OSバージョン 1809日本語版)
* 今回作ったソースコード
    * https://github.com/TakahiroMiyaura/HoloLensOnnxTinyYoLo  

今アップロードしているものは２Dアプリケーションです。

## 参考サイト
特に参考にしたのが以下のサイトです。この方はWindows MLでTiny Yolo V2のサンプルコードをGitHubに公開されています（ただし、そのままでは上手く動かないですが）。今回作ったサンプルの一部はこのコードを流用しています。流用している部分はTiny Yolo V2で推論した結果得られるデータ配列を解析して必要な情報を取り出す部分です。ここについて専用のクラスを実装されていたのでそのまま使わせてもらっています。
また、ONNXのモデルについてはAzure AI Galleryから探しました。

 * [Tiny YOLOv2(Azure AI Gallery)](https://gallery.azure.ai/Model/Tiny-YOLOv2)
 * [#WinML – Updated demo using Tiny YOLO V2 1.2, Windows 10 and YOLOV2 for Object Detection Series](https://elbruno.com/2018/08/07/winml-updated-demo-using-tiny-yolo-v2-1-2-windows-10-and-yolov2-for-object-detection-series/)

## 開発の大まかな手順とアプリについて
アプリ

開発手順は以下のような流れになります。

1. [Tiny Yolo V2用のONNXを取得](#Tiny Yolo V2用のONNXを取得)
1. [Visual StudioでUWPアプリケーションを作成](#Visual StudioでUWPアプリケーションを作成)
1. [ONNXをプロジェクトに追加](#ONNXをプロジェクトに追加)
1. [自動生成されるラッパークラスを修正](#自動生成されるラッパークラスを修正)
1. [画像を取り込むための実装](#画像を取り込むための実装)
1. [ONNXを使って取得した画像を推論にかける](#ONNXを使って取得した画像を推論にかける)
1. [推論結果を分析](#推論結果を分析)
1. [結果の描画](#結果の描画)

### Tiny Yolo V2用のONNXを取得
Tiny Yolo V2用のONNXを取得します。今回はすでに用意されているものがあったのでそれを使います。
[Azure AI Gallery](https://gallery.azure.ai)から取得できます。

* https://gallery.azure.ai/Model/Tiny-YOLOv2

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/8d013bc9-fcf2-8ccf-6ac8-315424f8e9b9.png)

ダウンロードするONNXのバージョンは「1.2」になります。これ間違うと動かないので気を付けてください。
また、ファイル名をわかりやすいもの(TinyYoloV1_2.onnx)に変更します（任意）。

### Visual StudioでUWPアプリケーションを作成
Visual Studioで空のUWPアプリケーションを作成します。今回は2Dアプリケーションとして作成しているのでUnityは使っていません。

### ONNXをプロジェクトに追加
プロジェクトが作成されたらプロジェクト内の任意の場所（プロジェクトルート or Assetsの配下当たり）にonnxをコピーします。
そのあとONNXを「プロジェクトに含める」でプロジェクトに追加してください。
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/f347fc32-9dec-3e6a-25bd-e7588589d843.png)

追加すると、自動的にONNXのファイル名を同名のクラスが自動的に生成されます。

最後にONNXのファイルを選択し「プロパティ」の中の「ビルドアクション」を「コンテンツ」に変更します。
ONNXファイルは実行時にファイルとして読み込みが必要なため、コンテンツとしてAPPX内に取り込めるようにしておきます。

### 自動生成されるラッパークラスを修正

#### 生成される自動生成の内容

生成されたソースコードは以下のようになります。

```csharp:TinyYoloV1_2.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Media;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.AI.MachineLearning;
namespace UwpOnnxTinyYoloV2_1809
{
    
    public sealed class TinyYoloV1_2Input
    {
        public TensorFloat image; // shape(-1,3,416,416)
    }
    
    public sealed class TinyYoloV1_2Output
    {
        public TensorFloat grid; // shape(-1,125,13,13)
    }
    
    public sealed class TinyYoloV1_2Model
    {
        private LearningModel model;
        private LearningModelSession session;
        private LearningModelBinding binding;
        public static async Task<TinyYoloV1_2Model> CreateFromStreamAsync(IRandomAccessStreamReference stream)
        {
            TinyYoloV1_2Model learningModel = new TinyYoloV1_2Model();
            learningModel.model = await LearningModel.LoadFromStreamAsync(stream);
            learningModel.session = new LearningModelSession(learningModel.model);
            learningModel.binding = new LearningModelBinding(learningModel.session);
            return learningModel;
        }
        public async Task<TinyYoloV1_2Output> EvaluateAsync(TinyYoloV1_2Input input)
        {
            binding.Bind("image", input.image);
            var result = await session.EvaluateAsync(binding, "0");
            var output = new TinyYoloV1_2Output();
            output.grid = result.Outputs["grid"] as TensorFloat;
            return output;
        }
    }
}
```

#### 変更が必要な場合
次にラッパークラスを見直します。使用するONNXによっては入出力パラメータがあっていない場合があります。自動生成として正しいのですが、WindowsMLが処理する型とあっていない場合があります。


Tiny Yolo V2のような「入力が画像」になるONNXファイルは注意が必要です。
1809で自動生成されるとたいていTensorFloatとなるのですが、子の形式では処理がうまく通らない場合があるようで、以下のいずれかを使用できるように変更します。

*  VideoFrame
*  ImageFeatureValue

```csharp:修正前
public sealed class TinyYoloV1_2Input
{
    public TensorFloat image; // shape(-1,3,416,416)
}
```

```csharp:修正後
public sealed class TinyYoloV1_2Input
{
    public VideoFrame image;
}
```

### 画像を取り込むための実装

画像の取込みについてはLowLagPhotoCaptureクラスを利用しました。今回はボタンを押したときにカメラ画像をキャプチャし、その情報をVideoFrameに格納して入力データとしています。動画で処理する場合であればMediaCaptureから動画をキャプチャする形で実装します。
画像の形式は「MediaPixelFormat.Bgra8」にします。

```csharp:CameraController.cs
// Copyright(c) 2018 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

using System;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using Windows.Graphics.Imaging;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;

namespace UwpOnnxTinyYoloV2_1809.TinyYoloV2
{
    /// <summary>
    /// Provides access to a camera device for client universal windows platform..
    /// </summary>
    public class CameraController
    {
        /// <summary>
        /// delgate to output processing messages.
        /// </summary>
        /// <param name="message">message</param>
        /// <param name="append">true-append,false-override</param>
        /// <returns></returns>
        public delegate Task ProcessNotice(string message, bool append = true);

        /// <summary>
        /// Define an action output processing messages.
        ///  </summary>
        public ProcessNotice OnProcessNotice;

        private static LowLagPhotoCapture _lowLagPhotoCapture;
        private static MediaCapture _capture;

        /// <summary>
        /// Initializes a new instance of the MediaCapture class setting a camera device.
        /// </summary>
        public async Task InitializeCameraAsync()
        {
            try
            {
                var devices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
                var device = devices[0];
                _capture = new MediaCapture();
                var settings = new MediaCaptureInitializationSettings
                {
                    VideoDeviceId = device.Id
                };
                await _capture.InitializeAsync(settings);

                _lowLagPhotoCapture = await _capture.PrepareLowLagPhotoCaptureAsync(
                    ImageEncodingProperties.CreateUncompressed(MediaPixelFormat.Bgra8));
                _capture.VideoDeviceController.Focus.TrySetAuto(true);

            }
            catch (Exception e)
            {
                await OnProcessNotice(e.ToString());
            }

        }

        /// <summary>
        /// Capture a photo.
        /// </summary>
        /// <param name="bitmapPixelFormat"><see cref="BitmapPixelFormat"/></param>
        /// <returns>Captured image</returns>
        public async Task<SoftwareBitmap> CapturePhotoAsync(BitmapPixelFormat bitmapPixelFormat)
        {

            SoftwareBitmap convert = null;
            try
            {
                CapturedPhoto asyncOperation = null;
                try
                {
                    asyncOperation = await _lowLagPhotoCapture.CaptureAsync();
                }
                catch (Exception exception)
                {
                    await OnProcessNotice(exception.ToString());
                }

                using (var asyncOperationFrame = asyncOperation.Frame)
                {
                    convert = SoftwareBitmap.Convert(asyncOperationFrame.SoftwareBitmap, bitmapPixelFormat);
                }
            }
            catch (Exception e)
            {
                await OnProcessNotice(e.ToString());
            }

            return convert;
        }
    }
}
```

### ONNXを使って取得した画像を推論にかける
画像データ用意できれば後は簡単です。後は画像を整形しONNXモデルによる推論を実施します。推論に必要な処理としては以下の３つです。

* ONNXのロード
* 入力データの設定
* 推論

ONNXのロードはラッパークラスにリソースからモデルを生成する機能があるのでそれを使います。ONNXファイルは先の設定でAPPX内に格納されていますので、APPX内のパス指定で対象ファイルをロードするようにします。読込んだファイルをラッパークラスのCreateFromStreamAsyncによってモデル化を実施します。

```csharp:TinyYoloEvaluatorFor1809.cs
/// <summary>
/// Load Onnx Model.
/// </summary>
/// <returns></returns>
public virtual async Task InitializeModelAsync()
{
    var modelFile =
        await StorageFile.GetFileFromApplicationUriAsync(
            new Uri("ms-appx:///Assets/TinyYoloV1_2.onnx"));
    _model = await TinyYoloV1_2Model.CreateFromStreamAsync(modelFile);
}        
```

推論についてもラッパークラスで提供されているので、入力データを設定しロードしたモデルに対して推論を実施する形になります。

```csharp
/// <summary>
///  Evaluate an image data by using ONNX.
/// </summary>
/// <param name="softwareBitmap">input image data.</param>
/// <returns></returns>
public virtual async Task EvaluatedByONNXAsync(SoftwareBitmap softwareBitmap)
{
    //入力データの設定
    var input = new TinyYoloV1_2Input();
    input.image =
                VideoFrame.CreateWithSoftwareBitmap(SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8));

    //モデルによる推論の実施
    var tinyYoloV10ModelOutput = await _model.EvaluateAsync(input);
    
    ・
    ・
    ・
    
}
```

### 推論結果を分析
最後に推論結果を分析します。Tiny Yolo V2はfloatの配列で結果が返ってくるのでこれをもとに自力で分析が必要になります。解析の方法論自体は[色々解説記事](http://machinethink.net/blog/object-detection-with-yolo/)があります。
今回はすでにUWPでそのあたりのParserを作っている人がいましたのでその人のコードをベースに使わせてもらっています。

 * [#WinML – Updated demo using Tiny YOLO V2 1.2, Windows 10 and YOLOV2 for Object Detection Series](https://elbruno.com/2018/08/07/winml-updated-demo-using-tiny-yolo-v2-1-2-windows-10-and-yolov2-for-object-detection-series/)

この解説記事の中にある以下のGithubにあるYolo9000フォルダ内のYoloWinMlParserクラスがParserになっているので推論結果を渡して検知した物体の種類と位置を取得します。

### 結果の描画(2D)

２Dアプリケーションの場合はカメラでキャプチャした画像と、Tiny Yolo V2で推論した検出した物体の枠を重ね合わせて表示します。
簡単な方法としてImageオブジェクトとCanvasを同じサイズで重ね合わせて、Imageオブジェクトにキャプチャした画像。Canvas側に枠を表示する形で処理を行いました。
後はAppBarButtonを2つ用意します。それぞれ以下の処理を実装しています。

* 画像ファイルをロードして推論を行う。
* カメラでキャプチャした画像を使って推論を行う。

詳細な実装はGithubに公開しているソースを参考にしてください。

```xml:MainPage.xaml
<Page
    x:Class="UwpOnnxTinyYoloV2_1809.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UwpOnnxTinyYoloV2_1809"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="8.5*" />
            <RowDefinition Height="1.5*" />
        </Grid.RowDefinitions>
        <Viewbox Name="Box1" Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Image Name="ImageData" />
        </Viewbox>
        <Viewbox Grid.Row="0">
            <Canvas Name="Canvas1" HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Viewbox>
        <ScrollViewer Grid.Row="1">
            <TextBlock Name="Ready" FontSize="12" TextAlignment="left">false</TextBlock>
        </ScrollViewer>
    </Grid>
    <Page.BottomAppBar>
        <CommandBar>
            <AppBarButton Label="LoadFile" Name="LoadFile" Icon="Page2" Click="LoadFile_OnClick" />
            <AppBarButton Label="CapturePhotoAsync" Name="Capture" Icon="Camera" Click="Capture_OnClick" />
        </CommandBar>
    </Page.BottomAppBar>
</Page>
```

### 結果の描画(3D)

(後日追加予定)

## まとめ
今回は最新のOSバージョン1809でWindows ML APIを活用したTiny Yolo V2による物体検知アプリの解説を行いました。Windows ML APIはまだできて日が浅いこともあり、上手く動作しない場合もあります。今回のサンプルを1つの例として参考にしてもらえると幸いです。
オフライン機械学習を手軽に導入できることはエッジデバイスやHoloLensのようなウェアラブルでの利用の幅が広がるので是非楽しんでください。