LoginSignup
8

More than 5 years have passed since last update.

HoloLensで始めるCognitive Services(Face API編)

Last updated at Posted at 2018-06-21

お久しぶりです。

さてさて、今回はCognitive ServicesのFace APIを使用して、HoloLensで顔認識を行ってみます。
ただし、2018年6月時点で公式のアカデミーに下記のサンプルがありますので、
これよりはちょっとアレンジを加えたものにしましょう。

MR and Azure 304: Face recognition

本記事で作成するアプリは、de:code 2018シアターセッション用に作成したサンプルアプリケーションになります。
簡単!!HoloLensで始めるCognitive Services~de:code 2018特別バージョン~

本記事の中ではまずFace APIで顔の認識をさせるまでを実装します。
次回、このアプリにAzure Functions、LogicApps連携を追加しますが、本記事だけでも単体で動作可能です。

Face APIとは

人間の顔が写った画像やURLを送信すると、Azure側でその人物を特定し、
ユーザ情報を返してくれるCognitive Services内のサービスです。
詳しくは下記をご参照ください。
https://azure.microsoft.com/ja-jp/services/cognitive-services/face/

環境

Unity:2017.1.2p3
HoloToolkit:HoloToolkit-Unity-v1.2017.1.2
VisualStudio:15.5.4

完成イメージ

流れ

1.Face APIのサービス登録
2.Face APIへ顔を登録
3.Face APIの学習
4.Unity側の実装
5.HoloLensへのデプロイ&実行

1.Face APIのサービス登録

下記の記事にサービスの登録とKeyの取得までが記載されているので、こちらを実施してください。
本記事内では、「場所」を東アジアで作成した前提で進めます。
Microsofrt AzureのFace APIを使って笑顔をAIに判断してもらおう

2.Face APIへ顔を登録

AcademyのMR and Azure 304にリンクが貼られているUWPアプリ「Person Maker」が使えそうですが
このアプリはRegionが米国西部でないと下記のエラーが出ます。

Microsoft.ProjectOxford.Face.FaceAPIException

2.png

ソースの該当箇所には以下のようにコメントが書かれています。

MainPage.xaml.cs
                    // You may experience issues with this below call, if you are attempting connection with
                    // a service location other than 'West US'
                    PersonGroup[] groups = await faceServiceClient.ListPersonGroupsAsync();
                    var matchedGroups = groups.Where(p => p.PersonGroupId == personGroupId);

実際に、DLLレベルでこの米国西部が記載されていたので、これを修正するのはちょっとしんどそうです。
なのでもし場所を米国西部(West US)にしている方は使用してみてもよいです。
ただし、画面上はuserDataの入力欄がないので、名前以外を出したい場合(今回のサンプルもそうですが)、後からUpdateが必要になります。

3.png

ここでは、Person Makerを使用せずに登録してみます。

2-1.グループの作成
まずはグループを作成します。
このグループごとに人物を管理することになります。

私は場所を東アジアにしたため、下記のリンクから直接登録しました。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395244/console

■入力例
4.PNG

personGroupIdに任意のIDを、
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。
ちなみにpersonGroupIdは大文字不可のようです。

5.PNG

Request BodyのJSON部分、nameとuserDataを入力してください(userDataは任意)。
入力が終わったらSendを押下してください。

レスポンスで200 OKが返ってきたら正常終了です。
6.PNG

2-2.人物の登録
2-1で作成したグループに人物を登録します。
下記のリンクから登録しましょう。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f3039523c/console

■入力例
7.PNG

personGroupIdには先ほどグループを作成した際のIDを入れてください。
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。

8.PNG

nameには名前を入れてください。
userDataは 会社名,役職名,画像用URL をカンマ区切りで入力します。
これが後程アプリに生きてきます。

9.PNG

Sendを押下し、200 OKが返ってきたら正常終了です。
ここのpersonIdは2-3で使用するのでコピーしておいてください。

10.PNG

2-3.顔の登録
人物を登録したら、次はその人物に紐づく顔を登録します。
顔の登録はローカルからアップロードする方法と、グローバルなURLで指定する方法があります。
私は自分の写真を5枚ほど撮影し、BLOBにアップロードの上、
パブリックアクセスを付与した状態で登録させました。
有名人であれば、ネットで検索して出てきた画像URLを使用すればよいと思います。

GyazoのようにURLを付与して共有できるサービスもあるため、適宜使用してください。

■入力例
11.PNG

personGroupIdには先ほどグループを作成した際のIDを入力してください。
personIdには2-2で登録したpersonIdを入力してください。
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。

12.PNG

Request Bodyのurlに、登録する顔画像のURLを入力し、Sendを押下してください。
200 OKが返ってきたら正常終了です。
同様に他の顔画像も登録してください。
5枚くらいあればだいたい識別できるようです。

3.Face APIの学習

人物の登録、顔の登録が終わったら忘れずに学習させましょう。
学習はグループ単位で行います。

こちらの画面から実行します。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395249/console

■入力例
13.PNG

personGroupIdには先ほどグループを作成した際のIDを入力してください。
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。

入力が終わったらSendを押下してください。
202 Acceptedが返ってきたら正常に開始しています。

学習の状況は下記のTrain Statusから取得することも可能です。
適宜実行の上、「succeeded」であることを確認してください(割愛してもよい)。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395247/console

14.PNG

以上でFace API側の設定は完了です。

4.Unity側の実装

4-1.HoloLens用のプロジェクト設定
こちらは詳細割愛します。
HoloToolKit 2017.1.2をインポートし、プロジェクト設定を行ってください。

15.png

CapabilitiesのInternetClient,WebCamは必須です!!
その後、適当な名前でシーンを保存しておいてください(ここではmain.unityとしました)。

デフォルトのカメラを削除し、HoloToolkitからHoloLensCameraを配置してください。
また、InputManagerとCursorも配置してください。

4-2.UI部品を配置


  1. prefabのインポート
    下記のプレファブをダウンロードし、インポートしてください。
    https://github.com/haveagit/HoloLensFaceAPISample/blob/master/UiImagePrefab.unitypackage

    prefabフォルダ配下にUiImagePrefabがインポートされます。
    Imageフォルダにはプレファブに使用する画像が入っています(特に触らなくてよいです)。

  2. メインとなるキャンバスの配置
    ヒエラルキーでUI/Canvasを配置し、名前をMainCanvasに変更してください。

    16.png

    設定値は下記とします。

    17.PNG

  3. システムメッセージ表示用キャンバスの配置
    ヒエラルキーでUI/Canvasを配置し、名前をSystemMassageCanvasに変更してください。

    設定値は下記とします。

    18.PNG

    さらにSystemMassageCanvas配下に
    UI/Text
    UI/Raw Image
    を作成します。

    設定値は下記とします。
    名前はそれぞれSystemText、DebugRawImageとします。

    ■Text
    19.PNG

    ■Raw Image
    20.PNG



ここまでで、Gameビューは下記のようになっているはずです。

21.PNG

4-3.JSONObjectのインポート
JSONの処理で外部ライブラリを使用しますので、下記の「JSONObject」をCloneまたはダウンロードし、Asset直下に配置してください。
https://github.com/mtschoen/JSONObject

22.PNG

4-4.スクリプト実装
画像をキャプチャし、APIを呼び出し、戻ってきた値を編集してテキストエリアに表示する処理を実装します。
Asset配下にScriptsフォルダを作成し、GetFaceInfo.csを新規作成してください。
半分くらいはカメラ設定であり、わりと単純なプログラムになります。
※行数が長いのでソース分割も考えましたが、諸事情によりやめました。

  • PostToFaceAPIでキャプチャした画像をAPIに送り、戻ってきた値のうち、一番確率が高いものを表示します。
  • CreateFaceUIにおいて、検出した顔毎に枠の生成、テキスト部品の生成を行います。
  • もしFace APIに登録されている顔であった場合、IdentifyByFaceId、GetPersonInfo内でユーザ情報の特定を行います。userDataのCSV部をパースし、会社名、役職名、画像URLを取得します。
  • GetFaceImageの中では外部からの画像取得を行い、パネルにセットして完了です。
  • 連続撮影には対応していません。一度撮影したらFace API側の処理が終わるまで待ってください。
GetFaceInfo.cs
// Copyright(c) 2018 Shingo Mori
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

using HoloToolkit.Unity.InputModule;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using UnityEngine.VR.WSA.WebCam;

public class GetFaceInfo : MonoBehaviour, IInputClickHandler
{
    // UI周りのフィールド
    public GameObject UiImagePrefab; //顔ごとに生成するUI部品のプレファブ
    public GameObject Canvas;        //UIを配置するためのメインのキャンバス
    public Text SystemMessage;       //状況表示用メッセージエリア
    public RawImage photoPanel;      //debug用のキャプチャ表示パネル

    // カメラ周りのパラメータ
    private Resolution cameraResolution;
    private PhotoCapture photoCaptureObject = null;
    private Quaternion cameraRotation;

    // Azure側のパラメータ群
    private string personGroupId = "YOUR_GROUP_ID"; //FaceAPIで設定したpersonGroupIdをセットする
    private string FaceAPIKey    = "YOUR_APP_KEY";  //FaceAPIのAPPキーをセットする
    private string region        = "YOUR_REGION";   //FaceAPIの地域をセットする
    private string DetectURL;
    private string IdentifyURL;
    private string GetPersonURL;

    public void OnInputClicked(InputClickedEventData eventData)
    {
        AnalyzeScene();
    }

    void Start()
    {
        InputManager.Instance.AddGlobalListener(gameObject);

        DetectURL = "https://" + region + ".api.cognitive.microsoft.com/face/v1.0/detect";
        GetPersonURL = "https://" + region + ".api.cognitive.microsoft.com/face/v1.0/persongroups/" + personGroupId + "/persons/";
        IdentifyURL = "https://" + region + ".api.cognitive.microsoft.com/face/v1.0/identify";

        cameraResolution = PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
    }

    private void AnalyzeScene()
    {
        DisplaySystemMessage("Detect Start...");
        PhotoCapture.CreateAsync(false, OnPhotoCaptureCreated);
    }

    //PhotoCaptureの取得は下記参照
    //https://docs.microsoft.com/ja-jp/windows/mixed-reality/locatable-camera-in-unity
    private void OnPhotoCaptureCreated(PhotoCapture captureObject)
    {
        DisplaySystemMessage("Take Picture...");
        photoCaptureObject = captureObject;

        CameraParameters c = new CameraParameters();
        c.hologramOpacity = 0.0f;
        c.cameraResolutionWidth = cameraResolution.width;
        c.cameraResolutionHeight = cameraResolution.height;
        c.pixelFormat = CapturePixelFormat.JPEG;

        captureObject.StartPhotoModeAsync(c, OnPhotoModeStarted);
    }

    private void OnPhotoModeStarted(PhotoCapture.PhotoCaptureResult result)
    {
        if (result.success)
        {
            photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);
        }
        else
        {
            Debug.LogError("Unable to start photo mode!");
        }
    }

    private void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame)
    {
        if (result.success)
        {
            // Face APIに送るimageBufferListにメモリ上の画像をコピーする
            List<byte> imageBufferList = new List<byte>();
            photoCaptureFrame.CopyRawImageDataIntoBuffer(imageBufferList);

            //ここはデバッグ用 送信画像の出力。どんな画像が取れたのか確認したい場合に使用。邪魔ならphotoPanelごと消してもよい。
            Texture2D debugTexture = new Texture2D(100, 100);
            debugTexture.LoadImage(imageBufferList.ToArray());
            photoPanel.texture = debugTexture;

            // カメラの向きをワールド座標に変換するためのパラメータ保持
            var cameraToWorldMatrix = new Matrix4x4();
            photoCaptureFrame.TryGetCameraToWorldMatrix(out cameraToWorldMatrix);
            cameraRotation = Quaternion.LookRotation(-cameraToWorldMatrix.GetColumn(2), cameraToWorldMatrix.GetColumn(1));


            Matrix4x4 projectionMatrix;
            photoCaptureFrame.TryGetProjectionMatrix(Camera.main.nearClipPlane, Camera.main.farClipPlane, out projectionMatrix);
            Matrix4x4 pixelToCameraMatrix = projectionMatrix.inverse;

            StartCoroutine(PostToFaceAPI(imageBufferList.ToArray(), cameraToWorldMatrix, pixelToCameraMatrix));
        }
        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
    }

    private void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result)
    {
        photoCaptureObject.Dispose();
        photoCaptureObject = null;
    }

    /*
     * 取得した画像をFaceAPIに送信し、顔を検出する
     */
    private IEnumerator<object> PostToFaceAPI(byte[] imageData, Matrix4x4 cameraToWorldMatrix, Matrix4x4 pixelToCameraMatrix)
    {
        DisplaySystemMessage("Call Face API...");
        var headers = new Dictionary<string, string>() {
            { "Ocp-Apim-Subscription-Key", FaceAPIKey },
            { "Content-Type", "application/octet-stream" }
        };

        WWW www = new WWW(DetectURL, imageData, headers);
        yield return www;
        string responseString = www.text;

        JSONObject json = new JSONObject(responseString); //JSONObjectライブラリはUnityアセットストアより別途ダウンロードが必要

        var node = json.list.FirstOrDefault();
        if (node != null)
        {
            DisplaySystemMessage("Face detect finished...");
            CreateFaceUI(json, cameraRotation, cameraToWorldMatrix, pixelToCameraMatrix);
        }
        else
        {
            DisplaySystemMessage("No Face detected...");
        }
    }

    private void CreateFaceUI(JSONObject json, Quaternion rotation, Matrix4x4 cameraToWorldMatrix, Matrix4x4 pixelToCameraMatrix)
    {
        // 古いUIの削除
        DestroyOldUI();

        Dictionary<string, GameObject> faceUImap = new Dictionary<string, GameObject>();
        //検出した顔の数だけ繰り返す
        foreach (var result in json.list)
        {
            //顔の枠の生成
            var rect = result.GetField("faceRectangle");
            float top = -(rect.GetField("top").f / cameraResolution.height - .5f);
            float left = rect.GetField("left").f / cameraResolution.width - .5f;
            float width = rect.GetField("width").f / cameraResolution.width;
            float height = rect.GetField("height").f / cameraResolution.height;

            // テキストエリアの生成とサイズ、向きの調整
            GameObject uiImageObject = (GameObject)Instantiate(UiImagePrefab);
            GameObject rectImageObject = uiImageObject.transform.Find("Image").gameObject;

            Vector3 txtOrigin = cameraToWorldMatrix.MultiplyPoint3x4(pixelToCameraMatrix.MultiplyPoint3x4(new Vector3(left + left, top + top, 0)));
            uiImageObject.transform.position = txtOrigin;
            uiImageObject.transform.rotation = rotation;
            uiImageObject.transform.Rotate(new Vector3(0, 1, 0), 180);
            uiImageObject.tag = "faceText";
            uiImageObject.transform.parent = Canvas.transform;

            RectTransform faceDetectedImageRectTransform = rectImageObject.GetComponent<RectTransform>();
            faceDetectedImageRectTransform.sizeDelta = new Vector2(width, height);

            // 生成したUIオブジェクトをFaceIDと紐づけて格納
            faceUImap.Add(result.GetField("faceId").str, uiImageObject);
        }

        // すべての顔のUI部品生成が終わったらIdentifyを呼び出す
        StartCoroutine(IdentifyByFaceId(faceUImap));
    }

    private void DestroyOldUI()
    {
        // 前回生成したUIの削除
        var existing = GameObject.FindGameObjectsWithTag("faceText");
        List<string> FaceIdList = new List<string>();
        foreach (var go in existing)
        {
            Destroy(go);
        }
    }

    /*
     * 取得した FaceId を使って、Person Group に登録されている顔情報を検索(Identify)する。
     * 顔情報が検出されると PersonId が取得できる。
     * 
     * PersonId取得後、PersonId を使って、ユーザ情報を取得する。
     */
    IEnumerator IdentifyByFaceId(Dictionary<string, GameObject> faceUImap)
    {
        DisplaySystemMessage("Start Identify...");

        //FaceIdの取得
        var header = new Dictionary<string, string>() {
            { "Content-Type", "application/json" },
            { "Ocp-Apim-Subscription-Key", FaceAPIKey }
        };

        // サーバへPOSTするデータを設定 
        PersonId id = new PersonId();
        id.personGroupId = this.personGroupId;

        //呼び出し回数節約のため一回で全部取得したいので、FaceIDをまとめて全部渡す
        id.faceIds = new string[faceUImap.Keys.Count];
        faceUImap.Keys.CopyTo(id.faceIds, 0);

        string json = JsonUtility.ToJson(id);
        byte[] bytes = Encoding.UTF8.GetBytes(json.ToString());

        WWW www = new WWW(IdentifyURL, bytes, header);
        yield return www;

        JSONObject j = new JSONObject(www.text);

        // faceIdの数だけ繰り返す
        foreach (var result in j.list)
        {

            string faceId = result.GetField("faceId").str;

            GameObject uiObject = faceUImap[faceId];
            Text txtArea = uiObject.GetComponentInChildren<Text>();
            var candidates = result.GetField("candidates");
            string personId = "";
            float confidence = 0.0f;

            if (candidates.list.Count > 0)
            {
                //候補者はconfidenceの高い順に格納されているので、1件目を採用する。
                personId = candidates.list[0].GetField("personId").str;
                confidence = candidates.list[0].GetField("confidence").f;
            }
            else
            {
                // personIdが取得できなかった(人物特定失敗)はSkip
                DisplaySystemMessage("Not registered...");
                txtArea.text = "Not registered Person";
                continue;
            }

            //ユーザ情報の取得
            StartCoroutine(GetPersonInfo(personId, confidence, txtArea, uiObject.GetComponentInChildren<RawImage>()));
        }

        DisplaySystemMessage("Done...");
    }


    /*
     * PersonIDを元にユーザー情報を取得する。
     * ユーザー情報(userData)は所属,肩書,URLのカンマ区切りで設定している前提。
     * 例:
     *   "name" : "Shingo Mori",
     *   "userData" : "TIS Inc,Section Chief,https://testBlob/image.jpg"
     */
    private IEnumerator GetPersonInfo(string personId, float confidence, Text txtArea, RawImage panel)
    {
        //ユーザ情報の取得
        UnityWebRequest request = UnityWebRequest.Get(GetPersonURL + personId);
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Ocp-Apim-Subscription-Key", FaceAPIKey);

        yield return request.Send();

        JSONObject req = new JSONObject(request.downloadHandler.text);
        string name = req.GetField("name").str;
        string userData = req.GetField("userData").str;

        // テキストエリアの編集
        string message = name;
        message += System.Environment.NewLine;
        string[] data = userData.Split(',');
        message += data[0] + System.Environment.NewLine;
        message += data[1] + System.Environment.NewLine;
        message += "Identify Confidence:" + confidence * 100 + "%";
        txtArea.text = message;

        // イメージ画像取得用URLを移送
        string imageUrl = data[2];

        //顔写真の取得
        StartCoroutine(GetFaceImage(panel, imageUrl));
    }

    /*
     * 画像ファイルをダウンロードし、テクスチャに貼り付ける
     */
    private IEnumerator GetFaceImage(RawImage imagePanel, string targetUrl)
    {
        WWW www = new WWW(targetUrl);
        // 画像ダウンロード完了を待機
        yield return www;

        // 画像をパネルにセット
        imagePanel.texture = www.textureNonReadable;
    }

    /*
     * 状況出力用メッセージ
     */
    private void DisplaySystemMessage(string message)
    {
        SystemMessage.text = message;
    }

    /*
     * Face API呼び出し用のクラス
     */
    [Serializable]
    public class PersonId
    {
        public string personGroupId;
        public string[] faceIds;
    }
}
  • personGroupId、FaceAPIKey、regionはそれぞれ自身で作成したサービスの設定に従って値を入力してください。

ヒエラルキーで空のGameObjectを作成し、名前をAppControllerに変更してください。
AppControllerにGetFaceInfo.csをAdd Componentしてください。

そして各publicパラメータにオブジェクトをセットしていきます。

パラメータ オブジェクト
UiImagePrefab Assets/Prefab配下のUiImagePrefab.prefabをセット
Canvas ヒエラルキー直下のMainCanvasをセット
SystemMessage ヒエラルキーにあるSystemMassageCanvas直下のSystemTextをセット
photoPanel ヒエラルキーにあるSystemMassageCanvas直下のDebugRawImageをセット

ここまでで、Unityエディタ上での実行は可能となりました。
Playボタンを押下し、Gameビュー上でShift+左クリックするとPCのWebカメラが起動して写真を撮り、
Face APIの呼び出しなど一連の処理が行われます。

23.PNG

ポイントはuserData部をCSVにしてURLも組み込んでいるところです。
この例では画像ファイルのダウンロード用に使っていますが、
WebAPIで叩けるものであれば、それこそAzure FunctionsのURLを格納しておいてもよいのです。
userDataは16KBまで格納できるので、CSVではなくJSON形式で入れてもいいかもしれませんね。

5.HoloLensへのデプロイ&実行

この手順は割愛します。
UnityからUWPプロジェクトをビルドし、Visual StudioでHoloLensへビルド&デプロイして実機で実行してみてください!
下記のようになれば成功です!!

20180621_202746_HoloLens.jpg

ソースコード

下記にアップしていますので、必要に応じてご参照ください。
https://github.com/haveagit/HoloLensFaceAPISample

次回はこのアプリにAzure FunctionsとAzure LogicAppsの呼び出しを組み込んでみます!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8