リプレイデータを使ったARクラウドのローカライズ

Hiroyuki Makino
nttlabs
Published in
12 min readFeb 16, 2021

〜記録したARデータで位置を復元し、テスト/デバッグに活用する〜

前回の記事では、AR開発におけるテスト/デバッグの効率化のために、ARの入力データを記録・エディタ上で再生し、テストを行うアプローチについてご紹介しました。先日、この技術を、x gardenさん(XRメディア新規事業支援クリエイターコミュニティを手掛けるXR業界のクリエイティブ共創カンパニー)が試していただき、AR開発におけるテストの効率化を評価いただきました。結果、1項目あたり270秒程度かかっていたARの確認が20秒程度と9割以上の工数の削減につながったとのフィードバックをいただきました。取り扱うARの種類が多かったため種類の数だけテスト項目数が増えるといった条件下で、端末にビルドすることなくテストを実行できた効果は大きかったとのことでした。

さて、今回はリプレイデータをさらに活用し、特定の位置に永続的にARコンテンツを出すような、より大規模なARのテストが可能か検証してみました。
特定の位置に永続的にARコンテンツを出すには、ARクラウドの技術を用います。

ARクラウドとは?

AR界で有名なOri Inber氏の説明を引用すると、ARクラウドは以下のように定義されています。
1) A persistent point cloud aligned with real world coordinates — a shared soft-copy of the world
実空間の座標に合わせた永続的な点群 — 共有された空間のソフトコピー
2) The ability to instantly localize (align the world’s soft-copy with the world itself) from anywhere and on multi devices
どこででも、マルチデバイスで即座にローカライズ(空間のソフトコピーと現実空間の位置合わせ)できる仕組み
3) The ability to place virtual content in the world’s soft-copy and interact with it in realtime, on-device and remotely
仮想コンテンツを空間のソフトコピーに配置し、リアルタイムにデバイス上で遠隔で相互に操作できる仕組み

つまりARクラウドを使うと、その場所の情報を収集、保存し、またそれをマルチデバイスで共有しながら、仮想コンテンツを紐付けて管理するといったことが可能になります。

ARクラウドを用いたARでは以下の流れで開発/利用します。

・マップの作成・・・空間の情報を収集します
・コンテンツの配置・・・マップをもとにコンテンツを配置します
・ローカライズ・・・空間の情報を入力し位置合わせを行います

ARクラウドを利用するアプリでは、場所にコンテンツが紐づくため、その場所に足を運んでARの挙動を確認するといったシーンが想定されます。遠隔地の場合、コードを変更するたびにテストのために現地に向かうのは容易ではないため、予め撮り溜めたデータでテストができると効率的と考えられます。

今回のテストでは、ローカライズしてコンテンツを確認する際に、リプレイデータを入力し、ローカライズが可能か見ていきます。

ARクラウドにはImmersal SDKを利用してみました。

Immersal SDKは、カメラ画像をベースとしたコンピュータビジョン技術でARクラウドを実現しており、またAR Foundationを利用して実装されているため、記録したリプレイデータとの親和性が高いと考えたためです。

デバイスでのローカライズと、エディタ上でリプレイデータを使ったローカライズのイメージ

実装

以下の流れで実装していきます。

記録したカメラ画像、記録したARデータ(CameraIntrinsics)を再生 → ImmersalのREST APIにリクエスト → Pose(カメラの位置や姿勢)情報を同期

前回の記事で紹介した、ARデータ再生時に画像を送る部分、Reproducer.csのOnCameraFrameReceived()処理で、ImmersalのLocalizer APIに送信するように実装します。ローカライザでは、端末のカメラから画像を取得する代わりに読み込んだ画像を利用してローカライズできるように処理を実装します。(ローカライズ成功後の座標変換処理はImmersal SDK内ARLocalizerのServerLocalizerと同じ処理です。)

void OnCameraFrameReceived(ARCameraFrameEventArgs args)
{
var cameraImagePath = dirPath + args.timestampNs.Value + ".jpg";
if (File.Exists(cameraImagePath))
{
FileStream stream = File.OpenRead(cameraImagePath);
var data = new byte[stream.Length];
stream.Read(data, 0, (int)stream.Length);
tex.LoadImage(data);
float curTime = Time.unscaledTime;
if (!arcloud.isLocalizing && (curTime - m_LastLocalizeTime) >= m_LocalizationInterval)
{
m_LastLocalizeTime = curTime;
arcloud.LocalizeServer(data);
}
stream.Close();
}
}
using UnityEngine;
using Immersal;
using Immersal.AR;
using Immersal.REST;
using static Immersal.AR.ARLocalizer;
public class ARCloud : LocalizerBase
{
public event MapChanged OnMapChanged = null;
public event PoseFound OnPoseFound = null;
GameObject arcontents;
MeshRenderer maprenderer;
int mapId;
public override void Start()
{
m_Sdk = ImmersalSDK.Instance;
mapId = GameObject.Find("AR Map").GetComponent<ARMap>().serverMapId;
arcontents = GameObject.Find("AR Contents");
arcontents.SetActive(false);
var armapmesh = GameObject.Find("AR MapMesh");
if (armapmesh != null)
{
armapmesh.SetActive(false);
}
maprenderer = GameObject.Find("AR Map").GetComponent<ARMap>().m_MeshRenderer;
maprenderer.enabled = false;
}
public async void LocalizeServer(byte[] pixels)
{
if (pixels != null)
{
JobLocalizeServerAsync j = new JobLocalizeServerAsync();
j.mapIds = new SDKMapId[1];
j.mapIds[0] = new SDKMapId();
j.mapIds[0].id = mapId;
Camera cam = Camera.main;
Vector3 camPos = cam.transform.position;
Quaternion camRot = cam.transform.rotation;
j.rotation = camRot;
j.position = camPos;
ARHelper.GetIntrinsics(out j.intrinsics);
//JobLocalizeServerAsyncリクエストjのパラメータj.imageにリプレイしたカメラ画像データを入力します
j.image = pixels;
[リクエスト,レスポンス処理はImmersal SDKのServerLocalizerと同じなので省略] // show up contents
arcontents.SetActive(true);
maprenderer.enabled = true;
}
}
}

ローカライザAPIのパラメータに必要なCameraIntrinsics(カメラの内部パラメータであるfocalLength(焦点距離)やprincipalPoint(主点))はデバイスごとに異なるため、ARデータの記録時に一緒に記録できるようにしています。

// Get camera intrinsics
cameraManager.TryGetIntrinsics(out cameraIntrinsics);
var packet = new ARKitRemotePacket()
{
cameraFrame = new ARKitRemotePacket.CameraFrameEvent()
{
timestampNs = args.timestampNs.Value,
projectionMatrix = args.projectionMatrix.Value,
displayMatrix = args.displayMatrix.Value
},
cameraIntrinsics = new ARKitRemotePacket.CameraIntrinsics()
{
focalLength = cameraIntrinsics.focalLength,
principalPoint = cameraIntrinsics.principalPoint,
resolution = cameraIntrinsics.resolution
}

};

CameraIntrinsics取得時には、TryGetIntrinsicsで再生されたARデータフレームからカメラの焦点距離、主点を返すようにします。

public override bool TryGetIntrinsics(out XRCameraIntrinsics cameraIntrinsics)
{
var remote = ARKitReceiver.Instance;
if (remote == null)
{
cameraIntrinsics = default(XRCameraIntrinsics);
return false;
}
var remoteFrame = ARKitReceiver.Instance.CameraIntrinsics;
if (remoteFrame == null)
{
cameraIntrinsics = default(XRCameraIntrinsics);
return false;
}
cameraIntrinsics = new CameraIntrinsics()
{
focalLength = remoteFrame.focalLength,
principalPoint = remoteFrame.principalPoint,
resolution = remoteFrame.resolution
};

return true;
}

結果

Unityエディタ上でPlayモードで実行すると、記録したARデータがリプレイされ、数秒でローカライズが行われ、マップ上に配置したARコンテンツが復元されました。

実際にデバイスにビルドして実行した場合と比較して同様の結果が得られることがわかります。

エディタ上でのローカライズ結果(左)と、ビルド後 端末上でのローカライズ結果(右)

これでコードを変更するたびにビルドして現地に行かなくても、リプレイデータを使って、見た目の確認やUnity Test Frameworkを使って正しい位置に配置されているか等の単体テストが実行できるようになりました。ARクラウドを使う大規模なアプリケーションでもCI/CDツール使ってテストを自動化することも可能となります。

まとめ

記録したARデータを再生して、ARクラウドのローカライズが可能であることが確認できました。これにより、エディタ上で現実空間の位置に紐づくコンテンツの挙動を確認できるようになり、現実空間とインタラクションするアプリの開発が身近になると考えられます。

今後の取り組みとして、ARクラウドに紐づくコンテンツのオーサリングや開発をより容易にするために、深層学習推論を組み合わせ、ロバストなテストのためのデータを生成したり、ARの見た目をテストするといったテストシナリオの高度化に向けた検討が必要であると考えられます。 NTTでは、その手の仕組みを創る仕事に興味のある仲間を募集中です。ご連絡お待ちしています。

--

--

Hiroyuki Makino
nttlabs
Writer for

XR Metaverse Researcher, R&D Engineer at NTT, Japan. Excited for the future of AR and what amazing people create.