概要
今回はUnityのARFoundationが扱うシステムからカメラ映像を抜き出す処理についてまとめたいと思います。
これを利用する目的は、カメラの映像をDeep Learningなどに応用してなにかしらの出力を得たいためです。
今回の画像データの取得に関してはドキュメントに書かれているものを参考にしました。
今回のサンプルを録画したのが以下の動画です。
AR Sub Systemで扱ってるカメラ映像データをRenderTextureに書き出せた。生データだから回転はシェーダ書かないとならんかった。 #Unity #madewithunity #AR pic.twitter.com/PQfsZcYJDk
— edom18@XR / MESON CTO (@edo_m18) 2020年8月22日
今回のサンプルはGitHubにアップしてあるので、詳細が気になる方はそちらをご覧ください。
全体の流れ
画像を取得する全体のフローを以下に示します。
XRCameraImage
を取得するXRCameraImage#Convert
を利用してデータを取り出す- 取り出したデータを
Texture2D
に読み込ませる Texture2D
の画像を適切に回転しRenderTexture
に書き出す
という流れになります。
ということでひとつずつ見ていきましょう。
XRCameraImageを取得し変換する
ここではXRCameraImage
からデータを取得し、Texture2D
に書き込むまでを解説します。
まずはARのシステムからカメラの生データを取り出します。
ある意味でこの工程が今回の記事のほぼすべてです。
取り出したらDeep Learningなどで扱えるフォーマットに変換します。
今回実装したサンプルのコードは以下の記事を参考にさせていただきました。
ドキュメントの方法でも同様の結果を得ることができますが、テクスチャの生成を制限するなど最適化が入っているのでこちらを採用しました。
以下に取り出し・変換する際のコード断片を示します。
private void RefreshCameraFeedTexture() { // TryGetLatestImageで最新のイメージを取得します。 // ただし、失敗の可能性があるため、falseが返された場合は無視します。 if (!_cameraManager.TryGetLatestImage(out XRCameraImage cameraImage)) return; // 中略 // デバイスの回転に応じてカメラの情報を変換するための情報を定義します。 CameraImageTransformation imageTransformation = (Input.deviceOrientation == DeviceOrientation.LandscapeRight) ? CameraImageTransformation.MirrorY : CameraImageTransformation.MirrorX; // カメライメージを取得するためのパラメータを設定します。 XRCameraImageConversionParams conversionParams = new XRCameraImageConversionParams(cameraImage, TextureFormat.RGBA32, imageTransformation); // 生成済みのTexture2D(_texture)のネイティブのデータ配列の参照を得ます。 NativeArray<byte> rawTextureData = _texture.GetRawTextureData<byte>(); try { unsafe { // 前段で得たNativeArrayのポインタを渡し、直接データを流し込みます。 cameraImage.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length); } } finally { cameraImage.Dispose(); } // 取得したデータを適用します。 _texture.Apply(); // 後略 }
Texture2Dの画像を適切に回転しRenderTextureに書き出す
前段でXRCameraImage
からデータを取り出しTexture2D
へ書き出すことができました。
ただ今回は最終的にTensorFlow Liteで扱うことを想定しているのでRenderTexture
に情報を格納するのがゴールです。
ぱっと思いつくのはGraphics.Blit
を利用してRenderTexture
にコピーすることでしょう。
しかし、取り出した画像は生のデータ配列のため回転を考慮していません。(つまりカメラからの映像そのままということです)
以下の質問にUnityの中の人からの返信があります。
TryGetLatestImage exposes the raw, unrotated camera data. It will always be in the same orientation (landscape right, I believe). The purpose of this API is to allow for additional CPU-based image processing, such as with OpenCV or other computer vision library. These libraries usually have a means to rotate images, or accept images in various orientations, so we there is no built-in functionality to rotate the image.
要は、だいたいの場合において利用する対象(OpenCVなど)に回転の仕組みやあるいは回転を考慮しないでそのまま扱える機構があるからいらないよね、ってことだと思います。
そのため、人が見て適切に見えるようにするためには画像を回転してコピーする必要があります。
ですが心配いりません。処理自体はとてもシンプルです。
基本的には時計回りに90度回転させるだけでOKです。
なにも処理しない画像をQuadに貼り付けると以下のような感じで90度回転したものが出力されます。
(ちょっと分かりづらいですが、赤枠で囲ったところはAR空間に置かれたQuad
で、そこにカメラの映像を貼り付けています)
これを90度回転させるためにはUVの値を少し変更するだけで達成することができます。
まずはシェーダコードを見てみましょう。
シェーダで画像を回転させる
見てもらうと分かりますが、基本はシンプルなImage EffectシェーダでUVの値をちょっと工夫しているだけです。
Shader "Hidden/RotateCameraImage" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } sampler2D _MainTex; fixed4 frag (v2f i) : SV_Target { float x = 1.0 - i.uv.y; float y = i.uv.x; float2 uv = float2(x, y); fixed4 col = tex2D(_MainTex, uv); return col; } ENDCG } } }
x
, y
を反転して、さらにy
の値を1.0
から引いているだけです。簡単ですね。
そしてこのシェーダを適用したマテリアルを用いてGraphics.Blit
を実行してやればOKです。
Graphics.Blit(texture, _previewTexture, _transposeMaterial);
分かりやすいように、グリッドの画像で適用したものを載せます。
90度右に回転しているのが分かるかと思います。
これで無事、画像が回転しました。
デバイスの回転を考慮する
実は上のコードだけでは少し問題があります。
デバイスの回転によって取得される画像データの見栄えが変わってしまうのです。
というのは、Portraitモードでは回転しているように見える画像でも、Landscapeモードだとカメラからの映像と見た目が一致して問題なくなるのです。
以下の動画を見てもらうと分かりますが、Portraitモードでは90度回転しているように見える画像が、Landscapeモードでは適切に見えます。
結論としてはPortraitモードのときだけ処理すればいいことになります。
private void PreviewTexture(Texture2D texture) { if (_needsRotate) { Graphics.Blit(texture, _previewTexture, _transposeMaterial); } else { Graphics.Blit(texture, _previewTexture); } _renderer.material.mainTexture = _previewTexture; }
デバイスが回転した際のイベントが実はUnityには用意されていないようで、以下の記事を参考に回転の検知を実装しました。
(まぁゲームにおいて回転を検知してなにかをする、っていうケースが稀だからでしょうかね・・・)
カメライメージを取得するタイミング
最後にカメライメージの取得タイミングについて書いておきます。
ドキュメントにも書かれていますが、ARCameraManagerのframeReceivedというイベントのタイミングでカメライメージを取得するのが適切なようです。
ARCameraManager#frameReceivedイベント
ARCameraManagerにはframeReceived
というイベントがドキュメントでは以下のように説明されています。
An event which fires each time a new camera frame is received.
カメラフレームを受信したタイミングで発火するようですね。
なのでこのタイミングで最新のカメラデータを取得することで対象の映像を取得することができるというわけです。
ということで、以下のようにコールバックを設定してその中で今回の画像取得の処理を行います。
[SerializeField] private ARCameraManager _cameraManager = null; // --------------------------- _cameraManager.frameReceived += OnCameraFrameReceived; // --------------------------- private void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs) { RefreshCameraFeedTexture(); }
最後に
無事、カメライメージを取得して扱える状態に変換することができました。
Texture2D
としてもRenderTexture
としても扱えるので用途に応じて使うといいでしょう。
気になる点としてはパフォーマンスでしょうか。
一度CPUを経由しているのでそのあたりが気になるところです。(まだ計測はしていませんが・・・)
が、シンプルな今回のデモシーンでは特に重さは感じなかったので、コンテンツが重すぎない限りは問題ないかなとも思います。