זיהוי פרטים של 'רשת פנים' באמצעות ML Kit ב-Android

אתם יכולים להשתמש ב-ML Kit כדי לזהות פנים בתמונות ובסרטונים דמויי סלפי.

ממשק API לזיהוי רשת פנים
שם ה-SDKface-mesh-detection
הטמעההקוד והנכסים מקושרים לאפליקציה באופן סטטי בזמן ה-build.
ההשפעה של גודל האפליקציה~6.4MB
ביצועיםזמן אמת ברוב המכשירים.

רוצה לנסות?

לפני שמתחילים

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את במאגר Maven בקטע ה-buildscript וגם בקטע של כל הפרויקטים.

  2. הוספת התלות לספריית זיהוי של רשת הפנים של ML Kit קובץ GRid ברמת האפליקציה של המודול, שהוא בדרך כלל app/build.gradle:

    dependencies {
     // ...
    
     implementation 'com.google.mlkit:face-mesh-detection:16.0.0-beta1'
    }
    

הנחיות להוספת תמונה

  1. יש לצלם את התמונות בטווח של כ-2 מטרים ממצלמת המכשיר, לכן שהפנים גדולות מספיק לזיהוי אופטימלי של רשת הפנים. לחשבון באופן כללי, ככל שהפנים גדולות יותר, כך זיהוי רשת הפנים טוב יותר.

  2. הפנים צריכות להיות פונה למצלמה, לפחות חצי מהפנים גלויות. עצם גדול שנמצא בין הפנים למצלמה עלול לגרום להקטנה מדויקות.

אם רוצים לזהות פנים באפליקציה בזמן אמת, צריך גם לוקחים בחשבון את הממדים הכוללים של תמונת הקלט. ניתן להשתמש בתמונות קטנות יותר מעובד מהר יותר, לכן צילום תמונות ברזולוציות נמוכות יותר מפחית את זמן האחזור. עם זאת, חשוב לזכור את דרישות הדיוק שמפורטות למעלה ולוודא הפנים של מושא הצילום תופסות כמה שיותר מהתמונה.

הגדרת המזהה של רשת הפנים

אם רוצים לשנות הגדרות ברירת מחדל כלשהן של מזהה רשת הפנים, צריך לציין את ההגדרות האלה FaceMeshDetectorOptions לאובייקט. אפשר לשנות את ההגדרות הבאות:

  1. setUseCase

    • BOUNDING_BOX_ONLY: מספקת תיבה תוחמת (bounding box) רק לרשת הפנים שזוהתה. זהו גלאי הפנים המהיר ביותר, אבל יש לו מגבלת טווח(פנים) חייב להיות במרחק של כ-2 מטרים מהמצלמה.

    • FACE_MESH (אפשרות ברירת המחדל): מספקת תיבה תוחמת ופנים נוספות מידע על הרשת (468 נקודות בתלת-ממד ופרטי משולש). בהשוואה תרחיש לדוגמה של BOUNDING_BOX_ONLY, זמן האחזור גדל בכ-15%, כפי שנמדד ב- Pixel 3.

לדוגמה:

Kotlin

val defaultDetector = FaceMeshDetection.getClient(
  FaceMeshDetectorOptions.DEFAULT_OPTIONS)

val boundingBoxDetector = FaceMeshDetection.getClient(
  FaceMeshDetectorOptions.Builder()
    .setUseCase(UseCase.BOUNDING_BOX_ONLY)
    .build()
)

Java

FaceMeshDetector defaultDetector =
        FaceMeshDetection.getClient(
                FaceMeshDetectorOptions.DEFAULT_OPTIONS);

FaceMeshDetector boundingBoxDetector = FaceMeshDetection.getClient(
        new FaceMeshDetectorOptions.Builder()
                .setUseCase(UseCase.BOUNDING_BOX_ONLY)
                .build()
        );

הכנת תמונת הקלט

כדי לזהות פנים בתמונה, צריך ליצור אובייקט InputImage מתוך Bitmap, media.Image, ByteBuffer, מערך בייטים או קובץ במכשיר. לאחר מכן מעבירים את האובייקט InputImage ל-method process של FaceDetector.

לזיהוי רשת פנים, עליך להשתמש בתמונה במידות של לפחות 480x360 פיקסלים. אם מזהים פנים בזמן אמת, אפשר לצלם פריימים ברזולוציה המינימלית הזו, יעזרו לכם לצמצם את זמן האחזור.

אפשר ליצור InputImage ממקורות שונים, מוסבר על כל אחד מהם בהמשך.

באמצעות media.Image

כדי ליצור InputImage מאובייקט media.Image, למשל כשמצלמים תמונה המצלמה של המכשיר, מעבירים את האובייקט media.Image ואת ל-InputImage.fromMediaImage().

אם משתמשים ספריית CameraX, OnImageCapturedListener ImageAnalysis.Analyzer מחלקות מחשבים את ערך הסבב עבורך.

Kotlin

private class YourImageAnalyzer : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

אם לא משתמשים בספריית מצלמה שמאפשרת לקבוע את כיוון הסיבוב של התמונה, הוא יכול לחשב אותו על סמך זווית הסיבוב של המכשיר וכיוון המצלמה החיישן במכשיר:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * 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, isFrontFacing: Boolean): 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)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * 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, boolean isFrontFacing)
        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);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

לאחר מכן, מעבירים את האובייקט media.Image הערך של מעלה הסיבוב ל-InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

שימוש ב-URI של קובץ

כדי ליצור InputImage מ-URI של קובץ, מעבירים את ההקשר של האפליקציה ואת ה-URI של הקובץ InputImage.fromFilePath() זה שימושי כאשר צריך להשתמש ב-Intent ACTION_GET_CONTENT כדי לבקש מהמשתמש לבחור תמונה מאפליקציית הגלריה.

Kotlin

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

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

שימוש ב-ByteBuffer או ב-ByteArray

כדי ליצור InputImage מ-ByteBuffer או מ-ByteArray, קודם צריך לחשב את התמונה מעלות סיבוב כפי שתואר קודם לכן עבור קלט media.Image. אחר כך יוצרים את האובייקט InputImage עם מאגר נתונים זמני או מערך, יחד עם גובה, רוחב, פורמט קידוד צבעים ומידת סיבוב:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

באמצעות Bitmap

כדי ליצור InputImage מאובייקט Bitmap, צריך ליצור את ההצהרה הבאה:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

התמונה מיוצגת על ידי אובייקט Bitmap ביחד עם מעלות סיבוב.

עיבוד התמונה

מעבירים את התמונה ל-method process:

Kotlin

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

Java


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

קבלת מידע על רשת הפנים שזוהתה

אם יזוהו פנים בתמונה, תוצג רשימה של FaceMesh אובייקטים אל 'המאזינים להצלחה'. כל FaceMesh מייצג פנים שזוהו תמונה. עבור כל רשת פנים אפשר לקבל את הקואורדינטות התוחמות בקלט וגם כל מידע אחר שהגדרתם את רשת הפנים את הגלאי כדי למצוא.

Kotlin

for (faceMesh in faceMeshs) {
    val bounds: Rect = faceMesh.boundingBox()

    // Gets all points
    val faceMeshpoints = faceMesh.allPoints
    for (faceMeshpoint in faceMeshpoints) {
      val index: Int = faceMeshpoints.index()
      val position = faceMeshpoint.position
    }

    // Gets triangle info
    val triangles: List<Triangle<FaceMeshPoint>> = faceMesh.allTriangles
    for (triangle in triangles) {
      // 3 Points connecting to each other and representing a triangle area.
      val connectedPoints = triangle.allPoints()
    }
}

Java

for (FaceMesh faceMesh : faceMeshs) {
    Rect bounds = faceMesh.getBoundingBox();

    // Gets all points
    List<FaceMeshPoint> faceMeshpoints = faceMesh.getAllPoints();
    for (FaceMeshPoint faceMeshpoint : faceMeshpoints) {
        int index = faceMeshpoints.getIndex();
        PointF3D position = faceMeshpoint.getPosition();
    }

    // Gets triangle info
    List<Triangle<FaceMeshPoint>> triangles = faceMesh.getAllTriangles();
    for (Triangle<FaceMeshPoint> triangle : triangles) {
        // 3 Points connecting to each other and representing a triangle area.
        List<FaceMeshPoint> connectedPoints = triangle.getAllPoints();
    }
}