カメラ映像で顔検出2016/08/04

前回の記事の通り、Androidアプリ「おかしな(ヵ_ォ)カメラ」では顔検出に Google Play Serivcesの FaceDetectorを使っています。

このAPIの基本的な使い方はチュートリアル等にありますが、今回はカメラの映像で顔検出をする場合の手順と、引っかかった所や気づいたところを解説してみます。

参考にするのは提供されているサンプル
その中(FaceTrackerの下じゃないけど)でも、カメラの映像を顔検出APIに送っているのは
あたりです。


◇手順
・Andoroidでのカメラ画像の表示
・FaceDetectorのプロジェクトへの組み込み
については省略します。

FaceDetectorの生成

FaceDetector detector = new FaceDetector.Builder(getApplicationContext()).build();

こんな感じで顔検出用のクラスを生成します。
このとき、いくつかのパラメータを設定することができますが、その中で使用してみて注意点があったものを挙げてみます。

・setMode(int)
 FAST_MODEで速度重視、ACCURATE_MODEで精度重視となります。
 顔の範囲やZ軸回転(首をかしげる)はどちらでも取得できますが、FAST_MODEではY軸回転(横を向く)が取れなくなりました(常に0が返る)。ちなみにX軸回転(うつむく、見上げる)は元々取得するメンバがありません。何故?

・setMinFaceSize(float)
 検出する顔の最小値(画面に対する比率)を設定します。
 ただこの関数で大きめの値を設定すると、Face.getWidth()や Face.getHeight()で得られる顔の大きさにズレが生じます(画面上の顔が小さくなるほど、実際の顔より大きめの値が返ってくる)。
 設定するとズレるというより設定する値が大きいとズレが顕著になっているようなのですが、残念ながら対処法はわかりません。

・setTrackingEnabled(boolean)
 リアルタイムの映像で検出を行う場合は trueに設定するのですが、ちょっと気を付けなければいけない点があります。それについては後述します。


カメラ映像の取得
このへんは顔検出特有の部分では無いので簡単に済ませます。

カメラ入力画像は、Camera.setPreviewCallbackWithBuffer(Camera.PreviewCallback)と Camera.addCallbackBuffer(byte[])でコールバックを設定して取得します。
CameraSourceでは createCamera()の最後の部分、そしてコールバック関数自体は CameraPreviewCallbackクラスの onPreviewFrame(byte[], Camera)です。
コールバック関数にデータが来る → 顔検出 の繰り返しとなります。

また、顔検出処理は時間がかかるので別スレッドで行います。
CameraSourceでは、FrameProcessingRunnableクラスでその処理を管理しています。


顔検出準備
取得した画像データから、Frameクラスを生成します。

Frame frame = new Frame.Builder().setImageData(data, width, height, ImageFormat.NV21).build();

YUV形式に対応していますのでカメラから取得したデータをそのまま使えます。

・画像の向き
 顔検出APIは、検出する顔と画像の上下が合っていることが前提となっています。
 カメラ画像は横向きなので、端末を縦に使う場合等は画像を回転する必要があります。

Frame frame = new Frame.Builder().setImageData(data, width, height, ImageFormat.NV21).setRotation(rotation).build();

 setRotation(int)によって回転角を指定します。
 引数は ROTATION_0 ~ ROTATION_270と、90°単位です。
 CameraSource(というよりFaceTracker)では、screenOrientationを fullSensorにして getDefaultDisplay().getRotation()から計算しています(大雑把に言うと)。

 さて前述の setTrackingEnabled(true)での注意点ですが、
 「おかしな(ヵ_ォ)カメラ」のようなリアルタイムで端末の向きを変えられるアプリの場合、setTrackingEnabled(true)としていると向きが変わったときにエラーが出て顔の検出ができなくなってしまいます。画像としては不連続になるからなんだとは思いますが。
 そのため端末の向きが変わったときは FaceDetectorを一旦 release()して生成し直す必要があります。


顔検出
 あとは FaceDetectorに Frameを放り込んで顔検出情報 Faceの配列を取得するだけです。

SparseArray <Face> faces = detector.detect(frame);

 CameraSourceは顔検出専用ではないので、汎用的にDetectorクラスの receiveFrame(Frame)を使っています。ご注意を。

 個々の検出情報は、valueAt(int)で取り出します。

for (int i = 0; i < faces.size(); i++) {
    Face face = faces.valueAt(i);

    // 顔で遊ぼう!
}

 Faceで得られる情報は、リファレンスからどうぞ。