凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

RealSense トラッキングカメラ T265 を Unity で遊んでみた

はじめに

1 月にプリオーダーしていた、V-SLAMバイスで行ってくれる RealseSense の新しい製品である T265 が先日届きました。

realsense.intel.com

T265 は次のようなデバイスです。

  • 低消費電力
    • V-SLAM 動作で 1.5 W
  • 正確なトラッキング
    • 様々な環境下での閉ループテストでドリフトによる誤差は 1% 以下*1
  • 小型で軽量
    • 108 x 25 x 13 mm / 55 g
  • 安価
    • 驚きの $199

実際の購入費用としては $199+送料($23.75)に別途税関代 2,080 円が着払いでかかりました。本エントリではデバイスの紹介と Unity での使い方を紹介していきたいと思います。

特徴

f:id:hecomi:20190309103356j:plain

Neural Compute Stick 2 に搭載されている Intel® Movidius™ Myriad™ 2.0 VPUVision Processing Unit)を搭載しています。

www.movidius.com

Movidius は Google Tango や DJI のドローンへチップの提供(参照)を行っていた会社ですが、Intel が 2016 年に買収しました。このチップを使い、 163±5° の魚眼レンズカメラ 2 機(OmniVision 製 OV9282)から取得した画像と、IMU (Bosch Sensortec 製 BMI055)から得られたジャイロ・加速度の情報を統合して SLAM を低消費電力に行っています。SLAM を行う際は特徴点検出ベースで行うため、デプスの情報は本カメラから得ることは出来ません(魚眼 2 機の情報を使って計算することは可能だが D400 シリーズと比べると品質は劣るとのことです)。デプスが欲しい際は、D400 シリーズとのマウンタなどを作成し、デプスは D シリーズの方から別途取得するのが良いと思われます。T265 や D400 シリーズは 5 cm 間隔でネジ穴が 2 つ背面に空いており、実際に 3D プリントしたマウンタを作成された方々もいらっしゃいます。

では次の章では実際に動かして見るところをやってみましょう。

まずは使ってみる

以下のページにアクセスしてセットアップ方法を参照します。

realsense.intel.com

SDKGitHub で配布されています。

github.com

執筆時点では最新の「Intel® RealSense™ SDK 2.0 (build 2.19.1)」を利用しました。セットアップガイドには「Intel.RealSense.Viewer.exe」をダウンロードと書いてありますが、「Intel.RealSense.SDK.exe」をインストールすればビューワも含めて SDK がインストールされます(C++ での開発をしない方はビューワだけでも良いと思います)。

Intel RealSense Viewer を起動してサイドバーの「Tracking Module」を On にすると、次のような各ストリームを見ることが出来ます。

f:id:hecomi:20190310184210p:plain

動作させた様子はこんな感じです。

f:id:hecomi:20190310190926g:plain

また、3D ビューもあるので見てみるとこんな感じです。

f:id:hecomi:20190310225422g:plain

中身を見てみると次のようなストリームが表示されています。

  • ステレオカメラのそれぞれの映像ストリーム
  • ジャイロセンサの情報
  • 加速度センサの情報
  • ポーズ情報

最も重要なのは、このポーズのストリームで、これが SLAM によって得られた位置姿勢情報になります。では次の章ではこのデータを Unity から取得する方法を見ていきましょう。

Unity で試してみる

Unity 上で簡単に使えるように、.unitypackage も配布されています。GitHub のリリースページから「Intel.RealSense.unitypackage」をダウンロードし、自プロジェクトに展開することで使えるようになります。使い方は以下のページにまとまっています。

github.com

なお、ビューワや SDK もですが、本 Unity 向けの Wrapper は T265 だけのものではなく、他の RealSense シリーズ(D400 系)も含めたサンプルになっています。

シーンを開いてみる

StartHere」シーンを起動してみると次のように利用可能なサンプルシーンが表示されます。T265 は Pose のサンプルシーンが利用できるようですが、デプスカメラの D400 シリーズを接続すれば他のサンプルが見えるものと思われます(筆者は D400 シリーズを有していないので残念ながら確認できず...)。「START」を押してみます。

f:id:hecomi:20190311222746p:plain

すると「SampleSceneUI」シーンと「SLAM」シーンが起動します。UI シーンの方はロゴなどを表示するだけで、重要なのは「SLAM」シーンの方です。こちらは実際の位置・姿勢に応じて T265 のモデルを表示しつつ軌跡をトレイルで表示するものです。

f:id:hecomi:20190311223252p:plain

右上の「First Person」を選択すると一人称視点にすることも出来ます。この SLAM シーンは T265 用にここ最近追加されたようです。

執筆時点では以下のドキュメントにも説明が載っていないようでした。

しかしながら Unity Wrapper for RealSense SDK 2.0 の設計思想として、ノンコーディングで色々な機能を提供できるようにするために様々なコンポーネントが用意されているので使い方は難しくありません。次の章で実際に自分でシーンを作成してみましょう。

自分でシーンを作成してみる

まず新しいシーンを作成したところから始めてみます。

RsDevice

起点となるのは RsDevice コンポーネントになります。このコンポーネントは RealSense のデバイスからのストリーミングを管理するものになります。Prefab が用意されているので、「Assets/RealSenseSDK2.0/Prefabs/RsDevice」をシーンに配置してみます。

f:id:hecomi:20190317131654p:plain

デフォルトのまま実行すると次のようなエラーが出てきてしまいます。

ExternalException: rs2_pipeline_start_with_config(pipe:00000000402F1FD0, config:00000000402F1DD0)
Rethrow as Exception: No device connected
Intel.RealSense.ErrorMarshaler.MarshalNativeToManaged (System.IntPtr pNativeData) (at <698caec4c7a4469b842739059e38a9b8>:0)
Intel.RealSense.Pipeline.Start (Intel.RealSense.Config cfg) (at <698caec4c7a4469b842739059e38a9b8>:0)
RsDevice.OnEnable () (at Assets/RealSenseSDK2.0/Scripts/RsDevice.cs:74)

これはコンフィグの Profiles に一致するデバイスが接続されていないためです。そこで、SLAM シーンと同じように Pose ストリームを取ってくるように次のように変更します。

f:id:hecomi:20190317140717p:plain

これで実行してもエラーが出なくなり Pose を取得する準備ができました。

RsPoseStreamTransfer

では実際にこの Pose を取ってきて Transform に反映してみましょう。キューブオブジェクトを作成し、RsPoseStreamTransfer コンポーネントをアタッチします。このとき、キューブオブジェクトを実際の T265 の大きさに合わせておくと良いです。

f:id:hecomi:20190317141800p:plain

これで実行すると次のようにキューブが動きます。

f:id:hecomi:20190317142621g:plain

Record / Playback

再び RsDevice に戻ります。インスペクタの上部で先程は Live を選択していましたが、Playback および Record というタブがあります。これらは、ROS の rosbag 形式のファイルを再生および作成する機能です。

github.com

wiki.ros.org

まずは、Record タブに移動して適当な Record Path を設定します。

f:id:hecomi:20190317143713p:plain

次にシーンを実行して T265 を適当に動かしてシーンを停止します。すると .bag ファイルが出来ているので、今度は Playback タブに移動し、生成した .bag ファイルを Playback File に指定します。

f:id:hecomi:20190317143933p:plain

これでシーンを実行すると記録した動きのとおりにオブジェクトが動くようになります。

f:id:hecomi:20190317144121p:plain

また、作成したファイルは Intel RealSense Viewer でも再生することが出来ます。

f:id:hecomi:20190317144452p:plain

RsDeviceInspector

RsDeviceInspector コンポーネントをアタッチするとシリアルナンバーなどのデバイスの情報を確認することが出来ます。

f:id:hecomi:20190317152938p:plain

このシリアルナンバーを RsDeviceRequested Serial Number に入れると複数デバイス接続時のデバイスの選択が出来るのかと思いましたがエラーになってしまいました。。

その他のストリーム

Viewer で見たような魚眼画像やジャイロ、加速度のストリームも取得することが出来ます。次のように RsDevice コンポーネントのインスペクタを設定してください。

f:id:hecomi:20190317164408p:plain

ストリームやフォーマットが異なるとエラーとなって起動できないので注意してください。また、解像度やフレームレートは設定しなくても問題ないようで、Viewer と同じ値が使われます(魚眼レンズカメラの画像は 848x800@30fps、ジャイロは 200fps、加速度は62fps、ちなみに Pose は 262fps です)。

では試しに画像を見てみましょう。RsStreamTextureRenderer コンポーネントを 2 つアタッチし、次のように設定してください。

f:id:hecomi:20190317164720p:plain

Texture Binding には新たに作成したマテリアルを設定します。Y8 の画像は Alpha8 となって描画されるので、Unlit や Standard シェーダでは単に真っ黒になってしまって見ることが出来ません。サンプルに付属の Custom/BW シェーダを使用してください。

f:id:hecomi:20190317164912p:plain

このマテリアルを適当なオブジェクトにアタッチすれば画像が見えるようになります。

f:id:hecomi:20190317165357g:plain

Relocalization

バイスを激しく振ったりすると徐々にドリフトにより位置がずれていきます。この時、時折 Relocalization が走るのですが次のように一気にパッと飛ぶ挙動をします。

実践編: Oculus Go と試してみる

f:id:hecomi:20190319223856j:plain

解説だけで終わってしまうと面白くないので、実際に使ってみました。題材としては、Oculus Go 前面に T265 貼り付けて位置トラッキングを追加しようというものです。現在は、Android 単体での T265 サポートはされていない(将来的にはされている!)ようなので、今回はノート PC を用意して取得した位置・回転情報を UDP で Oculus Go に送信する形を取ります。

ただ Oculus Go の回転も T265 で制御してしまうともともとの Oculus Go のトラッキング性能より悪くなってしまうので、姿勢に関しては Oculus Go 側にまかせることにします。このとき、Oculus Go 内で動いているアプリの座標系と T265 の座標系が異なるので、回転情報も位置と一緒に送信して座標系を合わせる計算を行います。T265 を写真のように Oculus Go と同じ向きに取り付ければ、Yaw のみ一致させれば良くなります。

サンプルプロジェクト

uOSC 自体のコード以外は NYSL ライセンスです。

github.com

送信側

メッセージの送受信には uOSC を使っています。

tips.hecomi.com

まずは送信側のコードを見てみましょう。

using UnityEngine;

public class TransformSender : MonoBehaviour
{
    [SerializeField]
    uOSC.uOscClient oscClient;

    [SerializeField]
    Transform target;

    void Update()
    {
        var p = target.position;
        oscClient.Send("/Pos", p.x, p.y, p.z);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            oscClient.Send("/Reset", target.eulerAngles.y);
        }
    }
}

target の位置を毎フレーム、また Space キーが押されたら "/Reset" メッセージをその時の Yaw と一緒に送信しています。この時、targetTransform は、次のように T265 自体のものではなく、Oculus Go の目の中心となるようなロケータを作成し、その座標を送るようにします。

f:id:hecomi:20190321180344p:plain

この Unity シーンは T265 を接続したノート PC 上で動かします。

受信側

次に受信側です。こちらは Virtual Reality Supported にチェックを入れ、Oculus (Android) パッケージを Package Manager からインポートしたプロジェクトで、Android 向けにビルドして Oculus Go 側で動かします。

using UnityEngine;

public class TransformReceiver : MonoBehaviour
{
    [SerializeField]
    uOSC.uOscServer oscServer;

    [SerializeField]
    Transform target;

    [SerializeField]
    Transform camera;

    Vector3 t265Pos = Vector3.zero;
    Quaternion calibRot = Quaternion.identity;

    void Start()
    {
        oscServer.onDataReceived.AddListener(OnDataReceived);
    }

    void OnDataReceived(uOSC.Message message)
    {
        if (message.address == "/Pos")
        {
            var v = message.values;
            t265Pos = new Vector3((float)v[0], (float)v[1], (float)v[2]);
        }
        else if (message.address == "/Reset")
        {
            var t265Yaw = (float)message.values[0];
            var goYaw = camera.localEulerAngles.y;
            calibRot = Quaternion.Euler(0f, goYaw - t265Yaw, 0f);
        }

        UpdateTransform();
    }

    void UpdateTransform()
    {
        target.transform.position = calibRot * t265Pos;
    }
}

"/Pos" メッセージが来たらその値を格納しておきます。"/Reset" メッセージが来たときは、送られてきたヨーと、現在のカメラのヨーの差分を見て、T265 の座標系を Oculus Go の動いている Unity の座標系に合わせる処理を行います。この回転誤差分を T265 から送られてきた位置に UpdateTransform() に書いてあるように適用してあげれば Oculus Go の中で正しい方向に位置トラッキングがされるようになります。

デモ

グリッドをおいて椅子から立ち上がって家の中をウロウロし、また椅子に戻ってくる様子になります。

動画だと分かりづらいですが、体験としてはトラッキング自体はものすごい悪いものではないですが、今回の作りが適当なのもあって他のインサイドアウトHMD と比べると性能はだいぶ劣る感じでした。要因の一つとしては、試したあとで気づいたのですが、Oculus Go の回転も位置は eye-neck 分のオフセットを持っているので、受け手でそのオフセット分をキャンセルする処理とかを書かないと駄目でした...。そのせいで回転のときの位置には違和感がありました。

おわりに

単体 SLAM を計算し、低消費電力で、しかも(将来的には Android も含む)クロスプラットフォームな小型軽量なカメラがこのお値段なのは本当にすごいです。

SDK やデプスとの組み合わせについてのより発展的な内容については D400 系も合わせて解説を書くともっと分かりやすくできそうな気もするので、別途 D400 系を手に入れたらもう少し俯瞰した記事を書こうかなと思います。

参考

更に詳しく知りたい方は、以下のあるしおうねさんのツイートのスレッドをお読みください。

*1:詳細はこちらの脚注を参照