お久しぶりです。
さてさて、今回は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
##完成イメージ
さっきから私がツイートしてる「〇〇さんに会ったよ!」ってやつはHoloからFaceAPI叩いて戻りのnameをFunctionsに投げ、LogicAppsから自動でツイートさせてるもの。
— morio🌭@6/10~ドイツCEBIT6/23HoloMeetup6/24xRTechTokyo (@morio36) 2018年5月23日
昨日の基調講演見て勢いで作った。
Face APIのuserData部分に外部処理叩くためのURL入れておくのは使い道多いと思う。
#decode18 pic.twitter.com/BtJ84GXy7e
##流れ
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
ソースの該当箇所には以下のようにコメントが書かれています。
// 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が必要になります。
ここでは、Person Makerを使用せずに登録してみます。
2-1.グループの作成
まずはグループを作成します。
このグループごとに人物を管理することになります。
私は場所を東アジアにしたため、下記のリンクから直接登録しました。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395244/console
personGroupIdに任意のIDを、
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。
ちなみにpersonGroupIdは大文字不可のようです。
Request BodyのJSON部分、nameとuserDataを入力してください(userDataは任意)。
入力が終わったらSendを押下してください。
2-2.人物の登録
2-1で作成したグループに人物を登録します。
下記のリンクから登録しましょう。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f3039523c/console
personGroupIdには先ほどグループを作成した際のIDを入れてください。
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。
nameには名前を入れてください。
userDataは 会社名,役職名,画像用URL をカンマ区切りで入力します。
これが後程アプリに生きてきます。
Sendを押下し、200 OKが返ってきたら正常終了です。
ここのpersonIdは2-3で使用するのでコピーしておいてください。
2-3.顔の登録
人物を登録したら、次はその人物に紐づく顔を登録します。
顔の登録はローカルからアップロードする方法と、グローバルなURLで指定する方法があります。
私は自分の写真を5枚ほど撮影し、BLOBにアップロードの上、
パブリックアクセスを付与した状態で登録させました。
有名人であれば、ネットで検索して出てきた画像URLを使用すればよいと思います。
GyazoのようにURLを付与して共有できるサービスもあるため、適宜使用してください。
personGroupIdには先ほどグループを作成した際のIDを入力してください。
personIdには2-2で登録したpersonIdを入力してください。
Ocp-Apim-Subscription-Keyには自身のFace APIのAppKeyを入力してください。
Request Bodyのurlに、登録する顔画像のURLを入力し、Sendを押下してください。
200 OKが返ってきたら正常終了です。
同様に他の顔画像も登録してください。
5枚くらいあればだいたい識別できるようです。
##3.Face APIの学習
人物の登録、顔の登録が終わったら忘れずに学習させましょう。
学習はグループ単位で行います。
こちらの画面から実行します。
https://eastasia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395249/console
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
以上でFace API側の設定は完了です。
##4.Unity側の実装
4-1.HoloLens用のプロジェクト設定
こちらは詳細割愛します。
HoloToolKit 2017.1.2をインポートし、プロジェクト設定を行ってください。
CapabilitiesのInternetClient,WebCamは必須です!!
その後、適当な名前でシーンを保存しておいてください(ここではmain.unityとしました)。
デフォルトのカメラを削除し、HoloToolkitからHoloLensCameraを配置してください。
また、InputManagerとCursorも配置してください。
4-2.UI部品を配置
- prefabのインポート
下記のプレファブをダウンロードし、インポートしてください。
https://github.com/haveagit/HoloLensFaceAPISample/blob/master/UiImagePrefab.unitypackage
prefabフォルダ配下にUiImagePrefabがインポートされます。
Imageフォルダにはプレファブに使用する画像が入っています(特に触らなくてよいです)。 - メインとなるキャンバスの配置
ヒエラルキーでUI/Canvasを配置し、名前をMainCanvasに変更してください。
設定値は下記とします。
- システムメッセージ表示用キャンバスの配置
ヒエラルキーでUI/Canvasを配置し、名前をSystemMassageCanvasに変更してください。
設定値は下記とします。
さらにSystemMassageCanvas配下に
UI/Text
UI/Raw Image
を作成します。設定値は下記とします。
名前はそれぞれSystemText、DebugRawImageとします。
4-3.JSONObjectのインポート
JSONの処理で外部ライブラリを使用しますので、下記の「JSONObject」をCloneまたはダウンロードし、Asset直下に配置してください。
https://github.com/mtschoen/JSONObject
4-4.スクリプト実装
画像をキャプチャし、APIを呼び出し、戻ってきた値を編集してテキストエリアに表示する処理を実装します。
Asset配下にScriptsフォルダを作成し、GetFaceInfo.csを新規作成してください。
半分くらいはカメラ設定であり、わりと単純なプログラムになります。
※行数が長いのでソース分割も考えましたが、諸事情によりやめました。
- PostToFaceAPIでキャプチャした画像をAPIに送り、戻ってきた値のうち、一番確率が高いものを表示します。
- CreateFaceUIにおいて、検出した顔毎に枠の生成、テキスト部品の生成を行います。
- もしFace APIに登録されている顔であった場合、IdentifyByFaceId、GetPersonInfo内でユーザ情報の特定を行います。userDataのCSV部をパースし、会社名、役職名、画像URLを取得します。
- GetFaceImageの中では外部からの画像取得を行い、パネルにセットして完了です。
- 連続撮影には対応していません。一度撮影したらFace API側の処理が終わるまで待ってください。
// 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の呼び出しなど一連の処理が行われます。
ポイントはuserData部をCSVにしてURLも組み込んでいるところです。
この例では画像ファイルのダウンロード用に使っていますが、
WebAPIで叩けるものであれば、それこそAzure FunctionsのURLを格納しておいてもよいのです。
userDataは16KBまで格納できるので、CSVではなくJSON形式で入れてもいいかもしれませんね。
##5.HoloLensへのデプロイ&実行
この手順は割愛します。
UnityからUWPプロジェクトをビルドし、Visual StudioでHoloLensへビルド&デプロイして実機で実行してみてください!
下記のようになれば成功です!!
##ソースコード
下記にアップしていますので、必要に応じてご参照ください。
https://github.com/haveagit/HoloLensFaceAPISample
次回はこのアプリにAzure FunctionsとAzure LogicAppsの呼び出しを組み込んでみます!