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

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

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

אני רוצה לנסות

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

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את מאגר ה-Maven של Google בקטעים buildscript ו-allprojects.
  2. מוסיפים את יחסי התלות של ספריות ה-Android של ערכת ה-ML, לבין קובץ הציונים ברמת האפליקציה שהמודול שלו הוא בדרך כלל 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: מריצים את המזהה באמצעות מעבד (CPU) ו-GPU

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

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

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

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

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

אם לא זוהה אדם במסגרת, האובייקט 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 Kit יזהה במדויק את התנוחה, האדם שבתמונה צריך להיות מיוצג על ידי מספיק נתוני פיקסלים. לקבלת הביצועים הטובים ביותר, המושא צריך להיות בגודל 256x256 פיקסלים לפחות.
  • אם אתם מזהים תנוחה באפליקציה בזמן אמת, מומלץ לשקול גם את המידות הכלליות של תמונות הקלט. אפשר לעבד תמונות קטנות יותר מהר יותר, כדי לקצר את זמן האחזור כדי לצלם תמונות ברזולוציות נמוכות יותר, אבל חשוב לזכור את דרישות הרזולוציה שלמעלה ולוודא שהאובייקט מופיע כמה שיותר בתמונה.
  • גם מיקוד לקוי של תמונה יכול להשפיע על הדיוק. אם לא מקבלים תוצאות מקובלות, מבקשים מהמשתמש לצלם מחדש את התמונה.

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

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

השלבים הבאים