זיהוי אובייקטים ומעקב אחריהם באמצעות ML Kit ב-Android

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

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

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

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

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את מאגר Maven של Google בקטע buildscript וגם בקטע allprojects.
  2. מוסיפים את יחסי התלות של ספריות ML Kit Android לקובץ GRid ברמת האפליקציה של המודול, שהוא בדרך כלל app/build.gradle:
    dependencies {
      // ...
    
      implementation 'com.google.mlkit:object-detection:17.0.1'
    
    }
    

‫1. הגדרת מזהה האובייקטים

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

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

    הגדרות של מזהה אובייקטים
    מצב זיהוי STREAM_MODE (ברירת מחדל) | SINGLE_IMAGE_MODE

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

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

    זיהוי של מספר אובייקטים ומעקב אחריהם false (ברירת מחדל) | true

    האם לזהות ולעקוב אחר עד חמישה אובייקטים או רק את האובייקט הבולט ביותר (ברירת המחדל).

    סיווג אובייקטים false (ברירת מחדל) | true

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

    ה-API לזיהוי אובייקטים ולמעקב מותאם לשני תרחישי השימוש העיקריים הבאים:

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

    כדי להגדיר את ה-API לתרחישים לדוגמה האלה:

    Kotlin

    // Live detection and tracking
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
            .enableClassification()  // Optional
            .build()
    
    // Multiple object detection in static images
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableMultipleObjects()
            .enableClassification()  // Optional
            .build()

    Java

    // Live detection and tracking
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
                    .enableClassification()  // Optional
                    .build();
    
    // Multiple object detection in static images
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
                    .enableMultipleObjects()
                    .enableClassification()  // Optional
                    .build();
  2. מקבלים מופע של ObjectDetector:

    Kotlin

    val objectDetector = ObjectDetection.getClient(options)

    Java

    ObjectDetector objectDetector = ObjectDetection.getClient(options);

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

כדי לזהות אובייקטים ולעקוב אחריהם, צריך להעביר תמונות ל-method process() של המכונה ObjectDetector.

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

מבצעים את הפעולות הבאות לכל פריים של סרטון או תמונה ברצף:

אפשר ליצור אובייקט 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 ביחד עם מעלות סיבוב.

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

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

Kotlin

objectDetector.process(image)
    .addOnSuccessListener { detectedObjects ->
        // Task completed successfully
        // ...
    }
    .addOnFailureListener { e ->
        // Task failed with an exception
        // ...
    }

Java

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

‫4. קבלת מידע על אובייקטים שזוהו

אם הקריאה ל-process() מצליחה, מועברת רשימה של DetectedObject למאזינים המוצלחים.

כל DetectedObject מכיל את המאפיינים (properties) הבאים:

תיבה קשורה Rect שמציין את המיקום של האובייקט בתמונה.
מזהה לצורכי מעקב מספר שלם שמזהה את האובייקט בתמונות. הערך ריק (null) ב-SINGLE_IMAGE_מצב.
Labels (תוויות)
תיאור תווית תיאור הטקסט של התווית. הוא יהיה אחד מקבועי המחרוזת שמוגדרים ב-PredefinedCategory.
אינדקס התוויות האינדקס של התווית בין כל התוויות שנתמכות על ידי המסווג. הוא יהיה אחד מהקבועים של המספרים השלמים המוגדרים ב-PredefinedCategory.
מידת הסמך של התוויות ערך הסמך של סיווג האובייקט.

Kotlin

for (detectedObject in detectedObjects) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
        val text = label.text
        if (PredefinedCategory.FOOD == text) {
            ...
        }
        val index = label.index
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : detectedObjects) {
    Rect boundingBox = detectedObject.getBoundingBox();
    Integer trackingId = detectedObject.getTrackingId();
    for (Label label : detectedObject.getLabels()) {
        String text = label.getText();
        if (PredefinedCategory.FOOD.equals(text)) {
            ...
        }
        int index = label.getIndex();
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        float confidence = label.getConfidence();
    }
}

הבטחת חוויית משתמש מעולה

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

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

בנוסף, כדאי לעיין באפליקציית התצוגה של ML Kit Material Design ובאוסף Templates for למידת מכונה לתכונות שמבוססות על למידת מכונה

Improving performance

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

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

  • אפשר להשבית את הסיווג אם לא צריך אותו.

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