凹みTips

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

VR 向け高性能ステレオカメラの Ovrvision Pro を手に入れたので詳しく調べてみた

本エントリは Oculus Rift Advent Calendar 2015 12 日目の記事になります。昨日は @MuRo_CG さんによる「アニメの雰囲気をVRに持ってくる方法」でした。以前 Gear VR のデモを見せていただきましたがすごいアニメでした!(語彙力がない)

はじめに

Ovrvision ProVR 向けのステレオカメラで、前モデルの Ovrvision 1 から大幅に性能がアップデートしたモデルです。

今年 4 月末に Indiegogo でキャンペーンを終了してから 7 ヶ月とかなり速い到着でした。キャンペーンサイトには拙作のデモも載せていただきました!

Indiegogo で back していた当時は $254 でしたが、現在は \49,800 で Amazon から購入することが出来ます。

本エントリでは、Ovrvision Pro の紹介と SDK の簡単な解説をしたいと思います。

環境

スペック

スペックの詳細は以下に記載されています。

抜粋すると以下の様な感じです。

解像度

解像度(片目) FPS 角度 (H / V)
2560x1920 15 115 / 105
1920x1080 30 87 / 60
1280x960 45 115 / 105
1280x800 60 115 / 90
960x950 60 100 / 98
640x480 90 115 / 105
320x240 120 115 / 105

FPS を落として解像度を上げたり、逆に解像度を下げて FPS を上げる方式もあり、VR 以外にも使えそうです。60 fpsVR モード時は Ovrvision 1(1280x480)の約 4 倍になります。

画角

  • 最大水平角 115°
  • 最大垂直角 105°
  • VR 推奨モードは 100°/ 98°

カメラ

  • F 1.8
  • EFL 2.5
  • WDR 69 db

Ovrvision 1 の頃の暗かった画像が改善されています。

重さ

  • 基板 35 g
  • ケース 30 g

その他

  • USB 3.0
    • Oculus Rift 本体の USB ポートでは動作しません
  • GPIO
    • ハードウェア連携用に GPIO が空いています
  • EEPROM
    • ハードウェア本体に設定(露出や輝度、ゲイン、ホワイトバランス等)を保存できます

SDK のダウンロード

以下より各プラットフォーム向けの SDK がダウンロード可能です。

現在(2015/12/08)のところ、Windows のみ(C++ / C#)対応で、Mac / Linux はそれぞれ 2015/122016/3 に公開予定とのことです。ゲームエンジンには Unity に対応していて Unreal Engine 4 には 2016/1 に対応予定のようです。ライセンスは MIT です。

ドキュメント

SDK にはドキュメントが同梱されており、docs/index.html を開くと doxygen で生成されたドキュメントを閲覧できます。一部のツールの使用方法と C++ のクラスや関数の説明のみ記載されています。

SDK 同梱アプリ

ovrvision_app_csharp.exe

bin/x86 に入っています。カメラのパラメタの設定、EEPROM への保存も可能です。解像度の変更のテストや、後述する Demosaic や Undistortion の ON / OFF も見れます。

f:id:hecomi:20151209021402p:plain

ovrvision_app.exe

C++ で書かれたもの。一番低負荷で高速。

ovrvision_oculus_app.exe

Oculus Rift 用のシンプルなビューワ。

ovrvision_figure.exe

demo に入っています。metaio を利用したサンプルで、パッケージの箱を移すとキャラクタがその上に立っている様子が見れます。

Calibration ツール

tool ディレクトリに入っています。これを行うことで視差やスケールが補正されるのでまず最初にやっておきましょう。

その他アプリ

SDK と同じページの下部からダウンロードできます。

Hand Tracking

付属の指サックを使ってトラッキングするデモ。ドキュメントの「How to Finger Tracking」に使い方が書いてあります。G キーでキャリブレーションのビュー(片目のみ表示)になり、青丸と赤丸が表示されるので青に指サックを、赤に指を合わせてスペースキーを押下して色を登録、その後に H キーを押すとキャリブレーションのビューが終了します。

Interactive Chroma Key

クロマキーのデモ。閾値が調整できず、本アプリではまだうまく動作させられていません。。代わりに Unity 上ではうまく動きます。

Mode Change

様々な解像度、FPS で Ovrvision Pro をテストできます。例えば 640x480_90_mode_oculus.cmd を動かすと Oculus Rift で 640x480 解像度、90 FPS で動くアプリが立ち上がります。ちなみに 90 FPS、120 FPS はめちゃめちゃ速くて遅延をあまり感じずとても感動しました。

今後も Effect Synthesis(リアルタイム画像エフェクト)、Image Analytics(距離測定など)、Config Save(EEPROM への設定保存)、Firmware(GPIO の活用)などのアプリケーションが順次配布予定のようです。

SDK 処理

Ovrvision から得た画像の生データは 1 ピクセル毎に 16 ビットのデータを持っており、上位 8 bit が左目、下位 8 bit の Raw Bayer データになります。

生画

f:id:hecomi:20151211235512j:plain

拡大

f:id:hecomi:20151212125937p:plain

像が重なっているのは左右の目の視差によるもので、ドット状になっているものが Raw Bayer によるものです。これを GPU 側で左右の画に分割し、Demosaic(Raw Bayer を普通の画に変換する、Debayer)処理をして Undistortion を行い、GPU から処理後の画をダウンロードして CPU 側で処理を行い表示します。

f:id:hecomi:20151130103909j:plain

Unity 向け SDK 導入

導入

Unity 用 SDK をダウンロードし、unity5_ovrvision.unitypackage をクリックしてインポートします。VR サポートのおかげでファイル数も少なくシンプルです。

f:id:hecomi:20151212142202p:plain

main シーンを開き、Player SettingsVirtual Reality Supported にチェックを入れて実行すれば Editor 上および Oculus Rift 上に画が映ります。

f:id:hecomi:20151212142816p:plain

f:id:hecomi:20151212142754p:plain

Unity 向け SDK 利用方法

外観

ルート要素の OvrvisionProCameraOvrvision コンポーネントがアタッチされており、インスペクタはゲーム停止時は以下のようになっています。

f:id:hecomi:20151212152851p:plain

解像度の選択

v1.2 から解像度がインスペクタから選択できるようになりました。

f:id:hecomi:20151212153001p:plain

実行後はプルダウンリストが消え変更はできません。

f:id:hecomi:20151212155408p:plain

カメラ画の調整

Overlay Ovrvision settings をチェックすると画を調整ためのスライダが表示されます。上から、露出、ゲイン、逆光補正、ホワイトバランスの自動調整の ON/OFF、ホワイトバランス RGB 値の補正になります。

f:id:hecomi:20151212155430p:plain

AR

OvrvisionSDK での ARSDK はマーカレスは Metaio、マーカ有は ArUco と Ovrvision 1 の頃と同じ構成になっており、Metaio に関してはデモがあるのみで未だユーザサイドで自由には使えないようです。余談ですが Metaio は Apple に買収されたので今後(Metaio の)SDK のアップデートはされなくなります。

Unity から使えるのは現状 ArUco のみになります。コードは色々変更されていますが、基本的な仕組みは以前と同じです。

Use the OvrvisionAR にチェックを入れると AR のための処理が行われます。現実の世界の大きさと Unity の世界の大きさを正確に結びつけるためにマーカのサイズを指定することが必要で、AR Marker size にメートル単位で数値を入力します。

f:id:hecomi:20151212165439p:plain

そしてマーカに紐付けてオブジェクトを表示するには 1 対 1 で対応する GameObject を用意する必要があり、サンプルでは OvrvisionTracker オブジェクトがそれにあたります。

f:id:hecomi:20151212165634p:plain

このオブジェクトがマーカの中心になるので、ローカル座標に適当なオブジェクトを配置すると表示されるようになります。

f:id:hecomi:20151212173127p:plain

マーカは 10 個まで増やせます(コード中で 10 個に制限されています)。将来的に可変になって欲しいですね([http://tips.hecomi.com/entry/2015/09/16/014119:title=可変の例)。

クロマキー

Camera Overlay からカメラ画のシェーダを選択できます。

f:id:hecomi:20151212174014p:plain

Normal Shader は単純に背景としてカメラ画を使うものです。Chroma-key Shader は指定した HSV(色相、彩度、明度)の範囲内の色を抜いてくれるものです。フラグメントシェーダで以下のように範囲内の色を discard(カメラ画を描画しない)する処理を行います。

float4 frag (v2f i) : COLOR0
{
    float3 colors = tex2D(_MainTex, i.uv_MainTex).rgb;
    float3 hsvcolor = RGBtoHSV(colors);

    if(hsvcolor.r >= _Color_minh && hsvcolor.r <= _Color_maxh) {
        if(hsvcolor.g >= _Color_mins && hsvcolor.g <= _Color_maxs) {
            if(hsvcolor.b >= _Color_minv && hsvcolor.b <= _Color_maxv)
                discard;
        }
    }

    return float4(colors,1.0); 
}

基本的には Ovrvision 1 と同じなので、詳細については以下の記事をご参照下さい。

例えばパラメタを調整して緑を抜くとこんな感じになります。

ハンドマスク

クロマキーの逆で特定の範囲の色だけ残します。HSV に加え YCbCr(輝度、青成分、赤成分)のパラメタが増えます。

f:id:hecomi:20151212190533p:plain

シェーダを読んでみたところ動作としては、指定した色相(Max Hue / Min Hue)(赤の彩度はちょうど範囲をまたがっているのでこのような指定になってると思われる)で、彩度・明度は範囲内の場合は色を残し、更にその残した色でも YCbCr の範囲内にあったら除く、という処理になっています。

//HSV
if(hsvcolor.r <= _Color_minh || hsvcolor.r >= _Color_maxh) {
    if(hsvcolor.g >= _Color_mins && hsvcolor.g <= _Color_maxs) {
        if(hsvcolor.b >= _Color_minv && hsvcolor.b <= _Color_maxv)
            alpha=1.0;
    }
}

//YCbCr
if(ycbcrcolor.r >= _Color_minY && ycbcrcolor.r <= _Color_maxY) {
    if(ycbcrcolor.g >= _Color_minCB && ycbcrcolor.g <= _Color_maxCB) {
        if(ycbcrcolor.b >= _Color_minCR && ycbcrcolor.b <= _Color_maxCR)
            alpha=0.0;
    }
}

なので流れとしては、残したい色を HSV の範囲でまず指定、そこから除外したい色を YCbCr で指定、という感じになると思います。クロマキー共にですが、抜く色はカメラから貰う画を元に判断するため、露出やホワイトバランスといったパラメタにも影響を受けるので注意しましょう。適当に調整してみた結果が以下の様な形です。

ディスプレイに映った手も抜けてしまっていますが...、グリーンバックを用意しなくても手を表示できます。

指サックトラッキング

同梱された指サックを Z も含めトラッキングします(もしくは視差でそう見えるだけかもしれません)。使い方は前述したデモアプリ同様です。ただ残念ながらトラッキングの結果は直接画像に描画されているようで、位置はコードから取得できないようです。また、まだ生値っぽい動きなので今後の改良に期待したいです。うまく調整できると以下のように動きます。

仕組み詳細

もう少し深く仕組みを見ていきましょう。

スクリプト概要

スクリプトは 3 つ、シェーダは 4 つあります。

  • COvrvisionUnity.cs
    • DLL のラッパーでカメラの Open / Close、画像の取得、カメラパラメタの設定・取得
  • Ovrvision.cs
  • OvrvisionTracker.cs
    • AR マーカに紐づくオブジェクトの位置姿勢の制御
  • ovTexture.shader
    • 通常のカメラ画を描くシェーダで全体のバックグラウンドに描画
    • Normal Shader で利用
  • ovChromaticMask.shader
    • 前述のクロマキー処理で利用するシェーダ
  • ovHandMaskRev.shader
    • 前述のハンドマスク処理で利用するシェーダ
  • ovShadowShader.shader
    • AR オブジェクト用に透明なプレーンに影を描画するシェーダ

画像の取得

今回、画像は低レベルネイティブプラグインインターフェースを使ってネイティブ側にポインタを渡すことで高速に描画する形になったようです。具体的には、Ovrvision.Update() 内で以下のように DLL の関数へテクスチャのポインタを渡しています。

OvrPro.UpdateImage(
    CameraTexLeft.GetNativeTexturePtr(),
    CameraTexRight.GetNativeTexturePtr());

低レベルネイティブプラグインインターフェースについては以下の記事をご参照下さい。

AR オブジェクトに影をつける

このシェーダを適用したマテリアルをつけたプレーンをボックスの下に置けば影が表示されます。

f:id:hecomi:20151212230821p:plain

デフォルトでは真っ黒なので、以下のように影の色を指定できるようにシェーダに変数を追加するのがオススメです。

Shader "Ovrvision/ovShadowShader" {
Properties { 
    _MainTex ("Base (RGB) TransGloss (A)", 2D) = "white" {}
    _ShadowColor ("Shadow Color", Color) = (0, 0, 0, 1)
} 
SubShader { 
    Tags {"RenderType"="Opaque" "Queue" = "Geometry" "LightMode" = "ForwardBase"}
    LOD 100
    Pass {
        Blend SrcAlpha OneMinusSrcAlpha 
        CGPROGRAM 
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase
            #include "AutoLight.cginc"

            struct v2f { 
                float2 uv_MainTex : TEXCOORD1;
                float4 pos : SV_POSITION;
                SHADOW_COORDS(3)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _ShadowColor;

            v2f vert (appdata_base v) {
                v2f o;
                o.uv_MainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                TRANSFER_SHADOW(o);
                return o;
            }

            float4 frag (v2f i) : COLOR {
                half4 c = tex2D(_MainTex, i.uv_MainTex);
                c.rgb = _ShadowColor.rgb;
                c.a *= (1 - min(SHADOW_ATTENUATION(i), 1)) * _ShadowColor.a;
                return c;
            }
        ENDCG
        }
    }
    Fallback "VertexLit"
}

結果はこんな感じです。

f:id:hecomi:20151212231152p:plain

AR の処理

AR の処理も DLL 内で行われ、これを Ovrvision.OvrvisionARRender() 内で以下のように取得しています。

float[] markerGet = new float[MARKERGET_MAXNUM10];
GCHandle marker = GCHandle.Alloc(markerGet, GCHandleType.Pinned);
int ri = OvrPro.OvrvisionGetAR(marker.AddrOfPinnedObject(), MARKERGET_MAXNUM10);
...
marker.Free();

得られた情報は以下のように取り出して ID の一致する OvrvisionTracker コンポーネントへ引き渡されます(ID はインスペクタから設定できます)。

var otobjs = GameObject.FindObjectsOfType<OvrvisionTracker>();
foreach (var otobj in otobjs) {
    otobj.UpdateTransformNone();
    for (int i = 0; i < ri; i++) {
        if (otobj.markerID == (int)markerGet[i * MARKERGET_ARG10]) {
            otobj.UpdateTransform(markerGet, i);
            break;
        }
    }
}

OvrvisionTracker.UpdateTransformNone() では、マーカを見えない場所へ移動させる処理が書かれていて、ひとまずすべての OvrvisionTracker コンポーネントを持つオブジェクトに対して行われます。

public void UpdateTransformNone() 
{
    if (!MovieOVRCameraRig) {
        hideTime += Time.deltaTime;
        if (hideTime >= stopTime) {
            transform.localPosition = 
                new Vector3(-10000.0f, -10000.0f, -10000.0f);
            hideTime = stopTime;
        }
    } else {
        if (OvrvisionProCameraObj != null) {
            OvrvisionProCameraObj.transform.localPosition = 
                new Vector3(-10000.0f, -10000.0f, -10000.0f);
        }
    }
}

ここで 2 つの変数、MovieOVRCameraRigstopTime が出てきました。これらはインスペクタから変更できる値で、以下の様な働きをします。

  • MovieOVRCameraRig
    • カメラのローカル座標系にするかどうか
    • Movetypo
  • stopTime
    • マーカを見失った後、何秒後に隠すか

f:id:hecomi:20151212220023p:plain

そして見つかっていた場合は以下のように移動を行います。

public void UpdateTransform(float[] markerGet, int elementNo)
{
    int i = elementNo * MARKERGET_ARG10;
    if (!MovieOVRCameraRig) {
        transform.localPosition = new Vector3(
            markerGet[i + 1] + offsetPos.x,
            markerGet[i + 2] + offsetPos.y,
            markerGet[i + 3] + offsetPos.z);
        transform.localRotation = new Quaternion(
            markerGet[i + 4],
            markerGet[i + 5],
            markerGet[i + 6],
            markerGet[i + 7]);
    } else {
        if (OvrvisionProCameraObj != null) {
            var pos = new Vector3(
                markerGet[i + 1],
                markerGet[i + 2],
                markerGet[i + 3]);
            var qat = new Quaternion(
                markerGet[i + 4],
                markerGet[i + 5],
                markerGet[i + 6],
                markerGet[i + 7]);
            setCameraTrackerPosition(pos, qat, OvrvisionProCameraObj);
        }
    }
    hideTime = 0.0f;
}

offsetPos はコードを読むと、おそらく IPD を 6.4 cm と仮定した時に中心にオブジェクトを持っていくために 3.2 cm だけ移動しているものだと思われます。カメラローカルな座標系で得られた位置・姿勢をセットすることによりキャリブレーションがしっかりしていれば現実と同じ大きさで AR オブジェクトが描画されます。

MovieOVRCameraRig にチェックが入っている場合は、setCameraTrackerPosition() を経由して得られた位置・姿勢のトランスフォームの逆行列を求めてカメラ側を AR オブジェクトが正しい位置に来るように移動します。これにより、使えるマーカは 1 つのみ、という制約ができますが、例えば揺れもの(RigidBody)がついているキャラクタを AR オブジェクトにした時に、その揺れものが影響を受けなくなったりはします。

AR オブジェクトのトラッキングの改善

Oculus Rift はポジショントラッキングがついており、ArUco では現実の世界と同じ位置・姿勢を返してくれます。なので、個人的にはカメラローカルにオブジェクトを置いたり、ワールドに置いてはいるもののカメラ自身を動かしたりするよりも、カメラは動かさずに AR オブジェクトをすべてカメラローカル座標になるようにワールドに置くのが良いのではないかと思います。こうするとマーカを見失った後でも Oculus Rift 側のヘッドトラッキングとポジショントラッキングによって AR オブジェクトが正しい位置に配置され続けると思われるからです。

方法はいたって簡単で以下のように位置・姿勢を与える前後で parent の登録・解除をしてあげるだけです。

public void UpdateTransform(float[] markerGet, int elementNo)
{
    ...
    if (!MovieOVRCameraRig) {
        transform.parent = GameObject.Find("LeftCamera").transform;
        transform.localPosition = new Vector3(markerGet[i + 1] + offsetPos.x, markerGet[i + 2] + offsetPos.y, markerGet[i + 3] + offsetPos.z);
        transform.localRotation = new Quaternion(markerGet[i + 4], markerGet[i + 5], markerGet[i + 6], markerGet[i + 7]);
        transform.parent = null;
    } else {
        ...
    }
}

ピッタリ合うわけではないですが、おおよその雰囲気はこっちの方が良いかな、と個人的に思いました。

その他生じたエラーなど

実行中のエラー

しばらく実行していると Unity、exe に限らず下記のように Runtime エラーが出ることがあります。

f:id:hecomi:20151212150000p:plain

おそらくスレッド周りの処理にバグが有るのかな...、と思うので近いうちに対応されると期待しています。

点滅

90 fps、120 fps で動かしている時に露出を 9000 以上あたりにすると周期的にホワイトバランスの調整が入るような点滅になります。公式に問い合わせてみます。

キャリブレーション

色々試していると以下のように画が正常に表示されなくなる時がありました。

f:id:hecomi:20151208191247p:plain

Demosaic は上手くいっているのでハードの問題ではなくソフトの問題で、おそらく Undistortion がダメだろうと思い tools ovrvision_calibration.exe を実行してキャリブレーションしたところ正常に戻りました。

f:id:hecomi:20151208191704p:plain

キャリブレーション情報が保存されているファイルが見当たらなかったので、ハードウェア側に保存されているのかもしれません(EEPROM への書き込み時にフリーズした後に起きた気もするので...)。

おわりに

Ovrvision 1 の頃と比べ、解像度、明るさ、速度どれを見ても向上していてすごいです。特に 90 fps 以上で動かした時は遅延が少なく自分の手を違和感なく動かすことが出来て驚きました。

作りたいネタがあるので頑張って作ってみようと思います。

明日は youten_redo さんの「GearVRでBLEポジトラするぞ」です。AR マーカでポジトラしようと思って諦めたことがあるので気になります!