Android Tips #16 カメラプレビューで顔を検出する

2012.09.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回の記事では、静止画から顔を検出する方法について解説しました。
Android SDK 4.0 (APIレベル14以降) からはそれに加えて FaceDetectionListener というカメラプレビューで顔を検出する機能が追加されました。 今回はこの FaceDetionListener を使ってカメラプレビュー中に顔を検出する実装方法について、簡単なサンプルアプリを通して解説したいと思います。

カメラプレビューの作りかた

はじめに、カメラのプレビューを表示するところから作ります。まずカメラをアプリで使用できるようにするには、カメラにアクセスする権限を与えなければいけません。AndroidManifest.xml に uses-permission を追加します。

<uses-permission android:name="android.permission.CAMERA"/>

なお、FaceDetionListener は APIレベル14からの機能なので、uses-sdk の android:minSdkVersion と android:targetSdkVersion は 14 以上に設定してください。

次に Activity を実装します。 Activity にカメラのプレビューを表示するためには、以下のクラスを使います。

Camera カメラを制御するクラス
SurfaceView カメラプレビューを表示する View
SurfaceHolder SurfaceView に表示する内容を保管・管理するクラス

SurfaceView は高速で描画できるため、カメラプレビューやゲームなどのような描画内容が頻繁に変わる処理を行いたい場合に使用されます。 また、SurfaceHolder はそのような高速描画を実現するために描画内容を保存・管理してくれるクラスになります。 SurfaceHolder が生成されたタイミングは SurfaceHolder.Callback というインターフェースを実装することでハンドリングできるので、 そのタイミングで Camera#setPreviewDisplay(SurfaceHolder holder) メソッドをコールし、カメラのプレビューを開始します。

CameraActivity.java

package jp.classmethod.android.sample.camerafacedetector;

import java.util.List;

import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Camera.Face;
import android.hardware.Camera.FaceDetectionListener;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup.LayoutParams;

/**
 * カメラプレビューを表示する {@link Activity} です。
 */
public class CameraActivity extends Activity {
	
	/** カメラのハードウェアを操作する {@link Camera} クラスです。 */
	private Camera mCamera;

	/** カメラのプレビューを表示する {@link SurfaceView} です。 */
	private SurfaceView mView;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mView = new SurfaceView(this);
		setContentView(mView);
	}

	@Override
	protected void onPostCreate(Bundle savedInstanceState) {
		super.onPostCreate(savedInstanceState);
		SurfaceHolder holder = mView.getHolder();
		holder.addCallback(surfaceHolderCallback);
	}

	/** カメラのコールバックです。 */
	private SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {

		@Override
		public void surfaceCreated(SurfaceHolder holder) {
			// 生成されたとき
			mCamera = Camera.open();
			try {
				// プレビューをセットする
				mCamera.setPreviewDisplay(holder);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		@Override
		public void surfaceChanged(SurfaceHolder holder, int format, int width,
				int height) {
			// 変更されたとき
			Camera.Parameters parameters = mCamera.getParameters();
			List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes();
			Camera.Size previewSize = previewSizes.get(0);
			parameters.setPreviewSize(previewSize.width, previewSize.height);
			// width, heightを変更する
			mCamera.setParameters(parameters);
			mCamera.startPreview();
		}

		@Override
		public void surfaceDestroyed(SurfaceHolder holder) {
			// 破棄されたとき
			mCamera.release();
			mCamera = null;
		}

	};
}

SurfaceHolder.Callback インターフェースのメソッドでは、以下の処理を行なっています。

surfaceCreated(SurfaceHolder holder) カメラの起動、プレビューを表示する View の登録
surfaceChanged(SurfaceHolder holder, int format, int width, int height) プレビューサイズの変更、プレビューの開始
surfaceDestroyed(SurfaceHolder holder) カメラのリリースとオブジェクトの破棄

実行結果

今回も弊社社員である平井にモデルになってもらいました!

カメラプレビューで顔を検出する

カメラプレビューで顔を検出するには Camera#startFaceDetection() メソッドをコールします。 検出結果を受け取るには FaceDetecionListener インターフェースを実装し、Camera#setFaceDetectionListener(FaceDetecionListener listener) メソッドでセットします。そうすると、FaceDetecionListener#onFaceDetection(Camera.Face[] faces, Camera camera) メソッドから顔検出結果の Camera.Face オブジェクトの配列が渡されます。
先ほどの SurfaceHolder.Callback#surfaceCreated(SurfaceHolder holder) メソッドを以下のように書き換えます。

@Override
public void surfaceCreated(SurfaceHolder holder) {
	// 生成されたとき
	mCamera = Camera.open();
	// リスナをセット
	mCamera.setFaceDetectionListener(faceDetectionListener);
	// 顔検出の開始
	mCamera.startFaceDetection();
	try {
		// プレビューをセットする
		mCamera.setPreviewDisplay(holder);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

FaceDetectionListener は以下のようになります。

private FaceDetectionListener faceDetectionListener = new FaceDetectionListener() {
	@Override
	public void onFaceDetection(Face[] faces, Camera camera) {
		Log.d("onFaceDetection", "顔検出数:" + faces.length);
		// 何かする
	}
};

このとき渡される Camera.Face オブジェクトは、 FaceDetector で顔検出するときに使う FaceDetector.Face クラスとは異なるので注意してください。Camera.Face クラスのプロパティは以下のとおりです。

Camera.Face クラス

id int 固有IDです。
leftEye Point 左目の中心座標です。サポートしていない場合は null を返します。
rightEye Point 右目の中心座標です。サポートしていない場合は null を返します。
mouth Point 口の中心座標です。サポートしていない場合は null を返します。
rect Rect 顔全体の短形です。
score int 検出の信頼度です(1〜100)。

FaceDetector.Face クラスのプロパティと大きく違うことがわかると思います。前回の記事では顔全体の短形をとるために比率で出さなければいけませんでしたが、検出できた顔の短形が Rect オブジェクトとして取れるので、顔全体に何か処理を施したい場合は簡単にできそうです。また、この Rect オブジェクトの値ですが、そのまま使うことができません。カメラドライバ側が -1000 〜 1000 の範囲で処理しているので、実際の画面上の座標にするには以下のように Matrix を使って座標の変換をかける必要があるので注意してください。

Matrix matrix = new Matrix();
// スケールの算出
matrix.postScale(getWidth() / 2000f, getHeight() / 2000f);
// 座標の移動
matrix.postTranslate(getWidth() / 2f, getHeight() / 2f);
// Canvas の保存
int saveCount = canvas.save();
// Matrix を適用
canvas.concat(matrix);
// 描画
canvas.drawRect(face.rect, mPaint);
// リストア
canvas.restoreToCount(saveCount);

leftEye や rightEye、 mouth など便利そうなプロパティもありますが、注意しなければいけないのは サポートされていない場合は null を返すというところです。 必ずしもどの端末でも取得できるわけではないということです。 アプリ(特にコンシューマ向けアプリ)に実装することを考えたときに、限られた端末でしか動作しないものはあまり意味がありません…。 そのため、この値ははじめからないものと考えて実装方法を考えたほうが良いと思います。 いずれにしろ、値が取得できない場合の処理は書かなければいけないので。

顔検出の結果をオーバーレイで表示する

最後に、顔検出した結果をプレビュー画面の上にオーバーレイで表示してみましょう。 カスタムViewを新たに作成し、プレビューである SurfaceView の上に重ねます。 あとはカスタムViewに Camera.Face#rect の情報を使って描画するだけで完成です。
以下のクラスでは、顔の大きさの短形を半透明で塗りつぶす処理を書いてみました。

CameraOverlayView.java

package jp.classmethod.android.sample.camerafacedetector;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.hardware.Camera.Face;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.View;

/**
 * カメラのプレビューに重ねる {@link View} です。
 */
public class CameraOverlayView extends View {

	/** 塗りつぶし設定の {@link Paint} です。 */
	private Paint mPaint;
	
	/** 検出した顔情報の配列です。 */
	private Face[] mFaces;

	public CameraOverlayView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initialize();
	}

	public CameraOverlayView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initialize();
	}

	public CameraOverlayView(Context context) {
		super(context);
		initialize();
	}
	
	/** 初期化します。 */
	private void initialize() {
		// 塗りつぶしの設定
		mPaint = new Paint();
		mPaint.setAntiAlias(true);
		mPaint.setDither(true);
		mPaint.setColor(Color.MAGENTA);
		mPaint.setAlpha(128);
		mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
	}

	/** 
	 * 検出した顔情報の配列をセットします。
	 * @param faces 検出した顔情報の配列
	 */
	public void setFaces(Face[] faces) {
		mFaces = faces;
		invalidate();
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		if (mFaces == null) {
			return;
		}
		for (Face face : mFaces) {
			if (face == null) {
				continue;
			}
			// 短形で塗りつぶす
			Matrix matrix = new Matrix();
			matrix.postScale(getWidth() / 2000f, getHeight() / 2000f);
			matrix.postTranslate(getWidth() / 2f, getHeight() / 2f);
			int saveCount = canvas.save();
			canvas.concat(matrix);
			canvas.drawRect(face.rect, mPaint);
			canvas.restoreToCount(saveCount);
		}

	}

}

あとはこの View を Activity のレイアウトの追加し、

mCameraOverlayView = new CameraOverlayView(this);
addContentView(mCameraOverlayView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

FaceDetecionListener#onFaceDetection(Camera.Face[] faces, Camera camera) メソッド内で View に Face 配列を渡せば完成です。

private FaceDetectionListener faceDetectionListener = new FaceDetectionListener() {
	@Override
	public void onFaceDetection(Face[] faces, Camera camera) {
		Log.d("onFaceDetection", "顔検出数:" + faces.length);
		// View に渡す
		mCameraOverlayView.setFaces(faces);
	}
};

実行結果

ソースコード

今回作成したアプリのソースコードを github で公開しました。実際に動作させ、挙動を確認してみてください。

suwa-yuki/CameraFaceDetectorSample

まとめ

前回の記事に引き続き、顔検出について取り扱ってみました。 今回作成したアプリを実際に動作させてみると分かりますが、FaceDetector の検出より Camera.FaceDetectionListener はかなり高速に検出ができているということがわかると思います。 しかしながらデメリットもあります。それは Camera.FaceDetector で検出できる情報は端末によって差異がある ということです。 FaceDetector は Bitmap から顔を検出しているので、端末に依存するということはありません。Camera.FaceDetectionListener はカメラの性能によるので、端末に大きく依存してしまいます。
実際のアプリで顔検出を組み込みたい場合は、それぞれのメリット・デメリットをよく理解して実装方法を検討するようにしましょう。

参考