概要
UnityのARKit Pluginを使えばARコンテンツを手軽に作ることができます。
しかし、当然ですがARは外界をカメラで撮影し、それを元に姿勢を判断しています。
つまり、QRコードリーダーやOpenCVなどを利用した画像認識など「カメラを利用した処理」をするには、ARKit Pluginで利用しているカメラ映像を利用しないとなりません。
個別にカメラを起動して、その映像を使う、ということができないからです。
今回はARKit Pluginのカメラの映像を利用して画像処理をするためのTipsをまとめておこうと思います。
大まかな流れ
主な処理はUnityARVideo
のコードを参考にしました。
- ARKitのセッションから
ARTextureHandles
を取得する Texture2D.CreateExternalTexture
メソッドを利用して、ネイティブテクスチャのポインタからテクスチャを生成するUpdateExternalTexture
メソッドを利用してテクスチャの内容をアップデートする- UnityのARKit Pluginが提供してくれているマテリアルを利用して、ふたつのテクスチャを合成する(カメラの映像として見れる形に復元する(YCbCrフォーマットでふたつのテクスチャとして取得するため))
RenderTexture
の内容をTexture2D
にコピーする
という流れになります。
以下、細かく見ていきましょう。
ARTextureHandlesを取得する
UnityARSessionNativeInterface
のGetARVideoTextureHandles
というメソッドを利用してARTextureHandles
という、テクスチャのハンドルを取得することができます。
このハンドルを利用して、ARKit側で生成したネイティブのテクスチャへ(ポインタを経由して)アクセスすることができます。
ネイティブで生成されたテクスチャからテクスチャの内容を取得するには以下のようにします。
ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface().GetARVideoTextureHandles(); if (handles.IsNull()) { return; } Resolution currentResolution = Screen.currentResolution; // _textureYはTexture2D if (_textureY == null) { _textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY); _textureY.filterMode = FilterMode.Bilinear; _textureY.wrapMode = TextureWrapMode.Repeat; _yuvMat.SetTexture("_textureY", _textureY); }
handles.TextureY
はネイティブテクスチャへのポインタとなっていてSystem.IntPtr
型です。
そしてTexture2D
にはこうしたネイティブテクスチャからテクスチャを生成することができるようになっています。
それが以下の部分です。
_textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY);
CreateExternalTexture
でネイティブテクスチャを元にテクスチャを生成することができます。
そしてテクスチャの内容を実際に取得して更新するにはUpdateExternalTexture
メソッドを使います。
_textureY.UpdateExternalTexture(handles.TextureY);
とすることで、生成したTexture2D
の内容をアップデートすることができます。
ふたつのテクスチャを合成する
無事、ネイティブテクスチャからふたつのテクスチャを得ることができました。
ただ、前述のように、これらのテクスチャはカメラの映像がそのまま、というわけではありません。
YCbCrというフォーマットになっていて、適切に合成しないと元のカメラの映像になりません。
Wikipediaから引用させてもらうと以下の意味のようです。
YUVやYCbCrやYPbPrとは、輝度信号Yと、2つの色差信号を使って表現される色空間。
Wikipediaから画像を引用させてもらうと、以下のような感じのテクスチャが得られます。
一番上が元画像、その下が輝度画像、そしてその下がそれぞれ2つの色差信号によって表現されたものです。
これを合成して元の形に復元するために、ARKitPluginが提供してくれている「YUVMaterial」を利用します。
前段でネイティブテクスチャの情報は取得しているので、あとはこれを合成するマテリアルを通してRenderTexture
に描き出してやればOKです。
// ネイティブテクスチャからテクスチャを生成し、マテリアルにセットしているところ _yuvMat.SetTexture("_textureY", _textureY); private void OnPostRender() { // ... 中略 ... // RenderTextureへ、マテリアルの内容を書き込み Graphics.Blit(null, _arTexture, _yuvMat); // RenderTextureの内容をTexture2Dにコピーするため、AsyncGPUReadbackを利用して読み出し _request = AsyncGPUReadback.Request(_arTexture); }
Graphics.Blit
を利用してRenderTexture
にマテリアルの内容を書き出します。
最終的に利用する形がRenderTexture
なのであれば以上で終了です。
が、大体の場合はTexture2D
にするなり、テクセルの配列を利用して処理するなりの「情報として扱える形」に変換する必要が出てくるでしょう。
RenderTextureの内容をTexture2Dにコピーする
最後の工程は、テクスチャの合成を施した結果であるRenderTexture
の内容をTexture2D
にコピーすることです。
コピーは以下のようにします。
RenderTexture back = RenderTexture.active; RenderTexture.active = _arTexture; _arTexture2D.ReadPixels(new Rect(0, 0, _arTexture.width, _arTexture.height), 0, 0); _arTexture2D.Apply(); RenderTexture.active = back;
しかしこれ、実はだいぶ重い処理になります。
おそらくCPUを利用して全テクセルを読み出していると思うので、時間がかかります。
特に問題なのはメインスレッドで実行されるため、UIを停止、あるいはFPSの低下を招きます。
なので数フレームに1度、などの最適化を行わないとならないかもしれません。
幸いにして、今回やりたかったのはARKitのカメラの映像を利用してQRコードの読み取りをする、というものです。
なのでQRコードの読み取りが必要なタイミングでだけ有効にすることで今回は回避しています。
余談:AsyncGPUReadbackを使ってRenderTexutreの内容を読み出す
上で説明したように、ReadPIxels
はとても重い処理です。
そこでAsyncGPUReadback
というメソッドが追加されました。
これはGPUでRenderTexture
の読み取りを実行し、結果を非同期で返してくれるメソッドです。
使い方などについては以下の記事が詳しく解説してくれているので参考にしてみてください。
なお、ここで紹介されている_tex.LoadRawTextureData(buffer);
という形で読み込むと正常にデータが読み込めず、おかしな表示になってしまっていました。
keijiroさんのこちらのサンプルを見ると_tex.SetPixels32(buffer.ToArray());
という形で読み込んでいて、こちらを試したところ正常に表示されました。
さて、最後に実際に実装したコードを掲載しておきます。
コード全文
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; using UnityEngine.Rendering; using UnityEngine.XR.iOS; namespace ARKitTextureSample { public delegate void OnReadQRCode(string url); public class ARQRReader : MonoBehaviour { public event OnReadQRCode OnReadQRCode; [SerializeField] private RawImage _preview = null; [SerializeField] private Text _text = null; [SerializeField] private Material _yuvMat = null; private string _result = null; private Matrix4x4 _displayTransform; private Texture2D _textureY = null; private Texture2D _textureCbCr = null; private RenderTexture _arTexture = null; private Texture2D _arTexture2D = null; private bool _isActive = false; #if !UNITY_EDITOR && UNITY_IOS private void Start() { _arTexture = new RenderTexture(Screen.width, Screen.height, 0); _arTexture2D = new Texture2D(_arTexture.width, _arTexture.height, TextureFormat.ARGB32, false); _yuvMat = Instantiate(_yuvMat); } public void Active(bool active) { if (_isActive == active) { return; } if (active) { UnityARSessionNativeInterface.ARFrameUpdatedEvent += UpdateFrame; } else { UnityARSessionNativeInterface.ARFrameUpdatedEvent -= UpdateFrame; } _isActive = active; } private void UpdateFrame(UnityARCamera cam) { _displayTransform = new Matrix4x4(); _displayTransform.SetColumn(0, cam.displayTransform.column0); _displayTransform.SetColumn(1, cam.displayTransform.column1); _displayTransform.SetColumn(2, cam.displayTransform.column2); _displayTransform.SetColumn(3, cam.displayTransform.column3); } public void OnPreRender() { if (!_isActive) { return; } ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface().GetARVideoTextureHandles(); if (handles.IsNull()) { return; } Resolution currentResolution = Screen.currentResolution; if (_textureY == null) { _textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY); _textureY.filterMode = FilterMode.Bilinear; _textureY.wrapMode = TextureWrapMode.Repeat; _yuvMat.SetTexture("_textureY", _textureY); } if (_textureCbCr == null) { _textureCbCr = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY); _textureCbCr.filterMode = FilterMode.Bilinear; _textureCbCr.wrapMode = TextureWrapMode.Repeat; _yuvMat.SetTexture("_textureCbCr", _textureCbCr); } _textureY.UpdateExternalTexture(handles.TextureY); _textureCbCr.UpdateExternalTexture(handles.TextureCbCr); _yuvMat.SetMatrix("_DisplayTransform", _displayTransform); } private void OnPostRender() { if (!_isActive) { return; } if (_textureY == null || _textureCbCr == null) { return; } Graphics.Blit(null, _arTexture, _yuvMat); RenderTexture back = RenderTexture.active; RenderTexture.active = _arTexture; _arTexture2D.ReadPixels(new Rect(0, 0, _arTexture.width, _arTexture.height), 0, 0); _arTexture2D.Apply(); RenderTexture.active = back; _preview.texture = _arTexture2D; _result = QRCodeHelper.Read(_arTexture2D); if (_result != "error") { _text.text = _result; OnReadQRCode?.Invoke(_result); } } #else public void Active(bool active) { _isActive = active; } #endif } }
まとめ
今回はARKitのカメラからの映像を利用して色々してみるという趣旨でしたが、ネイティブテクスチャからのテクスチャ生成および更新など、知らない機能についても知れたのでよかったです。
またそれ以外にも、(それなりに)高速にRenderTexture
からTexture2D
へ内容をコピーするAsyncGPUReadback
活用の幅が広そうです。
今後、ディープラーニングなど「画像解析」を経てなにかを行うことは増えていきそうなので、このあたりの処理は覚えておくとよさそうです。