Detecte, rastreie e classifique objetos com um modelo de classificação personalizado no Android

É possível usar 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, 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 frame a frame.

É possível usar um modelo de classificação de imagem personalizado para classificar os objetos detectados. Consulte Modelos personalizados com o kit de ML para orientações sobre os requisitos de compatibilidade de modelos, onde encontrar modelos pré-treinados e como treinar seus próprios modelos.

Há duas maneiras de integrar um modelo personalizado. Coloque o modelo na pasta de recursos do app para agrupar o modelo ou faça o download dele dinamicamente do Firebase. A tabela a seguir compara as duas opções.

Modelo em pacote Modelo hospedado
O modelo faz parte do APK do seu app, que aumenta o tamanho dele. O modelo não faz parte do seu APK. Ele é hospedado ao fazer upload para o Firebase Machine Learning.
O modelo estará disponível imediatamente, mesmo quando o dispositivo Android estiver off-line O download do modelo é feito sob demanda
Não é necessário ter um projeto do Firebase Requer um projeto do Firebase
É preciso republicar seu app para atualizar o modelo Enviar atualizações do modelo sem republicar o app
Sem testes A/B integrados Teste A/B fácil com a Configuração remota do Firebase

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 do Android do Kit de ML ao arquivo Gradle do módulo no nível do app, que geralmente é app/build.gradle:

    Para agrupar um modelo e seu app:

    dependencies {
      // ...
      // Object detection & tracking feature with custom bundled model
      implementation 'com.google.mlkit:object-detection-custom:17.0.1'
    }
    

    Para fazer o download dinâmico de um modelo do Firebase, adicione a dependência linkFirebase:

    dependencies {
      // ...
      // Object detection & tracking feature with model downloaded
      // from firebase
      implementation 'com.google.mlkit:object-detection-custom:17.0.1'
      implementation 'com.google.mlkit:linkfirebase:17.0.0'
    }
    
  3. Se você quiser fazer o download de um modelo, adicione o Firebase ao seu projeto do Android, caso ainda não tenha feito isso. Essa etapa não é necessária para empacotar o modelo.

1. Carregar o modelo

Configurar uma fonte de modelo local

Para agrupar o modelo e o aplicativo, siga estas etapas:

  1. Copie o arquivo do modelo (geralmente terminando em .tflite ou .lite) para a pasta assets/ do app. Talvez seja necessário criar a pasta primeiro clicando com o botão direito do mouse na pasta app/ e, em seguida, clicando em Novo > Pasta > Pasta de recursos.

  2. Em seguida, adicione o seguinte ao arquivo build.gradle do app para garantir que o Gradle não compacte o arquivo de modelo ao criar o app:

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
            // or noCompress "lite"
        }
    }
    

    O arquivo do modelo será incluído no pacote do app e vai estar disponível para o Kit de ML como um recurso bruto.

  3. Crie o objeto LocalModel, especificando o caminho para o arquivo de modelo:

    Kotlin

    val localModel = LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build()

    Java

    LocalModel localModel =
        new LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build();

Configurar uma fonte de modelo hospedada no Firebase

Para usar o modelo hospedado remotamente, crie um objeto CustomRemoteModel por FirebaseModelSource, especificando o nome que você atribuiu ao modelo quando o publicou:

Kotlin

// Specify the name you assigned in the Firebase console.
val remoteModel =
    CustomRemoteModel
        .Builder(FirebaseModelSource.Builder("your_model_name").build())
        .build()

Java

// Specify the name you assigned in the Firebase console.
CustomRemoteModel remoteModel =
    new CustomRemoteModel
        .Builder(new FirebaseModelSource.Builder("your_model_name").build())
        .build();

Em seguida, inicie a tarefa de download do modelo, especificando as condições sob as quais você quer permitir o download. Se o modelo não estiver no dispositivo ou se uma versão mais recente do modelo estiver disponível, a tarefa fará o download do modelo de forma assíncrona do Firebase:

Kotlin

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

Java

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(@NonNull Task task) {
                // Success.
            }
        });

Muitos apps iniciam a tarefa de download no código de inicialização, mas você pode fazer isso a qualquer momento antes de precisar usar o modelo.

2. Configurar o detector de objetos

Depois de configurar as origens do modelo, configure o detector de objetos para seu caso de uso com um objeto CustomObjectDetectorOptions. É 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 invocações 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 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 depois que a caixa delimitadora do objeto é determinada. Se você também ativar a classificação, ela retornará o resultado 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, no 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

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

Classificar objetos false (padrão) | true

Se os objetos detectados serão classificados ou não usando o modelo de classificador personalizado fornecido. Para usar seu modelo de classificação personalizado, é necessário defini-lo como true.

Limite de confiança da classificação

Pontuação de confiança mínima dos rótulos detectados. Se não for definido, qualquer limite do classificador especificado pelos metadados do modelo será usado. Se o modelo não contiver metadados ou os metadados não especificarem um limite de classificador, um limite padrão de 0,0 será usado.

Máximo de rótulos por objeto

Número máximo de rótulos por objeto que o detector vai retornar. Se não for definido, o valor padrão 10 será usado.

A API de detecção e rastreamento 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.
  • A detecção de vários objetos usando uma imagem estática.

Se quiser configurar a API para esses casos de uso com um modelo empacotado localmente, faça o seguinte:

Kotlin

// Live detection and tracking
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

// Multiple object detection in static images
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
        .enableMultipleObjects()
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)

Java

// Live detection and tracking
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

// Multiple object detection in static images
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableMultipleObjects()
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

ObjectDetector objectDetector =
    ObjectDetection.getClient(customObjectDetectorOptions);

Se você tiver um modelo hospedado remotamente, será necessário verificar se foi feito o download dele antes de executá-lo. É possível verificar o status da tarefa de download do modelo usando o método isModelDownloaded() do gerenciador de modelos.

Embora isso só precise ser confirmado antes de executar o detector, se você tiver um modelo hospedado remotamente e um modelo empacotado localmente, pode fazer sentido fazer essa verificação ao instanciar o detector de imagem: crie um detector usando o modelo remoto se ele tiver sido transferido por download. Caso contrário, use o modelo local.

Kotlin

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
    val optionsBuilder =
        if (isDownloaded) {
            CustomObjectDetectorOptions.Builder(remoteModel)
        } else {
            CustomObjectDetectorOptions.Builder(localModel)
        }
    val customObjectDetectorOptions = optionsBuilder
            .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableClassification()
            .setClassificationConfidenceThreshold(0.5f)
            .setMaxPerObjectLabelCount(3)
            .build()
    val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)
}

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener(new OnSuccessListener() {
        @Override
        public void onSuccess(Boolean isDownloaded) {
            CustomObjectDetectorOptions.Builder optionsBuilder;
            if (isDownloaded) {
                optionsBuilder = new CustomObjectDetectorOptions.Builder(remoteModel);
            } else {
                optionsBuilder = new CustomObjectDetectorOptions.Builder(localModel);
            }
            CustomObjectDetectorOptions customObjectDetectorOptions = optionsBuilder
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();
            ObjectDetector objectDetector =
                ObjectDetection.getClient(customObjectDetectorOptions);
        }
});

Se você tiver apenas um modelo hospedado remotamente, desative o recurso relacionado ao modelo (por exemplo, ocultando ou esmaecendo parte da interface) até confirmar que o download do modelo foi concluído. Para fazer isso, anexe um listener ao método download() do gerenciador de modelos:

Kotlin

RemoteModelManager.getInstance().download(remoteModel, conditions)
    .addOnSuccessListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(Void v) {
              // Download complete. Depending on your app, you could enable
              // the ML feature, or switch from the local model to the remote
              // model, etc.
            }
        });

3. Preparar a imagem de entrada

Crie um objeto InputImage com base na sua imagem. O detector de objetos é executado diretamente de um Bitmap, NV21 ByteBuffer ou media.Image YUV_420_888. Criar um InputImage com essas origens é recomendável se você tiver acesso direto a uma delas. Se você criar um InputImage usando outras origens, vamos processar a conversão internamente e talvez isso seja menos eficiente.

Você pode criar um objeto InputImage de diferentes origens, 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 biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer 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, será possível 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 com base no 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 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 ByteBuffer ou ByteArray

Para criar um objeto InputImage usando um ByteBuffer ou 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 objeto InputImage com base em 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.

4. Executar o detector de objetos

Kotlin

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (detectedObject in results) {
          // ...
        }
    });

Java

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (DetectedObject detectedObject : results) {
          // ...
        }
    });

5. Conseguir informações sobre os objetos rotulados

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 Uma 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 do texto do rótulo. Retornado somente se os metadados do modelo do TensorFlow Lite contiverem descrições de rótulos.
Índice de rótulos O índice do rótulo entre todos os rótulos compatíveis com o classificador.
Confiança do rótulo O nível de confiança da classificação do objeto.

Kotlin

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (detectedObject in results) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
      val text = label.text
      val index = label.index
      val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : results) {
  Rect boundingBox = detectedObject.getBoundingBox();
  Integer trackingId = detectedObject.getTrackingId();
  for (Label label : detectedObject.getLabels()) {
    String text = label.getText();
    int index = label.getIndex();
    float confidence = label.getConfidence();
  }
}

Como 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 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 compatíveis, implemente o tratamento especial para objetos desconhecidos.

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

Melhoria de performance

Caso você queira 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, porque a maioria dos dispositivos não será capaz de produzir taxas de frames adequadas.

  • Se você usa a API Camera ou camera2, limite as chamadas para o detector. Se um novo quadro de vídeo ficar disponível durante a execução do detector, elimine o frame. Consulte a classe VisionProcessorBase no app de amostra do guia de início rápido para conferir 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á enviada 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 colocadas na fila para entrega. Depois que a imagem em análise é 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 é renderizado na 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 conferir 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.