Tutti Lab

元シリコンバレー在住のおっさん技術者、モバイルVRアプリ開発に挑戦中

Unityで3DカメラIntel RealSenseを使ってみる

f:id:tuti107:20160923150741p:plain

これまで、ThetaSやスマホを2台使いすることで360度立体視映像を撮影するアプリケーションを作成してきました。これら端末を2台使いすることで左右視差のある映像を録画し、背景部分を除去して背景とCGを重畳することで、かんたんに360度立体視コンテンツをつくれる、というものです。
この方法はかんたんでそれなりのクォリティを実現可能なのですが、撮影映像とCGの重畳において問題が生じる場合があります。
右目・左目映像を用意し、Google Cardboard等のVRヘッドセットでそれぞれの目がそれぞれの目の映像だけを見るようにすれば「人間は立体的に感じる」ことが可能です。しかし、コンピュータがこれら左右眼用の映像から立体感(=奥行き)を認識することはかんたんではありません。
このため、例えば背景部分の除去には、クロマキー(緑色の部屋を用意し、そこで撮影、映像の緑色部分を透明化することで背景除去)や、背景差分(最初にだれもいない部屋を撮影(=背景画像)、背景画像と撮影した背景映像の変化分だけを抜き出す)のような簡易的な方法を利用してきました。しかしこの方法では抜き出した部分の「立体感」をコンピュータは認識できません。このためこれらの方法では、奥行感を無視したおかしな映像が出来上がってしまう恐れがあります。
f:id:tuti107:20160923151832p:plain

クロマキーや背景差分で抜き出した映像部分には奥行きの情報がないため、CG・抜き出した映像どちらを前に表示するか、判別ができないためです。

Intel RealSense等の3Dカメラは、映像に「奥行き情報(=デプス)」を付加した撮影が可能です。以下のように、カラー映像(=左側)と同時にその映像のデプス(=右側)を撮影します。
f:id:tuti107:20160923152027p:plain

デプスは黒(=最も遠い/位置を特定不能)~白(=最も近い)をグレースケール表します。真っ黒(最も遠い)と真っ白(最も近い)がぞれぞれ、カメラから具体的にどの程度の距離離れているかについては、3Dカメラ毎に異なります。例えば今回使用するIntel RealSense R200は、認識可能な距離が0.5m~4mのため、黒=概ね4m以上(又は0.5m未満)、白=概ね0.5mとなるかと思います。

デプスを利用すると、背景部分の除去もカンタンです。例えば0.5m~4mの範囲にないもの(=デプスの黒部分)は透明、としてしまえば、4m以上向こうにあるものは一切表示されなくなります。
今回は、RealSense R200を利用し、デプスによる背景除去に挑戦してみます。

RealSenseのセットアップ

まずはこちらから、ドライバとSDKのダウンロード、及びRealSense R200を購入します。
Step1では、R200 Camera Driverをダウンロードしてください。
f:id:tuti107:20160923110949p:plain
そしてStep2(SDKのダウンロード)、Step3(オプション機能、必要に応じて)を順次ダウンロードし、最後にRealSense R200を購入します。
f:id:tuti107:20160923152950p:plain
RealSense R200を購入するためには、アカウントを作成する必要があります。画面右上のSign Inを押下し、Login or Create an Account画面の右下 CREATE AN ACCOUNTボタンを押下してCreate an Account画面へ移動し、必要情報を入力してアカウントを作成してください。メール認証の後、アカウントがアクティベートされます。
サインインした状態で、Add to Cartボタンを押下し、画面右側のCHECK OUTボタンを押下してください。
f:id:tuti107:20160923111714p:plain
Billing Information画面では、領収書送付先の住所を入力します。
例えば 〒123-4567 北海道GHI市ABC町12-34 DEFビルディング1001号室 Tutti Labさん、電話番号090-1234-5678の場合は、以下の感じの入力になるかと思います。
f:id:tuti107:20160923112845p:plain
Shipping Informationでは、先程入力したBilling Addressを選択します。領収書と配送品の送り先が異なる場合のみ、こちらを別途入力します。
Shipping Methodでは配送方法を選択します。Standard (5 - 7 business days) とPriority (2 - 3 business days)が選べますが、ほとんど金額が変わらないため、Priorityのほうがよいのでは?と思います。なお、私の場合、Priorityを選択し、注文後4日で配送されました。
Payment Informationでは支払い方法が選べますが、Credit or Debit Cardを選択することになると思います。必要な情報を入力し、最後にOrder ReviewでPLACE ORDERを押せば完了です。
RealSense R200が無事配送されましたら、USB3.0でPCと接続の後、ダウンロード済みのドライバー、SDKをインストールしてください。

UnityでRealSense R200を利用する

SDKには、UnityでRealSense R200を利用するためのアセットが含まれています。
Unityを起動して適当なプロジェクトを生成の後、File→Build Settingsより、Target PlatformをWindowsに、Architectureをx86又はx86_64にそれぞれ設定します(Mac等でも使う方法があるようですが、割愛します)。
f:id:tuti107:20160923114911p:plain

次に、RealSense Unity Toolkitをインポートします。
ProjectのAssetsで右クリック→Import Package→Custom Packageより、C:\Program Files (x86)\Intel\RSSDK\framework\Unity\UnityToolkit.unitypackageをインポートしてください。
インポート後、まずはAssets/RSUnityToolkit/Prefabs/Imageをシーンにドラッグ&ドロップします。
この状態で実行すると、下記のように、画面中央に小さくカメラプレビューが表示されます。
f:id:tuti107:20160923115222p:plain

RealSense Unity Toolkitは、上記Imageプレファブのように、各種認識、3D撮像、3DスキャンやAR等の機能をプレファブとして提供しています。開発者はこれらプレファブを利用することで殆どコードを書くことなくRealSenseの機能を活用したアプリケーションを作れる、、はずなのですが、実際はそううまくはいきません。
試しに、Image以外のプレファブ、例えばDebug Viewerをシーンに放り込んで実行してみると、エラーが発生し、動作しません。

これは、R200には存在しない機能の利用を試みたために発生したエラーです。
f:id:tuti107:20160923115810p:plain

こちらのサイトに、R200とF200という2つのRealSenseの機能比較表があります。ご覧の通り、たくさんの機能に✕がついているのがわかります。
このF200(現在は後継機のSR300)とR200、用途が異なります。F200は例えばPCディスプレイの上部等に設置するインカメラであり、PC利用時に有用な機能(顔認識でのログイン、ハンドジェスチャー認識、~1.5m程度のデプス取得など)が想定されています。一方でR200は、スマホやタブレットのアウトカメラに設置されることを想定されており、建物の3Dスキャンや、~4mのデプス取得等に利用します。
F200側の機能も非常に魅力的ですので、これらを使ったアプリケーションを開発してみたい方は、F200の購入も検討いただければよいかと思います。

背景除去アプリケーションを作成してみる

Imageプレファブを利用して、背景除去機能を実装してみます。以下は、Imageプレファブのインスペクタです。Stream Outで出力される映像の種別を選択できます。
f:id:tuti107:20160923121228p:plain

  • STREAM_TYPE_COLORは、RGB映像を出力します
  • STREAM_TYPE_DEPTHは、デプスをグレースケールで出力します
  • STREAM_TYPE_IRは、赤外線センサー映像をグレースケールで出力します(F200のみ)
  • STREAM_TYPE_LEFTは、赤外線センサー(左側)映像をグレースケールで出力します(R200のみ)
  • STREAM_TYPE_RIGHTは、赤外線センサー(右側)映像をグレースケールで出力します(R200のみ)

今回は、1)STREAM_TYPE_DEPTHを指定したImageでデプスを生成、2)STREAM_TYPE_COLORを指定したImageは、1)のデプスを利用して一定距離の範囲外のピクセルを透明にする、として、背景を取り除きます。

まず、空オブジェクト「Images」を作成、その下に2つのImageプレファブを置きます。
f:id:tuti107:20160923121830p:plain
次に、Assets/RSUnityToolkit/Internal/Materials/UnlitMatを2つコピー(Ctrl+D)し、それぞれDepth, RGBという名前とします。
これらマテリアルDepth, RGBをそれぞれImageプレファブより生成したGameObject Depth, RGBにそれぞれ設定します。こうしておかないと、GameObject Depth, RGBはそれぞれ初期設定のマテリアルUnlitMatに映像を出力するため、Stream Outの設定にかかわらず両方ともRGB出力、もしくは両方ともデプス出力、となってしまいます。

次にデプスに応じて透過色設定をするためのシェーダーを作成します。
ProjectのAssets内の適当なフォルダにて、右クリック→Create→Shader→Image Effect Shaderを選択してください。ファイル名は、TransparentEffectとします。なお「シェーダーとは?」については、他の書籍・ブログ等をご参照いただければと思います。以下では、今回実装部のみ説明をしていきます。

Shader "Tuti/TransparentEffectShader"
{
  Properties
  {
    _MainTex ("Main Texture", 2D) = "white" {}
    [NoScaleOffset] _DepthTex("Depth Texture", 2D) = "white" {} // add
    _LowerLimitDepth("LowerLimitDepth", float) = 0.6 // add
    _UpperLimitDepth("UpperLimitDepth", float) = 1 // add
    _MainTexRegion("Main Texture region", Vector) = (0, 0, 1, 1) // add
    _DepthTexRegion("Depth Texture region", Vector) = (0, 0, 1, 1) // add
  }
  SubShader
  {
    // No culling or depth
    // Cull Off ZWrite Off ZTest Always // remove
    Tags{ "RenderType" = "Transparent" "Queue" = "Transparent" "ForceNoShadowCasting" = "True" } // add
    Cull Off // add
    ZWrite On // add
    Blend SrcAlpha OneMinusSrcAlpha // add

    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
			
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
      };

      struct v2f
      {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
      };

      v2f vert (appdata v)
      {
        v2f o;
        o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv = v.uv;
        return o;
      }
			
      sampler2D _MainTex;
      sampler2D _DepthTex; // add
      float _LowerLimitDepth; // add
      float _UpperLimitDepth; // add
      float4 _MainTexRegion; // add
      float4 _DepthTexRegion; // add

      // updated everything
      fixed4 frag(v2f i) : SV_Target
      {					
        fixed4 col = tex2D(_MainTex, i.uv);

        if (i.uv.x < _MainTexRegion.x || i.uv.x > _MainTexRegion.x + _MainTexRegion.z || i.uv.y < _MainTexRegion.y || i.uv.y > _MainTexRegion.y + _MainTexRegion.w) {
          col.a = 0;
        }
        else {
          float2 uv1 = float2((i.uv.x - _MainTexRegion.x)*1/_MainTexRegion.z, (i.uv.y - _MainTexRegion.y)*1/_MainTexRegion.w);
          float2 uv2 = float2(uv1.x*_DepthTexRegion.z + _DepthTexRegion.x, uv1.y*_DepthTexRegion.w + _DepthTexRegion.y);
          fixed4 dcol = tex2D(_DepthTex, uv2);
          if (dcol.r < _LowerLimitDepth || dcol.r > _UpperLimitDepth) {
            col.a = 0;
          }
        }


        return col;
      }
      ENDCG
    }
  }
}

上記で「add」「remove」及び「update」としている部分が、デフォルトのImage Effect Shaderとの差分です。
まずProperties部ですが、デプスのテスクチャー_DepthTex、画素透明化の下限(_LowerLimitDepth)と上限(_UpperLimitDepth)、RGB出力側の有効矩形(_MainTexRegion)、デプス側の有効矩形(_DepthTexRegion)です。具体的には、frag()関数部にて説明をしますが、このようにしておくことで、インスペクタ等から各値が指定可能となります。
f:id:tuti107:20160923125548p:plain

次にTags部等についてですが、透過表示を可能とするため、このような設定としています。
変数宣言部については、上記Properties部にて宣言したプロパティに対応する変数を宣言しています。なおテクスチャはsample2D型となります。
フラグメントシェーダー部(frag()関数)では、各座標(i.uv)について、

  • i.uvがRGB出力側の有効矩形(_MainTexRegion)の範囲外なら透明とする
  • RGB出力側の有効矩形(_MainTexRegion)の範囲内の座標i.uvを、デプス側の有効矩形内の座標uv2に変換
  • uv2のデプスが_LowerLimitDepth未満、又は_UpperLimitDepthより上なら、透明とする
  • それ以外の場合は、i.uvのテクスチャをそのまま表示する

との処理を行っています。
なぜこのような面倒なことをやる必要があるのか、ですが、私が現時点で調べた範囲では、残念ながらRGB映像とデプス映像の位置を一致させる方法がありません。このため、RGB映像とデプス映像の座標のずれを考慮の上で、RGB映像内のある座標aに対応するデプス映像a'(0:黒~1:白)が_LowerLimitDepth, _UpperLimitDepthの範囲内なら座標aのRGB映像の色を、範囲外なら透明を出力する、としています。
f:id:tuti107:20160923152757p:plain

また、このシェーダーにデプス映像のテクスチャを設定する目的で、以下のスクリプトも用意します。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(DrawImages))]
public class TransparentController : MonoBehaviour {

    public Material rgbMat;

    private Material depthMat;
    private Texture2D depthTex;

	// Use this for initialization
	void Start () {

        depthMat = GetComponent<Renderer>().material;
	}
	
	// Update is called once per frame
	void Update () {
	
        if (depthTex == null)
        {
            if (depthMat.mainTexture != null)
            {
                depthTex = (Texture2D)depthMat.mainTexture;
                rgbMat.SetTexture("_DepthTex", depthTex);
            }
        }
	}
}

このスクリプトは、DrawImages(Imageプレファブに設定されているコンポーネント)にて、RealSense R200から入力したデプス映像をTexture2Dとしてマテリアルに設定した際、このテクスチャを先シェーダー(TransparentEffect)の_DepthTexに設定します。
これにより、TransparentEffectは変数_DepthTexにてデプス映像を利用することが可能となります。

最後に、GameObject RGB, Depth, MainCameraを以下のとおり設定します。

f:id:tuti107:20160923135344p:plain
まずRGBですが、レイヤを新たに作成したShownとします(理由は後述します)。
画面いっぱいに映像を表示するため、Position/Rotation/Scaleは以上のとおりとします。
DrawImagesのStream OutはSTREAM_TYPE_COLORとします。RGB映像を出力するためです。
マテリアルは、上記で生成したマテリアルRGBを設定の上、Shaderは作成したTuti/TransparentEffectShaderとします。
透明化するデプス値の下限(LowerLimitDepth)、及び上限(UpperLimitDepth)はそれぞれ、0.4, 1としました。ここは任意調整してください。
RGB映像の有効矩形については、(X:0, Y:0, Width(表示はZ):0.94, Height(表示はW):1)としました。
デプス映像の有効矩形については、(X:0.0076, Y:0, Width(表示はZ):0.924, Height(表示はW):1)としました。
これら矩形範囲は、RGB映像・デプス映像のスクリーンショットから両映像の内容が一致する矩形となるよう調整してみましたが、もしかするとカメラ個体差等あるかもしれません。任意調整をお願いします。
今回は発見できませんでしたが、(必ず)RGB映像とデプス映像の位置を一致させるための設定があると信じています。見つけ次第またブログで書きたいと思います。

f:id:tuti107:20160923140254p:plain
Depthについては、こんな感じです。
レイヤは新たに作成したHiddenとします。デプス映像は画面に表示する必要がありませんので、カメラのカリング設定で非表示とするよう、レイヤを分けました。
デバッグ用にRGB映像の上に並べて配置したため、Position/Rotation/Scaleは以上としました。
DrawImagesのStream OutはSTREAM_TYPE_DEPTHとします。デプス映像を出力するためです。
上記で作成したスクリプトTransparentControllerをコンポーネントとして貼り付けます。RgbMatには、上記RGBマテリアルを設定してください。
マテリアルは、上記で生成したマテリアルDepthを設定します。

f:id:tuti107:20160923140653p:plain
最後にメインカメラは、こんな感じです。
RGB映像のみ画面表示とするため、Culling Maskは「Shown」とします。
また、ProjectionをOrthographicとします。Z方向の遠近感は不要とためです。

おわりに

今回は、(買いたてほやほやの)RealScene R200を使用して、背景除去にチャレンジしてみました。
とりあえず背景除去はできるのですが、ノイズがひどかったり、髪の毛のデプス情報がとれないため、髪の毛部分が透明になったりと、改善の余地がおおいです。今後、すこしずつ改善をしていこうと思います。
また、上記で説明のとおり、RGB映像とデプス映像の位置をあわせる設定がわかっておりません。ご存知の方おりましたら、コメント等いただけると幸いです。
最後に、USB3.0の延長ケーブルの利用には気をつけてください。付属のUSBケーブルは非常に短いため、2mの延長ケーブルを購入、接続を試みたのですが、カメラが認識されず。。ネットで調べてみると、既知の問題のようでした(延長するとカメラへの給電が落ち動かなくなる、ハブも同様)。セルフパワーのハブなら動く、との情報もありましたので、試してみようと思います。

【追記】
こちらのセルフパワー型HUBで試したところ、無事動作しました。