e.blog

主にUnity/UE周りのことについてまとめていきます

UnityのARKit Pluginのカメラ映像を利用してなにかする

概要

UnityのARKit Pluginを使えばARコンテンツを手軽に作ることができます。
しかし、当然ですがARは外界をカメラで撮影し、それを元に姿勢を判断しています。

つまり、QRコードリーダーやOpenCVなどを利用した画像認識など「カメラを利用した処理」をするには、ARKit Pluginで利用しているカメラ映像を利用しないとなりません。

個別にカメラを起動して、その映像を使う、ということができないからです。

今回はARKit Pluginのカメラの映像を利用して画像処理をするためのTipsをまとめておこうと思います。

大まかな流れ

主な処理はUnityARVideoのコードを参考にしました。

  1. ARKitのセッションからARTextureHandlesを取得する
  2. Texture2D.CreateExternalTextureメソッドを利用して、ネイティブテクスチャのポインタからテクスチャを生成する
  3. UpdateExternalTextureメソッドを利用してテクスチャの内容をアップデートする
  4. UnityのARKit Pluginが提供してくれているマテリアルを利用して、ふたつのテクスチャを合成する(カメラの映像として見れる形に復元する(YCbCrフォーマットでふたつのテクスチャとして取得するため))
  5. RenderTextureの内容をTexture2Dにコピーする

という流れになります。

以下、細かく見ていきましょう。

ARTextureHandlesを取得する

UnityARSessionNativeInterfaceGetARVideoTextureHandlesというメソッドを利用して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から画像を引用させてもらうと、以下のような感じのテクスチャが得られます。

YCbCr画像イメージ

一番上が元画像、その下が輝度画像、そしてその下がそれぞれ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というメソッドが追加されました。

これはGPURenderTextureの読み取りを実行し、結果を非同期で返してくれるメソッドです。

使い方などについては以下の記事が詳しく解説してくれているので参考にしてみてください。

qiita.com

なお、ここで紹介されている_tex.LoadRawTextureData(buffer);という形で読み込むと正常にデータが読み込めず、おかしな表示になってしまっていました。

keijiroさんのこちらのサンプルを見ると_tex.SetPixels32(buffer.ToArray());という形で読み込んでいて、こちらを試したところ正常に表示されました。

github.com

さて、最後に実際に実装したコードを掲載しておきます。

コード全文

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活用の幅が広そうです。

今後、ディープラーニングなど「画像解析」を経てなにかを行うことは増えていきそうなので、このあたりの処理は覚えておくとよさそうです。