Posted at

Unity ARKit Plugin のカメラ映像を使って何かするには

More than 1 year has passed since last update.

Unity ARKit Plugin を使いながらカメラの映像を使って何かする場合の手順を書きます。この記事ではQRコードの検出を行いますが、他にもOpenCVを使ったり映像の一部の拡大表示をしたりなども同様の手順でできます。

なお、以前にARKitで空間共有という記事を書いているのですが、そこでもQRコードの検出を行なっていて、その部分を詳しく書いたものとなります。また、その記事を書いた当時はカメラ映像のテクスチャから読み取ったQRコードの位置を雑に座標変換していて、画面のアスペクト比が16:9ではないiPhoneXやiPadなどで動かすと適切に動作しない状態でした(現在は修正してあり正常に動作します)。


使用する Unity ARKit Plugin のバージョン

Unity ARKit Plugin は 1.0.12 以降を使用してください。私が出したプルリクエストで、この記事に関連する部分のバグが修正されています。1.0.12より古いバージョンでも当該部分を修正して使えば大丈夫です。


サンプルプロジェクト

ソースコードは次の場所にあります。

https://github.com/rakusan/UnityARKit-displayTransform-example

このサンプルプロジェクトでは、ARKitを使いながらQRコードを検出してQRコードの場所を緑色に塗ります。

IMG_2992.PNG


カメラ映像のテクスチャを取得する


QRCodeReader.cs

void Update () {

ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface ().GetARVideoTextureHandles ();
if (handles.textureY != System.IntPtr.Zero) {
ReadQRCode (handles.textureY.ToInt64 ());
}
}

ARTextureHandlesを使用するときはGetARVideoTextureHandles()を毎回かならず呼び出して取得してください。GetARVideoTextureHandles()の実装を追っていくとわかりますが、フレームが更新されるたびに異なるテクスチャが返されるので、ARTextureHandlesを一度だけ取得しておいて使いまわすとハマります。

ARTextureHandlesのメンバにはtextureYtextureCbCrがあります。このサンプルプロジェクトではQRコードの検出をするので、輝度のtextureYだけ使います。


カメラ映像のテクスチャを使う(QRコードの検出)

上のコードでReadQRCodeはネイティブコードで、実装は次のようになっています。


QRCodeReader.m

static float qrcodeCorners[8];

static volatile BOOL reading = false;

void ReadQRCode(long long mtlTexPtr)
{
if (reading) return;
reading = YES;

MTLTextureRef mtlTex = (__bridge MTLTextureRef) (void*) mtlTexPtr;
CIImage *ciImage = [CIImage imageWithMTLTexture:mtlTex options:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat iw = ciImage.extent.size.width;
CGFloat ih = ciImage.extent.size.height;

CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:nil];
NSArray<CIFeature *> *features = [detector featuresInImage:ciImage];

if (features.count > 0) {
CIQRCodeFeature *feature = (CIQRCodeFeature*) [features objectAtIndex:0];

qrcodeCorners[0] = feature.topLeft.x / iw;
qrcodeCorners[1] = feature.topLeft.y / ih;
qrcodeCorners[2] = feature.topRight.x / iw;
qrcodeCorners[3] = feature.topRight.y / ih;
qrcodeCorners[4] = feature.bottomLeft.x / iw;
qrcodeCorners[5] = feature.bottomLeft.y / ih;
qrcodeCorners[6] = feature.bottomRight.x / iw;
qrcodeCorners[7] = feature.bottomRight.y / ih;

UnitySendMessage("QRCodeReader", "OnReadQRCode", "");
}

reading = NO;
});
}

void GetQRCodeCorners(int32_t **cornersPtr)
{
float *floatArray = malloc(sizeof(float) * 8);
memcpy(floatArray, qrcodeCorners, sizeof(qrcodeCorners));
*cornersPtr = (int32_t*) floatArray;
}


先ほどのARTextureHandlesから取得したテクスチャはMetalのテクスチャなので、MTLTextureRefにキャストして使います。CIDetectorを使ってQRコードを検出した結果はピクセル値で返ってくるので、テクスチャの幅、高さで割ってテクスチャ座標に変換します。これは、後述するdisplayTransformを使うにはテクスチャ座標が必要だからです。

なお、Vision.frameworkのVNDetectBarcodesRequestでQRコードを検出した場合はテクスチャ座標で結果が得られるので、その値をそのまま使えます。


座標変換する


QRCodeReader.cs

void Start () {

UnityARSessionNativeInterface.ARFrameUpdatedEvent += ARFrameUpdated;
}

private void ARFrameUpdated(UnityARCamera camera) {
Matrix4x4 tmp = new Matrix4x4 (
camera.displayTransform.column0,
camera.displayTransform.column1,
camera.displayTransform.column2,
camera.displayTransform.column3
);
displayTransformInverse = tmp.inverse;
}

private Vector3 VideoTextureToViewportPoint(Vector2 videoTexturePoint) {
Vector4 column0 = displayTransformInverse.GetColumn(0);
Vector4 column1 = displayTransformInverse.GetColumn(1);
float x = column0.x * videoTexturePoint.x + column0.y * videoTexturePoint.y + column0.z;
float y = column1.x * videoTexturePoint.x + column1.y * videoTexturePoint.y + column1.z;
return new Vector3 (x, y);
}

void OnReadQRCode(string arg) {
float[] videoTexCorners = GetQRCodeCorners ();

corners[0] = VideoTextureToViewportPoint(new Vector2 (videoTexCorners [0], videoTexCorners [1]));
corners[1] = VideoTextureToViewportPoint(new Vector2 (videoTexCorners [2], videoTexCorners [3]));
corners[2] = VideoTextureToViewportPoint(new Vector2 (videoTexCorners [4], videoTexCorners [5]));
corners[3] = VideoTextureToViewportPoint(new Vector2 (videoTexCorners [6], videoTexCorners [7]));
}


ARFrameUpdatedが随時呼ばれるので、displayTransformを保存しておきます。displayTransformビューポート座標からカメラ映像のテクスチャ座標への変換行列 です。そのためここでは逆行列にして保存しています。

VideoTextureToViewportPointが、カメラ映像のテクスチャ座標からビューポート座標へ変換する関数です。ここでdisplayTransformの逆行列を使います。

ネイティブコード側のQRコード検出が終わるとUnitySendMessageを介してOnReadQRCodeが呼ばれるので、座標変換をしてcornersにビューポート座標で保存しています。


おわりに

サンプルプロジェクトではOnRenderObjectでビューポートにそのまま描画していますが、得られた座標を元にUnityARSessionNativeInterface.HitTestCamera.ViewportPointToRay をしたりすれば、3次元上で扱うこともできます。

また、冒頭でも書いたようにOpenCVを使ったり映像の一部の拡大表示をしたりなどもできます。これはmeasuARというアプリで実際にやっていて、拡大表示はアプリを試して頂くと動作の様子を見ることができます。OpenCVについてはどのように使っているかわかりにくいですが、手ぶれ補正と測定点の自動補正に使っています。