ML Kit を使用して顔を検出する(Android)

ML Kit を使用すると、画像や動画内の顔を検出できます。

始める前に

  1. まだ Firebase を Android プロジェクトに追加していない場合は追加します。
  2. ML Kit Android ライブラリの依存関係をモジュール(アプリレベル)の Gradle ファイル(通常は app/build.gradle)に追加します:
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      // If you want to detect face contours (landmark detection and classification
      // don't require this additional model):
      implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
    }
    
  3. 省略可能、ただし推奨: アプリが Play ストアからインストールされたら自動で ML モデルをデバイスにダウンロードするようアプリを構成します。

    この構成を行うには、アプリの AndroidManifest.xml ファイルに次の宣言を追加します。

    <application ...>
      ...
      <meta-data
          android:name="com.google.firebase.ml.vision.DEPENDENCIES"
          android:value="face" />
      <!-- To use multiple models: android:value="face,model2,model3" -->
    </application>
    
    インストール時点でのモデルのダウンロードを有効にしない場合は、検出器の初回実行時にモデルがダウンロードされます。ダウンロードが完了する前にリクエストしても結果は生成されません。

入力画像に関するガイドライン

ML Kit で顔を正確に認識するには、入力画像に含まれている顔が十分なピクセルデータによって表現されている必要があります。一般に、画像内で検出する顔は少なくとも 100x100 ピクセルである必要があります。ML Kit で顔の輪郭を検出するには、より高い解像度の入力が必要です。その場合、顔は 200x200 ピクセル以上にする必要があります。

リアルタイム アプリケーションで顔を認識する場合は、入力画像の全体サイズも考慮する必要があります。サイズが小さいほど処理は高速になるため、レイテンシを短くするには画像を低い解像度でキャプチャし(上記の精度要件に留意)、対象の顔が画像のできるだけ多くの部分を占めるようにします。リアルタイムのパフォーマンスを改善するためのヒントもご覧ください。

画像がぼやけていると、認識精度が低下する可能性があります。満足のいく結果が得られない場合は、ユーザーに画像をキャプチャし直すよう求めてください。

カメラに対する顔の向きも、ML Kit で検出される顔の特徴に影響を与える可能性があります。顔検出のコンセプトをご覧ください。

1. 顔検出器を構成する

顔検出器のデフォルト設定のいずれかを変更する場合は、画像に顔検出を適用する前に、FirebaseVisionFaceDetectorOptions オブジェクトを使用して設定を指定します。次の設定を変更できます。

設定
パフォーマンス モード FAST(デフォルト)| ACCURATE

顔を検出する際に速度を優先するか精度を優先するか。

ランドマークを検出する NO_LANDMARKS(デフォルト)| ALL_LANDMARKS

顔の「ランドマーク」(目、耳、鼻、頬、口)を識別するかどうか。

輪郭を検出する NO_CONTOURS(デフォルト)| ALL_CONTOURS

顔の特徴の輪郭を検出するかどうか。輪郭は、画像内で最も目立つ顔についてのみ検出されます。

顔を分類する NO_CLASSIFICATIONS(デフォルト)| ALL_CLASSIFICATIONS

顔を「ほほ笑んでいる」や「目を開けている」などのカテゴリに分類するかどうか。

顔の最小サイズ float(デフォルト: 0.1f

画像を基準とした、検出する顔の最小サイズ。

顔トラッキングを有効にする false(デフォルト)| true

複数の画像間で顔をトラッキングするために使用できる ID を顔に割り当てるかどうか。

輪郭の検出が有効になっていると、検出される顔が 1 つだけになり、顔トラッキングで有用な結果が得られません。このため、輪郭の検出と顔トラッキングの両方を有効にしないでください。検出速度の向上にもつながります。

例:

Java

// High-accuracy landmark detection and face classification
FirebaseVisionFaceDetectorOptions highAccuracyOpts =
        new FirebaseVisionFaceDetectorOptions.Builder()
                .setPerformanceMode(FirebaseVisionFaceDetectorOptions.ACCURATE)
                .setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
                .setClassificationMode(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS)
                .build();

// Real-time contour detection of multiple faces
FirebaseVisionFaceDetectorOptions realTimeOpts =
        new FirebaseVisionFaceDetectorOptions.Builder()
                .setContourMode(FirebaseVisionFaceDetectorOptions.ALL_CONTOURS)
                .build();

Kotlin+KTX

// High-accuracy landmark detection and face classification
val highAccuracyOpts = FirebaseVisionFaceDetectorOptions.Builder()
        .setPerformanceMode(FirebaseVisionFaceDetectorOptions.ACCURATE)
        .setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
        .setClassificationMode(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS)
        .build()

// Real-time contour detection of multiple faces
val realTimeOpts = FirebaseVisionFaceDetectorOptions.Builder()
        .setContourMode(FirebaseVisionFaceDetectorOptions.ALL_CONTOURS)
        .build()

2. 顔検出器を実行する

画像内の顔を検出するには、Bitmapmedia.ImageByteBuffer、バイト配列、またはデバイス上のファイルから FirebaseVisionImage オブジェクトを作成します。次に、FirebaseVisionImage オブジェクトを FirebaseVisionFaceDetectordetectInImage メソッドに渡します。

顔認識には、480x360 ピクセル以上の画像を使用する必要があります。リアルタイムで顔を認識している場合、この最小解像度でフレームをキャプチャすると、レイテンシを短縮できます。

  1. 画像から FirebaseVisionImage オブジェクトを作成します。

    • FirebaseVisionImage オブジェクトを media.Image オブジェクトから作成するには(デバイスのカメラから画像をキャプチャする場合など)、media.Image オブジェクトと画像の回転を FirebaseVisionImage.fromMediaImage() に渡します。

      CameraX ライブラリを使用する場合は、OnImageCapturedListener クラスと ImageAnalysis.Analyzer クラスによって回転値が計算されるので、FirebaseVisionImage.fromMediaImage() を呼び出す前に、その回転を ML Kit の ROTATION_ 定数のいずれかに変換するだけで済みます。

      Java

      private class YourAnalyzer implements ImageAnalysis.Analyzer {
      
          private int degreesToFirebaseRotation(int degrees) {
              switch (degrees) {
                  case 0:
                      return FirebaseVisionImageMetadata.ROTATION_0;
                  case 90:
                      return FirebaseVisionImageMetadata.ROTATION_90;
                  case 180:
                      return FirebaseVisionImageMetadata.ROTATION_180;
                  case 270:
                      return FirebaseVisionImageMetadata.ROTATION_270;
                  default:
                      throw new IllegalArgumentException(
                              "Rotation must be 0, 90, 180, or 270.");
              }
          }
      
          @Override
          public void analyze(ImageProxy imageProxy, int degrees) {
              if (imageProxy == null || imageProxy.getImage() == null) {
                  return;
              }
              Image mediaImage = imageProxy.getImage();
              int rotation = degreesToFirebaseRotation(degrees);
              FirebaseVisionImage image =
                      FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
              // Pass image to an ML Kit Vision API
              // ...
          }
      }
      

      Kotlin+KTX

      private class YourImageAnalyzer : ImageAnalysis.Analyzer {
          private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
              0 -> FirebaseVisionImageMetadata.ROTATION_0
              90 -> FirebaseVisionImageMetadata.ROTATION_90
              180 -> FirebaseVisionImageMetadata.ROTATION_180
              270 -> FirebaseVisionImageMetadata.ROTATION_270
              else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
          }
      
          override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
              val mediaImage = imageProxy?.image
              val imageRotation = degreesToFirebaseRotation(degrees)
              if (mediaImage != null) {
                  val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                  // Pass image to an ML Kit Vision API
                  // ...
              }
          }
      }
      

      画像の回転を取得するカメラ ライブラリを使用しない場合は、デバイスの回転とデバイス内のカメラセンサーの向きから計算できます。

      Java

      private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
      static {
          ORIENTATIONS.append(Surface.ROTATION_0, 90);
          ORIENTATIONS.append(Surface.ROTATION_90, 0);
          ORIENTATIONS.append(Surface.ROTATION_180, 270);
          ORIENTATIONS.append(Surface.ROTATION_270, 180);
      }
      
      /**
       * Get the angle by which an image must be rotated given the device's current
       * orientation.
       */
      @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
      private int getRotationCompensation(String cameraId, Activity activity, Context context)
              throws CameraAccessException {
          // Get the device's current rotation relative to its "native" orientation.
          // Then, from the ORIENTATIONS table, look up the angle the image must be
          // rotated to compensate for the device's rotation.
          int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
          int rotationCompensation = ORIENTATIONS.get(deviceRotation);
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
          int sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION);
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          int result;
          switch (rotationCompensation) {
              case 0:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  break;
              case 90:
                  result = FirebaseVisionImageMetadata.ROTATION_90;
                  break;
              case 180:
                  result = FirebaseVisionImageMetadata.ROTATION_180;
                  break;
              case 270:
                  result = FirebaseVisionImageMetadata.ROTATION_270;
                  break;
              default:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  Log.e(TAG, "Bad rotation value: " + rotationCompensation);
          }
          return result;
      }

      Kotlin+KTX

      private val ORIENTATIONS = SparseIntArray()
      
      init {
          ORIENTATIONS.append(Surface.ROTATION_0, 90)
          ORIENTATIONS.append(Surface.ROTATION_90, 0)
          ORIENTATIONS.append(Surface.ROTATION_180, 270)
          ORIENTATIONS.append(Surface.ROTATION_270, 180)
      }
      /**
       * Get the angle by which an image must be rotated given the device's current
       * orientation.
       */
      @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
      @Throws(CameraAccessException::class)
      private fun getRotationCompensation(cameraId: String, activity: Activity, context: Context): Int {
          // Get the device's current rotation relative to its "native" orientation.
          // Then, from the ORIENTATIONS table, look up the angle the image must be
          // rotated to compensate for the device's rotation.
          val deviceRotation = activity.windowManager.defaultDisplay.rotation
          var rotationCompensation = ORIENTATIONS.get(deviceRotation)
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
          val sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          val result: Int
          when (rotationCompensation) {
              0 -> result = FirebaseVisionImageMetadata.ROTATION_0
              90 -> result = FirebaseVisionImageMetadata.ROTATION_90
              180 -> result = FirebaseVisionImageMetadata.ROTATION_180
              270 -> result = FirebaseVisionImageMetadata.ROTATION_270
              else -> {
                  result = FirebaseVisionImageMetadata.ROTATION_0
                  Log.e(TAG, "Bad rotation value: $rotationCompensation")
              }
          }
          return result
      }

      次に、media.Image オブジェクトと回転値を FirebaseVisionImage.fromMediaImage() に渡します。

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • FirebaseVisionImage オブジェクトをファイルの URI から作成するには、アプリ コンテキストとファイルの URI を FirebaseVisionImage.fromFilePath() に渡します。これは、ACTION_GET_CONTENT インテントを使用して、ギャラリー アプリから画像を選択するようにユーザーに促すときに便利です。

      Java

      FirebaseVisionImage image;
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri);
      } catch (IOException e) {
          e.printStackTrace();
      }

      Kotlin+KTX

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • FirebaseVisionImage オブジェクトを ByteBuffer またはバイト配列から作成するには、media.Image 入力について上記のように、まず画像の回転を計算します。

      次に、画像の高さ、幅、カラー エンコード形式、回転を含む FirebaseVisionImageMetadata オブジェクトを作成します。

      Java

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
              .setWidth(480)   // 480x360 is typically sufficient for
              .setHeight(360)  // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build();

      Kotlin+KTX

      val metadata = FirebaseVisionImageMetadata.Builder()
              .setWidth(480) // 480x360 is typically sufficient for
              .setHeight(360) // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build()

      メタデータ オブジェクトと、バッファまたは配列を使用して、FirebaseVisionImage オブジェクトを作成します。

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • FirebaseVisionImage オブジェクトを Bitmap オブジェクトから作成するコードは、以下のとおりです。

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      Bitmap オブジェクトによって表される画像は、これ以上回転させる必要がないように、正しい向きになっている必要があります。
  2. FirebaseVisionFaceDetector のインスタンスを取得します。

    Java

    FirebaseVisionFaceDetector detector = FirebaseVision.getInstance()
            .getVisionFaceDetector(options);

    Kotlin+KTX

    val detector = FirebaseVision.getInstance()
            .getVisionFaceDetector(options)
  3. 最後に、画像を detectInImage メソッドに渡します。

    Java

    Task<List<FirebaseVisionFace>> result =
            detector.detectInImage(image)
                    .addOnSuccessListener(
                            new OnSuccessListener<List<FirebaseVisionFace>>() {
                                @Override
                                public void onSuccess(List<FirebaseVisionFace> faces) {
                                    // Task completed successfully
                                    // ...
                                }
                            })
                    .addOnFailureListener(
                            new OnFailureListener() {
                                @Override
                                public void onFailure(@NonNull Exception e) {
                                    // Task failed with an exception
                                    // ...
                                }
                            });

    Kotlin+KTX

    val result = detector.detectInImage(image)
            .addOnSuccessListener { faces ->
                // Task completed successfully
                // ...
            }
            .addOnFailureListener { e ->
                // Task failed with an exception
                // ...
            }

3. 検出された顔に関する情報を取得する

顔認識オペレーションが成功すると、FirebaseVisionFace オブジェクトのリストが成功リスナーに渡されます。各 FirebaseVisionFace オブジェクトは画像内で検出された顔を表します。顔ごとに、入力画像の境界座標と、顔検出器に検出するよう構成したその他の情報を取得できます。次に例を示します。

Java

for (FirebaseVisionFace face : faces) {
    Rect bounds = face.getBoundingBox();
    float rotY = face.getHeadEulerAngleY();  // Head is rotated to the right rotY degrees
    float rotZ = face.getHeadEulerAngleZ();  // Head is tilted sideways rotZ degrees

    // If landmark detection was enabled (mouth, ears, eyes, cheeks, and
    // nose available):
    FirebaseVisionFaceLandmark leftEar = face.getLandmark(FirebaseVisionFaceLandmark.LEFT_EAR);
    if (leftEar != null) {
        FirebaseVisionPoint leftEarPos = leftEar.getPosition();
    }

    // If contour detection was enabled:
    List<FirebaseVisionPoint> leftEyeContour =
            face.getContour(FirebaseVisionFaceContour.LEFT_EYE).getPoints();
    List<FirebaseVisionPoint> upperLipBottomContour =
            face.getContour(FirebaseVisionFaceContour.UPPER_LIP_BOTTOM).getPoints();

    // If classification was enabled:
    if (face.getSmilingProbability() != FirebaseVisionFace.UNCOMPUTED_PROBABILITY) {
        float smileProb = face.getSmilingProbability();
    }
    if (face.getRightEyeOpenProbability() != FirebaseVisionFace.UNCOMPUTED_PROBABILITY) {
        float rightEyeOpenProb = face.getRightEyeOpenProbability();
    }

    // If face tracking was enabled:
    if (face.getTrackingId() != FirebaseVisionFace.INVALID_ID) {
        int id = face.getTrackingId();
    }
}

Kotlin+KTX

for (face in faces) {
    val bounds = face.boundingBox
    val rotY = face.headEulerAngleY // Head is rotated to the right rotY degrees
    val rotZ = face.headEulerAngleZ // Head is tilted sideways rotZ degrees

    // If landmark detection was enabled (mouth, ears, eyes, cheeks, and
    // nose available):
    val leftEar = face.getLandmark(FirebaseVisionFaceLandmark.LEFT_EAR)
    leftEar?.let {
        val leftEarPos = leftEar.position
    }

    // If contour detection was enabled:
    val leftEyeContour = face.getContour(FirebaseVisionFaceContour.LEFT_EYE).points
    val upperLipBottomContour = face.getContour(FirebaseVisionFaceContour.UPPER_LIP_BOTTOM).points

    // If classification was enabled:
    if (face.smilingProbability != FirebaseVisionFace.UNCOMPUTED_PROBABILITY) {
        val smileProb = face.smilingProbability
    }
    if (face.rightEyeOpenProbability != FirebaseVisionFace.UNCOMPUTED_PROBABILITY) {
        val rightEyeOpenProb = face.rightEyeOpenProbability
    }

    // If face tracking was enabled:
    if (face.trackingId != FirebaseVisionFace.INVALID_ID) {
        val id = face.trackingId
    }
}

顔の輪郭の例

顔の輪郭検出を有効にすると、検出された顔の特徴が点のリストで表示されます。これらの点は特徴の形状を表します。輪郭の表現方法については、顔検出のコンセプトの概要をご覧ください。

次の図は、これらの点が顔にどのようにマッピングされるかを示します(画像をクリックすると拡大します)。

リアルタイム顔検出

リアルタイムのアプリケーションで顔検出を使用する場合は、適切なフレームレートを得るために次のガイドラインに従ってください。

  • 顔の輪郭検出、または分類とランドマークの検出のいずれかを使用するように顔検出器を構成してください。両方は使用できません。

    輪郭検出
    ランドマーク検出
    分類
    ランドマークの検出と分類
    輪郭検出とランドマーク検出
    輪郭検出と分類
    輪郭検出、ランドマーク検出、分類

  • FAST モードを有効にします(デフォルトで有効)。

  • より低い解像度で画像をキャプチャすることを検討してください。ただし、この API の画像サイズに関する要件にも留意してください。

  • 検出器の呼び出しのスロットル調整を行います。検出器の実行中に新しい動画フレームが使用可能になった場合は、そのフレームをドロップします。
  • 検出器の出力を使用して入力画像の上にグラフィックスをオーバーレイする場合は、まず ML Kit から検出結果を取得し、画像とオーバーレイを 1 つのステップでレンダリングします。これにより、ディスプレイ サーフェスへのレンダリングは入力フレームごとに 1 回で済みます。
  • Camera2 API を使用する場合は、ImageFormat.YUV_420_888 形式で画像をキャプチャします。

    古い Camera API を使用する場合は、ImageFormat.NV21 形式で画像をキャプチャします。