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

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

Lorsque vous transmettez une image à ML Kit, il détecte jusqu'à cinq objets dans l'image, ainsi que la position de chaque objet dans l'image. Lors de la détection d'objets dans des flux vidéo, chaque objet possède un identifiant unique que vous pouvez utiliser pour suivre l'objet d'une image à l'autre. Vous pouvez également activer la classification approximative des objets, qui étiquette les objets avec des descriptions de catégories générales.

Essayer

Avant de commencer

  1. Dans votre fichier build.gradle au niveau du projet, veillez à inclure le dépôt Maven de Google dans les sections buildscript et allprojects.
  2. Ajoutez les dépendances des bibliothèques Android ML Kit au fichier Gradle de votre module au niveau de l'application, qui est généralement app/build.gradle :
    dependencies {
      // ...
    
      implementation 'com.google.mlkit:object-detection:17.0.2'
    
    }

1. Configurer le détecteur d'objets

Pour détecter et suivre des objets, commencez par créer une instance de ObjectDetector et, si vous le souhaitez, spécifiez 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 ObjectDetectorOptions. Vous pouvez modifier les paramètres suivants :

    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 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. De plus, dans STREAM_MODE, le détecteur attribue des ID de suivi aux objets, que vous pouvez utiliser pour suivre les objets d'une image à l'autre. Utilisez ce mode lorsque vous souhaitez suivre des objets ou lorsque la faible latence est importante, par exemple lors du traitement de flux vidéo en temps réel.

    Dans SINGLE_IMAGE_MODE, le détecteur d'objets renvoie le résultat une fois que le cadre de délimitation de l'objet est déterminé. Si vous activez également la classification, le résultat est renvoyé une fois que le cadre de délimitation et l'étiquette de catégorie sont tous deux disponibles. 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 et que vous ne souhaitez pas gérer les résultats partiels.

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

    Indique s'il faut détecter et suivre jusqu'à cinq objets ou seulement l'objet le plus visible (par défaut).

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

    Indique si les objets détectés doivent être classés dans des catégories générales. Lorsqu'il est activé, le détecteur d'objets classe les objets dans les catégories suivantes : articles de mode, aliments, articles pour la maison, lieux et plantes.

    L'API de détection et de suivi d'objets est optimisée pour ces deux cas d'utilisation principaux :

    • Détection et suivi en direct de l'objet le plus visible dans le viseur de l'appareil photo.
    • La détection de plusieurs objets à partir d'une image statique.

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

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

    Kotlin

    val objectDetector = ObjectDetection.getClient(options)

    Java

    ObjectDetector objectDetector = ObjectDetection.getClient(options);

2. Préparer l'image d'entrée

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

Le détecteur d'objets s'exécute directement à partir d'un Bitmap, d'un ByteBuffer NV21 ou d'un media.Image YUV_420_888. Nous vous recommandons de créer un InputImage à partir de ces sources si vous avez un accès direct à l'une d'entre elles. Si vous construisez un InputImage à partir d'autres sources, nous gérons la conversion en interne pour vous, mais cela peut être moins efficace.

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

Vous pouvez créer un objet InputImage à partir de différentes sources, chacune étant expliquée ci-dessous.

Utiliser un media.Image

Pour créer un objet InputImage à partir d'un objet media.Image, par exemple lorsque vous capturez une image avec l'appareil photo d'un appareil, transmettez l'objet media.Image et la rotation de l'image à InputImage.fromMediaImage().

Si vous utilisez la bibliothèque CameraX, les classes OnImageCapturedListener et ImageAnalysis.Analyzer calculent la valeur de rotation pour vous.

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

Si vous n'utilisez pas de bibliothèque d'appareil photo qui vous indique le degré de rotation de l'image, vous pouvez le calculer à partir du degré de rotation de l'appareil et de l'orientation du capteur de l'appareil photo dans l'appareil :

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

Ensuite, transmettez l'objet media.Image et la valeur du degré de rotation à InputImage.fromMediaImage() :

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Utiliser un URI de fichier

Pour créer un objet InputImage à partir d'un URI de fichier, transmettez le contexte de l'application et l'URI du fichier à InputImage.fromFilePath(). Cela est utile lorsque vous utilisez un intent ACTION_GET_CONTENT pour inviter l'utilisateur à sélectionner une image dans son application de galerie.

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();
}

Utiliser un ByteBuffer ou un ByteArray

Pour créer un objet InputImage à partir d'un ByteBuffer ou d'un ByteArray, commencez par calculer le degré de rotation de l'image, comme décrit précédemment pour l'entrée media.Image. Ensuite, créez l'objet InputImage avec le tampon ou le tableau, ainsi que la hauteur, la largeur, le format d'encodage des couleurs et le degré de rotation de l'image :

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
);

Utiliser un Bitmap

Pour créer un objet InputImage à partir d'un objet Bitmap, effectuez la déclaration suivante :

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

L'image est représentée par un objet Bitmap, ainsi que par des degrés de rotation.

3. Traiter l'image

Transmettez l'image à la méthode 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. Obtenir des informations sur les objets détectés

Si l'appel de process() aboutit, une liste de DetectedObject est transmise à l'écouteur de réussite.

Chaque DetectedObject contient les propriétés suivantes :

Cadre de délimitation Un Rect qui indique la position de l'objet dans l'image.
ID de suivi Entier qui identifie l'objet dans les images. Valeur nulle dans SINGLE_IMAGE_MODE.
Étiquettes
Description de libellé Description textuelle de l'étiquette. Il s'agit de l'une des constantes de chaîne définies dans PredefinedCategory.
Index d'étiquette Index de l'étiquette parmi toutes les étiquettes acceptées par le classificateur. Il s'agit de l'une des constantes d'entier définies dans PredefinedCategory.
Confiance des étiquettes Valeur de confiance de la classification d'objets.

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();
    }
}

Garantir une expérience utilisateur optimale

Pour 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. Pour être détectés, les objets présentant un petit nombre de caractéristiques visuelles peuvent avoir besoin d'occuper une plus grande partie de l'image. 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 des objets qui n'entrent pas clairement dans les catégories acceptées, implémentez une gestion spéciale pour les objets inconnus.

Consultez également l'application de démonstration ML Kit Material Design et la collection Material Design Modèles pour les fonctionnalités optimisées par le machine learning.

Amélioration des performances

Si vous souhaitez utiliser la détection d'objets dans une application en temps réel, suivez ces consignes pour obtenir les meilleurs fréquences d'images :

  • Lorsque vous utilisez le mode de streaming dans une application en temps réel, n'utilisez pas la détection d'objets multiples, car la plupart des appareils ne pourront pas produire de fréquences d'images adéquates.

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

  • Si vous utilisez l'API Camera ou camera2, limitez les appels au détecteur. Si une nouvelle image vidéo devient disponible pendant l'exécution du détecteur, supprimez-la. Pour obtenir un exemple, consultez la classe VisionProcessorBase dans l'exemple d'application du guide de démarrage rapide.
  • Si vous utilisez l'API CameraX, assurez-vous que la stratégie de contre-pression est définie sur sa valeur par défaut ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Cela garantit qu'une seule image sera envoyée pour analyse à la fois. Si d'autres images sont produites lorsque l'analyseur est occupé, elles sont automatiquement supprimées et ne sont pas mises en file d'attente pour être diffusées. Une fois l'image analysée fermée en appelant ImageProxy.close(), l'image la plus récente suivante est envoyée.
  • Si vous utilisez la sortie du détecteur pour superposer des graphiques sur l'image d'entrée, commencez par obtenir le résultat de ML Kit, puis affichez l'image et la superposition en une seule étape. Le rendu n'est effectué qu'une seule fois sur la surface d'affichage pour chaque frame d'entrée. Pour obtenir un exemple, consultez les classes CameraSourcePreview et GraphicOverlay dans l'exemple d'application du guide de démarrage rapide.
  • Si vous utilisez l'API Camera2, capturez des images au format ImageFormat.YUV_420_888. Si vous utilisez l'ancienne API Camera, capturez les images au format ImageFormat.NV21.