זיהוי פנים באמצעות ערכת ML ב-Android

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

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

תכונהלא מקובצותבחבילה
יישוםמתבצעת הורדה דינמית של המודל באמצעות שירותי Google Play.המודל מקושר באופן סטטי לאפליקציה שלך בזמן היצירה.
גודל האפליקציהגודל של כ-800KB.גודל של כ-6.9MB.
זמן האתחולייתכן שתצטרכו להמתין עד שהמודל יוריד לפני השימוש הראשון.המודל זמין באופן מיידי

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

  1. בקובץ build.gradle ברמת הפרויקט, חשוב להקפיד לכלול את מאגר הנתונים של Google&#39 גם בקטע buildscript וגם בקטע allprojects.

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

    כדי לקבץ את המודל באפליקציה:

    dependencies {
      // ...
      // Use this dependency to bundle the model with your app
      implementation 'com.google.mlkit:face-detection:16.1.5'
    }
    

    כדי להשתמש במודל בשירותי Google Play:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded model in Google Play Services
      implementation 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
    }
    
  3. אם בחרתם להשתמש במודל ב-Google Play Services, תוכלו להגדיר שהאפליקציה תוריד באופן אוטומטי את הדגם למכשיר לאחר ההתקנה מחנות Play. כדי לעשות את זה, צריך להוסיף את ההצהרה הבאה לקובץ AndroidManifest.xml של האפליקציה:

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="face" >
          <!-- To use multiple models: android:value="face,model2,model3" -->
    </application>
    

    אפשר גם לבדוק באופן מפורש את זמינות הדגם ולבקש הורדה דרך ModuleInstallClient API של Google Play.

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

הנחיות להזנת תמונה

כדי לזהות פנים, עליכם להשתמש בתמונה במידות 480x360 פיקסלים לפחות. כדי ש-ML Kit יזהה פנים באופן מדויק, תמונות הקלט חייבות להכיל פנים שמיוצגות על ידי נתוני פיקסלים מספיקים. באופן כללי, כל פנים שרוצים לזהות בתמונה צריכות להיות בגודל של 100x100 פיקסלים לפחות. כדי לזהות קווי מתאר של הפנים, לערכת ה-ML Kit נדרש רזולוציה גבוהה יותר: כל פנים צריכות להיות בגודל של 200x200 פיקסלים לפחות.

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

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

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

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

לפני שמחילים את זיהוי הפנים על תמונה, אם רוצים לשנות את אחת מהגדרות ברירת המחדל של מזהה הפנים #-39, צריך לציין את ההגדרות האלה עם אובייקט FaceDetectorOptions. ניתן לשנות את ההגדרות הבאות:

הגדרות
setPerformanceMode PERFORMANCE_MODE_FAST (ברירת מחדל) | PERFORMANCE_MODE_ACCURATE

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

setLandmarkMode LANDMARK_MODE_NONE (ברירת מחדל) | LANDMARK_MODE_ALL

האם לנסות לזהות את פנים האף והפנים, סימני הפה, האוזניים, האף, הלחיים הפה וכו'.

setContourMode CONTOUR_MODE_NONE (ברירת מחדל) | CONTOUR_MODE_ALL

אם לזהות קווי מתאר של תכונות הפנים. קונטורים מזוהים רק עבור הפנים הבולטות ביותר בתמונה.

setClassificationMode CLASSIFICATION_MODE_NONE (ברירת מחדל) | CLASSIFICATION_MODE_ALL

האם לסווג קטגוריות לפי קטגוריות כמו " smiling" ו-"עיניים פתוחות"

setMinFaceSize float (ברירת מחדל: 0.1f)

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

enableTracking false (ברירת מחדל) | true

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

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

למשל:

Kotlin

// High-accuracy landmark detection and face classification
val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
        .build()

// Real-time contour detection
val realTimeOpts = FaceDetectorOptions.Builder()
        .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
        .build()

Java

// High-accuracy landmark detection and face classification
FaceDetectorOptions highAccuracyOpts =
        new FaceDetectorOptions.Builder()
                .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
                .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
                .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
                .build();

// Real-time contour detection
FaceDetectorOptions realTimeOpts =
        new FaceDetectorOptions.Builder()
                .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
                .build();

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

כדי לזהות פנים בתמונה, יש ליצור אובייקט InputImage מBitmap, media.Image, ByteBuffer, ממערך בייט או מקובץ במכשיר. לאחר מכן, צריך להעביר את האובייקט InputImage אל השיטה 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(). האפשרות הזו שימושית כשמשתמשים בכוונת 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 יחד עם מעלות סיבוב.

3. קבלת מופע של FaceDetector

Kotlin

val detector = FaceDetection.getClient(options)
// Or, to use the default option:
// val detector = FaceDetection.getClient();

Java

FaceDetector detector = FaceDetection.getClient(options);
// Or use the default options:
// FaceDetector detector = FaceDetection.getClient();

4. עיבוד התמונה

יש להעביר את התמונה לשיטה process:

Kotlin

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

Java

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

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

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

Kotlin

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(FaceLandmark.LEFT_EAR)
    leftEar?.let {
        val leftEarPos = leftEar.position
    }

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

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

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

Java

for (Face 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):
    FaceLandmark leftEar = face.getLandmark(FaceLandmark.LEFT_EAR);
    if (leftEar != null) {
        PointF leftEarPos = leftEar.getPosition();
    }

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

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

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

דוגמה לעיצוב פנים

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

התמונה הבאה מראה איך הנקודות האלה ממופות אל פנים, לוחצים על התמונה כדי להגדיל אותה:

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

זיהוי פנים בזמן אמת

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

  • צריך להגדיר את מזהה הפנים כך שישתמש בזיהוי או בזיהוי הפנים, וגם בזיהוי של שניהם, אבל לא בשניהם:

    זיהוי של קונטורל
    זיהוי קונטורינג
    סיווג
    זיהוי וסיווג של ציוני דרך
    זיהוי וזיהוי של קווי מתאר
    זיהוי וסיווג של קווי מתאר
    זיהוי וזיהוי של קונטורים

  • הפעלת המצב FAST (מופעל כברירת מחדל).

  • מומלץ לצלם תמונות ברזולוציה נמוכה יותר. עם זאת, חשוב גם לזכור את דרישות המידות של תמונת ה-API.

  • אם משתמשים ב- Camera או ב-camera2 API, מווסתים את השיחות למזהה. אם פריים חדש של וידאו הופך לזמין בזמן שהמזהה פועל, משחררים את הפריים. לדוגמה, אפשר לעיין בקורס VisionProcessorBase באפליקציה למתחילים.
  • אם בחרת להשתמש ב-API CameraX, יש לוודא ששיטת ההחזרה מוגדרת לערך ברירת המחדל ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. כך ניתן להבטיח שתמונה אחת בלבד תישלח לניתוח בכל פעם. אם יופקו תמונות נוספות כשהמנתח יהיה עסוק, הן יושמטו אוטומטית ולא יתווספו לתור. אחרי שהתמונה תנתח, על ידי קריאה ל-Imageproxy.close() , התמונה הבאה שתישלח.
  • אם משתמשים בפלט של מזהה-העל כדי להציג גרפיקה על תמונת הקלט, תחילה יש לקבל את התוצאה מ-ML Kit, ולאחר מכן לעבד את התמונה ואת שכבת-העל בפעולה אחת. כך מתבצע עיבוד למשטח התצוגה פעם אחת בלבד עבור כל מסגרת קלט. כדי לראות דוגמה, אפשר לעיין בכיתות CameraSourcePreview ו- GraphicOverlay באפליקציה למתחילים.
  • אם משתמשים ב-Camera2 API, צריך לצלם את התמונות בפורמט ImageFormat.YUV_420_888. אם משתמשים בגרסה הישנה יותר של ממשק ה-API של המצלמה, יש לצלם תמונות בפורמט ImageFormat.NV21.