Detectar e rastrear objetos com o Kit de ML no Android

É possível usar o kit de aprendizado de máquina para detectar e rastrear objetos em frames de vídeo sucessivos.

Quando você passa uma imagem para o kit de ML, ele detecta até cinco objetos na imagem com a posição de cada objeto na imagem. Ao detectar objetos em streams de vídeo, cada objeto tem um ID exclusivo que pode ser usado para rastrear o objeto de quadro para quadro. Também é possível ativar a classificação aproximada de objetos, que rotula objetos com descrições de categorias amplas.

Testar

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 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.0'
    
    }
    

1. Configurar o detector de objetos

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

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

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

    Em 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 categoria não especificados, nas primeiras chamadas do detector. Além disso, no STREAM_MODE, o detector atribui IDs de rastreamento a objetos, que podem ser usados para rastrear objetos em frames. Use esse modo quando você quiser rastrear objetos ou quando a baixa latência for importante, como ao processar streams de vídeo em tempo real.

    Em SINGLE_IMAGE_MODE, o detector de objetos retorna o resultado após a caixa delimitadora do objeto ser 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 acompanhamento não são atribuídos. Use esse modo se a latência não for crítica 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 é necessário classificar objetos detectados em categorias não específicas. Quando ativado, o detector de objetos os classifica nas seguintes categorias: artigos de moda, alimentos, artigos para casa, lugares e plantas.

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

    • 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 nestes 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. Consiga 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, ByteBuffer NV21 ou um media.Image YUV_420_888. É recomendável criar um InputImage a partir dessas fontes se você tiver acesso direto a uma delas. Se você criar um InputImage a partir de outras origens, processaremos a conversão internamente para você, e ela pode ser menos eficiente.

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

É possível criar um objeto InputImage a partir de diferentes origens, cada um explicado abaixo.

Como usar um media.Image

Para criar um objeto InputImage a partir de um objeto media.Image, como ao capturar 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 biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer calcularão 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ê poderá calculá-lo usando o grau de rotação do dispositivo e a orientação do sensor da câmera:

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 objeto InputImage a partir de um URI de arquivo, transmita o contexto do app e o URI do arquivo para InputImage.fromFilePath(). Isso é útil ao usar um intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do 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 objeto InputImage a partir de ByteBuffer ou ByteArray, primeiro calcule o grau de rotação de imagem conforme descrito anteriormente para a entrada media.Image. Em seguida, crie o objeto InputImage com o buffer ou a matriz, junto 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 objeto InputImage a partir de um objeto Bitmap, 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 DetectedObjects 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 acompanhamento Um número inteiro que identifica o objeto nas 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 entre todos os rótulos compatíveis com o 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();
    }
}

Garantir uma ótima experiência do usuário

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

  • A detecção bem-sucedida de objetos depende da complexidade visual deles. Para ser detectado, objetos com um pequeno número de recursos visuais podem precisar ocupar uma parte maior da imagem. É necessário fornecer 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 compatíveis, implemente um tratamento especial para objetos desconhecidos.

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

Melhoria de performance

Se você quiser usar a detecção de objetos em um aplicativo em tempo real, siga estas diretrizes para ter 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, já que a maioria dos dispositivos não conseguirá produzir taxas de frames adequadas.

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

  • Se você usar a API Camera ou camera2, as chamadas para o detector serão limitadas. Se um novo frame de vídeo for disponibilizado enquanto o detector estiver em execução, descarte esse frame. Consulte a classe VisionProcessorBase no app de amostra do guia de início rápido para ver um exemplo.
  • Se você usar a API CameraX, verifique se a estratégia de pressão de retorno está definida como o valor padrão ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Isso garante que apenas uma imagem será exibida por vez para análise. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não ficarão na fila para entrega. Quando a imagem que está sendo analisada é fechada chamando ImageProxy.close(), a próxima imagem mais recente é entregue.
  • Se você usar a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do kit de ML e, em seguida, renderize a imagem e a sobreposição em uma única etapa. Isso renderiza a superfície de exibição apenas uma vez para cada frame de entrada. Consulte as classes CameraSourcePreview e GraphicOverlay no app de amostra do guia de início rápido para ver um exemplo.
  • Se você usar a API Camera2, capture imagens no formato ImageFormat.YUV_420_888. Se você usar a API Camera mais antiga, capture imagens no formato ImageFormat.NV21.