HoloLensで顔検出やってみました。

#顔検出について
前回HoloLensでUWPのOcrEngine使って文字認識しました。ついでにUWPにあるライブラリでFaceDetectorをいう顔検出の機能もあるのでこれも少し試してみました。前回UWPの部品は呼出せていたのでFaceDetectorで実装するだけでおわりました。

## HoloLensの焦点距離
問題というか制約になるのだと思います。今回は検出した顔の枠をCanvas使ってImageで表示しているんですが、スクリーンショットだとちょっとずれてる？程度なのですが実際に見ると最初焦点が合わなくて困っていました。
HoloLensの仕組み知ってる方はわかると思うんですがHoloLensは網膜投射型のシステムなので、一般的なVRの装置みたいに焦点がずれるということ自体存在しないと思ってました。
いろいろ調べてみたところ、現実世界と仮想物体を両方（実際の顔＋仮想物体としての枠）に注視が必要な場合は注視対象から2m離れないときれいに重ならないということが判明[^1]。これに気付いてからアプリを使うとほぼ思った形になりました。なぜ、HoloLensのスィートスポットが2mなのか今更理解しました。なお、重ね合わせでも別々に注視している分には2mより手前でも問題なさそうです。

[^1]: [https://developer.microsoft.com/ja-jp/windows/holographic/hologram_stability](https://developer.microsoft.com/ja-jp/windows/holographic/hologram_stability)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/152548/46442521-31a0-edaa-ba57-99d453b10681.png)
データ上は顔認識できている状態。プロットがずれるのは焦点や座標の調整がうまくいっていないためです。

## 環境
今回使用した環境は以下の通りです。
Windows 10 Pro
Visual Studio 2015 Community Edition update 3
Unity 5.5.0P2 Personal
HoloToolkit 1.5.5.0

## サンプルソース

GitHubで公開していますので試したい方はダウンロードしてみてください。
https://github.com/TakahiroMiyaura/HoloFaceSamples

### Unity側での実装
基本的には前回と同じ手法を用いています。Staticな領域を利用してUnity側からUWP側で登録したオブジェクトを取り出して利用します。

#### 顔検出に必要なクラスの準備
顔検出を行うと結果として検出した顔の分以下の2つの情報が返ってきます。

- 座標（X,Y)
- サイズ（幅、高さ）

X,Y座標は検出に使用した画像の左上起点での座標になります。幅、高さは検出時の顔のサイズです。
これらを処理するために検出した顔の情報を保持するクラスと顔検出するためのUWP側で実装する抽象クラスを以下のように定義しています。

```csharp:CameraScript.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
public abstract class FaceDetectBase : IUWPBridgeService
{
    public delegate void SetFaceObject(List<FaceInformation> list);

    public int FrameSizeWidth;
    public int FrameSizeHeight;

    public SetFaceObject OnDetected;
    public abstract void DetectFace();
}

public class FaceInformation
{
    public float Height;
    public float Width;
    public float X;
    public float Y;
}
```
FaceDetectBase.FrameSizeHeight,FaceDetectBase.FrameSizeWidthの二つのプロパティは顔検出に使用した画像のサイズを格納します。

#### 初期化と顔検出処理の実装
Unity側ではUWP側で作ったクラスのインスタンスを取得し、定期的に画像から顔検出をおこないます。
１フレーム毎に検出すると負荷がかかるので20フレーム毎くらいでサンプルは動かしています。
途中「#if UNITY_EDITOR」を書いている箇所がありますが、これはデバッグ用です。
Unity上でデバッグする際にはUWP側から情報もらえないので描画の確認するためにダミーデータを返すスタブクラスを用意しました。

```csharp:CameraScript.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
private void Update()
{
    if (Time.frameCount % FRAME_INTERVAL == 0)
    {
        if (Service == null)
        {
#if UNITY_EDITOR
            // For Debug.when this application execute by unity,call this. 
            Service = new FaceDetectStub();
#else
// execute For HoloLens. 
            Service = UWPBridgeServiceManager.Instance.GetService<FaceDetectBase>();
            TextData.text = "Service Initialized.";
#endif
            Service.OnDetected = SetFaceObject;
        }
        Service.DetectFace();
    }
}
```
その後処理結果を受け取って顔と思しき場所に枠をImageで出しています。処理自体はUWP側で顔検出の完了後行います。このためSetFaceObjectメソッドを用意しそこに実装しています。
実際のキャプチャサイズはフルHD相当になっていますが、描画上そのままのサイズで扱えなかったのでCanvasのサイズに合わせて縮尺を変更します。また、検出される顔の分だけデータが返ってくるので、検出された顔の数に合わせてImageオブジェクトを再利用しながら数の調整を行っています。

### UWP側の実装
UWP側では初期処理としてカメラのプレビュー準備を行います。
[OCRで実装した準備作業](http://qiita.com/miyaura/items/c316ad39028ecfae50a9#uwp%E5%81%B4%E3%81%AE%E5%87%A6%E7%90%86)と同じです。プレビュー用のコントロールを貼り付けておいてHoloLensのカメラをプレビューで動作させておきます。
次に顔検出のロジックを実装します。使用するクラスはFaceDetectorクラスでこのクラスに画像渡すだけで顔らしい所の座標(X,Y)と顔のサイズ(幅、高さ)をリストで返してくれる便利な部品です。このクラス使うのに一点だけ注意が必要です。解析に使う画像は8bitグレースケールじゃないと動きません。ですのでプレビューからキャプチャする際に**BitmapPixelFormat.Gray8**指定でキャプチャします。

```csharp:CameraScript.cs
// Copyright(c) 2017 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
public override void DetectFace()
{
    AppCallbacks.Instance.InvokeOnUIThread(async () =>
    {
        var properties =
            _capture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview) as
                VideoEncodingProperties;
        if (properties == null)
            return;

        //coution! face detect is only supported 'BitmapPixelFormat.Gray8'.
        var videoFrame = new VideoFrame(BitmapPixelFormat.Gray8, (int) properties.Width, (int) properties.Height);

        this.FrameSizeWidth = (int) properties.Width;
        this.FrameSizeHeight = (int) properties.Height;


        var previewFrame = await _capture.GetPreviewFrameAsync(videoFrame);

        var detector = await FaceDetector.CreateAsync();
        var detectFaces = await detector.DetectFacesAsync(previewFrame.SoftwareBitmap);
        var faceInformations = detectFaces.Select(x => new FaceInformation
        {
            X = x.FaceBox.X,
            Y = x.FaceBox.Y,
            Width = x.FaceBox.Width,
            Height = x.FaceBox.Height
        }).ToList();
        AppCallbacks.Instance.InvokeOnAppThread(() => { OnDetected(faceInformations); }, false);
    }, true);
}

```
顔検出が完了したらUnity側で登録したCanvasで顔の部分を赤枠にして描画するための処理を呼び出します。
最後にUWP側で実装したクラスをUWPBridgeServieManagerに登録する処理を実装して完成です。

## 所感など
### Face APIについて
Microsoft Azureで提供されているFace API使うときっとその人の顔の近くに感情マークとか出したりできると思います。そのうち、実装してみたいと思います。