>   >  Unityでつくるインタラクティブコンテンツ:第2回:OpenCVで顔検出
第2回:OpenCVで顔検出

第2回:OpenCVで顔検出

<4>顔検出のしくみを探る

次にこの「Face Detection Example」の中身を見てみます。OpenCVForUnity / Examples / MainModules / objdetect / FaceDetectionExampleフォルダを開いてみてください。以下の3つのスクリプトとシーンがそれぞれあるはずです。

・Asynchronous Face Detection Web Cam Texture Example
・Face Detection Web Cam Texture Example
・Face Detection Example

「Face Detection Example」は「レナ画像」を使っての顔検出のサンプルです。レナ画像(下記)は画像処理のサンプルとして広く使われている画像です。

「Asynchronous Face Detection Web Cam Texture Example」は顔検出処理を非同期で行うサンプルです。非同期処理はパフォーマンスを出すために重要な要素なので、今後の記事で説明していきます。

シーンを細かく見ていくために、「Face Detection Web Cam Texture Example」シーンを開いてください。Quadゲームオブジェクトにアタッチされている「Face Detection Web Cam Texture Example.cs」を開いてください。ダブルクリックで開きます。

ソースのコメントを見ると、OpenCVの「Cascade Classifier(カスケード型分類器)」サンプルのUnity実装のようで、cv::CascadeClassifierというクラスを使って顔検出を行なっています。

では、Cascade Classifierとは何でしょう? 「Cascade(カスケード)」とは「数珠つなぎになったもの」を意味します。このしくみについては、先ほど紹介した書籍「コンピュータビジョン―広がる要素技術と応用―」内の9.2.1「顔検出」で解説されています。私ではひと言で説明することができないので解説を引用します。

Haar-Like特徴量を用いた顔識別器の概要、図中の白丸は弱識別子を表し、上の画像はその弱識別子が用いるHarr-Like特徴量を示す。入力された画像は左の識別器から順に特徴量抽出、識別がされ、顔と識別されたものは次の弱識別器の処理に進む、顔でないと識別されたものはその時点で棄却される。

一番左の場合は、顔の中心より少し上に、上が明るくて下が暗い横長の領域があればそれを目として認識します。このように目、鼻、口などを調べる検出器を数珠つなぎに繋げて、それが顔かそうでないかを最終的に決定します。この方法は人間の顔の特徴をみていくだけですので、個人を識別することはできません。

FaceDetectionWebCamTextureExample .csに戻ります。Start関数でhaarcascade_frontalface_alt.xmlを読み込んでいます。これはOpenCVが提供している標準の特徴量のファイルです。細かく見ていくと大変なのでキーとなる部分だけ見てみます。Update関数を見てください(コメントは消してあります)。

void Update ()
{
	if (webCamTextureToMatHelper.IsPlaying () &&
		webCamTextureToMatHelper.DidUpdateThisFrame ()) {

		Mat rgbaMat = webCamTextureToMatHelper.GetMat();
		Imgproc.cvtColor(rgbaMat, grayMat, Imgproc.COLOR_RGBA2GRAY);
		Imgproc.equalizeHist(grayMat, grayMat);
		if (cascade != null) {
			cascade.detectMultiScale (grayMat, faces, 1.1, 2, 2, 
				new Size (grayMat.cols() * 0.2, grayMat.rows() * 0.2), new Size ());

		OpenCVForUnity.Rect[] rects = faces.toArray ();
		for (int i = 0; i < rects.Length; i++) { 
			Imgproc.rectangle (rgbaMat, 
				new Point(rects[i].x, rects[i].y), 
				new Point(rects [i].x + rects[i].width, rects[i].y + rects[i].height), 
				new Scalar(255, 0, 0, 255), 2);
		}
		Utils.fastMatToTexture2D (rgbaMat, texture);
	}
}

上から5行目に下記のような記述があると思います。

Mat rgbaMat = webCamTextureToMatHelper.GetMat();

ここでは、カメラ画像を取得してOpenCVの画像フォーマットに変換しています。OpenCVで処理を行うためにはMat型で画像をもつことが必要なのです。

次の2行はモノクロ画像に変換して、明るさを調整しています。

Imgproc.cvtColor(rgbaMat, grayMat, Imgproc.COLOR_RGBA2GRAY);
Imgproc.equalizeHist(grayMat, grayMat);

このように、ゼロから実装すると面倒な画像処理が、OpenCVを使うことで簡単に実現できるのです。最後に顔検出の処理に画像を渡すと、facesに結果が返ってきます。

cascade.detectMultiScale (
	grayMat, // 入力画像
	faces, // 検出結果(長方形領域)
	1.1, // 縮尺比の係数
	2, 2, // 隣接数の最小
	new Size(grayMat.cols()*0.2, grayMat.rows()*0.2), // 考慮する最小サイズ
	new Size ()); //考慮する最大サイズ

ここでは細かなパラメータの説明は省きます。facesはMatOfRect型です。MatOfRect型はOpenCVがもつ型ではありません。OpenCV for Unityで実装されている、矩形を表す型です。Javaのラッパーなども同様の実装があるようです。これで検出した矩形が配列で取得できます。そして以下のループで検出した矩形領域を表示しています。

for (int i = 0; i < rects.Length; i++) {
	Imgproc.rectangle(
		rgbaMat, 
		new Point(rects[i].x, rects[i].y), 
		new Point(rects[i].x + rects[i].width, rects[i].y + rects[i].height), 
		new Scalar (255, 0, 0, 255), 2);
}

rectsのx、yは検出範囲の左上を表し、widthとheightは幅と高さを示します。画像処理の実装の詳細はわからなくても簡単に顔検出を行うことができました。さらに、Unityで使えるということはそのまま様々な演出を容易に加えていくことが可能なわけです。

<5>認識した顔に画像をかぶせてみる

では顔にお面をかぶせてみます。時期的にちょっと早いですが、ジャック・オランタンをかぶってみましょう。Hierarchyを見るとメニューなどはuGUIで作られていますので、uGUIでお面を作っていきます。

お面用のImageゲームオブジェクトをCanvasの中につくってください。素材はフリー素材配布サイト「Frame illust」の「[ハロウィン]カボチャランタン(ジャックランタン)のイラスト」を使わせてもらいます。

ProjectのAssetsの下にImageフォルダを作り画像をインポートします。Assetsの下に直接ゲームオブジェクトを置いていくと整理がつかなくなるので、フォルダ構成や命名規則を考えて作業していってください。インポートした画像を選択して、インスペクタのTexture TypeがSprite(2D and UI)になっていることを確認してください。Unityのテンプレートには2Dと3Dがあり、インポート時の挙動がそれぞれちがいます。このプロジェクトは2Dで作っているので、uGUIで使いやすいフォーマットでインポートされます。インポートした画像を、先ほど作ったImageゲームオブジェクトのImageコンポーネントにアタッチします。Set Native Sizeボタンを押して元のサイズにセットしておいてください。

FaceDetectionWebCamTextureExample.csを少し書き換えます。以下のようにクラスの先頭に2行追加してください。

public class FaceDetectionWebCamTextureExample : MonoBehaviour
{
	[SerializeField] private UnityEngine.UI.Image _mask;
	[SerializeField] private Vector2 _pos = new Vector2(320, 240);

[SerializeField]属性を指定するとメンバーをprivateのままコンポーネントに表示してくれます。意味なくpublicなメンバーを作りたくないのでこの方法を採ります。

次にUpdate関数内の結果を表示する部分を以下のように変更します。

for (int i = 0; i < rects.Length; i++) {
	var cx = rects[i].x + rects[i].width / 2f;
	var cy = rects[i].y + rects[i].height / 2f;
	_mask.transform.localPosition = new Vector3(cx-_pos.x, cy-_pos.y, 0); 
	break;
}

これでジャックオランタンを検出領域の中心に移動できます。ここでは検出結果に対して1つだけ描きたいのでBreakで無条件にループを抜けています。エディタに戻るとFaceDetectionWebCamTextureExampleコンポーネントにアトリビュートが追加されます。最後にMaskにImageゲームオブジェクトをアタッチしてください。

Posの値はジャック・オランタンの表示位置をオフセットするためのものです。それでは実行してみましょう。

検出された顔にジャック・オランタンが重なりました。OpenCV for Unityのおかげで、Unityそのものの作業自体は非常に簡単だったと思います。このような素晴らしいアセットを使うことができるのもUnityの大きな魅力です。しかし、このままでは検出結果が暴れているのでもう少し動きを押さえ、さらに複数人にも対応できれば表現の幅が広がりそうです。次回では検出ノイズを押さえる方法と複数人に対応する方法を解説したいと思います。



Profile.

高田稔則/Toshinori Takata(Codelight)
Codelight株式会社 代表取締役・インタラクションエンジニア
フリーランス、株式会社TBSテレビ等で映画CG制作、株式会社ソニー・コンピュータエンタテインメント(現 ソニー・インタラクティブエンタテインメント)でPS4のOSD開発などを経て2006年にCodelight株式会社を設立。インタラクティブコンテンツの制作を中核として、製造業向けのプロトタイプ開発なども行う
www.codelight.co.jp




その他の連載