HoloLensでUWP側の機能を利用するには
ほとんどをUnityの機能で作ってしまう場合は、さほど気にすることはないのかもしれないですがたまにUWPの機能やUnityで使えないC#のコードが必要になってそんな時はどうするのか。
四苦八苦して思いついたのがunity側からUWP側の機能をつかうコツで書いたUWP側で書いたクラスを使う方法です。
せっかくなので何かできないものかと思いHoloLensでOCRをさせてみました。
仕組み的にはUWPでHoloLensのカメラを利用してOCREngineに画像を送り文字解析する形になります。
解析した文字はUnity側でテキストとして出力しています。
環境
今回使用した環境は以下の通りです。
Windows 10 Pro
Visual Studio 2015 Community Edition update 3
Unity 5.5.0P2 Personal
HoloToolkit 1.5.5.0
サンプルソース
GitHubで公開していますので試したい方はダウンロードしてみてください。
https://github.com/TakahiroMiyaura/HoloOcrSamples
Unity側での実装
Unity側ではunity側からUWP側の機能をつかうコツで書いたUWP側のオブジェクトを管理するインターフェースの定義とOCRから読み取った文字列をセットするテキスト及びそのロジックを実装します。
MainCameraに対するコンポーネントとしてCameraScriptを用意し処理を実装します。
OCR用のインターフェースとして今回は以下のようなものを実装しました。
実際にOCRの処理を行うRecognizeメソッドと文字認識の完了時に呼び出すメソッドを設定するプロパティの2つのメンバーを持つインターフェースです。
/// <summary>
/// Represents a class that optical character recognition processing.
/// </summary>
public abstract class IOcrService : UWPBridgeService.IUWPBridgeService
{
public delegate void SetRecognizeText(string text);
/// <summary>
/// Gets or sets the action to be performed after optical character recognition processing.
/// </summary>
public SetRecognizeText OnRecognized;
/// <summary>
/// Perform optical character recognition
/// </summary>
public abstract void Recognize();
}
Unity側での実装としてOCRを実施する部分を実装します。OCR自体は1フレームごとに実行するととんでもなく負荷がかかるので、適度な間隔で実行するようにしています。サンプルでは120フレーム毎に処理します。この中でUWP側でインスタンス化した上記インターフェースを実装したクラスを取得し、OCR完了時のメソッドとしてテキストオブジェクトに文字列を設定する処理をかきます。
// Update is called once per frame
private void Update()
{
if (Time.frameCount % FrameInterval == 0)
{
//Initialize
if (Service == null)
{
Service = UWPBridgeService.GetService<IOcrService>();
Service.OnRecognized = SetString;
}
//Perform optical character recognition
Service.Recognize();
}
}
/// <summary>
/// Set processing result to <see cref="Text"/>.
/// </summary>
/// <param name="text">Processing result.</param>
private void SetString(string text)
{
OcrDatas.text = text;
}
UWP側の処理
UWP側ではOCRを利用するために先のUnity側で作ったインターフェースの実装を行います。
カメラを利用してOCRを行うために今回はCaptureElementコントロールを利用します。流れとしてCaptureElementのソースとしてHoloLensのカメラを割り当てプレビュー表示しておき、その画像をUnity側からの処理要求があったときにキャプチャしてOCRにかけます。
OcrServiceクラスは先のインターフェースを実装して作ります。コンストラクタではCaptureElementに定義したソースを渡します。
また、プレビューのための初期化処理もこのクラス内で実装しています。
HoloLensのカメラデバイスの情報についてはDeviceInformation.FindAllAsyncメソッドを用いて取得できます。
/// <summary>
/// constructer.
/// </summary>
/// <param name="mediaCapture">Sets <see cref="MediaCapture"/> used for optical character recognition</param>
public OcrService(MediaCapture mediaCapture)
{
_mediaCapture = mediaCapture;
}
/// <summary>
/// Prepare the device to perform optical character recognition and initialize the <see cref="OcrService" />.
/// </summary>
public static async Task InitializeOcrServiceAsync(SetMediaCaptureObjectAsync action)
{
//Get camera device of HoloLens.
var deviceInformationCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
var deviceInformation = deviceInformationCollection[0];
var captureElementSource = new MediaCapture();
await captureElementSource.InitializeAsync(new MediaCaptureInitializationSettings
{
VideoDeviceId = deviceInformation.Id
});
await action(captureElementSource);
UWPBridgeService.AddService<CameraScript.IOcrService>(new OcrService(captureElementSource));
}
実際のOcrの処理の実装ですが、ここでポイントになるのがスレッドの取り回しになります。Unity側のイベント等をきっかけに動く処理の中でUWPのUIスレッド上での動作を前提とする処理を実装すると例外が発生します。
この場合はAppCallbacks.Instance.InvokeOnUIThreadメソッドを利用してUIスレッド上で動作するように実装します。
一方、Unity側のオブジェクトに対してはAppスレッド上で動作させる必要があり、この場合はAppCallbacks.Instance.InvokeOnAppThreadメソッドを用いて対処します。今回の場合OCRの処理をUIスレッド上で実行しますが、その中でOCRの完了時にUnity側で設定したメソッドを呼び出す際には元のAppスレッドに返す必要があります。このメソッドの中はUnity側のライブラリを利用しているためです。
/// <summary>
/// Perform optical character recognition.
/// </summary>
public override void Recognize()
{
if (_isRecognizing) return;
_isRecognizing = true;
var item = new AppCallbackItem(async () =>
{
var previewProperties =
_mediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview)
as VideoEncodingProperties;
if (previewProperties == null)
return;
var videoFrame = new VideoFrame(BitmapPixelFormat.Rgba8, (int) previewProperties.Width,
(int) previewProperties.Height);
_videoFrame = await _mediaCapture.GetPreviewFrameAsync(videoFrame);
var ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
if (ocrEngine == null)
return;
var recognizeAsync = await ocrEngine.RecognizeAsync(_videoFrame.SoftwareBitmap);
var str = new StringBuilder();
foreach (var ocrLine in recognizeAsync.Lines)
str.AppendLine(ocrLine.Text);
var recoginzeText = str.ToString();
_isRecognizing = false;
if (OnRecognized != null)
AppCallbacks.Instance.InvokeOnAppThread(() => { OnRecognized(recoginzeText); }, false);
});
AppCallbacks.Instance.InvokeOnUIThread(item, false);
}
OCRの処理自体はUWPのOcrEngineを利用しています。現状HoloLens自体が英語しかサポートしていないため英語しか認識できませんが。。。
最後にMainPage.xamlのどこかでOcrService.InitializeOcrServiceAsyncメソッドを呼び出して初期化すれば完了です!
その他
今回の方法はカメラを動画状態にしているのでそれなりに負荷がかかっていると思います。この辺りはスクリーンショットを利用すべきかもしれないです。また、全画面をOCRの対象にしているため、思ったところの文字を読むなどもさせてみたいところです。