読者です 読者をやめる 読者になる 読者になる

ナノカ技術メモ

ただのエンジニア。何でも屋みたいな扱い受けてます。

OpenCV for Unityで輪郭を検出する

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」を利用してカメラで撮影した顔の輪郭を検出して座標を取得するまでの手順をメモしています。
こちらはUnity5.4.1で作業しました。

開発準備

  • 開発を行うためにAssetをImportしてください。
    madgenius.hateblo.jp
  • こちらのソースを使用して進めます。
    madgenius.hateblo.jp
    • SimpleScript.csをFaceScript.csに置き換えて進めます。
      先にカメラ映像の描画のみで動作するかお試しください。
  • 検出に使うカスケード*1を用意します。
    今回は輪郭を検出するhaarcascade_frontalface_alt.xmlを用意しました。

カスケードの追加

顔検出で使えるカスケードをScriptから使えるようにプロジェクトに追加します。
Assets > StreamingAssets > Cascadeにhaarcascade_frontalface_alt.xmlを追加しました。
f:id:nanokanato:20170412160845p:plain:w300

Scriptの修正

SimpleScript.csを顔検出のScript、FaceScript.csとして作り直しました。

  • 修正点まとめ
    • 検出した顔の取得にListを使うため「using System.Collections.Generic;」を追加
    • 取得したカスケードを保持するためのクラス変数 faceCascadeを用意
    • Start()メソッドでカスケードの読み込み
    • Update()メソッドにある「/* 画像加工開始 */」と「/* 画像加工終了 */」の間にカスケードを使用した顔検出の処理を追加


  • 引数による精度の調整
    顔検出には「detectMultiScale();」メソッドを使います。
    • 第一引数:CV_8U型のMat、このMatから顔を検出する
    • 第二引数:検出結果を格納するMatOfRect、検出した座標の配列になる。
    • 第三引数:画像スケールにおける縮小量、顔検出の精度を指定。
    • 第四引数:物体候補となる矩形が含まないといけない近傍矩形の数?
    • 第五引数:処理モードを指定?
    • 第六引数:物体として認識する最小サイズ
    • 第七引数:物体として認識する最大サイズ


using UnityEngine;
using UnityEngine.UI;
using OpenCVForUnity;
using System.Collections.Generic;

[RequireComponent(typeof(CVCameraMat))]
public class FaceScript : MonoBehaviour {

	public RawImage rawImage;
	private CVCameraMat cvCameraMat;
	private Texture2D texture;
	private bool startCVCam = false;

	private CascadeClassifier faceCascade;

	/*--------------------------------
     : Default Method
     --------------------------------*/
	// Use this for initialization
	void Start()
	{
		//カスケードファイルを読み込ませる
		string faceCascadePath = Application.streamingAssetsPath + "/Cascade/haarcascade_frontalface_alt.xml";
		faceCascade = new CascadeClassifier(faceCascadePath);
		if (faceCascade.empty())
		{
			Debug.LogError("NotFound file:'" + faceCascadePath + "'");
		}
		//カメラの初期化
		Init();
	}

	// Update is called once per frame
	void Update()
	{
		//カメラ画像の表示先があるか確認
		if (rawImage) {
			//カメラの取得準備ができているか確認
			if (startCVCam) {
				//CVCameraMatの初期化が終了している
				if (cvCameraMat) {
					//CVCameraMatが撮影中か確認
					if (cvCameraMat.isPlaying ()) {
						//CVCameraMatのフレームが更新されているか確認
						if (cvCameraMat.didUpdateThisFrame ()) {
							//Texture2Dが初期化されているか確認
							if (texture != null) {
								//カメラのMat画像を取得
								Mat cvCamMat = cvCameraMat.GetMat ();
								//Matが取得できているか確認
								if (cvCamMat != null) {
									//Matが空のデータじゃないか確認
									if (!cvCamMat.empty ()) {

										/* 画像加工開始 */

										if (!faceCascade.empty ()) {
											//全体のグレースケールの作成
											Mat grayMat = new Mat ();
											Imgproc.cvtColor (cvCamMat, grayMat, Imgproc.COLOR_RGBA2GRAY);

											//グレースケール画像のヒストグラムの均一化
											Mat equalizeHistMat = new Mat ();
											Imgproc.equalizeHist (grayMat, equalizeHistMat);

											//カスケードで検出
											MatOfRect faces = new MatOfRect ();
											faceCascade.detectMultiScale (equalizeHistMat, faces, 1.1f, 2, 0 | Objdetect.CASCADE_SCALE_IMAGE, new Size (equalizeHistMat.cols () * 0.13, equalizeHistMat.cols () * 0.13), new Size ());

											if (faces.rows () > 0) {
												List<OpenCVForUnity.Rect> rectsList = faces.toList ();
												for (int i = 0; i < rectsList.ToArray().Length; i++) {
													//検出した顔の座標取得
													OpenCVForUnity.Rect faceRect = rectsList[i];

													/* 検出した顔に対する処理開始 */


													/* 検出した顔に対する処理終了 */
												}
											}
										}

										/* 画像加工終了 */

										//加工したMatが存在するか確認
										if (cvCamMat != null) {
											//Matが空のデータじゃないか確認
											if (!cvCamMat.empty ()) {
												try {
													//cvCamMatをTexture2Dに変換して反映する
													cvCameraMat.matToTexture2D (cvCamMat, texture);
												} catch (System.ArgumentException e) {
													Debug.Log (e.Message);
												} catch (System.Exception e) {
													Debug.Log ("OtherError");
												}
											}
										}
									}
									cvCamMat = null;
								}
							}
						}
					}
				}
			}
		} else {
			Debug.LogError("NotFound:rawImage");
		}
	}

	/* 以下SimpleScriptと同じのため省略 */
}

顔の輪郭を取得してみる

  • Scriptを置き換えて実行してみる

実行してカメラで顔を撮影することで輪郭を検出してその座標(faceRect)を取得することができます。(複数可)

    • Update()メソッド内の「/* 検出した顔に対する処理開始 */」 と 「/* 検出した顔に対する処理終了 */」の間に描画処理などが発生します。
      • OpenCVでMatに対しての描画,加工処理は別でまとめさせていただきます。
    • faceRectはMatを基準にした距離が入っています。
      • 顔座標に応じてGameObjectを移動したい場合は3D座標に変換する必要があります。
    • 横や逆さまに映り込んだ顔は検出できません。また、顔の角度も取得できません。(以下2つ、未検証だが方法として記載)
      • 入力Matを回転して検出させた時に一番適切に検出できているものを使用する。
        • 適切の判断基準は検出した顔に対してさらに目や口の検出を行い個数や位置が顔に対して正しいもの。目は2個,口は1つなど...
        • 顔の角度は入力Matを回転させた角度になる
      • 回転した顔に対応したカスケードを使用する。

*1:カスケードとは顔や目や口などの特定のものを画像から認識し位置を特定するのに使用します。
自分でもわかるように訳すとパーツの特徴を学習した内容のファイルです。
OpenCVGitHub)をダウンロードすることで標準的なカスケードを手に入れることができます。その他特殊なのは検索して探してみてください。