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コードの場所を緑色に塗ります。
#カメラ映像のテクスチャを取得する
void Update () {
ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface ().GetARVideoTextureHandles ();
if (handles.textureY != System.IntPtr.Zero) {
ReadQRCode (handles.textureY.ToInt64 ());
}
}
ARTextureHandles
を使用するときはGetARVideoTextureHandles()
を毎回かならず呼び出して取得してください。GetARVideoTextureHandles()
の実装を追っていくとわかりますが、フレームが更新されるたびに異なるテクスチャが返されるので、ARTextureHandles
を一度だけ取得しておいて使いまわすとハマります。
ARTextureHandles
のメンバにはtextureY
とtextureCbCr
があります。このサンプルプロジェクトではQRコードの検出をするので、輝度のtextureY
だけ使います。
#カメラ映像のテクスチャを使う(QRコードの検出)
上のコードでReadQRCode
はネイティブコードで、実装は次のようになっています。
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コードを検出した場合はテクスチャ座標で結果が得られるので、その値をそのまま使えます。
#座標変換する
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.HitTest
やCamera.ViewportPointToRay
をしたりすれば、3次元上で扱うこともできます。
また、冒頭でも書いたようにOpenCVを使ったり映像の一部の拡大表示をしたりなどもできます。これはmeasuARというアプリで実際にやっていて、拡大表示はアプリを試して頂くと動作の様子を見ることができます。OpenCVについてはどのように使っているかわかりにくいですが、手ぶれ補正と測定点の自動補正に使っています。