זיהוי של אובייקטים ומעקב אחריהם באמצעות ערכת למידת מכונה ב-Android

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

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

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

  1. אם עדיין לא עשיתם זאת, מוסיפים את Firebase לפרויקט Android.
  2. מוסיפים את יחסי התלות של ספריות ML Kit ל-Android לקובץ Gradle של המודול (ברמת האפליקציה) (בדרך כלל app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-object-detection-model:19.0.6'
    }

1. הגדרת הכלי לזיהוי אובייקטים

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

  1. מגדירים את ה-Object Detector לתרחיש לדוגמה באמצעות אובייקט FirebaseVisionObjectDetectorOptions. אפשר לשנות את ההגדרות הבאות:

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

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

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

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

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

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

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

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

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

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

    Java

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

    Kotlin

    // Live detection and tracking
    val options = FirebaseVisionObjectDetectorOptions.Builder()
            .setDetectorMode(FirebaseVisionObjectDetectorOptions.STREAM_MODE)
            .enableClassification()  // Optional
            .build()
    
    // Multiple object detection in static images
    val options = FirebaseVisionObjectDetectorOptions.Builder()
            .setDetectorMode(FirebaseVisionObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableMultipleObjects()
            .enableClassification()  // Optional
            .build()
    
  2. אחזור מופע של FirebaseVisionObjectDetector:

    Java

    FirebaseVisionObjectDetector objectDetector =
            FirebaseVision.getInstance().getOnDeviceObjectDetector();
    
    // Or, to change the default settings:
    FirebaseVisionObjectDetector objectDetector =
            FirebaseVision.getInstance().getOnDeviceObjectDetector(options);
    

    Kotlin

    val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector()
    
    // Or, to change the default settings:
    val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(options)
    

2. הפעלת הכלי לזיהוי אובייקטים

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

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

  1. יוצרים אובייקט FirebaseVisionImage מהתמונה.

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

      אם אתם משתמשים בספרייה CameraX, הערך של הזווית מסתובב בעצמו על ידי הכיתות OnImageCapturedListener ו-ImageAnalysis.Analyzer, כך שצריך רק להמיר את הזווית לאחד מהקבועים ROTATION_ של ML Kit לפני שמפעילים את FirebaseVisionImage.fromMediaImage():

      Java

      private class YourAnalyzer implements ImageAnalysis.Analyzer {
      
          private int degreesToFirebaseRotation(int degrees) {
              switch (degrees) {
                  case 0:
                      return FirebaseVisionImageMetadata.ROTATION_0;
                  case 90:
                      return FirebaseVisionImageMetadata.ROTATION_90;
                  case 180:
                      return FirebaseVisionImageMetadata.ROTATION_180;
                  case 270:
                      return FirebaseVisionImageMetadata.ROTATION_270;
                  default:
                      throw new IllegalArgumentException(
                              "Rotation must be 0, 90, 180, or 270.");
              }
          }
      
          @Override
          public void analyze(ImageProxy imageProxy, int degrees) {
              if (imageProxy == null || imageProxy.getImage() == null) {
                  return;
              }
              Image mediaImage = imageProxy.getImage();
              int rotation = degreesToFirebaseRotation(degrees);
              FirebaseVisionImage image =
                      FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
              // Pass image to an ML Kit Vision API
              // ...
          }
      }

      Kotlin

      private class YourImageAnalyzer : ImageAnalysis.Analyzer {
          private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
              0 -> FirebaseVisionImageMetadata.ROTATION_0
              90 -> FirebaseVisionImageMetadata.ROTATION_90
              180 -> FirebaseVisionImageMetadata.ROTATION_180
              270 -> FirebaseVisionImageMetadata.ROTATION_270
              else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
          }
      
          override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
              val mediaImage = imageProxy?.image
              val imageRotation = degreesToFirebaseRotation(degrees)
              if (mediaImage != null) {
                  val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                  // Pass image to an ML Kit Vision API
                  // ...
              }
          }
      }

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

      Java

      private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
      static {
          ORIENTATIONS.append(Surface.ROTATION_0, 90);
          ORIENTATIONS.append(Surface.ROTATION_90, 0);
          ORIENTATIONS.append(Surface.ROTATION_180, 270);
          ORIENTATIONS.append(Surface.ROTATION_270, 180);
      }
      
      /**
       * 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, Context context)
              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);
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
          int sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION);
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          int result;
          switch (rotationCompensation) {
              case 0:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  break;
              case 90:
                  result = FirebaseVisionImageMetadata.ROTATION_90;
                  break;
              case 180:
                  result = FirebaseVisionImageMetadata.ROTATION_180;
                  break;
              case 270:
                  result = FirebaseVisionImageMetadata.ROTATION_270;
                  break;
              default:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  Log.e(TAG, "Bad rotation value: " + rotationCompensation);
          }
          return result;
      }

      Kotlin

      private val ORIENTATIONS = SparseIntArray()
      
      init {
          ORIENTATIONS.append(Surface.ROTATION_0, 90)
          ORIENTATIONS.append(Surface.ROTATION_90, 0)
          ORIENTATIONS.append(Surface.ROTATION_180, 270)
          ORIENTATIONS.append(Surface.ROTATION_270, 180)
      }
      /**
       * 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, context: Context): 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)
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
          val sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          val result: Int
          when (rotationCompensation) {
              0 -> result = FirebaseVisionImageMetadata.ROTATION_0
              90 -> result = FirebaseVisionImageMetadata.ROTATION_90
              180 -> result = FirebaseVisionImageMetadata.ROTATION_180
              270 -> result = FirebaseVisionImageMetadata.ROTATION_270
              else -> {
                  result = FirebaseVisionImageMetadata.ROTATION_0
                  Log.e(TAG, "Bad rotation value: $rotationCompensation")
              }
          }
          return result
      }

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

      Java

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

      Kotlin

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • כדי ליצור אובייקט FirebaseVisionImage מכתובת URI של קובץ, מעבירים את הקשר של האפליקציה ואת כתובת ה-URI של הקובץ אל FirebaseVisionImage.fromFilePath(). האפשרות הזו שימושית כשמשתמשים בכוונה ACTION_GET_CONTENT כדי לבקש מהמשתמש לבחור תמונה מאפליקציית הגלריה שלו.

      Java

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

      Kotlin

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • כדי ליצור אובייקט FirebaseVisionImage מ-ByteBuffer או ממערך בייטים, קודם מחשבים את סיבוב התמונה כפי שמתואר למעלה עבור קלט media.Image.

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

      Java

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
              .setWidth(480)   // 480x360 is typically sufficient for
              .setHeight(360)  // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build();

      Kotlin

      val metadata = FirebaseVisionImageMetadata.Builder()
              .setWidth(480) // 480x360 is typically sufficient for
              .setHeight(360) // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build()

      משתמשים במאגר או במערך ובאובייקט המטא-נתונים כדי ליצור אובייקט FirebaseVisionImage:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

      Kotlin

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • כדי ליצור אובייקט FirebaseVisionImage מאובייקט Bitmap:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      התמונה שמיוצגת על ידי האובייקט Bitmap חייבת להיות מוצבת בצורה זקופה, ללא צורך בסיבוב נוסף.
  2. מעבירים את התמונה לשיטה processImage():

    Java

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

    Kotlin

    objectDetector.processImage(image)
            .addOnSuccessListener { detectedObjects ->
                // Task completed successfully
                // ...
            }
            .addOnFailureListener { e ->
                // Task failed with an exception
                // ...
            }
    
  3. אם הקריאה ל-processImage() תצליח, רשימה של FirebaseVisionObjects תועבר למאזין להצלחה.

    כל FirebaseVisionObject מכיל את המאפיינים הבאים:

    תיבה תוחמת (bounding box) Rect שמציין את המיקום של האובייקט בתמונה.
    מזהה לצורכי מעקב מספר שלם שמזהה את האובייקט בתמונות. Null ב-SINGLE_IMAGE_MODE.
    קטגוריה הקטגוריה הכללית של האובייקט. אם הסיווג לא מופעל בגלאי האובייקטים, הערך הוא תמיד FirebaseVisionObject.CATEGORY_UNKNOWN.
    ודאות ערך האמון של סיווג האובייקט. אם הסיווג לא מופעל בזיהוי האובייקטים, או שהאובייקט מסווג כ'לא ידוע', הערך הוא null.

    Java

    // The list of detected objects contains one item if multiple object detection wasn't enabled.
    for (FirebaseVisionObject obj : detectedObjects) {
        Integer id = obj.getTrackingId();
        Rect bounds = obj.getBoundingBox();
    
        // If classification was enabled:
        int category = obj.getClassificationCategory();
        Float confidence = obj.getClassificationConfidence();
    }
    

    Kotlin

    // The list of detected objects contains one item if multiple object detection wasn't enabled.
    for (obj in detectedObjects) {
        val id = obj.trackingId       // A number that identifies the object across images
        val bounds = obj.boundingBox  // The object's position in the image
    
        // If classification was enabled:
        val category = obj.classificationCategory
        val confidence = obj.classificationConfidence
    }
    

שיפור הנוחות והביצועים

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

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

כדאי גם לעיין ב[אפליקציית התצוגה של ML Kit ל-Material Design][showcase-link]{: .external } ובאוסף דוגמאות לתכונות מבוססות-למידת מכונה של Material Design.

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

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

  • אם אתם לא צריכים סיווג, תוכלו להשבית אותו.

  • צמצום מספר הקריאות למזהה. אם מסגרת וידאו חדשה זמינה בזמן שהגלאי פועל, צריך להסיר את המסגרת.
  • אם אתם משתמשים בפלט של הגלאי כדי להוסיף שכבת-על של גרפיקה לתמונה הקלט, קודם צריך לקבל את התוצאה מ-ML Kit, ואז לבצע עיבוד (רנדור) של התמונה ולהוסיף את שכבת-העל בשלב אחד. כך תוכלו לבצע עיבוד (render) למשטח התצוגה רק פעם אחת לכל מסגרת קלט.
  • אם אתם משתמשים ב-Camera2 API, כדאי לצלם תמונות בפורמט ImageFormat.YUV_420_888.

    אם משתמשים ב-Camera API הקודם, צריך לצלם תמונות בפורמט ImageFormat.NV21.