HoloLens2 × Azure Cognitive Services（Face APIで顔検出）

# はじめに
HoloLensアドベントカレンダー1日目の記事です！
APIよくわからないと弟子から相談があったので、Cognitive Services系をまとめていきたいと思いまーす。
今日は、Cognitive ServicesのFace APIをHoloLens2でやってみました。
実機なしでもできるのでやってみてください。

# 開発環境
- Azure 
   - Face API
- HoloLens2
- Unity 2019.4.1f1
- [MRTK 2.5.1](https://github.com/microsoft/MixedRealityToolkit-Unity/releases/tag/v2.5.1)
- [OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088)
- Windows PC
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">プライムデーで安かったので、予備PC<br>【基本スペック】<br>OS：Windows 10 Home 64bit、CPU：インテル Core i7-9750H、GPU：NVIDIA GeForce GTX 1650 Ti Max-Q デザイン 4GB GDDR5、メモリ：16GB（8GB ×2）、SSD：512GB、液晶パネル：15.6インチ、フルHD（1,920×1,080）、リフレッシュレート120Hz <a href="https://t.co/S8RIsayFPY">pic.twitter.com/S8RIsayFPY</a></p>&mdash; 藤本賢志(ガチ本)@pixivFANBOXはじめました (@sotongshi) <a href="https://twitter.com/sotongshi/status/1317758401946947591?ref_src=twsrc%5Etfw">October 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 導入

1．AzureポータルからFace APIを作成し、エンドポイントとサブスクリプションキーをメモしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/f8415626-3f01-f8bb-8040-98ecaacc19c9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/8c06fb54-f1a7-4a62-ba36-83371b8e4c12.png)

2．Unityでプロジェクトを作成、MRTK2.5.1をインポートします。なんかウィンドウでたらApplyします。

3．メニューのMixed Reality Toolkit->Add to Scene and Configureしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/a24fb502-ac17-861e-2898-cd6aa03f76c6.png)

4．Build Settingsから、Universal Windows PlatformにSwitch Platformして、以下のように設定してください。あとAdd Open ScenesでScenes/SampleSceneにチェックが入っていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/63f5009c-8365-6090-5d4c-e4150d98d7cb.png)

5．MixedRealityToolkitのDefaultHoloLens2ConfigureProfileをcloneし、Diagnostics->Enable Diagnostics Systemのチェックを外します。これでCPU使用率とかのデバッグ情報を非表示にできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/0f86d1a5-a316-7bd4-aab8-3a8cef87a335.png)

6．Project SettingsのXR Settings、Publishing Settings->Capabilitiesを以下のように設定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/e7787f97-bb76-0d34-03ae-1ccf3b5a4246.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/8a62df04-914e-2805-4014-b3046e579df2.png)

7．空のGameObjectを作成し、名前を「TapToCapture」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/c6124763-2aad-fc0e-0192-8ab231893520.png)

8．Add Componentから「TapToCapture.cs」スクリプトを作成します。エアタップしたら、画像をキャプチャし、Face APIに投げるスクリプトになります。

~~~TapToCapture.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using UnityEngine;
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Threading.Tasks;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.UnityUtils;
using OpenCVForUnity.ImgprocModule;

public class TapToCapture : MonoBehaviour
{
    public GameObject quad;

    [System.Serializable]
    public class Face
    {
        public string faceId;
        public FaceRectangle faceRectangle;
        public FaceAttribute faceAttributes;
    }

    [System.Serializable]
    public class FaceRectangle
    {
        public int top;
        public int left;
        public int width;
        public int height;
    }

    [System.Serializable]
    public class FaceAttribute
    {
        public float age;
        public string gender;
    }

    UnityEngine.Windows.WebCam.PhotoCapture photoCaptureObject = null;
    Texture2D targetTexture = null;

    private string endpoint = "https://<Insert Your Endpoint>/face/v1.0/detect";
    private string subscription_key = "<Insert Your API Key>";
    private bool waitingForCapture;

    void Start(){
        waitingForCapture = false;
    }

    public void AirTap()
    {
        if (waitingForCapture) return;
        waitingForCapture = true;

        Resolution cameraResolution = UnityEngine.Windows.WebCam.PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
        targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);

        // PhotoCapture オブジェクトを作成します
        UnityEngine.Windows.WebCam.PhotoCapture.CreateAsync(false, delegate (UnityEngine.Windows.WebCam.PhotoCapture captureObject) {
            photoCaptureObject = captureObject;
            UnityEngine.Windows.WebCam.CameraParameters cameraParameters = new UnityEngine.Windows.WebCam.CameraParameters();
            cameraParameters.hologramOpacity = 0.0f;
            cameraParameters.cameraResolutionWidth = cameraResolution.width;
            cameraParameters.cameraResolutionHeight = cameraResolution.height;
            cameraParameters.pixelFormat = UnityEngine.Windows.WebCam.CapturePixelFormat.BGRA32;

            // カメラをアクティベートします
            photoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result) {
                // 写真を撮ります
                photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemoryAsync);
            });
        });
    }

    async void OnCapturedPhotoToMemoryAsync(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result, UnityEngine.Windows.WebCam.PhotoCaptureFrame photoCaptureFrame)
    {
        // ターゲットテクスチャに RAW 画像データをコピーします
        photoCaptureFrame.UploadImageDataToTexture(targetTexture);
        byte[] bodyData = targetTexture.EncodeToJPG();

        Response response = new Response();

        try
        {
            // string query = endpoint + "?detectionModel=detection_02&returnFaceId=true";
            // string query = endpoint + "?detectionModel=detection_01&returnFaceId=true&returnFaceLandmarks=false&returnFaceAttributes=age,gender,headPose,smile,facialHair,glasses,emotion,hair,makeup,occlusion,accessories,blur,exposure,noise";
            string query = endpoint + "?detectionModel=detection_01&returnFaceId=true&returnFaceAttributes=age,gender";
            var headers = new Dictionary<string, string>();
            headers.Add("Ocp-Apim-Subscription-Key", subscription_key);
            // headers.Add("Content-Type", "application/octet-stream");

            response = await Rest.PostAsync(query, bodyData, headers, -1, true);
        }
        catch (Exception e)
        {
            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
            return;
        }

        if (!response.Successful)
        {
            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
            return;
        }

        Debug.Log(response.ResponseCode);
        Debug.Log(response.ResponseBody);
        string newResponseBody = "{ \"results\": " + response.ResponseBody + "}";
        Face[] faces = JsonHelper.FromJson<Face>(newResponseBody);

        Mat imgMat = new Mat(targetTexture.height, targetTexture.width, CvType.CV_8UC4);

        Utils.texture2DToMat(targetTexture, imgMat);
        // Debug.Log("imgMat.ToString() " + imgMat.ToString());

        foreach (var face in faces){
            //Debug.Log(face.faceId);
            //Debug.Log(face.faceRectangle.left);
            //Debug.Log(face.faceRectangle.top);
            //Debug.Log(face.faceRectangle.width);
            //Debug.Log(face.faceRectangle.height);
            Imgproc.putText(imgMat, face.faceAttributes.age.ToString()+","+face.faceAttributes.gender, new Point(face.faceRectangle.left, face.faceRectangle.top-10), Imgproc.FONT_HERSHEY_SIMPLEX, 1.5, new Scalar(0, 0, 255, 255), 2, Imgproc.LINE_AA, false);
            Imgproc.rectangle(imgMat, new Point(face.faceRectangle.left, face.faceRectangle.top), new Point(face.faceRectangle.left + face.faceRectangle.width, face.faceRectangle.top + face.faceRectangle.height), new Scalar(0, 0, 255, 255), 2);
        }

        Texture2D texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);
        Utils.matToTexture2D(imgMat, texture);

        // テクスチャが適用されるゲームオブジェクトを作成
        // GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);   
        
        Renderer quadRenderer = quad.GetComponent<Renderer>() as Renderer;
        // quadRenderer.material = new Material(Shader.Find("Unlit/UnlitTexture"));

        // quad.transform.parent = this.transform;
        // quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f);

        quadRenderer.material.SetTexture("_MainTex", texture);

        // カメラを非アクティブにします
        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
    }

    void OnStoppedPhotoMode(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result)
    {
        // photo capture のリソースをシャットダウンします
        photoCaptureObject.Dispose();
        photoCaptureObject = null;
        waitingForCapture = false;
    }
}

~~~

9．PhotoCaptureのサンプルは[こちら](https://docs.unity3d.com/ja/2018.4/Manual/windowsholographic-photocapture.html)です。エアタップしたら、画像キャプチャするようにInputActionHandlerをAdd Componentし、AirTap関数を作成します。エアタップ（On Input Action Started）したらAirTap関数が発火するように設定します。

10．撮影できたら、targetTextureに画像データが入っているので、JPGにエンコードして、Face APIに投げます。FaceAPIのサンプルはこちら[C#](https://docs.microsoft.com/ja-jp/azure/cognitive-services/face/quickstarts/csharp)、[Python](https://docs.microsoft.com/ja-jp/azure/cognitive-services/face/quickstarts/python)です。

11．endpointとsubscription_keyにメモしておいたものを貼り付けてください。

12．クエリパラメータは、detection_01モデルを使用、FaceId、年齢と性別を返すように設定しています。

~~~
https://<Insert Your Endpoint>/face/v1.0/detect?detectionModel=detection_01&returnFaceId=true&returnFaceAttributes=age,gender"
~~~

ちなみにfaceAttributesはsmile, headPose, gender, age, facialHair, glasses, emotion, blur, exposure, noise, makeup, accessories, occlusion, hairといった情報が取れます。

13．MRTKのRestを用いてHTTPリクエストします。
ヘッダーは、"Ocp-Apim-Subscription-Key": subscription_keyを指定、"Content-Type": "application/octet-stream"はRestの中でやってくれるのでコメントアウトします。

14．クエリと画像データ、ヘッダーをPOSTします。
response = await Rest.PostAsync(query, bodyData, headers, -1, true);

15．response.ResponseBodyが下記のように返ってくればOKです。

~~~
[{"faceId":"f1b97cf1-58d0-4dc9-9169-e19cb0655e48","faceRectangle":{"top":347,"left":451,"width":285,"height":285},"faceAttributes":{"gender":"male","age":23.0}}]
~~~

16．Face APIのResponseBodyがリストのjsonになっているので、パースできるようにJsonHelper.csスクリプトを作成します。

~~~JsonHelper.cs
using UnityEngine;
using System;

public class JsonHelper
{
    public static T[] FromJson<T>(string json)
    {
        Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(json);
        return wrapper.results;
    }

    [Serializable]
    private class Wrapper<T>
    {
        public T[] results;
    }
}
~~~

JsonHelperについて
- [yuiyoichi/JsonHelper.cs](https://gist.github.com/yuiyoichi/792af1cb87011e1f648956826c0d0aa0)
- [How to load an array with JsonUtility?](https://forum.unity.com/threads/how-to-load-an-array-with-jsonutility.375735/)
- [UnityのJsonUtilityでJSON配列を処理する](https://qiita.com/akira-sasaki/items/71c13374698b821c4d73)

17．返ってきたResponseBodyを次のようにすることで、パースすることが可能になります。

~~~json
{
    "results" : [ {...} ]
}
~~~

18．あとは仕様に合わせてFaceクラスとFaceRectangleクラス、FaceAttributeクラスを作成しました。

19．顔検出結果をOpenCVを使って画像に描画し、Quadのマテリアルに割り当てます。3D Object->Quadを作成しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/ee88e9c7-f750-5e35-c37d-48b43377cd54.png)

OpenCV for Unity サンプルはこちら
- [Texture2DからMatに変換](https://github.com/EnoxSoftware/OpenCVForUnity/blob/master/Assets/OpenCVForUnity/Examples/Basic/Texture2DToMatExample/Texture2DToMatExample.cs)
- [矩形を描画（Imgproc.rectangle）](https://github.com/EnoxSoftware/OpenCVForUnity/blob/master/Assets/OpenCVForUnity/Examples/ContribModules/face/FaceMarkExample/FaceMarkExample.cs)
- [テキストを描画（Imgproc.putText）](https://github.com/EnoxSoftware/OpenCVForUnity/blob/master/Assets/OpenCVForUnity/Examples/ContribModules/face/FaceRecognizerExample/FaceRecognizerExample.cs)

20．OrbitalをAdd Componentし、Quadがカメラに追従するようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/0a050182-495f-d3d0-be6a-326389706dc3.png)

21．TapToCaptureにQuadをD&Dしてアタッチしたら完成です。

# 実行

HoloLens2にデプロイして、実行した結果がこちらになります。Editor上でもできるので試してみてください。
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">HoloLens2 × Cognitive Services Face API<a href="https://twitter.com/hashtag/HoloLens2?src=hash&amp;ref_src=twsrc%5Etfw">#HoloLens2</a> <a href="https://twitter.com/hashtag/Azure?src=hash&amp;ref_src=twsrc%5Etfw">#Azure</a> <a href="https://twitter.com/hashtag/CognitiveServices?src=hash&amp;ref_src=twsrc%5Etfw">#CognitiveServices</a> <a href="https://twitter.com/hashtag/MRTK?src=hash&amp;ref_src=twsrc%5Etfw">#MRTK</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://twitter.com/hashtag/AI?src=hash&amp;ref_src=twsrc%5Etfw">#AI</a> <a href="https://t.co/B3G38kEveT">pic.twitter.com/B3G38kEveT</a></p>&mdash; がちもとさん@熊本 (@sotongshi) <a href="https://twitter.com/sotongshi/status/1333580101552726016?ref_src=twsrc%5Etfw">December 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

お疲れ様でした。
明日は弟子（@Horomoto-Asahi）による「HoloLens 2のSpatialAwarenessの調査」です。