これまでにMobile Vision APIを使用して、顔・バーコード・文字を認識させるサンプルアプリを独立して作成してきました。
そうなると、最後に試してみたくなるのは、全てを一緒に解析することは出来るのかとなりました。
一応、Mobile Vision APIに"MultiDetector"というものがあり、FaceDetector / BarcodeDetector / TextRecognizerを纏めるものがありますが、解析結果がDetector毎にTrackerに通知されるので、実装を工夫しないと顔 / バーコード / 文字だけを囲う線が順番に表示されることになります。
理想としては、認識された顔 / バーコード / 文字を全て線で囲いたかったの自前実装でやってみることにしました。

ただし静止画、動画はそれぞれのDetector / Recognizerで順番に認識させれば出来るので、今回のターゲットはカメラからの入力映像に対して、(ほぼ)同時に認識できるかとなります。

構成と処理の流れ

そもそも、カメラの入力映像に対するMobile Vision APIの処理の流れについて確認すると、以下のようなシーケンスになっています。
  1. CameraSourceがカメラを起動し、カメラからの入力映像を取得できるようにする
  2. カメラからの入力映像をFrameインスタンス化
  3. CameraSourceに設定されているDetectorにFrameインスタンスを渡す(Detector.receiveFrame)
  4. Detectorにて解析処理を実施(Detector.detect)
  5. 解析結果をProcessorに渡す(Processor.receiveDetection)
  6. Processorにて、設定されているTrackerを使用して、解析結果をアプリに渡す(Tracker.onNewItem)

上記の実装を踏襲しつつ、以下のように処理の流れを変えています。
AllDetector: FaceDetector、BarcodeDetector、TextRecognizerを持つWrapper Detector
AllTracker: 解析結果をFace、Barcode、TextBlockのどれか確認して、それぞれに合った処理が実装されたTracker
  1. CameraSourceがカメラを起動し、カメラからの入力映像を取得できるようにする
  2. カメラからの入力映像をFrameインスタンス化
  3. CameraSourceに設定されているAllDetectorにFrameインスタンスを渡す(AllDetector.receiveFrame)
  4. AllDetectorからFaceDetector、BarcodeDetector、TextRecognizerの解析処理(detect)を実施
  5. 各Detector/Recognizerから返却された結果を1つのSparceArrayに纏め、Processorに渡す(Processor.receiveDetection)
  6. Processorにて、設定されているAllTrackerを使用して、解析結果をアプリに渡す(AllTracker.onNewItem)
  7. AllTrackerにて渡されたインスタンスを確認して、顔認識、バーコード認識、文字認識に合った処理を実施

実装

AllDetector

一旦、全ての解析依頼を受け持つAllDetectorを用意しています。
内部で、FaceDetector、BarcodeDetector、TextRecognizerに処理を振り分ける形をとりました。
app/src/main/java/jp/eq_inc/testmobilevision/detector/AllDetector.java
public class AllDetector extends Detector {
    public enum DetectorType {
        Face, Barcode, Text;

        public static DetectorType getDetectorTypeFromItem(Object item) {
            if (item instanceof com.google.android.gms.vision.face.Face) {
                return DetectorType.Face;
            } else if (item instanceof com.google.android.gms.vision.barcode.Barcode) {
                return DetectorType.Barcode;
            } else if (item instanceof TextBlock) {
                return DetectorType.Text;
            }

            return null;
        }
    }

    private Detector[] mDetectorArray;
    private Detector.Detections[] mDetectionsArray;

    private AllDetector(FaceDetector faceDetector, BarcodeDetector barcodeDetector, TextRecognizer textRecognizer) {
        super();

        mDetectorArray = new Detector[DetectorType.values().length];
        mDetectionsArray = new Detector.Detections[DetectorType.values().length];

        mDetectorArray[DetectorType.Face.ordinal()] = faceDetector;
        mDetectorArray[DetectorType.Barcode.ordinal()] = barcodeDetector;
        mDetectorArray[DetectorType.Text.ordinal()] = textRecognizer;
    }

    @Override
    public SparseArray detect(Frame frame) {
        SparseArray[] detectedItemsArray = new SparseArray[DetectorType.values().length];

        // 全てのDetector/Recognizerにて解析を実施
        for(int i = 0, size = DetectorType.values().length; i<size; i++){
            detectedItemsArray[i] = mDetectorArray[i].detect(frame);
        }

        // 解析結果を1つのSparceArrayに纏めて返却
        SparseArray ret = new SparseArray();
        for(SparseArray detectedItems : detectedItemsArray){
            for(int i=0, size=detectedItems.size(); i<size; i++){
                ret.append(detectedItems.keyAt(i), detectedItems.valueAt(i));
            }
        }

        return ret;
    }

    @Override
    public void release() {
        super.release();
        for (DetectorType detectorType : DetectorType.values()) {
            mDetectorArray[detectorType.ordinal()].release();
        }
    }

    @Override
    public boolean setFocus(int i) {
        boolean ret = false;

        for (DetectorType detectorType : DetectorType.values()) {
            Detections detections = mDetectionsArray[detectorType.ordinal()];

            if (detections != null) {
                Object detectedItem = detections.getDetectedItems().get(i, null);
                if (detectedItem != null) {
                    ret = mDetectorArray[detectorType.ordinal()].setFocus(i);
                    break;
                }
            }
        }

        return ret;
    }

    @Override
    public void setProcessor(Processor processor) {
      // Processorを指定していないと、Detector.receiveFrameにてIllegalStateExceptionが発生するので、必ず設定
      super.setProcessor(processor);
      for (DetectorType detectorType : DetectorType.values()) {
          mDetectorArray[detectorType.ordinal()].setProcessor(processor);
      }
    }
}

AllTracker

Face、Barcode、TextBlockを受けれる共通のTrackerとして、AllTrackerを用意しています。
ただし、それぞれの解析結果を得るためのインターフェースが統一されていないので、別途TrackerCommonIfを定義して、それにアクセスするようにしています。
app/src/main/java/jp/eq_inc/testmobilevision/fragment/AllDetectFromCameraFragment.java - TrackerCommonIf
private interface TrackerCommonIf {
    String getDisplayValue(Object item);

    RectF getBounds(Object item);

    String logOutput(Object item);
}

app/src/main/java/jp/eq_inc/testmobilevision/fragment/AllDetectFromCameraFragment.java - AllTracker
private class AllTracker extends AbstractLocalTracker {
    private TrackerCommonIf[] mTrackerArray;

    public AllTracker() {
        mTrackerArray = new TrackerCommonIf[]{
                new FaceTracker(),
                new BarcodeTracker(),
                new TextBlockTracker(),
        };
    }

    @Override
    public String getDisplayValue(Object item) {
        AllDetector.DetectorType detectorType = AllDetector.DetectorType.getDetectorTypeFromItem(item);
        return mTrackerArray[detectorType.ordinal()].getDisplayValue(item);
    }

    @Override
    public RectF getBounds(Object item) {
        AllDetector.DetectorType detectorType = AllDetector.DetectorType.getDetectorTypeFromItem(item);
        return mTrackerArray[detectorType.ordinal()].getBounds(item);
    }

    @Override
    public String logOutput(Object item) {
        AllDetector.DetectorType detectorType = AllDetector.DetectorType.getDetectorTypeFromItem(item);
        return mTrackerArray[detectorType.ordinal()].logOutput(item);
    }
}

これらのものを使用して、カメラからの入力映像に対する顔認識等と同じように実装します。
処理の流れとしては、以下のようになります。
  1. カメラプレビューを表示するためのSurfaceViewを用意
  2. AllDetectorインスタンスを生成
  3. Processorインスタンスを生成し、AllDetectorに設定
  4. CameraSourceインスタンスを生成し、startメソッドをコール

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

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

2. AllDetectorインスタンスを生成

AllDetectorインスタンスを生成します。AllDetectorインスタンスの生成は、他のDetectorと同じようにAllDetector.Builderを経由して生成します。
app/src/main/java/jp/eq_inc/testmobilevision/fragment/AllDetectFromCameraFragment.java - initAllDetector
AllDetector.Builder builder = new AllDetector.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());

// barcode format
Integer selectedFormat = (Integer) ((Spinner) activity.findViewById(R.id.spnrBarcodeFormat)).getSelectedItem();
if (selectedFormat == null) {
    selectedFormat = Barcode.ALL_FORMATS;
}
builder.setBarcodeFormats(selectedFormat);

mAllDetector = builder.build();

3. Processorインスタンスを生成し、AllDetectorに設定

カメラからの入力映像に顔を見つけたときに、APIからの通知を受けるProcessorインスタンスを生成し、AllDetectorに設定します。
サンプルアプリでは、標準で用意されている"MultiProcessor"と、顔・バーコード・文字のそれぞれ最も面積の大きなものだけにフォーカスを当てる以下の自前Processor"EachFocusingProcessor"のいずれかを使用できるようにしました。
app/src/main/java/jp/eq_inc/testmobilevision/processor/EachFocusingProcessor.java
public class EachFocusingProcessor implements Detector.Processor {
    private FocusingProcessor[] mFocusingProcessorArray;

    public EachFocusingProcessor(AllDetector detector, Tracker tracker) {
        mFocusingProcessorArray = new FocusingProcessor[]{
                new LargestFaceFocusingProcessor(detector, tracker),
                new BarcodeFocusingProcessor(detector, tracker),
                new TextFocusingProcessor(detector, tracker),
        };
    }

    @Override
    public void release() {
        for (int i = 0, size = mFocusingProcessorArray.length; i < size; i++) {
            mFocusingProcessorArray[i].release();
        }
    }

    @Override
    public void receiveDetections(Detector.Detections detections) {
        SparseArray detectedItems = detections.getDetectedItems();
        if (detectedItems.size() > 0) {
            Object detectedItem = detectedItems.valueAt(0);
            AllDetector.DetectorType detectorType = AllDetector.DetectorType.getDetectorTypeFromItem(detectedItem);

            if (detectorType != null) {
                mFocusingProcessorArray[detectorType.ordinal()].receiveDetections(detections);
            }
        }
    }

    private static class BarcodeFocusingProcessor extends FocusingProcessor<Barcode> {

        public BarcodeFocusingProcessor(Detector<Barcode> detector, Tracker<Barcode> tracker) {
            super(detector, tracker);
        }

        @Override
        public int selectFocus(Detector.Detections<Barcode> detections) {
            SparseArray<Barcode> detectedItems = detections.getDetectedItems();
            int selectedItem = 0;
            int largestSize = 0;
            for (int i = 0, size = detectedItems.size(); i < size; i++) {
                Barcode detectedItem = detectedItems.valueAt(i);
                Rect bounds = detectedItem.getBoundingBox();
                int tempBoundsSize = bounds.width() * bounds.height();
                if (largestSize < tempBoundsSize) {
                    largestSize = tempBoundsSize;
                    selectedItem = detectedItems.keyAt(i);
                }
            }

            return selectedItem;
        }
    }

    private static class TextFocusingProcessor extends FocusingProcessor<TextBlock> {

        public TextFocusingProcessor(Detector<TextBlock> detector, Tracker<TextBlock> tracker) {
            super(detector, tracker);
        }

        @Override
        public int selectFocus(Detector.Detections<TextBlock> detections) {
            SparseArray<TextBlock> detectedItems = detections.getDetectedItems();
            int selectedItem = 0;
            int largestSize = 0;
            for (int i = 0, size = detectedItems.size(); i < size; i++) {
                TextBlock detectedItem = detectedItems.valueAt(i);
                Rect bounds = detectedItem.getBoundingBox();
                int tempBoundsSize = bounds.width() * bounds.height();
                if (largestSize < tempBoundsSize) {
                    largestSize = tempBoundsSize;
                    selectedItem = detectedItems.keyAt(i);
                }
            }

            return selectedItem;
        }
    }
}

app/src/main/java/jp/eq_inc/testmobilevision/fragment/AllDetectFromCameraFragment.java - initAllDetector
// use multi processor
tempSwitch = (SwitchCompat) activity.findViewById(R.id.scUseMultiProcessor);
if (tempSwitch.isChecked()) {
    MultiProcessor.Builder multiProcessorBuilder = new MultiProcessor.Builder(mMultiProcessFactory);
    mAllDetector.setProcessor(multiProcessorBuilder.build());
} else {
    EachFocusingProcessor processor = new EachFocusingProcessor(mAllDetector, new AllTracker());
    mAllDetector.setProcessor(processor);
}

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

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

// 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/AllDetectFromCameraFragment.java - start
try {
    // プレビューを表示したいSurfaceViewのSurfaceHolderを指定
    mCameraSource.start(mCameraPreview.getHolder());
    mRealPreviewSize = mCameraSource.getPreviewSize();
    ret = true;
} catch (IOException e) {
    e.printStackTrace();
}
これで、カメラの入力映像で顔・バーコード・文字を認識すると、AllProcessorに指定したAllTrackerが動作します。

サンプルアプリ

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

コメントの投稿