OpenCV for Unityで炎を操る


はじめに

たまたまセールで半額になっていたので,思い切ってOpenCV for Unityを購入しました.
せっかくなのでマーカレスARをやろうと意気込んだものの,OpenCV for UnityでマーカレスARをやっている記事があまりなかったので躓いたところを書き残します.

OpenCV for Unityとは

OpenCV for Unityは,UnityでOpenCVを使用するための有料アセットです.
UnityでOpenCVを使用する方法はいくつかあり,以下の記事にわかりやすい比較があります.

以前WindowsでOpenCVSharpを使ったことがあったのですが,Macに乗り換えたのと,半額の衝撃でOpenCV for Unityを購入しました.

作ったもの

https://github.com/takaya901/DetectHand
(OpenCV for Unityのフォルダはignoreしています)


今回は練習ということで,手を認識して炎を操ってる感をだしてみました.

Unity+OpenCVでマーカレスAR

OpenCVで画像認識を行い,その情報をもとにUnityで3Dモデルやエフェクトを重畳することで,VuforiaやOpenCV単体ではできなかったマーカレスかつ3次元のARを実現することができます.Unityなのでクロスプラットフォームというのも強みですね.

ただし,ここで問題となるのが,カメラ映像はUnityのシーンと違って2次元であるということです.
マーカを使わない場合,我々はカメラ映像の奥行き情報を得ることができません.したがって,今回は2次元平面上の位置合わせのみを行いました.

仕組みは以下のとおりです.QuadにWebカメラからの映像を投影します.そして,映像から手の2次元位置を取得して,その位置にオブジェクトを生成します(Z座標はQuadの少し手前で固定).これらをMainCameraで撮影して画面に表示することで,無理やりですがマーカレスARを実現できます.

手順

シーン作成とスクリプトは"OpenCVForUnity/Examples/Advanced/ComicFilterExample"を参考にしました.

環境:
・MacOS Mojave
・Unity 2018.2.15f1
・OpenCV for Unity 2.3.2

OpenCV for Unityのセットアップ

以下の記事を参考にさせていただきました.
UnityでOpenCVを利用した顔検出・画像処理アプリ事始め

シーン作成

スクリプトでCamera.mainを参照するため,Main CameraのTagをMainCameraに設定します.また,ProjectionをOrthographicにします.Lightは不要です.

次に,Webカメラの映像を写すQuadを用意します.このQuadにスクリプトをアタッチしていきます.
Webカメラから取得したWebCamTexture型のデータをOpenCVで扱うMat型に変換するための機能が,WebCamTextureToMatHelperというクラスとして用意されています.解像度やFPS,モバイルのリアカメラ/フロントカメラの切り替えも管理してくれます.これをQuadにアタッチします.FPSを画面に表示してくれるFpsMonitorも必要に応じてアタッチします.
最後に,自分で作成する画像処理スクリプトをアタッチします(今回はDetectHnad.csとします).

  

ちなみに,Quadのマテリアルがデフォルトのままだと下の画像のように暗くなってしまいます."Assets/OpenCVForUnity/Examples/"にあるExampleMaterialに変えたらちゃんと表示されました.

スクリプト作成

WebCamTextureToMatHelperに関するメソッド

まず,ComicFilterExample.csから以下のメソッドをそのままコピペします.

  • OnWebCamTextureToMatHelperInitialized()
  • OnWebCamTextureToMatHelperDisposed()
  • OnWebCamTextureToMatHelperErrorOccurred()

OnWebCamTextureToMatHelperInitialized()内で,QuadのテクスチャにTexture2D型のフィールドを設定します.これにより,Update()内でそのフィールドを更新するとQuadに表示されるようになります.また,カメラの画角いっぱいにQuadが表示されるよう設定しています.

Update()は基本的に以下の流れになると思います.

1.WebCamTextureToMatHelperのGetMat()でカメラ映像をMat(RGBA)として取得
2.1で取得した画像に対して処理を行う
3.Utils.fastMatToTexture2D()で処理後のMatをQuadのテクスチャに変換する

手の検出

今回は簡単に,以下のような手順で検出しました.

1.画像から肌色の領域を抽出する
2.1で得た2値画像(肌色領域が255,それ以外は0)に対してラベリングを行う
3.ラベリングした中で面積が最大の領域を手とする

とても単純なやり方なので顔にも反応してしまいます.手のカスケード分類器を使えば顔の誤検出は防げますが,手は形状的にカスケード分類器による検出に向いていないらしく,諦めました.

メソッドは以下のとおりです.ただし,クラス名省略のために

using static OpenCVForUnity.Imgproc;
using static OpenCVForUnity.Core;

を指定しています.

void SetFire(Mat webcamMat)
{
    using(var hsvMat = new Mat()) 
    using(var handMask = new Mat())
    using(var centroids = new Mat())
    using(var stats = new Mat())
    {
        //RGBAをHSVに変換
        cvtColor(webcamMat, hsvMat, COLOR_RGBA2RGB);
        cvtColor(hsvMat, hsvMat, COLOR_RGB2HSV);

        //肌色領域を抽出
        inRange(hsvMat, SKIN_LOWER, SKIN_UPPER, handMask);

        //ラベリング
        var nLabels = connectedComponentsWithStats(handMask, new Mat(), stats, centroids);

        //最大の領域の重心を取得
        var maxAreaLabel = 0;
        var maxArea = 0.0;
        for (int i = 1; i < nLabels; i++) {           //0番目のラベルは背景のため飛ばす
            var area = stats.get(i, CC_STAT_AREA)[0];
            if (area > maxArea) {
                maxArea = area;
                maxAreaLabel = i;
            }
        }

        //画像上の重心位置をワールド座標に変換
        var ctrdOnImg = new Point(centroids.get(maxAreaLabel, 0)[0], centroids.get(maxAreaLabel, 1)[0]);
        var ctrdOnWorld = new Point((float)ctrdOnImg.x - webcamMat.width() / 2f, webcamMat.height() / 2f - (float)ctrdOnImg.y);

        //炎を手の位置に移動
        _fire.transform.position = new Vector3((float)ctrdOnWorld.x, (float)ctrdOnWorld.y, FIRE_Z_POS);
    }
}

引数のwebcamMatは,WebCamTextureToMatHelperのGetMat()メソッドで取得したRGBA画像です.

こちらの記事にあるように,Mat型のローカル変数はusing等を使ってリリースしたほうがいいです.Androidで実行すると結構落ちました.
ラベリング後の面積や重心の取得についてはこちらの記事に書きました.

おわりに

公式リファレンスがあんまり親切じゃないので,Java用OpenCVのリファレンスやコードを参考にするといいと思います.

手の検出に関しては"Assets/OpenCVForUnity/Examples/Advanced/HandPoseEstimationExample/"にサンプルがあります(参考動画).検出方法自体は肌色検出+ラベリングのようですが,立てている指の本数などの情報も取れるようなので,時間があるときにコードを見てみようと思います.