MR and Azure 302:画像認識

2.ホロレンズで表示される画面を実装する

まずはじめに、[Main Camera]を編集していきたいと思います。[Hierarchy]→[Main Camera]を選択します。[Inspector]→[Transform]を以下のように変更します。次に、[Camera]→[Clear Flags]をSolid Colorにし、[Background]を#0000000とし黒にします。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-22.png)

さらに、[Main Camera]を右クリックし[3D Objects]→[Sphere]とし、名前をCursorとします。今作成したCursorの[Inspector]を以下のように編集していきます。これは、画像解析の出力位置となります。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-23.png)

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-24.jpg)

続いて、分析結果を表示するタグを作っていきたいと思います。ホロレンズを通して取得した画像は、Azure Computer Vision API Serviceに送信され分析されます。その分析結果がタグとよばれるリストとして返されます。今から画像を認識したい物体に、タグを表示するためのものをつくっていきます。

まずはじめに、[Herarchy]→[Create]→[3D Object]→[3D Text]としてテキストをつくり、名前をLabelTextとします。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-25.png)

続いて[LabelText]の[Inspector]を編集していきます。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-26.png)

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-27.png)

今作った[LabelText]をプレハブにしていきたいと思います。[LabelText]を[Hierarchy]から[Project]へD&Dします。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-28.jpg)

これによりコードでインスタンス化できるようになりました。また、[Hierarchy]の[LabelText]を削除します。これで最初からシーンに表示されないようになります。以上で表示される画面は実装できました。次のステップからはそれらに動きをつけていきたいと思います。

3.スクリプトの作成

このステップでは、認識した画像を画像処理した結果を表示するまでの動きを作っていきます。まず最初に行うのがResultsLabelクラスのスクリプト作成です。これは、画像解析した結果(タグ)を表示するためのものです。

はじめに今後記述していくスクリプトをまとめておくためのフォルダーをつくっていきます。これは直接動作には関係ありませんが、あとで整理しやすくなるのでつくります。まず、[Project]→[Create]→[Folder]として名前をScriptsとしておきます。以後つくるスクリプトはすべてこの中にしまっていきましょう。

①続いて、今つくった[Scripts]フォルダーを選択し、右クリックして[Create]→[C# Script]をクリックして中にスクリプトを作っていきます。名前はResultsLabelとしておきましょう。

[ResultsLabel]をダブルクリックすると、visual studioが開きます。では、スクリプトを記述していきたいと思います。コードは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ResultsLabel : MonoBehaviour {
public static ResultsLabel instance;

public GameObject cursor;

public Transform labelPrefab;

[HideInInspector]
public Transform lastLabelPlaced;

[HideInInspector]
public TextMesh lastLabelPlacedText;

private void Awake()
{
// allows this instance to behave like a singleton
instance = this;
}

/// <summary>
/// Instantiate a Label in the appropriate location relative to the Main Camera.
/// </summary>
public void CreateLabel()
{
lastLabelPlaced = Instantiate(labelPrefab, cursor.transform.position, transform.rotation);

lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>();

// Change the text of the label to show that has been placed
// The final text will be set at a later stage
lastLabelPlacedText.text = "Analysing...";
}

/// <summary>
/// Set the Tags as Text of the last Label created.
/// </summary>
public void SetTagsToLastLabel(Dictionary<string, float> tagsDictionary)
{
lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>();

// At this point we go through all the tags received and set them as text of the label
lastLabelPlacedText.text = "I see: \n";

foreach (KeyValuePair<string, float> tag in tagsDictionary)
{
lastLabelPlacedText.text += tag.Key + ", Confidence: " + tag.Value.ToString("0.00 \n");
}
}
}

保存しUnityに戻ります。このスクリプトを[Hierarchy]の[Main Camera]にD&Dします。すると、[Main Camera]内の[Inspector]に[Results Label]という項目が増えています。[Cursor]という項目に[Hierarchy]の[Cursor]を、[Label Prefab]という項目に[Project]の[Label Text]をアタッチします。

②次に、ImageCaptureクラスを作っていきたいと思います。このクラスの役割は二点です。

・ホロレンズで画像をとり、それをAppフォルダーに保存する。

・画像をとるときのタップジェスチャーを認識する。

スクリプトのつくりかたは先ほどと同じなのでここでは省略させてもらいます。名前をImageCaptureとして保存します。ダブルクリックをしてvisual studioの編集画面になったら以下のコードを記述していきます。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.XR.WSA.Input;
using UnityEngine.XR.WSA.WebCam;

public class ImageCapture : MonoBehaviour {

public static ImageCapture instance;
public int tapsCount;
private PhotoCapture photoCaptureObject = null;
private GestureRecognizer recognizer;
private bool currentlyCapturing = false;

private void Awake()
{
// Allows this instance to behave like a singleton
instance = this;
}

void Start()
{
// subscribing to the Hololens API gesture recognizer to track user gestures
recognizer = new GestureRecognizer();
recognizer.SetRecognizableGestures(GestureSettings.Tap);
recognizer.Tapped += TapHandler;
recognizer.StartCapturingGestures();
}

/// <summary>
/// Respond to Tap Input.
/// </summary>
private void TapHandler(TappedEventArgs obj)
{
// Only allow capturing, if not currently processing a request.
if (currentlyCapturing == false)
{
currentlyCapturing = true;

// increment taps count, used to name images when saving
tapsCount++;

// Create a label in world space using the ResultsLabel class
ResultsLabel.instance.CreateLabel();

// Begins the image capture and analysis procedure
ExecuteImageCaptureAndAnalysis();
}
}

/// <summary>
/// Register the full execution of the Photo Capture. If successful, it will begin
/// the Image Analysis process.
/// </summary>
void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result)
{
// Call StopPhotoMode once the image has successfully captured
photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
}

void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result)
{
// Dispose from the object in memory and request the image analysis
// to the VisionManager class
photoCaptureObject.Dispose();
photoCaptureObject = null;
StartCoroutine(VisionManager.instance.AnalyseLastImageCaptured());
}

/// <summary>
/// Begin process of Image Capturing and send To Azure
/// Computer Vision service.
/// </summary>
private void ExecuteImageCaptureAndAnalysis()
{
// Set the camera resolution to be the highest possible

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

Texture2D targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);

// Begin capture process, set the image format
PhotoCapture.CreateAsync(false, delegate (PhotoCapture captureObject)
{
photoCaptureObject = captureObject;
CameraParameters camParameters = new CameraParameters();
camParameters.hologramOpacity = 0.0f;
camParameters.cameraResolutionWidth = targetTexture.width;
camParameters.cameraResolutionHeight = targetTexture.height;
camParameters.pixelFormat = CapturePixelFormat.BGRA32;

// Capture the image from the camera and save it in the App internal folder
captureObject.StartPhotoModeAsync(camParameters, delegate (PhotoCapture.PhotoCaptureResult result)
{
string filename = string.Format(@"CapturedImage{0}.jpg", tapsCount);

string filePath = Path.Combine(Application.persistentDataPath, filename);

VisionManager.instance.imagePath = filePath;

photoCaptureObject.TakePhotoAsync(filePath, PhotoCaptureFileOutputFormat.JPG, OnCapturedPhotoToDisk);

currentlyCapturing = false;
});
});
}
}

保存した後、Unity画面に戻ります。ここでコードのエラーが表示されるかもしれませんが、次につくるクラスをつくれば、解消されるので気にしなくて大丈夫です。

③最後にVisionManagerクラスを作っていきたいと思います。このクラスの役割は以下の3点です

・撮った画像をバイト配列としてロードする。(画像はRGBAの配列であらわされるのですがわからなくても問題ないです)

・ロードした配列をAzure Computer Vision API Serviceに送信する。

・その結果を受け取り人間が分かるように変換した後、その結果をResultsLabelクラスへ渡す。

スクリプトの作り方は今回も省略させてもらいます。名前をVisionManagerとし、visual studioで開きます。

以下のコードを記述していきましょう。ここで重要な注意点が二点あります。

・authorizationKeyに0章で取得したkey1,key2のいずれかをいれる。

・visionAnalysisEndpointに0章で取得した、地域を表すURLをいれる。

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class VisionManager : MonoBehaviour {

[System.Serializable]
public class TagData
{
public string name;
public float confidence;
}

[System.Serializable]
public class AnalysedObject
{
public TagData[] tags;
public string requestId;
public object metadata;
}

public static VisionManager instance;

// you must insert your service key here!
private string authorizationKey = "先ほど取得したkey1,key2のいずれか";
private const string ocpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key";

private string visionAnalysisEndpoint = "先ほど取得したURL/vision/v1.0/analyze?visualFeatures=Tags"; // This is where you need to update your endpoint, if you set your location to something other than west-us.

internal byte[] imageBytes;

internal string imagePath;

private void Awake()
{
// allows this instance to behave like a singleton
instance = this;
}

/// <summary>
/// Call the Computer Vision Service to submit the image.
/// </summary>
public IEnumerator AnalyseLastImageCaptured()
{
WWWForm webForm = new WWWForm();
using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(visionAnalysisEndpoint, webForm))
{
// gets a byte array out of the saved image
imageBytes = GetImageAsByteArray(imagePath);
unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream");
unityWebRequest.SetRequestHeader(ocpApimSubscriptionKeyHeader, authorizationKey);

// the download handler will help receiving the analysis from Azure
unityWebRequest.downloadHandler = new DownloadHandlerBuffer();

// the upload handler will help uploading the byte array with the request
unityWebRequest.uploadHandler = new UploadHandlerRaw(imageBytes);
unityWebRequest.uploadHandler.contentType = "application/octet-stream";

yield return unityWebRequest.SendWebRequest();

long responseCode = unityWebRequest.responseCode;

try
{
string jsonResponse = null;
jsonResponse = unityWebRequest.downloadHandler.text;

// The response will be in Json format
// therefore it needs to be deserialized into the classes AnalysedObject and TagData
AnalysedObject analysedObject = new AnalysedObject();
analysedObject = JsonUtility.FromJson<AnalysedObject>(jsonResponse);

if (analysedObject.tags == null)
{
Debug.Log("analysedObject.tagData is null");
}
else
{
Dictionary<string, float> tagsDictionary = new Dictionary<string, float>();

foreach (TagData td in analysedObject.tags)
{
TagData tag = td as TagData;
tagsDictionary.Add(tag.name, tag.confidence);
}

ResultsLabel.instance.SetTagsToLastLabel(tagsDictionary);
}
}
catch (Exception exception)
{
Debug.Log("Json exception.Message: " + exception.Message);
}

yield return null;
}
}

/// <summary>
/// Returns the contents of the specified file as a byte array.
/// </summary>
private static byte[] GetImageAsByteArray(string imageFilePath)
{
FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
BinaryReader binaryReader = new BinaryReader(fileStream);
return binaryReader.ReadBytes((int)fileStream.Length);
}

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}
}

これを保存し、Unity画面に戻ります。先ほどつくった[ImageCapture]スクリプトおよび[VisionManager]スクリプトを[Hierarchy]の[Main Camera]にD&Dします。[Main Camera]内の[Inspector]に以下の項目が増えていることを確認しましょう。

以上で必要なスクリプトをすべて記述し終わりました。

4.ホロレンズで使ってみよう

最後のステップです。今までに実装したものをホロレンズ上で実行してみたいと思います。ビルドする方法はこちらを参照してください。最初に行った設定を変えずに[Build]をすることに注意します。

認識したい物体に視点をあわせて

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-32.jpg)

エアタップするとanalysing..と表示された後に結果が表示されます。

![](http://azurerecipe.blob.core.windows.net/2018-11-29-03/302-33.jpg)

以上となります。お疲れさまでした。

この記事を書いた人

azure-recipe-user