Android Tips #15 FaceDetectorで Bitmap から顔を検出する

catch

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

はじめに

顔検出とは、画像データから人の顔を探し出す機能のことです。最近のデジタルカメラにはほとんど付いている機能だと思います。一度は使ってみたい機能ですよね。
今回はこの顔検出を使って、下図のような画像を生成する簡単な画像加工アプリを作ってみたいと思います。

顔検出と顔認識の違い

弊社姫野の記事 でも述べているとおり、顔検出は顔認識とよく間違えられる機能です。
「顔検出」は画像から顔を探し出す機能
「顔認識」はDBなどに保存してある情報と照合して認識を行う機能
のことを指します。
この機会に間違いのないよう覚えておきましょう!

FaceDetector クラスでできること

顔検出を実装する一番簡単な方法は、 AndroidSDK に含まれている FaceDetector クラスを使用することです。 FaceDetector クラスを使うことで、 Bitmap データに含まれる顔の情報(Face クラス)を取得することができます。
Face クラスで得られる情報は以下のとおりです。

FaceDetector.Face クラス

confidence() float 顔検出の信頼度。0〜1の範囲で返します。
eyesDistance() float 左目と右目の距離。
getMidPoint(PointF point) void 左目と右目の間の中心の座標。
pose(int euler) float 顔の傾き。引数に EULER_X, EULER_Y, EULER_Z のいずれかを渡します。

この中の pose() メソッドですが、どの Bitmap でも必ず 0 を返してしまうようです。なので、現状は使用できないことになります。何故そうなっているかは不明ですが、傾きはとても必要な情報だと思うので、使えるようになることを切に願っています。。
また、 FontDetector クラスは APIレベル1から存在するクラスなので、どのバージョン向けのアプリにも組み込むことができます。

顔検出アプリを作る

1.画像を取得する

まずはじめに、検出する画像が必要です!ということで、以前の記事と同様の方法でギャラリーから画像を取得しましょう。 ContentProvider クラスで端末内の画像を取得します。選択するしくみなどは特に必要ないので、最新の画像を一枚だけ取得するロジックにしておきます。

FaceDetectorActivity.java

package jp.classmethod.android.sample.facedetector;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import android.app.Activity;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.Media;
import android.widget.ImageView;

public class FaceDetectorActivity extends Activity {

	private ImageView mImageView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mImageView = new ImageView(this);
		setContentView(mImageView);
		// ローカルデータから写真ファイルを取得
		Bitmap original = getLocalPicture();
		// 顔認証する処理は以下のメソッドに記述する
		Bitmap bitmap = detect(original);
		mImageView.setImageBitmap(bitmap);
	}

	private Bitmap getLocalPicture() {
		// ContentProvider から最新の画像ファイルを取得する
		Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
		String sortOrder = Media.DATE_TAKEN + " DESC";
		Cursor c = getContentResolver().query(uri, null, null, null, sortOrder);
		c.moveToFirst();
		// ファイル名を取得
		int index = c.getColumnIndex(MediaStore.MediaColumns.DATA);
		String path = c.getString(index);
		// Bitmap を取得
		InputStream in = null;
		Bitmap bitmap = null;
		try {
			in = new FileInputStream(path);
			bitmap = BitmapFactory.decodeStream(in);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return null;
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
					return null;
				} 
			}
		}
		return bitmap;
	}
	
	private Bitmap detect(Bitmap original) {
		// ここに顔検出して加工する処理を記述する
		Bitmap bitmap = original.copy(Bitmap.Config.RGB_565, true);
		original.recycle();
		return bitmap;
	}
}

実行結果

弊社社員の平井にモデルになってもらいました!

2.FaceDetector クラスで顔を検出する

事前準備は済んだので、次に FaceDetector インスタンスを生成します。FaceDetector のコンストラクタ引数には、検出したい範囲(width, height)と、検出する顔の最大数を渡します。
顔検出は findFaces(Bitmap bitmap, Face[] faces) メソッドを使います。第一引数には検出したい Bitmap 、第二引数には Face の配列を渡します(メソッドをコールする前に作っておく)。

// 顔認証
final int MAX_FACES = 4;
Face[] faces = new Face[MAX_FACES];
FaceDetector detector = new FaceDetector(result.getWidth(), result.getHeight(), MAX_FACES);
int num = detector.findFaces(result, faces);
if (num > 0) {
	for (int i = 0; i < num; i++) {
		Face face = faces[i];
		PointF point = new PointF();
		face.getMidPoint(point);
		
		Log.d("FaceDetector", "Confidence:" + face.confidence());
		Log.d("FaceDetector", "MidPoint X:" + point.x);
		Log.d("FaceDetector", "MidPoint Y:" + point.y);
		Log.d("FaceDetector", "EyesDistance:" + face.eyesDistance());
		
		// 何かする
	}
}

上記処理の注意点は大きく2点あります。
まず一つ目は 顔検出を行う Bitmap データは 16bit である必要があります。 これは恐らく APIレベル1 から使える機能のため、全バージョンで使えるようにするためだと思われます。 32bit の Bitmap からは検出することができないので注意してください。
もう一点は、 Face クラスの配列の最大数と FaceDetector のコンストラクタ引数に渡す最大数を同じ数にしなければいけません。 これが異なるとエラーになるので注意してください。
しかし、顔検出する前に取得する Face の最大数が決まってしまうのは少し気になりますね。例えば「Bitmap の中にあるすべての顔に加工したい!」という実装はなかなか難しそうです。上手く効率的にできる方法があれば良いのですが。。

3.Face情報を使って Bitmap を 加工する

ここまでで顔検出によって Bitmap にある顔の位置情報が取得できました。この情報を使って画像を少し加工したいと思います。 Face クラスで取得した情報(右目と左目の距離と中心座標)があれば、顔の大きさやパーツ(口、鼻など)のおおよその位置を算出することができます。今回は2つのサンプルを作ってみたいと思います。

注: 今回の実装は画像加工処理をメインスレッドで行なっています。画像ファイルによっては処理が終わるまでアプリがしばらく固まった状態になってしまうことがあるのでご了承ください。実際に使いたい場合は AsyncTask クラスを使用して別スレッドで処理しましょう。

目をハートマークにする

まずはじめに思いつきそうなのがこれですね。右目と左目の距離と中心座標が分かれば、簡単に実装することができます。サクッと実装したいので、ハートマークは画像ではなく機種依存文字を使いました。テキストの幅と高さが取得できれば実装することができます。

ソース

private Bitmap mark(Bitmap original) {
	// 目にマークをつける
	// Bitmap のコピー (16bitにする)
	Bitmap bitmap = original.copy(Bitmap.Config.RGB_565, true);
	Canvas canvas = new Canvas(bitmap);

	// テキスト色の設定
	Paint paint = new Paint();
	paint.setAntiAlias(true);
	paint.setDither(true);
	paint.setColor(Color.MAGENTA);
	paint.setStyle(Paint.Style.FILL_AND_STROKE);
	paint.setTextSize(100);

	// 顔認証
	final int MAX_FACES = 4;
	Face[] faces = new Face[MAX_FACES];
	FaceDetector detector = new FaceDetector(bitmap.getWidth(),
			bitmap.getHeight(), MAX_FACES);
	int num = detector.findFaces(bitmap, faces);

	if (num > 0) {
		// マーク
		String mark = "♥";
		// テキストの width, height を取得
		FontMetrics fontMetrics = paint.getFontMetrics();
		int textWidth = (int) FloatMath.ceil(paint.measureText(mark));
		int textHeight = (int) FloatMath
				.ceil(Math.abs(fontMetrics.ascent)
						+ Math.abs(fontMetrics.descent)
						+ Math.abs(fontMetrics.leading));
		for (Face face : faces) {
			if (face == null) {
				continue;
			}
			PointF point = new PointF();
			face.getMidPoint(point);
			float distH = face.eyesDistance() / 2;
			// 左目
			float lx = point.x - distH - (textWidth / 2);
			float ly = point.y + (textHeight / 2);
			canvas.drawText(mark, lx, ly, paint);
			// 右目
			float rx = point.x + distH - (textWidth / 2);
			float ry = point.y + (textHeight / 2);
			canvas.drawText(mark, rx, ry, paint);
		}
	}
	original.recycle();
	return bitmap;
}

実行結果

顔全体をモザイクにする

顔だけをモザイクにするには、顔の大きさを算出しなければいけません。 顔の各パーツの大きさや距離には一定の比率があります。 この比率を使って使って顔の大きさを求めることができます。 もちろん顔のパーツの比率には個人差があるので、必ずしもこの比率で完璧な顔の大きさがとれるわけではありません。 あくまで "だいたいこのくらい" という、おおよその大きさを計算することができます。 あとは実際に見てみて、バランスの良い比率に調整していくのがベストだと思います。 一般的な顔のパーツの比率は下記を参考にしましょう。

  • 顔の縦の長さを考えたとき、中央の高さに両目がある。
  • 顔を横に3分割したとき、頭頂から眉毛の上まで、眉毛から鼻の下まで、鼻の下からあごの先までがほぼ同じ長さである。
  • 顔の横幅は、目の横幅の4倍から5倍である。
  • 両目の間隔は、目の横幅に等しい。
  • 耳の高さは、ほぼ鼻の下から目尻までである。
  • 鼻の横幅は目の横幅にほぼ等しい。
  • 口の横幅は二つの瞳の距離に等しい。

(Wikipediaより)

またモザイク処理ですが、以下の記事が大変参考になりました。一定の範囲を同じ色で塗りつぶすように描画すれば簡単に作ることができます。
Androidでモザイク画像を作ってみる

ソース

private Bitmap mosaic(Bitmap original) {
	// モザイクをかける
	// Bitmap のコピー (16bitにする)
	Bitmap bitmap = original.copy(Bitmap.Config.RGB_565, true);
	Canvas canvas = new Canvas(bitmap);

	// Paintの設定
	Paint paint = new Paint();
	paint.setAntiAlias(true);
	paint.setDither(true);

	// 顔認証
	final int MAX_FACES = 4;
	Face[] faces = new Face[MAX_FACES];
	FaceDetector detector = new FaceDetector(bitmap.getWidth(),
			bitmap.getHeight(), MAX_FACES);
	int num = detector.findFaces(bitmap, faces);

	if (num > 0) {
		for (Face face : faces) {
			if (face == null) {
				continue;
			}
			PointF point = new PointF();
			face.getMidPoint(point);
			
			// 幅・高さ
			int width = (int) face.eyesDistance() * 3;
			int height = (int) (width * 1.5);

			float left = point.x - width / 2;
			float top = point.y - height / 2;

			// 画像を顔のサイズに切り出す
			Bitmap dist = Bitmap.createBitmap(bitmap, (int) left,
					(int) top, width, height);
			// モザイクをかける
			int w = dist.getWidth();
			int h = dist.getHeight();
			int dot = 20;
			for (int i = 0; i < w / dot; i++) {
				for (int j = 0; j < h / dot; j++) {
					int rr = 0;
					int gg = 0;
					int bb = 0;
					for (int k = 0; k < dot; k++) {
						for (int l = 0; l < dot; l++) {
							int dotColor = dist.getPixel(i * dot + k, j
									* dot + l);
							rr += Color.red(dotColor);
							gg += Color.green(dotColor);
							bb += Color.blue(dotColor);
						}
					}
					rr = rr / (dot * dot);
					gg = gg / (dot * dot);
					bb = bb / (dot * dot);
					for (int k = 0; k < dot; k++) {
						for (int l = 0; l < dot; l++) {
							dist.setPixel(i * dot + k, j * dot + l,
									Color.rgb(rr, gg, bb));
						}
					}
				}
			}

			// 描画する
			canvas.drawBitmap(dist, left, top, paint);

			// おわり
			dist.recycle();
		}
	}
	original.recycle();
	return bitmap;
}

実行結果

ソースコード

今回作ったアプリのソースコードは github に公開しています。実際の端末で見てみたい場合はぜひ動作させてみてください。
suwa-yuki / FaceDetectorSample

まとめ

今回は、 FaceDetector クラスを使った顔検出の方法を解説しました。
FaceDetector を使った顔検出自体は非常に簡単に実装できることがわかったと思います。ただ実際に動作させると分かりますが、少し処理速度が遅いです。しかしながら、少しだけ加工したい用途であればこのクラスを使ってサクッと実装できるのがお分かりいただけたかと思います。
顔検出を上手く使って、ぜひ面白いアプリを作りましょう!!

参考