Detectar e rastrear objetos com o Kit de ML no Android

Use o Kit de ML para detectar e rastrear objetos em frames de vídeo sucessivos.

Quando você transmite uma imagem para o Kit de ML, ele detecta até cinco objetos na imagem além da posição de cada um deles. Ao detectar objetos em streams de vídeo, cada objeto tem um ID exclusivo que pode ser usado para rastrear o objeto de frame a frame. É possível também, como opção, ativar a classificação abrangente de objetos , que marca objetos com descrições de categorias amplas.

Faça um teste

Antes de começar

  1. No arquivo build.gradle no nível do projeto, inclua o repositório Maven do Google nas seções buildscript e allprojects.
  2. Adicione as dependências das bibliotecas do Android do Kit de ML ao arquivo Gradle do módulo no nível do app, que geralmente é app/build.gradle:
    dependencies {
      // ...
    
      implementation 'com.google.mlkit:object-detection:17.0.2'
    
    }

1. Configurar o detector de objetos

Para detectar e rastrear objetos, primeiro crie uma instância de ObjectDetector e especifique, se quiser, as configurações padrão que você quer alterar no detector.

  1. Configure o detector de objetos para seu caso de uso com um objeto ObjectDetectorOptions. É possível mudar as seguintes configurações:

    Configurações do detector de objetos
    Modo de detecção STREAM_MODE (padrão) | SINGLE_IMAGE_MODE

    No STREAM_MODE (padrão), o detector de objetos é executado com baixa latência, mas pode produzir resultados incompletos, como caixas delimitadoras ou rótulos de categorias não especificados, nas primeiras chamadas do detector. Além disso, em STREAM_MODE, o detector atribui IDs de rastreamento a objetos, que podem ser usados para rastrear objetos em frames. Use esse modo quando quiser rastrear objetos ou quando a baixa latência for importante, como ao processar streams de vídeo em tempo real.

    No SINGLE_IMAGE_MODE, o detector de objetos retorna o resultado depois que a caixa delimitadora do objeto é determinada. Se você também ativar a classificação, o resultado será retornado depois que a caixa delimitadora e o rótulo da categoria estiverem disponíveis. Como consequência, a latência de detecção é potencialmente maior. Além disso, em SINGLE_IMAGE_MODE, os IDs de rastreamento não são atribuídos. Use esse modo se a latência não for essencial e você não quiser lidar com resultados parciais.

    Detectar e rastrear vários objetos false (padrão) | true

    Se for preciso detectar e rastrear até cinco objetos ou apenas o objeto mais proeminente (padrão).

    Classificar objetos false (padrão) | true

    Se for preciso ou não classificar os objetos detectados em categorias abrangentes. Quando ativado, o detector de objetos os classifica nas seguintes categorias: artigos de moda, alimentos, artigos domésticos, locais e plantas.

    A API de detecção e rastreamento de objetos é otimizada para os dois casos de uso principais a seguir:

    • Detecção ao vivo e rastreamento do objeto mais proeminente no visor da câmera.
    • Detecção de vários objetos em uma imagem estática.

    Para configurar a API para esses casos de uso:

    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. Receba uma instância de ObjectDetector:

    Kotlin

    val objectDetector = ObjectDetection.getClient(options)

    Java

    ObjectDetector objectDetector = ObjectDetection.getClient(options);

2. Preparar a imagem de entrada

Para detectar e rastrear objetos, transmita imagens para o método process() da instância ObjectDetector.

O detector de objetos é executado diretamente de um Bitmap, NV21 ByteBuffer ou um media.Image YUV_420_888. A construção de um InputImage dessas fontes é recomendada se você tiver acesso direto a uma delas. Se você construir um InputImage de outras fontes, vamos processar a conversão internamente para você, e isso pode ser menos eficiente.

Para cada frame de vídeo ou imagem em uma sequência, faça o seguinte:

É possível criar um InputImage objeto de diferentes fontes, cada uma explicada abaixo.

Como usar um media.Image

Para criar um objeto InputImage usando um objeto media.Image, como quando você captura uma imagem da câmera de um dispositivo, transmita o objeto media.Image e a rotação da imagem para InputImage.fromMediaImage().

Se você usar a CameraX biblioteca, as OnImageCapturedListener e ImageAnalysis.Analyzer classes vão calcular o valor de rotação automaticamente.

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

Se você não usar uma biblioteca de câmera que ofereça o grau de rotação da imagem, você pode calcular usando o grau de rotação do dispositivo e a orientação do sensor da câmera no dispositivo:

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

Em seguida, transmita o objeto media.Image e o valor do grau de rotação para InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Como usar um URI de arquivo

Para criar um InputImage objeto usando o URI de um arquivo, transmita o contexto do app e o URI do arquivo para InputImage.fromFilePath(). Isso é útil ao usar uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem no app de galeria dele.

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

Como usar um ByteBuffer ou ByteArray

Para criar um InputImage objeto com base em um ByteBuffer ou um ByteArray, primeiro calcule o grau de rotação da imagem conforme descrito anteriormente para a entrada media.Image. Em seguida, crie o objeto InputImage com o buffer ou a matriz, com a altura, a largura, o formato de codificação de cores e o grau de rotação da imagem:

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

Como usar um Bitmap

Para criar um InputImage objeto com base em um Bitmap objeto, faça a seguinte declaração:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

A imagem é representada por um objeto Bitmap com os graus de rotação.

3. Processar a imagem

Transmita a imagem para o método 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. Receber informações sobre objetos detectados

Se a chamada para process() for bem-sucedida, uma lista de DetectedObject será transmitida para o listener de êxito.

Cada DetectedObject contém as seguintes propriedades:

Caixa delimitadora Um Rect que indica a posição do objeto na imagem.
ID de rastreamento Um número inteiro que identifica o objeto em imagens. Nulo em SINGLE_IMAGE_MODE.
Rótulos
Descrição do rótulo A descrição de texto do rótulo. Será uma das constantes de string definidas em PredefinedCategory.
Índice do rótulo O índice do rótulo entre todos os rótulos aceitos pelo classificador. Será uma das constantes inteiras definidas em PredefinedCategory.
Confiança do rótulo O nível de confiança da classificação do objeto.

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

Como garantir uma ótima experiência do usuário

Para oferecer a melhor experiência do usuário, siga estas diretrizes no seu app:

  • A detecção bem-sucedida de objetos depende da complexidade visual do objeto. Para serem detectados, objetos com um pequeno número de recursos visuais podem precisar ocupar uma parte maior da imagem. Forneça aos usuários orientações sobre como capturar entradas que funcionem bem com o tipo de objeto que você quer detectar.
  • Ao usar a classificação, se você quiser detectar objetos que não se enquadrem nas categorias aceitas, implemente o tratamento especial para objetos desconhecidos.

Além disso, verifique o app de demonstração do Kit de ML com Material Design e a coleção de Material Design de Padrões para recursos com tecnologia de machine learning.

Como melhorar o desempenho

Se você quiser usar a detecção de objetos em um aplicativo em tempo real, siga estas diretrizes para conseguir as melhores taxas de frames:

  • Ao usar o modo de streaming em um aplicativo em tempo real, não use a detecção de vários objetos, porque a maioria dos dispositivos não será capaz de produzir taxas de frames adequadas.

  • Desative a classificação se ela não for necessária.

  • Se você usar a Camera ou camera2 API, limite as chamadas ao detector. Se um novo frame de vídeo ficar disponível durante a execução do detector, descarte esse frame. Consulte a VisionProcessorBase class no app de amostra do guia de início rápido para conferir um exemplo.
  • Se você usar a CameraX API, verifique se a estratégia de contrapressão está definida como o valor padrão ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Isso garante que apenas uma imagem seja entregue para análise por vez. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não serão enfileiradas para entrega. Depois que a imagem analisada for fechada chamando ImageProxy.close(), a próxima imagem mais recente será entregue.
  • Se você estiver usando a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do Kit de ML. Em seguida, renderize a imagem e faça a sobreposição de uma só vez. Isso renderiza a superfície de exibição apenas uma vez para cada quadro de entrada. Consulte as CameraSourcePreview e GraphicOverlay classes no app de exemplo do guia de início rápido para conferir um exemplo.
  • Se você usar a API Camera2, capture imagens no ImageFormat.YUV_420_888 formato. Se você usar a API Camera mais antiga, capture imagens no ImageFormat.NV21 formato.