Détecter et suivre des objets avec ML Kit sur Android

Vous pouvez utiliser ML Kit pour détecter et suivre des objets dans les images d'une vidéo.

Lorsque vous transmettez des images ML Kit, ML Kit renvoie, pour chaque image, une liste de cinq objets détectés maximum et de leur position dans l'image. Lorsque vous détectez des objets dans des flux vidéo, chaque objet est associé à un ID que vous pouvez utiliser pour le suivre dans les images. Vous pouvez aussi activer la connectivité de classification, qui attribue aux objets des descriptions de catégories générales.

Avant de commencer

  1. Si ce n'est pas encore fait, ajoutez Firebase à votre projet Android.
  2. Ajouter les dépendances des bibliothèques Android ML Kit à votre module Fichier Gradle (au niveau de l'application) (généralement 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. Configurer le détecteur d'objets

Pour commencer à détecter et à suivre des objets, créez d'abord une instance de FirebaseVisionObjectDetector, en spécifiant éventuellement les paramètres du détecteur que vous souhaitez modifier par rapport aux paramètres par défaut.

  1. Configurez le détecteur d'objets pour votre cas d'utilisation avec un objet FirebaseVisionObjectDetectorOptions. Vous pouvez modifier les paramètres suivants : paramètres:

    Paramètres du détecteur d'objets
    Mode de détection STREAM_MODE (par défaut) | SINGLE_IMAGE_MODE

    Dans STREAM_MODE (par défaut), le détecteur d'objets s'exécute avec une faible latence, mais il peut produire des résultats incomplets (tels que des cadres de délimitation ou des libellés de catégorie non spécifiés) lors des premières invocations du détecteur. Dans STREAM_MODE, le détecteur attribue des identifiants de suivi aux objets, que vous pouvez utiliser pour suivre des objets sur des frames. Utilisez ce mode pour suivre ou lorsqu'une faible latence est importante, par exemple des flux vidéo en temps réel.

    Dans SINGLE_IMAGE_MODE, le détecteur d'objets attend que le cadre de délimitation d'un objet détecté et (si vous avez activé la classification) l'étiquette de catégorie soient disponibles avant de renvoyer un résultat. Par conséquent, la latence de détection est potentiellement plus élevée. De plus, dans SINGLE_IMAGE_MODE, les ID de suivi ne sont pas attribués. Utilisez ce mode si la latence n'est pas critique traiter des résultats partiels.

    Détecter et suivre plusieurs objets false (par défaut) | true

    Indique si vous souhaitez détecter et suivre jusqu'à cinq objets ou uniquement l'objet le plus proéminent (par défaut).

    Classer des objets false (par défaut) | true

    Indique s'il faut classer ou non les objets détectés en catégories approximatives. Lorsqu'il est activé, le détecteur d'objets classe les objets dans les catégories suivantes: articles de mode, alimentation, articles pour la maison, de lieux, de plantes et d’inconnus.

    L'API de détection et de suivi des objets est optimisée pour ces deux utilisations principales cas:

    • Détection et suivi en direct de l'objet le plus proéminent de la caméra viseur
    • Détection de plusieurs objets à partir d'une image statique

    Pour configurer l'API pour ces cas d'utilisation :

    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+KTX

    // 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. Obtenez une instance de FirebaseVisionObjectDetector :

    Java

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

    Kotlin+KTX

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

2. Exécuter le détecteur d'objets

Pour détecter et suivre des objets, transmettez des images à la méthode processImage() de l'instance FirebaseVisionObjectDetector.

Pour chaque image ou frame vidéo d'une séquence, procédez comme suit :

  1. Créez un objet FirebaseVisionImage à partir de votre image.

    • Pour créer un objet FirebaseVisionImage à partir d'un un objet media.Image, par exemple lors de la capture d'une image à partir d'un l'appareil photo de l'appareil, transmettez l'objet media.Image et l'image la rotation sur FirebaseVisionImage.fromMediaImage().

      Si vous utilisez la bibliothèque CameraX, les classes OnImageCapturedListener et ImageAnalysis.Analyzer calculent la valeur de rotation à votre place. Il vous suffit donc de convertir la rotation en l'une des constantes ROTATION_ de ML Kit avant d'appeler 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+KTX

      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
                  // ...
              }
          }
      }
      

      Si vous n'utilisez pas de bibliothèque d'appareils photo qui vous permet de faire pivoter l'image, peut la calculer à partir de la rotation de l'appareil et de l'orientation de la caméra capteur de l'appareil:

      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+KTX

      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
      }

      Transmettez ensuite l'objet media.Image et la valeur de rotation à FirebaseVisionImage.fromMediaImage() :

      Java

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

      Kotlin+KTX

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • Pour créer un objet FirebaseVisionImage à partir d'un URI de fichier, transmettez le contexte de l'application et l'URI de fichier à FirebaseVisionImage.fromFilePath(). Cela est utile lorsque vous utilisez un intent ACTION_GET_CONTENT pour inviter l'utilisateur à sélectionner une image dans son application Galerie.

      Java

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

      Kotlin+KTX

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • Pour créer un objet FirebaseVisionImage à partir d'un ByteBuffer ou un tableau d'octets, calculez d'abord l'image comme décrit ci-dessus pour l'entrée media.Image.

      Créez ensuite un objet FirebaseVisionImageMetadata contenant la hauteur, la largeur, le format d'encodage des couleurs et la rotation de l'image :

      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+KTX

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

      Utilisez le tampon ou le tableau, ainsi que l'objet de métadonnées, pour créer un objet FirebaseVisionImage :

      Java

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

      Kotlin+KTX

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • Pour créer un objet FirebaseVisionImage à partir d'un objet Bitmap :

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      L'image représentée par l'objet Bitmap doit être à la verticale, sans effectuer de rotation supplémentaire.
  2. Transmettez l'image à la méthode 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+KTX

    objectDetector.processImage(image)
            .addOnSuccessListener { detectedObjects ->
                // Task completed successfully
                // ...
            }
            .addOnFailureListener { e ->
                // Task failed with an exception
                // ...
            }
    
  3. Si l'appel à processImage() aboutit, une liste de FirebaseVisionObject est transmise à l'écouteur de succès.

    Chaque FirebaseVisionObject contient les propriétés suivantes:

    Cadre de délimitation Rect indiquant la position de l'objet dans l'image.
    ID de suivi Entier qui identifie l'objet dans les images. Null dans SINGLE_IMAGE_MODE.
    Catégorie Catégorie approximative de l'objet. Si la classification n'est pas activée pour le détecteur d'objets, la valeur est toujours FirebaseVisionObject.CATEGORY_UNKNOWN.
    Confiance Valeur de confiance de la classification d'objets. Si la classification n'est pas activée pour le détecteur d'objets ou si l'objet est classé comme inconnu, la valeur est 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+KTX

    // 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
    }
    

Amélioration de l'usabilité et des performances

Pour offrir une expérience utilisateur optimale, suivez ces consignes dans votre application :

  • La réussite de la détection d'objets dépend de la complexité visuelle de l'objet. Objets avec un petit nombre de caractéristiques visuelles peuvent avoir besoin d’occuper une plus grande partie l'image à détecter. Vous devez fournir aux utilisateurs des conseils sur la capture d'entrées qui fonctionnent bien avec le type d'objets que vous souhaitez détecter.
  • Lorsque vous utilisez la classification, si vous souhaitez détecter les objets qui ne tombent pas correctement dans les catégories prises en charge, implémenter un traitement spécial pour les d'objets.

Consultez également les [Application de présentation Material Design de ML Kit][showcase-link]{: .external } et Material Design Modèles pour la collection de caractéristiques basées sur le machine learning.

Lorsque vous utilisez le mode de traitement par flux dans une application en temps réel, suivez ces consignes pour d'obtenir les meilleures fréquences d'images:

  • N'utilisez pas la détection de plusieurs objets en mode streaming, car la plupart des appareils ne pourront pas produire des fréquences d'images adéquates.

  • Désactivez la classification si vous n'en avez pas besoin.

  • Limiter les appels au détecteur. Si un nouveau frame vidéo devient disponible pendant l'exécution du détecteur, supprimez-le.
  • Si vous utilisez la sortie du détecteur pour superposer des images l'image d'entrée, récupérez d'abord le résultat à partir de ML Kit, puis effectuez le rendu de l'image. et les superposer en une seule étape. Cela vous permet d'afficher sur la surface d'affichage une seule fois pour chaque trame d'entrée.
  • Si vous utilisez l'API Camera2, capturez des images au format ImageFormat.YUV_420_888.

    Si vous utilisez l'ancienne API Camera, capturez des images au format ImageFormat.NV21.