Tutti Lab

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

Vuforiaの認識精度を上げるには

はじめに

前回、Vuforia for UnityのExtended Tracking機能を利用してGoogle Cardboardでポジショントラッキングということで記事を書いたところ、ブログ始めて以来の反響、非常に驚きました。モバイルVRでもポジショントラッキングをやりたい、というニーズは高いのかもしれません。
前回の記事では、結局十分な精度を実現できず、自分が動かなくても、プルプルとポジションが動いてしまう状態のままで公開をしましたが、その後、精度を上げる方法について調べましたので、これをご紹介しようと思います。

理想的なイメージとは?

Vuforiaは、イメージの特徴点を比較することで、イメージを認識します。特徴点の詳細については割愛しますが、Vuforiaの場合、この特徴点は「イメージを白黒化した上で、尖った部分
(三角形や長方形の角のような箇所)」に多く現れるようです(詳細はこちら)。
理想的なイメージは、
- コントラストがはっきりしている。コントラストが低いと「尖り」が分かりづらくなるため
- ごちゃごちゃしている。シンプルだと特徴点が少なくなってしまう
- 規則性がない。詳細は分かりませんが、特徴点を比較する上で、規則的な配列の特徴点は、認識率を悪化させるのだと思われます
もの、となります。

f:id:tuti107:20161201161146j:plain f:id:tuti107:20161201161155j:plain

前回は、Vuforiaがサンプルとして提供している「stones」というイメージを利用しており、上記の条件は満たしております。

印刷品質・サイズ

次に疑われるのは、印刷したイメージの印刷品質・サイズです(詳細はこちら)。
印刷したイメージのサイズがあまりにも小さすぎると、カメラより入力した画像から十分な特徴点を抽出できず、認識を失敗したり・精度が不安定となります。サイズについては、端末ー印刷物間の距離(m)<印刷物のイメージの幅✖️10以下、とするのが良いようです。前回私は幅30cmでイメージを印刷したので、距離は3m以内、ということになります。私はせいぜい2m程度の距離で試したため、印刷サイズの問題ではないようです。
印刷品質については、「コントラストはっきり」「テカらない」、かつ「印刷物に折り目などがない」ものである必要があります。
私は前回、ペラッペラの再生紙に、印刷品質「普通」で印刷したため、コントラストが低く、紙がよれてしまい一部部屋の光をかなり反射している、という最悪の状況になっていました。
そこで、ためしにイメージをディスプレイに表示し、試してみたところ、品質はかなり安定しました。早速こちらの紙を注文しました!

終わりに

前回品質が低かった理由は、単に印刷物が粗末だったためでした。ディスプレイ表示によるポジショントラッキングは、ゲーム等の開発に十分耐えうる品質(Oculus RiftやHTC Viveと比べればかなり低いですが)です。
ちなみに、名刺サイズの用紙に認識用イメージをプリントし、手の甲に貼り付ければ、簡易ハンドトラッキング(ハンドポジショントラッキングというのが正確)も可能な旨、確認しました。これらを駆使すれば、モバイルVRでも、Oculus RiftやHTC Viveの様な没入感のあるアプリを開発することもできそうですね。

Vuforia for UnityのExtended Tracking機能を利用してGoogle Cardboardでポジショントラッキング

はじめに

Google CardboardやGear VRなど、スマホを利用したいわゆる「モバイルVR」は、Oculus Rift/HTC Vive等と比較し、値段が安くお手軽にVR体験を楽しむことができます。一方で、Oculus Rift/HTC Viveには搭載されているハンドデバイス(Oculus RiftのOculus Touchはこの2016/12に出荷予定)や、ポジショントラッキング等、VRの没入感を生み出すための重要な機能が、モバイルVRには搭載されておりません。
モバイルVRでは、立体視+ヘッドトラッキング(顔=デバイスの向きをトラッキング)により、顔の向きとCG空間内の視線方向を一致させることで、あたかも自分が向いている方向のCGを立体視しているような錯覚を生み出し、これが没入感を生み出します。この「立体視+ヘッドトラッキング」だけでも、相当の没入感であり、私も最初にGoogle Cardboard(Oculus Rift DK1も)を見たときには相当な感動を覚えました。しかし、ポジショントラッキングやハンドデバイスが実装されているVR、特にHTC Viveのルームスケールなんかを体験してしまうと、「立体視+ヘッドトラッキング」だけでは物足りなさを感じるようになってしまいます。
ポジショントラッキングとは、その名の通り、ユーザの位置をトラッキングする機能です。ヘッドトラッキングでは顔の向きだけ任意の方向に向けることができましたが、例えば立ち上がったり・しゃがんだり・肩を左右に振っても、ユーザの視点位置は変化しません。しかし、ポジショントラッキング搭載のものであれば、例えば向こうから飛んでくるボールを避けるゲーム、なんてものも実現することができます。
Oculus Rift (DK2/CV1)では、ユーザの正面の専用のカメラを設置し、このカメラがOculus Rift本体の位置を捉えることでポジショントラッキングを実現しています。HTC Viveは、Oculus Riftと逆の方法、部屋の2角に赤外線を発光するベースステーションを設置し、ヘッドセットやハンドデバイスに内蔵のカメラで赤外線を受光することでトラッキングをしているようです。これにより、ルームスケールと呼ばれる「CG空間を歩き回る体験」や、まるで自分の手がCG空間に現れたかのような秀悦な操作感が実現されています。
f:id:tuti107:20161127153421p:plain
しかし、これらポジショントラッキングやハンドデバイスには高度が技術が必要となり、廉価版であるモバイルVRでの実現はコスト的に困難です。Vico VRのように、ポジショントラッキングや手足による入力に対応している(らしい、コンシュマー版は未出荷)ものもありますが、それなりのお値段のようです。
今回は、有名なARエンジンである「Vuforia」を利用して、簡易なポジショントラッキングを実現してみます。

Daydream Technical Preview

はじめに、前回説明した、Daydream technical previewのUnityを起動し、適当なプロジェクトを作成してください。
おそらく通常のUnity + Google VR SDK for Unityの組み合わせでの開発も可能かと思いますが、AndroidManifestがVuforia/Google VR SDKで競合するなど、いろいろと面倒な問題が発生するため、今回は割愛します。

Vuforiaのライセンスキー登録

Vuforiaを利用したアプリを開発する際、以下の手順でライセンスキーの登録が必要となります(下記の手順の場合は無料です)。

  1. Vuforiaにログインの後、こちらより、Add License Keyボタンを押下する。
  2. Project TypeよりDevelopmentを選択、Project Detailは、App Nameには適当な名前、DeviceはMobileを選択し、Nextボタンを押下する。
  3. "By clicking "Confirm" below, ..."をチェックし、Confirmボタンを押下する。
  4. License Managerに表示されるアプリ一覧より、2でApp Nameとして入力したものを選択する。
  5. License Keyが表示されるので、これをコピーする。
  6. Unityに戻り、ARCameraのインスペクタを開く。Vuforia BehaviorコンポーネントのApp License Keyにペーストする。

Targetの登録

次に、認識対象となるイメージの登録を、以下の手順で行います。

  1. Target Managerで、Add Databaseボタンを押下する。
  2. Create Databaseポップアップにて、Nameには適当なデータベース名を、TypeはDeviceを選択し、Createボタンを押下する。
  3. Target Managerのデータベース一覧に2で追加したデータベースが追加されるので、これを選択する。
  4. Add Targetボタンを押下する。
  5. Add Targetポップアップにて、TypeはSingle Image、Fileには適当なイメージファイル、widthには実世界(プリントアウトしたイメージ)のサイズ(単位はメートル)、Nameには適当な名前を入力し、Addボタンを押下する。
  6. Download Databaseボタンを押下し、Download Databaseポップアップにて、Unity Editorを選択し、Downloadボタンを押下して、データベース(unitypackage形式)をダウンロードする。
  7. ダウンロードしたパッケージをインポートする。すると、Assets/StreamingAssets/QCARに、データベースが読み込まれる。
  8. ARCAmeraオブジェクトのDatabaseLoadBehaviorコンポーネントに、Load データベース名 Databaseというチェックが現れるので、これをチェックする。また、これをチェックすると現れる「Activate」もチェックする。

f:id:tuti107:20161127200849p:plain

VuforiaがTargetを認識した時の処理

  1. Assets/Vuforia/Prefabs/ImageTargetをシーンに追加する
  2. ImageTargetオブジェクトのImageTargetBehaviorコンポーネントについて、インスペクタにて、TypeはPredefined、Databaseはデータベース名、Image Targetはターゲットの名前を選択する。また、Enable Extended Trackingにチェックを入れる。
  3. f:id:tuti107:20161127201605p:plain
  4. ImageTargetオブジェクト配下に適当なオブジェクト(以下の図の場合はCube)を適当なサイズで配置する。なお、Extended Trackingの効果をわかりやすくするために、縦方向に長くしています。
  5. f:id:tuti107:20161127202058p:plain
  6. 認識用のイメージを印刷する。サイズはAdd Targetにて設定した横幅とする。

フロントカメラ搭載のノートPCやMacをご利用の方は、エディタ上にて実行し、印刷したターゲットのイメージにカメラを向けると、上記のCubeが撮影イメージ上に表示されます。また、Extended Trackingをチェックすると、カメラからターゲットのイメージが外れてしまってもCubeが適切な位置に表示され続けます。つまり、一度でもターゲットのイメージを認識すると、その後はターゲットのイメージの周囲の情報をリアルタイムに認識することで、ターゲットのイメージがカメラ内に無くてもまるでターゲットのイメージがカメラ内に捉えられているかのごとく振舞います(当然、周囲に動くものがある場合はダメなようです)。
www.youtube.com

Google Cardboardとのインテグレーション

  1. ARCamera配下のCameraをDisableする。また、ARCameraオブジェクトのVideoBackgroundManagerコンポーネントのEnable video backgroundチェックを外す。これらは、スマホで撮影したカメラプレビューを背景映像として表示するためのものですが、ポジショントラッキングの用途には不要なためです。
  2. ImageTarget配下のCube(もしくは上記で追加した何らかの3Dオブジェクト)をDisableする。こちらもポジショントラッキングには不要なためです。
  3. シーンのルートにEmptyObject(名前をPlayerとする)を配置、そこにCameraをぶら下げる。このCameraのTagはMainCameraとする。
  4. 以下のPositionTracker.csを作成し、Playerにコンポーネントとして設定する。
  5. using UnityEngine;
    using System.Collections;
    
    public class PositionTracker : MonoBehaviour {
      public GameObject arCamera;
    	
      void Update () {
        var p = arCamera.transform.position;
        gameObject.transform.position = new Vector3 (p.x, p.z, -p.y);
      }
    }
    

  6. Cameraの前に適当な3Dオブジェクト(Cubeなど)をシーンに配置する。

  7. Assets/Plugins/Android/AndroidManifest.xmlのandroid:minSdkVersionを"16"に変更する。
  8. 前回説明した手順にて、Virtual Reality Supportedをチェック、SDKをCardboardとして、Android向けにビルドする

品質は微妙

アプリ起動前に、ターゲットイメージを印刷した紙を(多少ごちゃごちゃした)壁・棚などに貼り付け、この紙の方を向いて端末を起動してください。
f:id:tuti107:20161127220737p:plain

紙に近づけばCubeが大きく、左右に動いたりしゃがんだりすればCubeはそれぞれの方向に動きます。ちゃんとシーンを構成すれば一応ポジショントラッキングのあるVR、な感じになります。以下は、Editorで実行し、前後上下左右にMacを動かした際のものです。
f:id:tuti107:20161128211500g:plain

ただ、品質があまりよくありません。静止していてもプルプル動くし、時々変な方向に飛んでしまうし。。シンプルにARCameraの座標を使うのでは無く、ちょっと一工夫が必要のようです。
また、私のAndroid端末(Galaxy S6)でGoogle Cardboard使用中にデバイスのカメラを利用するためには、Google Cardboardに切れ込みを入れる必要がありました。。
f:id:tuti107:20161127220810j:plain

Daydream technical previewを使ってみる

はじめに

先月注文したPixelとDaydream View、ようやく発送されました。ただ諸事情により手元に届くのは、あともう二週間後になりそうです。
今回は、実機入手後すぐに開発にとりかかるべく、Unity Daydream technical previewの導入を試してみたいと思います。

Daydream technical previewのインストール

f:id:tuti107:20161126122619p:plain
Daydream technical previewは、こちらよりダウンロードできます。本ページをずっと下にスクロールしていくと、インストーラのダウンロードボタンが見つかります。

インストールが完了しましたら、デスクトップに生成されたショートカットより、Daydream technical previewを起動します。
f:id:tuti107:20161126122959p:plain
すると、「普通」にUnityが起動します。ただしタイトルバーを見ると「Unity 5.4.2f2-GVR12」となっており、これがGoogle VR向けのものであることが確認できます。
f:id:tuti107:20161126123258p:plain

NativeサポートされたGoogle Cardboard

まずは(手元に実機もありませんので)Google Cardboard向けのアプリを作ってみます。
前回Google VR SDK for Unityを利用したアプリの作成について詳細をした際は、「GoogleVRForUnity.unitypackage」をインポートしましたが、Daydream technical previewではGoogle CardboardがNativeサポートされているため、立体視表示やヘッドトラッキング等のVRの最低限の機能については、本SDKを利用する必要はありません。その代わりに以下の通りの設定を行います。

  1. File→Build Settingsより、PlatformをAndroidとする
  2. Player SettingsのOther Settingsにて、Virtual Reality Supportedにチェックを入れる
  3. +ボタンを押下し、Cardboardを選択する
  4. 同じくOther Settingsにて、Minimum API Levelを16以上とする
  5. Bundle Identifierに適当なものを設定する

f:id:tuti107:20161126125443p:plain

f:id:tuti107:20161126125524p:plain

以上を設定の後、Build and Runにて、Android端末上にCardboard向けVRアプリが起動します。
アプリといっても、Sceneに何も配置していないので、デフォルトのSkyboxが表示されるのみですが。。
f:id:tuti107:20161126130036p:plain

さいごに

今回は、Daydream technical previewのインストールし、とりあえず実機上で動くアプリをビルド・実行してみました。
次回は、Gaze入力等、もう少し詳細まで踏み込んでみたいと思います。

Google VR SDK for Unityをより詳細に見てみる

前回は、Google VR SDK for Unityをインストールし、付属のDemo SceneをとりあえずAndroid端末上で実行してみました。特段なにもはまることなく、スムーズに実行まですすめることができました。
一方で、Demo Scene内のオブジェクトを見てみると、Google Cardboard SDKのときにはなかった色々な名前のオブジェクトがあることに気づきました。実行画面のぱっと見、Demo SceneはGoogle Cardboard SDKのものとほぼ同じなのですが、SDK自体は大きく異なるのかも?
f:id:tuti107:20161021214817p:plain
ということで、今回はGoogle Cardboard SDKとの差異を中心に、Google VR SDK for Unityついて、詳細に見てみました。

ほぼ一緒でした

結論からいうと、Google VR SDK for UnityとGoogle Cardboard SDKは概ねよく似ております。名前や機能分割による違いがメインかと思います。
Google Cardboard SDKは、

  1. CardboardMainというprefabを設置→これだけで左右眼分割表・樽状のゆがみ表示・ヘッドトラッキングの「VRっぽい機能」が動きます
  2. EventSystemにGaze Input Moduleを追加し、CardboardMainにGazeポインタを設置→ユーザが見ている位置にポインタが表示されます
  3. EventTriggerを実装した適当なオブジェクトを用意→上記ポインタがオブジェクトに入る・出る・クリックする等のイベントを受けることができます

との手順で、VRアプリを簡単に作成することができましたが、Google VR SDK for Unityでは、

  1. GvrViewerMain prefabを設置→左右分割・樽状のゆがみ表示・ヘッドトラッキングを行う
  2. EventSystemにGaze Input Moduleを追加し、Main CameraにGazeポインタとなるGvrReticle(円形のポインタ)を置く
  3. EventTriggerを実装した適当なオブジェクトを用意

ということでほぼ一緒です。Google Cardboard SDKでごちゃごちゃしていて複雑だったところが、すっきりした、という感じです。

GvrViewerMain

本オブジェクトは、左右分割・樽状のゆがみ表示・ヘッドトラッキングといったVR的機能を提供します。
f:id:tuti107:20161021220843p:plain

  • VR Mode Enabledは、VR表示(左右眼分割表示)をするかどうかを設定
  • Distortion Correction Enabledは、樽状の歪み表示をするかどうかを設定
  • Stereo Screen Scaleは、1より低い値とすると画質悪いが高速、1より大きい値(最大2)とすると高精細だが低速となる
  • Neck Model Scaleは、首のオフセットを考慮したより正確なヘッドトラッキングを行う。0(未使用)~1の値を設定。Daydream端末だとわかる感じなのでしょうか?私には設定時の違いがあまりわからず。。

Main Camera

こちらは、Google Cardboard SDKとほぼ一緒です。
f:id:tuti107:20161022083056p:plain
留意点としては、AudioListenerがGvrの名称に変わっていること、くらいです。なお、Google Cardboard SDKと同様なのですが、Physics Raycasterを追加しておかないと、Gazeポインタが表示されません。
GvrAudioListenerによる音空間設定については、また別途の機会に触れたいと思います。

GvrReticle

円形上のGazeポインタです。MainCamera配下にこれを配置することで、向いている方向中央に
- ポイントが乗った・外れたこと(PointerEnter/PointerExit)通知を受けるオブジェクトがある場合は、大きな円に
- それ以外は点で
ポインターが表示されます。
f:id:tuti107:20161022084337g:plain
インスペクタで、表示される円について詳細設定ができます。
f:id:tuti107:20161022091236p:plain

  • Reticle Segmentsは、円描画の点の数を指定します。この値が少ないと円ではなく多角形な感じになります(以下は5を指定の場合)

f:id:tuti107:20161022090925p:plain

  • Reticle Growth Speedは、ポインタがオブジェクト上に乗った際に、点→円になる速度を指定します。すくないほどゆっくりとなります。

GvrReticleは、後述のGazeInputModuleを利用して、オブジェクトに乗った・外れた等の判定をします。なお、この(複雑な)GazeInputModuleを使用せず、シンプルにポインタ表示を行う手段としてGvrGazeがあります(こちらもいずれ紹介しようと思っています)。

GazeInputModule

EventSystemにGazeInputModuleを追加することで、Unityにおけるマウス・キーボード等の入力イベントの共通配信システムであるEventSystemを利用してGazeポインタ関連イベントを利用できるようになります。
例えば、DemoSceneの足元には、以下のようなボタンが配置されていますが、
f:id:tuti107:20161022093134p:plain
これらボタンが押下された場合の処理は、プログラムを記述する必要なく、簡単に設定することができます。
f:id:tuti107:20161022093422p:plain
この場合は、Cubeオブジェクト、Teleportコンポーネントの、ToggleVRMode()メソッドが、ボタンクリック時に呼び出されます。
EventSystemによるGaze関連イベントはボタン等のGUI部品のみならず、自作のGameObjectにも設定可能です。
DemoScene内のCubeオブジェクトには、以下のようにEventTriggerコンポーネントが追加されており、そこでPointerEnter/PointerExit/PointerClickがそれぞれ追加されております。このようにすることで、このCubeオブジェクトはGazeポインターが入ったとき・出たとき・クリックされたときにそれぞれ指定のメソッドが呼び出されるようになります。
f:id:tuti107:20161022094323p:plain

さいごに

以上、今回はGoogle Cardboard SDKとGoogle VR SDK for Unityの差異を中心に、Google VR SDK for Unityの詳細を見ました。これら2つは非常に似た感じですが、Google VR SDK for Unityのほうが、よりシンプルにまとめられていると感じました。
今回ご紹介の内容だけで、簡単なVRコンテンツは充分に開発可能かと思います。次回は、音響周りを中心に見ていこうかな、と思っています。

Google VR SDK for Unityを使ってみる

Googleの10月4日のイベントで、最新のVRヘッドセットDaydream Viewが発表されました。軽くて装着感もよさそうだし、$79という低価格は非常に魅力的です!私も早速手に入れて色々開発してみたいところなのですが、当面日本での発売は予定されていないとのこと。残念・・
でも遅かれ早かれDaydream Viewも対応のAndroidデバイスも手に入るでしょうし、まずはDaydream向け/Cardboard向け統合のVR開発環境(?)であるGoogle VR SDKを入手し、事前に触ってみました。
今回は、Cardboard向けのデモアプリ実行まで。
f:id:tuti107:20161016150402p:plain

環境の準備

Unity用のGoogle VR SDKであるGoogle VR SDK for Unityを利用するためには、以下をインストールする必要があります。

まずは、デモをエディタ上で実行してみる

上記全てをダウンロード・インストールしましたら、Unityを起動して適当なプロジェクトを作成します。開発画面が起動しましたら、ProjectのAsset->右クリック->Import Package->Custom Packageを選択し、先程ダウンロードしたUnity VR SDK for Unityに含まれる「GoogleVRForUnity.unitypackage」をインポートします。
f:id:tuti107:20161016151320p:plain
インポートしたら、Assets→Google VR→DemoScenes→HeadsetDemoのDemoScene.unityをダブルクリックします。
f:id:tuti107:20161016151456p:plain
これでとりあえず実行してみると、
f:id:tuti107:20161016151520p:plain
f:id:tuti107:20161016151528p:plain
はい、無事に動きました。
デモは、Google Cardboard SDKと変わらない感じです。ただし、SDKの作りはGoogle Cardboard SDKとは異なる感じです(しっかりと見ていませんが、ぱっと見、シーンに配置されたオブジェクトには見覚えのないものが色々配置されているようです)。

Android端末上で実行してみる

次に、上記エディタ上で動かしたデモをAndroidの実機にて動かしてみます。
File→Build Settingsにて、Androidを選択し、
f:id:tuti107:20161016152132p:plain
Player Settingsにて、

  • Other Settingsで、Bundle Identifierを適当に、Minimum API levelをAPI level 19に設定

f:id:tuti107:20161016152322p:plain

  • Resolution and Presentationにて、Default OrientationをLandscape Leftに設定

f:id:tuti107:20161016152422p:plain
します。あとは、Build And Runボタンを押すだけです。
特段問題なく、Android端末上でデモアプリが動作しました。

おわりに

今回は、来るDaydreamに備えて、まずはGoogle VR SDK for Unityの導入と、Google Cardboard向けのデモアプリの実行までやってみました。
デモアプリを動かすまでは特に問題なく、という感じですが、上記のとおりSDKの使い方は、Google Cardboard SDKとは異なる印象です。
次回は、過去に作ったGoogle Cardboard SDK利用のアプリの移植を行って、Google VR SDK for Unityの分析を進めてみたいと思います。

ちなみに、過去Google Cardboard向けにKeyboardの開発を行っていましたが、
f:id:tuti107:20160625095127p:plain
Googleが同等のものを提供する予定のようですね。
f:id:tuti107:20161016153803j:plain
もっと早くに開発しておけばよかった。。

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で試したところ、無事動作しました。