今回はDepth Perception(深度認識)についてです。

Depth Perceptionとは

Depth Perceptionとは、カメラで撮影している世界において、奥行きを認識する機能となります。
深度カメラで深度を認識しているため、デバイスの性能によっては、近い距離だと認識に失敗するようです。
また、細かくは測定していないようで、ちょっとした段差だと認識に失敗することもありました。

深度認識例

ユーザが画面をタッチした箇所の深度を測定し、そこにPlaneを設置するアプリを作ってみました。
実際に使用したときの例が以下のような感じになります。

赤枠内にある灰色のPlaneがアプリで表示させたものとなります。
概要
左上のPlaneがティッシュボックスの側面をタッチしたことによるもの
右下のPlaneがスマートフォンをタッチしたことによるもの
ノートPCのアームレストをタッチしたことによるもの
ノートPCのパネル部分をタッチしたことによるもの
拡張ディスプレイのパネル部分をタッチしたことによるもの

Planeの色合いから、深度と傾きが考慮された表示に見えるかと思います。

実装概要

※ Tango Manager、Tango Point Cloud、Tango Camera prefabの追加設定については、以下のサイトを参照してください。
Setup for All Unity Tango Apps
Unity How-go Guide: Depth Perception

ただし、今回はTango Point Cloudのポイント表示機能を使用したくなかったので、以下のTango Point Cloudの設定はオフにしています。

TangoApplication、TangoPointCloudのインスタンス化

MonoBehaviour.Startをオーバーライドして、TangoApplicationとTangoPointCloudをインスタンス化しています。
そして、権限確認の実施とTangoPointCloudの開始します。

Assets/Scripts/DepthPerceptionMainController.cs - Startメソッド
mTangoApplication = FindObjectOfType<TangoApplication>();
if(mTangoApplication != null)
{
    // Depth Perceptionの有効化
    mTangoApplication.EnableDepth = true;
    // コールバックを受けるインスタンスの登録
    mTangoApplication.Register(this);
    // 権限要求 -> ユーザ確認 -> OnTangoPermissionsコールバック
    mTangoApplication.RequestPermissions();
}

Assets/Scripts/DepthPerceptionMainController.cs - OnTangoPermissionsメソッド
if (permissionsGranted) // 権限取得判定
{
    // TangoPointCloudのインスタンス化
    mTangoPointCloud = FindObjectOfType<TangoPointCloud>();
    if (mTangoPointCloud != null)
    {
        // Tango Point Cloudを開始 -> 本インスタンスにて後程Depth Perceptionを実施
        mTangoPointCloud.Start();
    }

    // Tango Serviceとの接続開始
    mTangoApplication.Startup(null);
}

タッチ位置にPlaneを設置

タッチした位置をハンドリングして、Tangoで管理しているPlaneを見つけます。見つかったら、そこに自分で用意したPlane型のPrefabをGameObject化して表示しています。
Assets/Scripts/DepthPerceptionMainController.cs - Updateメソッド
if (Input.touchCount >= 1)
{
    Touch touch = Input.touches[0];
    if (touch.phase == TouchPhase.Ended)
    {
        StartCoroutine(DisplayFoundPlane(touch));
    }
}
Assets/Scripts/DepthPerceptionMainController.cs - DisplayFoundPlaneメソッド
UnityEngine.Camera camera = UnityEngine.Camera.main;
UnityEngine.Vector3 foundPlaneCenter = new UnityEngine.Vector3();
UnityEngine.Plane foundPlane = new UnityEngine.Plane();

// タッチ位置の深度認識結果をPlaneとその中心座標で取得。
if (!mTangoPointCloud.FindPlane(camera, touch.position, out foundPlaneCenter, out foundPlane))
{
    mLogger.CategoryLog(LogCategoryMethodTrace, "not found plane");
    yield break;
}

// 表示するPlaneの重複判定
if (!mPlaneObjectTable.ContainsKey(foundPlane))
{
    mLogger.CategoryLog(LogCategoryMethodTrace, "first find plane: plane center = " + foundPlaneCenter.ToString());

    // Ensure the location is always facing the camera.  This is like a LookRotation, but for the Y axis.
    Vector3 up = foundPlane.normal;
    Vector3 forward;
    if (Vector3.Angle(foundPlane.normal, camera.transform.forward) < 175)
    {
        Vector3 right = Vector3.Cross(up, camera.transform.forward).normalized;
        forward = Vector3.Cross(right, up).normalized;
    }
    else
    {
        // Normal is nearly parallel to camera look direction, the cross product would have too much
        // floating point error in it.
        forward = Vector3.Cross(up, camera.transform.right);
    }

    /*
     * タッチされた場所の深度を測定して、そこのplaneに合うplaneを表示しようとしているけど、スクリーン座標のタッチ座標はZ軸方向の値が存在しない。
     * その状態でCamera.ScreenToWorldPointを実行するとXY座標もずれてしまうので、見つかったplaneの中心座標(ワールド座標)をスクリーン座標化し、
     * それで得られたplaneの中心座標のZ軸の値を疑似的にタッチ位置のZ軸方向の座標として使用する。
     */
    // planeの中心座標(ワールド座標)をスクリーン座標に変換
    Vector3 screenFoundPlaneCenter = camera.WorldToScreenPoint(foundPlaneCenter);

    // タッチ座標(XY軸方向のみのスクリーン座標)にplaneの中心座標(スクリーン座標)のZ軸方向の値を設定した上で、ワールド座標に変換
    Vector3 worldTouchPoint = camera.ScreenToWorldPoint(new Vector3(touch.position.x, touch.position.y, screenFoundPlaneCenter.z));

    // 疑似的に算出されたタッチ座標(ワールド座標)にオブジェクトを生成
    GameObject basePlane = Instantiate(mPlaneBase, worldTouchPoint, Quaternion.LookRotation(forward, up));
    basePlane.SetActive(true);
    mPlaneObjectTable[foundPlane] = basePlane;
}
今回は画面タッチから、そのままTangoPointCloud.FindPlaneを実施していますが、あくまで既に認識済みのPlaneから一致するものを見つけているだけのようです。
なので、Googleが推奨する実装方法としては、以下のようにTangoPointCloud.FindPlaneを実施する前にTangoApplication.SetDepthCameraRate(TangoEnums.TangoDepthCameraRate.MAXIMUM)に変更して、深度認識を一旦促進し、結果が出てからTangoPointCloud.FindPlaneをコールするようです。

https://github.com/googlesamples/tango-examples-unity.git - UnityExamples/Assets/TangoSDK/Examples/AreaLearning/Scripts/AreaLearningInGameController.cs - _WaitForDepthAndFindPlane
m_findPlaneWaitingForDepth = true;// <- OnTangoDepthAvailableにてfalseに戻される

// Turn on the camera and wait for a single depth update.
m_tangoApplication.SetDepthCameraRate(TangoEnums.TangoDepthCameraRate.MAXIMUM);
while (m_findPlaneWaitingForDepth)
{
    // OnTangoDepthAvailableがコールバックされるまで処理なし
    yield return null;
}

m_tangoApplication.SetDepthCameraRate(TangoEnums.TangoDepthCameraRate.DISABLED);

// Find the plane.
Camera cam = Camera.main;
Vector3 planeCenter;
Plane plane;
if (!m_pointCloud.FindPlane(cam, touchPosition, out planeCenter, out plane))
{
    yield break;
}

ただ、1回の深度認識を挟むことで、必ずしも知りたい部分の深度が認識されているかは保証されないので、必要に応じてGoogle推奨の方式にするか、今回弊社で試したような方式にするかを決めればよいかと思います。
上記(弊社作成の)サンプルアプリはこちらにアップしているので、興味がありましたら、参照してみてください。

コメントの投稿