chromium/services/shape_detection/android/java/src/org/chromium/shape_detection/FaceDetectionImplGmsCore.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.shape_detection;

import android.graphics.PointF;
import android.util.SparseArray;

import com.google.android.gms.vision.Frame;
import com.google.android.gms.vision.face.Face;
import com.google.android.gms.vision.face.FaceDetector;
import com.google.android.gms.vision.face.Landmark;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.gfx.mojom.RectF;
import org.chromium.mojo.system.MojoException;
import org.chromium.shape_detection.mojom.FaceDetection;
import org.chromium.shape_detection.mojom.FaceDetectionResult;
import org.chromium.shape_detection.mojom.FaceDetectorOptions;
import org.chromium.shape_detection.mojom.LandmarkType;

import java.util.ArrayList;
import java.util.List;

/**
 * Google Play services implementation of the FaceDetection service defined in
 * services/shape_detection/public/mojom/facedetection.mojom
 */
public class FaceDetectionImplGmsCore implements FaceDetection {
    private static final String TAG = "FaceDetectionImpl";
    private static final int MAX_FACES = 32;
    // Maximum rotation around the z-axis allowed when computing a tighter bounding box for the
    // detected face.
    private static final int MAX_EULER_Z = 15;
    private final int mMaxFaces;
    private final boolean mFastMode;
    private final FaceDetector mFaceDetector;

    FaceDetectionImplGmsCore(FaceDetectorOptions options) {
        FaceDetector.Builder builder =
                new FaceDetector.Builder(ContextUtils.getApplicationContext());
        mMaxFaces = Math.min(options.maxDetectedFaces, MAX_FACES);
        mFastMode = options.fastMode;

        try {
            builder.setMode(mFastMode ? FaceDetector.FAST_MODE : FaceDetector.ACCURATE_MODE);
            builder.setLandmarkType(FaceDetector.ALL_LANDMARKS);
            if (mMaxFaces == 1) {
                builder.setProminentFaceOnly(true);
            }
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Unexpected exception " + e);
            assert false;
        }

        mFaceDetector = builder.build();
    }

    @Override
    public void detect(org.chromium.skia.mojom.BitmapN32 bitmapData, Detect_Response callback) {
        // The vision library will be downloaded the first time the API is used
        // on the device; this happens "fast", but it might have not completed,
        // bail in this case.
        if (!mFaceDetector.isOperational()) {
            Log.e(TAG, "FaceDetector is not operational");

            // Fallback to Android's FaceDetectionImpl.
            FaceDetectorOptions options = new FaceDetectorOptions();
            options.fastMode = mFastMode;
            options.maxDetectedFaces = mMaxFaces;
            FaceDetectionImpl detector = new FaceDetectionImpl(options);
            detector.detect(bitmapData, callback);
            return;
        }

        Frame frame = BitmapUtils.convertToFrame(bitmapData);
        if (frame == null) {
            Log.e(TAG, "Error converting Mojom Bitmap to Frame");
            callback.call(new FaceDetectionResult[0]);
            return;
        }

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

        FaceDetectionResult[] faceArray = new FaceDetectionResult[faces.size()];
        for (int i = 0; i < faces.size(); i++) {
            faceArray[i] = new FaceDetectionResult();
            final Face face = faces.valueAt(i);

            final PointF corner = face.getPosition();
            faceArray[i].boundingBox = new RectF();
            faceArray[i].boundingBox.x = corner.x;
            faceArray[i].boundingBox.y = corner.y;
            faceArray[i].boundingBox.width = face.getWidth();
            faceArray[i].boundingBox.height = face.getHeight();

            final List<Landmark> landmarks = face.getLandmarks();
            ArrayList<org.chromium.shape_detection.mojom.Landmark> mojoLandmarks =
                    new ArrayList<org.chromium.shape_detection.mojom.Landmark>(landmarks.size());

            for (int j = 0; j < landmarks.size(); j++) {
                final Landmark landmark = landmarks.get(j);
                final int landmarkType = landmark.getType();
                if (landmarkType != Landmark.LEFT_EYE
                        && landmarkType != Landmark.RIGHT_EYE
                        && landmarkType != Landmark.BOTTOM_MOUTH
                        && landmarkType != Landmark.NOSE_BASE) {
                    continue;
                }

                org.chromium.shape_detection.mojom.Landmark mojoLandmark =
                        new org.chromium.shape_detection.mojom.Landmark();
                mojoLandmark.locations = new org.chromium.gfx.mojom.PointF[1];
                mojoLandmark.locations[0] = new org.chromium.gfx.mojom.PointF();
                mojoLandmark.locations[0].x = landmark.getPosition().x;
                mojoLandmark.locations[0].y = landmark.getPosition().y;

                if (landmarkType == Landmark.LEFT_EYE) {
                    mojoLandmark.type = LandmarkType.EYE;
                } else if (landmarkType == Landmark.RIGHT_EYE) {
                    mojoLandmark.type = LandmarkType.EYE;
                } else if (landmarkType == Landmark.BOTTOM_MOUTH) {
                    mojoLandmark.type = LandmarkType.MOUTH;
                } else {
                    assert landmarkType == Landmark.NOSE_BASE;
                    mojoLandmark.type = LandmarkType.NOSE;
                }
                mojoLandmarks.add(mojoLandmark);
            }
            faceArray[i].landmarks =
                    mojoLandmarks.toArray(
                            new org.chromium.shape_detection.mojom.Landmark[mojoLandmarks.size()]);
        }
        callback.call(faceArray);
    }

    @Override
    public void close() {
        mFaceDetector.release();
    }

    @Override
    public void onConnectionError(MojoException e) {
        close();
    }
}