HoloLens2 × Azure Cognitive Services(Face APIで顔検出)

はじめに

HoloLensアドベントカレンダー1日目の記事です!

APIよくわからないと弟子から相談があったので、Cognitive Services系をまとめていきたいと思いまーす。

今日は、Cognitive ServicesのFace APIをHoloLens2でやってみました。

実機なしでもできるのでやってみてください。

開発環境

導入

image.png

1.AzureポータルからFace APIを作成し、エンドポイントとサブスクリプションキーをメモしておいてください。

image.png

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

3.メニューのMixed Reality Toolkit->Add to Scene and Configureしてください。

image.png

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

image.png

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

image.png
6.Project SettingsのXR Settings、Publishing Settings->Capabilitiesを以下のように設定してください。
image.png

6.Project SettingsのXR Settings、Publishing Settings->Capabilitiesを以下のように設定してください。

image.png

7.空のGameObjectを作成し、名前を「TapToCapture」にします。

image.png

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

1TapToCapture.cs
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using System;
6using UnityEngine;
7using Microsoft.MixedReality.Toolkit.Utilities;
8using System.Threading.Tasks;
9using OpenCVForUnity.CoreModule;
10using OpenCVForUnity.UnityUtils;
11using OpenCVForUnity.ImgprocModule;
12
13public class TapToCapture : MonoBehaviour
14{
15    public GameObject quad;
16
17    [System.Serializable]
18    public class Face
19    {
20        public string faceId;
21        public FaceRectangle faceRectangle;
22        public FaceAttribute faceAttributes;
23    }
24
25    [System.Serializable]
26    public class FaceRectangle
27    {
28        public int top;
29        public int left;
30        public int width;
31        public int height;
32    }
33
34    [System.Serializable]
35    public class FaceAttribute
36    {
37        public float age;
38        public string gender;
39    }
40
41    UnityEngine.Windows.WebCam.PhotoCapture photoCaptureObject = null;
42    Texture2D targetTexture = null;
43
44    private string endpoint = "https:///face/v1.0/detect";
45    private string subscription_key = "";
46    private bool waitingForCapture;
47
48    void Start(){
49        waitingForCapture = false;
50    }
51
52    public void AirTap()
53    {
54        if (waitingForCapture) return;
55        waitingForCapture = true;
56
57        Resolution cameraResolution = UnityEngine.Windows.WebCam.PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
58        targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);
59
60        // PhotoCapture オブジェクトを作成します
61        UnityEngine.Windows.WebCam.PhotoCapture.CreateAsync(false, delegate (UnityEngine.Windows.WebCam.PhotoCapture captureObject) {
62            photoCaptureObject = captureObject;
63            UnityEngine.Windows.WebCam.CameraParameters cameraParameters = new UnityEngine.Windows.WebCam.CameraParameters();
64            cameraParameters.hologramOpacity = 0.0f;
65            cameraParameters.cameraResolutionWidth = cameraResolution.width;
66            cameraParameters.cameraResolutionHeight = cameraResolution.height;
67            cameraParameters.pixelFormat = UnityEngine.Windows.WebCam.CapturePixelFormat.BGRA32;
68
69            // カメラをアクティベートします
70            photoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result) {
71                // 写真を撮ります
72                photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemoryAsync);
73            });
74        });
75    }
76
77    async void OnCapturedPhotoToMemoryAsync(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result, UnityEngine.Windows.WebCam.PhotoCaptureFrame photoCaptureFrame)
78    {
79        // ターゲットテクスチャに RAW 画像データをコピーします
80        photoCaptureFrame.UploadImageDataToTexture(targetTexture);
81        byte[] bodyData = targetTexture.EncodeToJPG();
82
83        Response response = new Response();
84
85        try
86        {
87            // string query = endpoint + "?detectionModel=detection_02&returnFaceId=true";
88            // 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";
89            string query = endpoint + "?detectionModel=detection_01&returnFaceId=true&returnFaceAttributes=age,gender";
90            var headers = new Dictionary<string, string>();
91            headers.Add("Ocp-Apim-Subscription-Key", subscription_key);
92            // headers.Add("Content-Type", "application/octet-stream");
93
94            response = await Rest.PostAsync(query, bodyData, headers, -1, true);
95        }
96        catch (Exception e)
97        {
98            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
99            return;
100        }
101
102        if (!response.Successful)
103        {
104            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
105            return;
106        }
107
108        Debug.Log(response.ResponseCode);
109        Debug.Log(response.ResponseBody);
110        string newResponseBody = "{ \"results\": " + response.ResponseBody + "}";
111        Face[] faces = JsonHelper.FromJson(newResponseBody);
112
113        Mat imgMat = new Mat(targetTexture.height, targetTexture.width, CvType.CV_8UC4);
114
115        Utils.texture2DToMat(targetTexture, imgMat);
116        // Debug.Log("imgMat.ToString() " + imgMat.ToString());
117
118        foreach (var face in faces){
119            //Debug.Log(face.faceId);
120            //Debug.Log(face.faceRectangle.left);
121            //Debug.Log(face.faceRectangle.top);
122            //Debug.Log(face.faceRectangle.width);
123            //Debug.Log(face.faceRectangle.height);
124            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);
125            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);
126        }
127
128        Texture2D texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);
129        Utils.matToTexture2D(imgMat, texture);
130
131        // テクスチャが適用されるゲームオブジェクトを作成
132        // GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);   
133
134        Renderer quadRenderer = quad.GetComponent() as Renderer;
135        // quadRenderer.material = new Material(Shader.Find("Unlit/UnlitTexture"));
136
137        // quad.transform.parent = this.transform;
138        // quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f);
139
140        quadRenderer.material.SetTexture("_MainTex", texture);
141
142        // カメラを非アクティブにします
143        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
144    }
145
146    void OnStoppedPhotoMode(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result)
147    {
148        // photo capture のリソースをシャットダウンします
149        photoCaptureObject.Dispose();
150        photoCaptureObject = null;
151        waitingForCapture = false;
152    }
153}

9.PhotoCaptureのサンプルは こちら です。エアタップしたら、画像キャプチャするようにInputActionHandlerをAdd Componentし、AirTap関数を作成します。エアタップ(On Input Action Started)したらAirTap関数が発火するように設定します。

10.撮影できたら、targetTextureに画像データが入っているので、JPGにエンコードして、Face APIに投げます。FaceAPIのサンプルはこちら C# 、 Python です。

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

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

1https:///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です。

1[{"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スクリプトを作成します。

1JsonHelper.cs
2using UnityEngine;
3using System;
4
5public class JsonHelper
6{
7    public static T[] FromJson(string json)
8    {
9        Wrapper wrapper = JsonUtility.FromJson<Wrapper>(json);
10        return wrapper.results;
11    }
12
13    [Serializable]
14    private class Wrapper
15    {
16        public T[] results;
17    }
18}

JsonHelperについて

–  yuiyoichi/JsonHelper.cs

–  How to load an array with JsonUtility?

–  UnityのJsonUtilityでJSON配列を処理する

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

1{
2    "results" : [ {...} ]
3}

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

19.顔検出結果をOpenCVを使って画像に描画し、Quadのマテリアルに割り当てます。3D Object->Quadを作成しましょう。

image.png
OpenCV for Unity サンプルはこちら– Texture2DからMatに変換– 矩形を描画(Imgproc.rectangle)– テキストを描画(Imgproc.putText)

20.OrbitalをAdd Componentし、Quadがカメラに追従するようにしています。

image.png

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

実行

HoloLens2にデプロイして、実行した結果がこちらになります。Editor上でもできるので試してみてください。

お疲れ様でした。

明日は弟子( @Horomoto-Asahi )による「HoloLens 2のSpatialAwarenessの調査」です。

この記事を書いた人

azure-recipe-user