Face Detectを使用して、カメラから入力された映像内の顔を認識してみます。
今回はFace Detect APIでサポートされている機能ですので、簡単にできます。
静止画や動画のときの顔認識と大きく違うところは、以下のようになります。
静止画、動画 カメラ
FaceDetector.detectの戻り値で顔認識結果が得られる カメラからの入力映像に顔が見つかったときにアプリに通知される

処理の順序は以下のようになります。
  1. カメラプレビューを表示するためのSurfaceViewを用意
  2. FaceDetectorインスタンスを生成
  3. Processorインスタンスを生成し、FaceDetectorに設定
  4. CameraSourceインスタンスを生成し、startメソッドをコール

カメラプレビューを表示するためのSurfaceViewを用意

厳密には必ずしもSurfaceViewである必要はありませんが、SurfaceHolderが必要になるので、多分SurfaceViewを使用するのが簡単でしょう。
後述するCameraSourceにSurfaceHolderを指定することで、カメラからの入力を、指定されたSurfaceに描画してくれます。
ただし、カメラプレビューを表示したくない場合は、SurfaceViewは不要です。
※ カメラプレビューを表示しない場合は、顔認識開始時にSurfaceHolderを指定しないCameraSource.start(引数なし)メソッドをコールすることになります。

FaceDetectorインスタンスを生成

FaceDetectorインスタンスを生成します。FaceDetectorインスタンスの生成は、ここと同じようにFaceDetector.Buildを経由して生成します。

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromCameraFragment.java - initFaceDetector
FaceDetector.Builder builder = new FaceDetector.Builder(activity);

// classification
SwitchCompat tempSwitch = (SwitchCompat) activity.findViewById(R.id.scClassification);
if (tempSwitch.isChecked()) {
    builder.setClassificationType(FaceDetector.ALL_CLASSIFICATIONS);
} else {
    builder.setClassificationType(FaceDetector.NO_CLASSIFICATIONS);
}

// landmark
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scLandmark);
if (tempSwitch.isChecked()) {
    builder.setLandmarkType(FaceDetector.ALL_LANDMARKS);
} else {
    builder.setLandmarkType(FaceDetector.NO_LANDMARKS);
}

// mode
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scDetectMode);
if (tempSwitch.isChecked()) {
    builder.setMode(FaceDetector.FAST_MODE);
} else {
    builder.setMode(FaceDetector.ACCURATE_MODE);
}

// prominent face only
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scProminentFaceOnly);
builder.setProminentFaceOnly(tempSwitch.isChecked());

// face tracking
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scFaceTracking);
builder.setTrackingEnabled(tempSwitch.isChecked());

mFaceDetector = builder.build();

Processorインスタンスを生成し、FaceDetectorに設定

カメラからの入力映像に顔を見つけたときに、APIからの通知を受けるProcessorインスタンスを生成し、FaceDetectorに設定します。
Processor自体はインターフェースなので、自分で実装したものを設定するのでも問題ないですが、APIにて以下の2種類が用意されています。
Processor名称 概要 備考
LargestFaceFocusingProcessor 一番大きな顔のみ通知を受けるProcessor -
MultiProcessor 認識された全ての顔の通知を受けるProcessor MultiProcessor.Factoryを実装する必要がある

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromCameraFragment.java - initFaceDetector
// use multi processor
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scUseMultiProcessor);
if (tempSwitch.isChecked()) {
    MultiProcessor.Builder multiProcessorBuilder = new MultiProcessor.Builder(mMultiProcessFactory);
    mFaceDetector.setProcessor(multiProcessorBuilder.build());
} else {
    LargestFaceFocusingProcessor.Builder focusingProcessorBuilder = new LargestFaceFocusingProcessor.Builder(mFaceDetector, new FaceTracker());
    mFaceDetector.setProcessor(focusingProcessorBuilder.build());
}

CameraSourceインスタンスを生成し、startメソッドをコール

カメラの操作を隠蔽してくれるCameraSourceインスタンスを生成します。生成時にFaceDetectorをリンクするので、カメラからの入力映像がそのままFaceDetectorに渡され、逐次解析されていきます。
app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromCameraFragment.java - initCameraSource
CameraSource.Builder builder = new CameraSource.Builder(activity, mFaceDetector);

// previewサイズ
builder.setRequestedPreviewSize(mCameraPreview.getWidth(), mCameraPreview.getHeight());

// auto focus
SwitchCompat tempSwitch = (SwitchCompat) activity.findViewById(R.id.scAutoFocus);
builder.setAutoFocusEnabled(tempSwitch.isChecked());

// facing
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scFacing);
if (tempSwitch.isChecked()) {
    builder.setFacing(CameraSource.CAMERA_FACING_FRONT);
} else {
    builder.setFacing(CameraSource.CAMERA_FACING_BACK);
}

// detect fps
try {
    EditText etDetectFps = (EditText) activity.findViewById(R.id.etDetectFps);
    float detectFps = Float.parseFloat(etDetectFps.getText().toString());
    builder.setRequestedFps(detectFps);
} catch (NumberFormatException e) {

}

mCameraSource = builder.build();

app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromCameraFragment.java - start
try {
    // プレビューを表示したいSurfaceViewのSurfaceHolderを指定
    mCameraSource.start(mCameraPreview.getHolder());
    mRealPreviewSize = mCameraSource.getPreviewSize();
    ret = true;
} catch (IOException e) {
    e.printStackTrace();
}
これで、カメラの入力映像で顔を認識すると、Processorに指定したTrackerが動作します。
private class FaceTracker extends Tracker {
    private Paint mLinePaint;
    private boolean mVisible = false;

    public FaceTracker() {
        super();
        mLinePaint = new Paint();
        mLinePaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.face_line_width));
    }

    @Override
    public void onNewItem(int i, Face face) {
        LogUtil.d("", "onNewItem: " + face.getId());

        super.onNewItem(i, face);
        mVisible = true;
        mDrawFaceMap.put(this, face);
        mPreviewOverlay.postInvalidate();

        // 顔毎に線色を変える。ただし、同じIDの人は同じ色にする
        int id = face.getId();
        int idModThree = id % 3;
        if (idModThree == 0) {
            mLinePaint.setARGB(100, 256 - id, 0, 0);
        } else if (idModThree == 1) {
            mLinePaint.setARGB(100, 0, 256 - id, 0);
        } else {
            mLinePaint.setARGB(100, 0, 0, 256 - id);
        }
    }

    @Override
    public void onUpdate(Detector.Detections<Face> detections, Face face) {
        super.onUpdate(detections, face);

        mVisible = true;
        mDrawFaceMap.put(this, face);
        mPreviewOverlay.postInvalidate();
    }

    @Override
    public void onMissing(Detector.Detections<Face> detections) {
        if (mDrawFaceMap.containsKey(this)) {
            LogUtil.d("", "onMissing: " + mDrawFaceMap.get(this).getId());
        }

        super.onMissing(detections);
        mVisible = false;
        mPreviewOverlay.postInvalidate();
    }

    @Override
    public void onDone() {
        if (mDrawFaceMap.containsKey(this)) {
            LogUtil.d("", "onDone: " + mDrawFaceMap.get(this).getId());
        }

        super.onDone();
        mDrawFaceMap.remove(this);
    }
}
上記のonNewItem、onUpdate、onMissing、onDoneは以下のようなタイミングでコールされます。
メソッド名称 タイミング 備考
onNewItem 顔を認識したとき -
OnUpdate カメラプレビュー上の表示位置が変わったとき 複数回コールされる
onMissing カメラプレビュー上に表示されなくなったか、認識できる最小サイズ以下になってしまったとき -
onDone この顔に対するTrackingを終了したとき onMissingの後、コールされる
また、Face毎に発行されるIDはonNewItem ~ onDoneまで同じ値になりますが、その後同じ顔が表示されても別のIDになります。

注意点

サンプルアプリを作成するにあたりハマった点を。。。

カメラプレビューの表示サイズがAPIで認識しているサイズと異なることがある

上記の画像はCameraSourcec.Builder.setRequestedPreviewSizeメソッドにて幅1779、高さ926pxを指定しているのですが、座標(X, Y)は実際の顔の位置と異なったところになってしまっています。
このとき、CameraSource.getPreviewSizeメソッドで確認すると、1920x1080になっています。
よって、FaceDetectorで解析した際のフレームのサイズは1920x1080になっているので、そのフレームにおける座標(X, Y)がFace.getPositionメソッドで返却されてきます。
そのため、1779x926のサイズしかないViewに描画するとズレて表示されてしまいます。

CameraSourcec.Builder.setRequestedPreviewSizeメソッドはアプリからの要望を伝えるだけで、必ずそのサイズになることが保証されていないので、アプリ側で描画位置を調整する必要があります。
サンプルアプリでは以下のように描画しているサイズとAPI側で認識しているカメラプレビューサイズの比率で描画位置を調整しました。
app/src/main/java/jp/eq_inc/testmobilevision/fragment/FaceDetectFromCameraFragment.java - onDraw
※ 以下のmRealPreviewSizeはAPIで認識しているカメラプレビューのサイズ
   mShownPreviewSizeは実際に表示されているカメラプレビューのサイズ
float xRate = mShownPreviewSize.x / mRealPreviewSize.getWidth();
float yRate = mShownPreviewSize.y / mRealPreviewSize.getHeight();

PointF facePosition = face.getPosition();
FaceDetectFromCameraFragment.this.drawFaceLine(canvas, face.getId(), facePosition.x * xRate, facePosition.y * yRate, face.getWidth() * xRate, face.getHeight() * yRate, tracker.mLinePaint, Frame.ROTATION_0);

サンプルアプリ

ここにサンプルアプリを置きました。 試してみる場合は、以下の手順で確認してみてください。
  1. ここから環境をクローン
  2. タグ"v0.3.0"をチェックアウト
  3. ビルドしてAPKをインストール
  4. "Face Detect from Camera"を選択
  5. カメラプレビューが表示されるので、人を表示させる

参考サイト

Add Face Tracking To Your App

コメントの投稿