凹みTips

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

Intel RealSense デプスカメラ D435 を Unity で遊んでみた

はじめに

以前、Intel® RealSense™ のトラッキングカメラである T265 の記事を書きましたが、今回はデプスカメラである D435 を手に入れましたので、まずはカメラの概要と Unity のサンプルの紹介をしようと思います。

tips.hecomi.com

デモ

画像ストリーム

f:id:hecomi:20190602141621p:plain

ポイントクラウド

f:id:hecomi:20190602200922g:plain

ポリゴンとマージ

f:id:hecomi:20190604003616g:plain

特徴

D400 シリーズの種類

購入できる D400 シリーズは、D415D435D435i の 3 種類があります。いずれも 28nm プロセスの Intel® RealSense™ D4ビジョンプロセッサ(D4 VPU)を内蔵し、デバイス側でデプスの計算を行うことが出来ます。この D4 VPU とそれぞれ D410 / D430 デプスモジュールを組み合わせカメラとしてパッケージングしたものが D415 および D435 になります(モジュール単体もあるようです)。D435 に T265 と同じように小型の 6 軸ジャイロセンサである BMI055 を追加したものが D435i になります。

外観

横幅は 10 cm よりも小さく、かなりコンパクトです。

f:id:hecomi:20190604233724j:plain

色々なデプスカメラと比較するとこんな感じです(手前右)。

f:id:hecomi:20190604234759j:plain

スペック

D415 D435
使用環境 屋内 / 屋外
デプス方式 アクティブ IR ステレオ
シャッター方式 ローリングシャッター グローバルシャッター
センサ(ピクセルサイズ) 1.4 um x 1.4 um 3 um x 3 um
プロセッサ D4 VPU
モジュール D410 D430
寸法 99 mm x 20 mm x 23 mm 90 mm x 25 mm x 25 mm
コネクタ USB Type-C
デプスカメ
解像度 最大 1280 x 720
FOV 63.4° x 40.4° (+/- 3°) 85.2° x 58° (+/- 3°)
フレームレート 最大 90 fps
最小距離 0.16 m 0.11 m
最大距離 約 10 m(環境による)
RGB カメラ
解像度 1920 x 1080
フレームレート 30 fps
FOV 69.4° x 42.5° (+/- 3°) 69.4° x 42.5° (+/- 3°)

software.intel.com

D415 と D435 では、いずれも IR プロジェクタでパターン照射して、それを 2 機の IR カメラで撮影してデプスを算出する方式(アクティブ IR ステレオ)を使用しています。シャッター方式と最小距離、デプス画角以外は基本的には共通している形になっています。その他より詳細なスペックについては、PDF から確認できます。

Intel® RealSense™ D400 Series (DS5) Product Family Datasheet

SDK について

software.intel.com

F200 の時の Intel® RealSense™ SDK とは異なり、Intel® RealSense™ SDK 2.0 は基本的にはデータのストリームのみを提供し、手や顔のトラッキングという機能は入っていないようです(issue を覗いていると将来的に予定はある、と述べられたりしていましたが...)。

tips.hecomi.com

認識系の機能を使いたい場合は、取得したストリームを別の認識器(e.g. OpenCV)などに流す必要があるようです。

購入

公式サイトからも購入できますがスイッチサイエンスからも買うことが出来ます。

公式

スイッチサイエンス

www.switch-science.com

環境

本記事は以下の環境で確認しています:

セットアップ

ハードウェア

まずは以下の公式の Get Started を参考にデバイスのセットアップをします。

www.intelrealsense.com

SDK

RealSense SDK は以下の場所からダウンロードしましょう:

www.intelrealsense.com

f:id:hecomi:20190526191042p:plain

f:id:hecomi:20190526191059p:plain

T265 など他のカメラのセットアップで既にインストールしてある人も、バージョンが新しくなっているかもしれないのでチェックしておきましょう。

動作確認

インストールされた SDK に含まれる Intel RealSense Viewer を開くと、カメラの動作確認を行うことが出来ます。

f:id:hecomi:20190526191204p:plain

同時にデプスと RGB カメラの画を確認することも出来ます。上記スペックの通り画角が違うのも見て取れます。

f:id:hecomi:20190531234746g:plain

ファームウェアアップデート

SDK のリリースノートには推奨される FW のバージョンが記載されています。

github.com

例えば執筆時点で最新の SDK 2.21.0 では 5.11.6.200 よりも新しいファームウェアが要求されています。

Viewer で Info をクリックしてデバイス詳細を表示すると、そのデバイスファームウェアのバージョンを確認することが出来ます。私のものは「!」が表示されており、必要とされる最小バージョンよりも古いファームウェアとなっている模様でした。ファームウェアの更新に必要なファイルは「!」をクリックするか、以下のページにアクセスすることで最新のものをインストールすることが出来ます。.bin ファイルとアップデータの .exe をインストールしてコマンドラインから最新のファームウェアにアップデートします。

downloadcenter.intel.com

downloadcenter.intel.com

ダウンロードした .exe を開き、1. Update Camera Devices を選択したあと、対象の RealSense を選択(1 つしか選択していない場合は 1)、その後にダウンロードした .bin のパスを記入してエンターを押します。

f:id:hecomi:20190526233201p:plain

f:id:hecomi:20190526233316p:plain

f:id:hecomi:20190526233447p:plain

再度 RealSense Viewer から確認してみると、先程表示されていた「!」がなくなり新しいバージョンになっていることで更新されていることがわかります(コマンドラインのアップデータからも確認できます)。

f:id:hecomi:20190526233749p:plain

Unity

GitHub のリリースから最新のものをインストールしてきます。

github.com

これを新規作成したプロジェクトにインポートして、Assets > RealSenseSDK2.0 > Scenes > StartHere シーンを開いて以下のようなページが表示されることを確認します。

f:id:hecomi:20190527004501p:plain

これで Unity で遊ぶ準備ができました。

Unity Wrapper for RealSense SDK 2.0 について

サンプルを見ていく前に、Unity Wrapper for RealSense SDK 2.0 の設計について簡単に見ておきましょう。前回の T265 の記事を読まれた方はおさらいになりますが、RsDevice コンポーネントが起点ととなっています。ここにデバイスから取ってきたい(これる)情報を登録すると、RsStreamTextureRenderer で指定した Texture に画を反映させることが出来る仕組みです。

RsDevice

f:id:hecomi:20190317131654p:plain

Process Mode は RealSense から与えられる Frame データをどのスレッドで扱うかを指定できます。デフォルトでは Multithreaded になっており、この場合は Thread を新たに作成してそこで飛んできた(正確にはポーリングして取得した)Frame のイベントを取り扱います。Untiy Thread が指定された場合は Unity のメインスレッドでその処理を行います。Unity の API を簡単に使いたいなど特別なケース以外では Multithreaded で良いと思います。

Requested Serial Number は複数の RealSense デバイスが接続されたときにシリアル番号を指定することで特定のデバイスを指定できるものです。シリアル番号は RealSense Viewer や、後述の RsDeviceInspector コンポーネントから簡単に調べることが出来ます。

Profiles には取得するデータの定義を設定します。Size には取得したいストリームの数を記入し、D435 では DepthIRColor をサポートしているので最大 3 になります。各要素は RsVideoStreamRequest になっており、ここではストリームの種類や取ってくるデータ(テクスチャなど)のフォーマット、フレームレートや解像度を指定します。ここが一致していないと ExternalException: rs2_pipeline_start_with_config というコンフィグエラーで怒られます。

f:id:hecomi:20190602153901p:plain

Stream Index は 1 つしかないもの(RGB ストリームやデプス)では 0 を指定し、2 つ以上あるもの(IR カメラ)は、1 ~ N のインデックスを指定します。

余談ですが、デバイス毎に取れるストリームは決まっているので、RsDevice をもう一段ラップしてデバイス毎のコンポーネント用意したり、またはプロファイルを ScriptableObject にして RsDevice コンポーネントに与えられる仕組みが SDK に欲しいかな、などと思いました。。

RsStreamTextureRenderer

f:id:hecomi:20190601233009p:plain

サンプルではたいてい RsStreamTextureRenderer コンポーネントのついた GameObject が RsDevice のついた GameObject の子要素になっています。

f:id:hecomi:20190602142045p:plain

RsDeviceInspector

RsDeviceInspector コンポーネントRsDevice と同じ GameObject にアタッチするとそのデバイスの設定を確認したり、調整したり出来ます。シリアル番号もここから確認できます。

f:id:hecomi:20190602163752p:plain

では D435 で何が出来るのか勘所を掴むために用意されているサンプルをそれぞれ見ていきたいと思います。

UI Image Sample (Depth & Color)

実行すると TexturesDepthAndColor シーンが起動します。これは Canvas 上の 2 つの RawImage に、それぞれデプスの画と RGB カメラの画を表示するサンプルになります。

f:id:hecomi:20190602153024p:plain

f:id:hecomi:20190602141621p:plain

前項で書いたように、RsDevice でデバイスから取ってくるストリーム(DepthIRColor)を指定し、 デプスの方の色付けはシェーダで行われています。スライダで色付けしたい深度の範囲を指定できます。またプルダウンメニューで JetInferno など別のタイプに指定すると配色が変わります。

f:id:hecomi:20190602143420p:plain

これは次のような画像が 256 * 8 px で用意されていて、カラーマップの種類が v(0 ~ 7 の値)、スライダで指定した範囲を正規化した値が u で与えられて色付けされています(ここでは見やすいように画像を拡大しています)。

f:id:hecomi:20190602152847p:plain

Assets/RealSenseSDK2.0/Shaders/Depth.shader の該当箇所を見てみるとこんな感じです(分かりやすいように一部コードを修正しています):

half4 frag (v2f_img pix) : SV_Target
{
    // デプスの範囲を ushort から meter に変換
    float z = tex2D(_MainTex, pix.uv).r * 0xffff * _DepthScale;

    // スライダで指定された値に正規化
    z = (z - _MinRange) / (_MaxRange - _MinRange);
    if (z <= 0) return 0;

    // カラーマップの選択(0 ~ 7)
    float2 v = 1 - (_Colormap + 0.5) * _Colormaps_TexelSize.y;

    // カラーマップから色をサンプリング
    return tex2D(_Colormaps, float2(z, v));
}

UI Image Sample (Depth & IR)

先程のサンプルではカラー画像を表示していましたが、こちらは IR 画像を表示するサンプルになります。

f:id:hecomi:20190602154732p:plain

アクティブ方式なので照射しているパターンを見ることが出来ますね。IR 画像を取ってくる方法は RsDevice で IR ストリームの設定をし、RsStreamTextureRenderer でそれを指定したテクスチャに流す、という形になります。

f:id:hecomi:20190602165950p:plain

f:id:hecomi:20190602155729p:plain

全てのテクスチャの取得

サンプルにはありませんが、全てのテクスチャを持ってくる方法もこの流れで見ておきましょう。RsDevice は次のように設定します。

f:id:hecomi:20190602170921p:plain

IR カメラは 2 個あるので、それぞれ Index を 1 と 2 に指定して左・右を分けています。RsStreamTextureRenderer も 4 つ用意して、これを Canvas 内の各 RawImagetexture フィールドに流し込むように設定すると次のように 4 つのテクスチャが取得できます。

f:id:hecomi:20190602171122p:plain

Point Cloud Processing Blocks (Depth)

次はポイントクラウドです。グリグリと動かすことが出来ます。

f:id:hecomi:20190602182218g:plain

また、UI 上でデプスの穴埋めなどのパラメタ指定をすることが出来ます。

f:id:hecomi:20190602182627p:plain

ちょっと込み入っているので順を追って説明していきます。

描画の仕方

メッシュは RsPointCloudRenderer コンポーネントを使って次のように更新されます。

  1. 描画はデプステクスチャのピクセル数と同等の頂点を持つメッシュ(32-bit インデックスフォーマット)をランタイムで生成
  2. 提供されるデータ(Frame)をキューに入れておく(別スレッド)
  3. LateUpdate() タイミングでキューからデータを取り出す(メインスレッド)
  4. Intel.RealSense.Points.CopyVertices() で生成したメッシュの頂点にコピー
  5. 以降 2. から繰り返し

このメッシュを専用のマテリアル(PointCloudMat)で描画します。このマテリアルのシェーダ(Custom/PointCloudGem)では、ジオメトリシェーダで各頂点を四角ポリゴンへ変形させる処理が書いてあります。

余談ですが、Intel.RealSense.Points.CopyVertices() のように Intel.RealSense 名前空間の関数は Unity 上のプロジェクトではビルド済みの DLL になっているため実装を見ることは出来ません。しかしながら GitHub の方ではコードが公開されているので、中の処理を知りたくなったら直接 GitHub を見ると確認することが出来ます。主に NativeMethods クラス経由でネイティブ DLL(librealsense.dll)とやり取りを行っています。

github.com

RsProcessingBlock

描画の仕方の 2. で書いた「提供されるデータ」は、RsDevice から直接取ってくるのではなく、RsProcessingPipe というコンポーネントを経由して取得しています。このコンポーネントRsProcessingProifle という ScriptableObject を継承したパラメタオブジェクトを指定することが出来ます。

f:id:hecomi:20190602192309p:plain

f:id:hecomi:20190602192320p:plain

RsProcessingProfile には上図のように複数の RsProcessingBlock を継承したクラスのインスタンスを登録することが出来ます。これらのブロックはポストプロセスとしてフレームデータを処理するネイティブ実装側の機能につながっています。具体的には、間引きをしたり(RsDecimationFilter)、特定のエリア間の値に限定したり(RsThreasholdFilter)、穴埋めをしたり(RsHoleFillingFilter)をするようなフィルタ処理になります。

github.com

これを UI でポチポチといじりながら確認できるサンプル、という形になります。

Point Cloud Sample (Color)

これは先程のサンプルに RGB カメラから取った色を付与するものです。

f:id:hecomi:20190602200922g:plain

基本的には先程の RsPointCloudRenderer と同じ仕組みでメッシュを生成し、そのマテリアルに RsStreamTextureRenderer でテクスチャを渡している形です。しかしながら UI Image Sample (Depth & Color) サンプルで見たように、RGB カメラの画とデプスの画の画角は異なるので、ここをなんとかしなくてはなりません。

画角を合わせるために、RsPointCloudRendereruvmap という RGB カメラのテクスチャのどの座標のデータを取ってくるか、というテクスチャを生成しています。ポイントクラウド用のメッシュの各頂点は自身の UV を一旦このテクスチャを通じて変換し、得られた UV で RGB 画像をサンプリングします。

f:id:hecomi:20190603001637p:plain

fixed4 frag (g2f i) : SV_Target
{
    float2 uv = tex2D(_UVMap, i.uv);
    if(any(uv <= 0 || uv >= 1)) discard;
    uv += 0.5 * _MainTex_TexelSize.xy;
    return tex2D(_MainTex, uv) * _Color;
}

この _UVMapFrame データから Intel.RealSense.Points.TextureData を通じて取得できます。ではこのマップはどこから来ているのでしょうか。

RsPointCloud

ブロックの一つに RsPointCloud というものがあります。

f:id:hecomi:20190603225045p:plain

ポイントクラウドを使う場合はこのブロックをプロファイルに登録します。ちょっと深いですが実装を見てみると、どうやらこのブロックがマップを提供しているようです。

まず C# 側で、このブロックは Intel.RealSense.PointCloud を生成し、PointCloud.Process() を実行しています。PointCloud の実装は次になります。

ベースクラスである ProcessingBlockNativeMethods.rs2_create_pointcloud() を渡しています。これはネイティブ側(C++)で生成した pointcloud クラスのインスタンスのポインタを返す関数になります。他のブロックの場合はそれぞれの処理を行うクラス(e.g. hole_filling_filterdecimation_filter)があり、いずれも間にいくつか継承をはさみますが、rs2::processing_block を継承しています。

C#ProcessingBlock に戻ります。このクラスはこの与えられたポインタを更に基底の Base.Object に渡して保持します。

この Base.Object クラスは Dispose() 時に C++delete を呼んでくれる機能と、Handle を通じてそのポインタを提供する機能を持っています。この Handle を通じて、色々な経路を経て pointcloud::process_frame() が呼び出されます。この中で pointcloud::process_depth_frame() が実行され、ここで pointcloud::get_texture_map() によってマップが計算されます。

この中では rs2_project_point_to_pixel() によってデプステクスチャによる 3 次元座標が RGB 座標上の 2 次元平面座標へマップされます。このバッファを Texture2D.LoadRawTextureData() して Unity で使えるテクスチャに変換しシェーダに渡してシェーダ内で各ポイントクラウドに色を付けている、という形になっているようです。

Alignment Sample (Depth & Color)

カラー画像をデプス画像へ、またはデプス画像をカラー画像へ一致させるサンプルです。

f:id:hecomi:20190603232021g:plain

これは ProcessingBlock で、RsAlign を追加することで可能です。

f:id:hecomi:20190603232601p:plain

また、RsStreamTextureRenderer で、SourceRsDevice ではなく、RsProcessingPipe にして処理された結果を取得する形になっています。

f:id:hecomi:20190603232753p:plain

仕組みは先程見た RsPointCloud と同じで、C++ 側のネイティブの関数を呼び出しています。

Depth Segmentation (Depth & Color)

Z の距離で 2 次元画像内でセグメンテーションを行うサンプルです。

f:id:hecomi:20190603234326g:plain

先程の RsAlign と同じようにカラー画とデプス画を重ねています。手前の RGB のテクスチャ側は Custom/BgSeg シェーダを適用したマテリアルで描画されていて、これはスライダで与えられた値を使ってシェーダ内で smoothstep し、特定の Z 以内にある色をアルファで抜いてくる処理が行われています。

f:id:hecomi:20190604002721p:plain

AR Background (Depth & Color)

最後はかなり面白いポリゴンのオブジェクトとマージするサンプルです。

f:id:hecomi:20190604003616g:plain

仕組みは CommandBuffer を使ってデプスカメラの画をデプスバッファに書き込んでいる形になります。デプステクスチャは RsAlign でカラー画に揃えてあり、カラーテクスチャも同様に CommandBuffer を使って描画しています。

CommandBuffer についてわからない方は以下の記事を読んでみてください。

tips.hecomi.com

また、以前同様の仕組みを Deferred でですがやったこともあったので貼っておきます。

tips.hecomi.com

では実際にコードを見てみましょう。

public class RsARBackgroundRenderer : MonoBehaviour
{
    ...
    IEnumerator Start()
    {
        ...
        // 背景を描画する UnityEngine.XR.ARBackgroundRenderer の生成
        // 指定したマテリアル・テクスチャが背景に表示されるようになる
        bg = new ARBackgroundRenderer()
        {
            backgroundMaterial = material,
            mode = ARRenderMode.MaterialAsBackground,
            backgroundTexture = material.mainTexture
        };

        // デプスカメラとポリゴンオブジェクトのマージ用にデプスバッファを生成するようにする
        cam.depthTextureMode |= DepthTextureMode.Depth;

        // デプスバッファにデプスカメラから得たテクスチャを書き込むカメラのコマンドバッファを作成する
        // また、コピー用のシャドウマップをクリア(白で塗りつぶす)しておく
        var updateCamDepthTexture = new CommandBuffer() { name = "UpdateDepthTexture" };
        updateCamDepthTexture.Blit(BuiltinRenderTextureType.None, BuiltinRenderTextureType.CurrentActive, material);
        updateCamDepthTexture.SetGlobalTexture("_ShadowMapTexture", Texture2D.whiteTexture);
        cam.AddCommandBuffer(CameraEvent.AfterDepthTexture, updateCamDepthTexture);

        // ディレクショナルライトを取ってくる(最低 1 つあることを前提にしている)
        var light = FindObjectOfType<Light>();

        // シャドウマップをコピーする
        var copyScreenSpaceShadow = new CommandBuffer { name = "CopyScreenSpaceShadow" };
        int shadowCopyId = Shader.PropertyToID("_ShadowMapTexture");
        copyScreenSpaceShadow.GetTemporaryRT(shadowCopyId, -1, -1, 0);
        copyScreenSpaceShadow.CopyTexture(BuiltinRenderTextureType.CurrentActive, shadowCopyId);
        copyScreenSpaceShadow.SetGlobalTexture(shadowCopyId, shadowCopyId);
        light.AddCommandBuffer(LightEvent.AfterScreenspaceMask, copyScreenSpaceShadow);
    }
    ...
    void Update()
    {
        ...
        // スクリーンサイズが変化してるか調べる
        var s = new Vector2Int(Screen.width, Screen.height);
        if (screenSize == s) return;
        screenSize = s;

        // 変化していたらプロジェクション行列を作って書き換え
        var projectionMatrix = new Matrix4x4
        {
            m00 = intrinsics.fx,
            m11 = -intrinsics.fy,
            m03 = intrinsics.ppx / intrinsics.width,
            m13 = intrinsics.ppy / intrinsics.height,
            m22 = (cam.nearClipPlane + cam.farClipPlane) * 0.5f,
            m23 = cam.nearClipPlane * cam.farClipPlane,
        };
        float r = (float)intrinsics.width / Screen.width;
        projectionMatrix = Matrix4x4.Ortho(
            0, 
            Screen.width * r, 
            Screen.height * r, 
            0, 
            cam.nearClipPlane, 
            cam.farClipPlane) * projectionMatrix;
        projectionMatrix.m32 = -1;

        // なのでインスペクタ上の FOV などのカメラパラメタは効かなくなる
        cam.projectionMatrix = projectionMatrix;
    }
    ...
}

UnityEngine.XR.ARBackgroundRenderer を使うと簡単にカラー画を背景にしたり、それをオフしたり出来ます。背景はこれを使って表示しています。

docs.unity3d.com

また、コマンドバッファはデプスカメラの値を書き込む用のカメラに登録するものだけでなく、影が落ちるようにライトのコマンドバッファを用意してシャドウマップである _ShadowMapTexture を取ってきて、SetGlobalTexture() するコマンドを登録しています。カメラのコマンドバッファで描画するシェーダ次のような感じです。

fixed4 frag (v2f i, out float depth : DEPTH) : SV_Target
{
    ...
    float d = tex2D(_DepthTex, i.uv);
    d = d * 0xffff * 0.001;
    depth = LinearEyeDepth(d);
    ...
    float4 col = tex2D(_MainTex, i.uv);
    ...
    col *= tex2D(_ShadowMapTexture, i.uv1);

    return col;
}

フレームデバッガで流れを見てみるとより分かりやすいです。

f:id:hecomi:20190604231304g:plain

おわりに

Intel RealSense D435(D400系)についての調査をしてみました。前世代のものと比べると認識系がなくなってしまった関係で、RealSense SDK だけでインタラクションを作るのが難しくなってしまったデメリットもありますが、デプスやカラー画を組み合わせた表現はとても作りやすくなりました。特にデプスを使った表現を作りたいようなケースでは、とても小さいデバイスでケーブル一本でハード側で色々と処理をしてくれて、というメリットはとても大きいと思います。また、複数台の異なる種類の RealSense にも対応しているので、T265 と組み合わせたりしても面白いかもしれません(参考)。次回の応用編では色々試してみたいと思います。