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

ערכת ML מספקת שתי ערכות SDK שעברו אופטימיזציה לזיהוי תנוחות.

שם ה-SDKזיהוי תנוחותתנוחת זיהוי מדויק
יישוםהקוד והנכסים מקושרים באופן סטטי לאפליקציה בזמן היצירה.הקוד והנכסים מקושרים באופן סטטי לאפליקציה בזמן היצירה.
ההשפעה של גודל האפליקציה (כולל קוד ונכסים)כ-11MBכ-14MB
ביצועיםPixel 3XL: כ-30FPSPixel 3XL: ~23FPS עם מעבד (CPU) , ~30FPS עם GPU

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

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

    dependencies {
      // If you want to use the base sdk
      implementation 'com.google.mlkit:pose-detection:18.0.0-beta3'
      // If you want to use the accurate sdk
      implementation 'com.google.mlkit:pose-detection-accurate:18.0.0-beta3'
    }
    

1. יצירת מכונה של PoseDetector

אפשרויות PoseDetector

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

מצב זיהוי

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

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

הגדרת חומרה

ה-PoseDetector תומך בכמה תצורות חומרה לשיפור הביצועים:

  • CPU: מריצים את המזהה באמצעות מעבד בלבד
  • CPU_GPU: מריצים את המזהה באמצעות המעבד ומעבד ה-GPU

בעת בניית האפשרויות של המזהה, ניתן להשתמש ב-API setPreferredHardwareConfigs כדי לשלוט בבחירה של החומרה. כברירת מחדל, כל הגדרות החומרה מוגדרות כמועדף.

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

שימושים לדוגמה ב-setPreferredHardwareConfigs:

  • כדי לאפשר ל-ML Kit לבחור את התצורה הטובה ביותר, אין לקרוא ל-API הזה.
  • אם לא רוצים להפעיל האצה, אפשר להעביר רק CPU.
  • אם ברצונך להשתמש ב-GPU כדי לנתק את המעבד (CPU) גם אם הוא עשוי להיות איטי יותר, עליך להעביר אותו רק ב-CPU_GPU.

מציינים את האפשרויות של מזהה התנוחות:

Kotlin

// Base pose detector with streaming frames, when depending on the pose-detection sdk
val options = PoseDetectorOptions.Builder()
    .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
    .build()

// Accurate pose detector on static images, when depending on the pose-detection-accurate sdk
val options = AccuratePoseDetectorOptions.Builder()
    .setDetectorMode(AccuratePoseDetectorOptions.SINGLE_IMAGE_MODE)
    .build()

Java

// Base pose detector with streaming frames, when depending on the pose-detection sdk
PoseDetectorOptions options =
   new PoseDetectorOptions.Builder()
       .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
       .build();

// Accurate pose detector on static images, when depending on the pose-detection-accurate sdk
AccuratePoseDetectorOptions options =
   new AccuratePoseDetectorOptions.Builder()
       .setDetectorMode(AccuratePoseDetectorOptions.SINGLE_IMAGE_MODE)
       .build();

לבסוף, יוצרים מכונה של PoseDetector. העברת האפשרויות שציינת:

Kotlin

val poseDetector = PoseDetection.getClient(options)

Java

PoseDetector poseDetector = PoseDetection.getClient(options);

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

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

בזיהוי תנוחות, מומלץ להשתמש בתמונה עם מידות של 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. עיבוד התמונה

יש להעביר את האובייקט InputImage מוכן לשיטה process של PoseDetector.

Kotlin

Task<Pose> result = poseDetector.process(image)
       .addOnSuccessListener { results ->
           // Task completed successfully
           // ...
       }
       .addOnFailureListener { e ->
           // Task failed with an exception
           // ...
       }

Java

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

4. קבלת מידע על התנוחות שזוהו

אם מזוהה אדם בתמונה, ה-API של זיהוי תנוחות מחזיר Pose אובייקט עם 33 PoseLandmark שנ'.

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

אם לא זוהה אדם במסגרת, האובייקט ב-Pose לא מכיל PoseLandmark.

Kotlin

// Get all PoseLandmarks. If no person was detected, the list will be empty
val allPoseLandmarks = pose.getAllPoseLandmarks()

// Or get specific PoseLandmarks individually. These will all be null if no person
// was detected
val leftShoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER)
val rightShoulder = pose.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER)
val leftElbow = pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW)
val rightElbow = pose.getPoseLandmark(PoseLandmark.RIGHT_ELBOW)
val leftWrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST)
val rightWrist = pose.getPoseLandmark(PoseLandmark.RIGHT_WRIST)
val leftHip = pose.getPoseLandmark(PoseLandmark.LEFT_HIP)
val rightHip = pose.getPoseLandmark(PoseLandmark.RIGHT_HIP)
val leftKnee = pose.getPoseLandmark(PoseLandmark.LEFT_KNEE)
val rightKnee = pose.getPoseLandmark(PoseLandmark.RIGHT_KNEE)
val leftAnkle = pose.getPoseLandmark(PoseLandmark.LEFT_ANKLE)
val rightAnkle = pose.getPoseLandmark(PoseLandmark.RIGHT_ANKLE)
val leftPinky = pose.getPoseLandmark(PoseLandmark.LEFT_PINKY)
val rightPinky = pose.getPoseLandmark(PoseLandmark.RIGHT_PINKY)
val leftIndex = pose.getPoseLandmark(PoseLandmark.LEFT_INDEX)
val rightIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_INDEX)
val leftThumb = pose.getPoseLandmark(PoseLandmark.LEFT_THUMB)
val rightThumb = pose.getPoseLandmark(PoseLandmark.RIGHT_THUMB)
val leftHeel = pose.getPoseLandmark(PoseLandmark.LEFT_HEEL)
val rightHeel = pose.getPoseLandmark(PoseLandmark.RIGHT_HEEL)
val leftFootIndex = pose.getPoseLandmark(PoseLandmark.LEFT_FOOT_INDEX)
val rightFootIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_FOOT_INDEX)
val nose = pose.getPoseLandmark(PoseLandmark.NOSE)
val leftEyeInner = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_INNER)
val leftEye = pose.getPoseLandmark(PoseLandmark.LEFT_EYE)
val leftEyeOuter = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_OUTER)
val rightEyeInner = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_INNER)
val rightEye = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE)
val rightEyeOuter = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_OUTER)
val leftEar = pose.getPoseLandmark(PoseLandmark.LEFT_EAR)
val rightEar = pose.getPoseLandmark(PoseLandmark.RIGHT_EAR)
val leftMouth = pose.getPoseLandmark(PoseLandmark.LEFT_MOUTH)
val rightMouth = pose.getPoseLandmark(PoseLandmark.RIGHT_MOUTH)

Java

// Get all PoseLandmarks. If no person was detected, the list will be empty
List<PoseLandmark> allPoseLandmarks = pose.getAllPoseLandmarks();

// Or get specific PoseLandmarks individually. These will all be null if no person
// was detected
PoseLandmark leftShoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER);
PoseLandmark rightShoulder = pose.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER);
PoseLandmark leftElbow = pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW);
PoseLandmark rightElbow = pose.getPoseLandmark(PoseLandmark.RIGHT_ELBOW);
PoseLandmark leftWrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST);
PoseLandmark rightWrist = pose.getPoseLandmark(PoseLandmark.RIGHT_WRIST);
PoseLandmark leftHip = pose.getPoseLandmark(PoseLandmark.LEFT_HIP);
PoseLandmark rightHip = pose.getPoseLandmark(PoseLandmark.RIGHT_HIP);
PoseLandmark leftKnee = pose.getPoseLandmark(PoseLandmark.LEFT_KNEE);
PoseLandmark rightKnee = pose.getPoseLandmark(PoseLandmark.RIGHT_KNEE);
PoseLandmark leftAnkle = pose.getPoseLandmark(PoseLandmark.LEFT_ANKLE);
PoseLandmark rightAnkle = pose.getPoseLandmark(PoseLandmark.RIGHT_ANKLE);
PoseLandmark leftPinky = pose.getPoseLandmark(PoseLandmark.LEFT_PINKY);
PoseLandmark rightPinky = pose.getPoseLandmark(PoseLandmark.RIGHT_PINKY);
PoseLandmark leftIndex = pose.getPoseLandmark(PoseLandmark.LEFT_INDEX);
PoseLandmark rightIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_INDEX);
PoseLandmark leftThumb = pose.getPoseLandmark(PoseLandmark.LEFT_THUMB);
PoseLandmark rightThumb = pose.getPoseLandmark(PoseLandmark.RIGHT_THUMB);
PoseLandmark leftHeel = pose.getPoseLandmark(PoseLandmark.LEFT_HEEL);
PoseLandmark rightHeel = pose.getPoseLandmark(PoseLandmark.RIGHT_HEEL);
PoseLandmark leftFootIndex = pose.getPoseLandmark(PoseLandmark.LEFT_FOOT_INDEX);
PoseLandmark rightFootIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_FOOT_INDEX);
PoseLandmark nose = pose.getPoseLandmark(PoseLandmark.NOSE);
PoseLandmark leftEyeInner = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_INNER);
PoseLandmark leftEye = pose.getPoseLandmark(PoseLandmark.LEFT_EYE);
PoseLandmark leftEyeOuter = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_OUTER);
PoseLandmark rightEyeInner = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_INNER);
PoseLandmark rightEye = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE);
PoseLandmark rightEyeOuter = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_OUTER);
PoseLandmark leftEar = pose.getPoseLandmark(PoseLandmark.LEFT_EAR);
PoseLandmark rightEar = pose.getPoseLandmark(PoseLandmark.RIGHT_EAR);
PoseLandmark leftMouth = pose.getPoseLandmark(PoseLandmark.LEFT_MOUTH);
PoseLandmark rightMouth = pose.getPoseLandmark(PoseLandmark.RIGHT_MOUTH);

טיפים לשיפור הביצועים

האיכות של התוצאות תלויה באיכות של תמונת הקלט:

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

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

  • שימוש בערכת הבסיס לזיהוי זהות ו-STREAM_MODE.
  • מומלץ לצלם תמונות ברזולוציה נמוכה יותר. עם זאת, חשוב גם לזכור את הדרישות לגבי מידות התמונה של ה-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.

השלבים הבאים