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 nela junto com a posição de cada um. Ao detectar objetos em streams de vídeo, cada um tem um ID exclusivo que pode ser usado para rastrear o objeto de frame a frame.

É possível usar um modelo personalizado de classificação de imagens para classificar os objetos detectados. Consulte Modelos personalizados com o Kit de ML para orientações sobre 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. Você pode empacotar o modelo colocando-o na pasta de recursos do app ou fazer o download dele dinamicamente do Cloud Storage. A tabela a seguir compara as duas opções.

Modelo agrupado Modelo hospedado
O modelo faz parte do APK do seu app, o que aumenta o tamanho dele. O modelo não faz parte do seu APK. Ele é hospedado por upload no Cloud Storage. Recomendamos usar o Cloud Storage para Firebase.
O modelo estará disponível imediatamente, mesmo quando o dispositivo Android estiver off-line O app precisa incluir código para fazer o download do modelo sob demanda.
Não é necessário ter um projeto do Firebase Requer um projeto do Firebase (se você estiver usando o Cloud Storage para Firebase).
Você precisa republicar o app para atualizar o modelo Enviar atualizações do modelo sem republicar o app
Sem teste A/B integrado Teste A/B com a Configuração remota do Firebase

Faça um teste

Antes de começar

1. No arquivo build.gradle.kts no nível do projeto, inclua o repositório Maven do Google nas seções buildscript e allprojects.

  1. 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.kts:

    Para empacotar um modelo e seu app:

    dependencies {
      // ...
      // Object detection & tracking feature with custom bundled model
      implementation("com.google.mlkit:object-detection-custom:17.0.2")
    }
    
  2. Se você quiser fazer o download de um modelo do Cloud Storage para Firebase, adicione o Firebase ao seu projeto do Android, caso ainda não tenha feito isso. Isso não é necessário se o modelo for empacotado.

1. Carregar o modelo

É possível carregar o modelo de uma fonte agrupada localmente ou de uma fonte hospedada remotamente.

Configurar uma fonte de modelo local

Para agrupar o modelo e o app:

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

  2. Crie um objeto LocalModel, especificando o caminho para o arquivo modelo:

    Kotlin

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

    Java

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

Configurar uma fonte de modelo hospedada remotamente

Para usar o modelo hospedado remotamente, faça o download do arquivo modelo para o armazenamento local do dispositivo usando sua própria lógica de app e carregue-o como um modelo local. Recomendamos usar o Cloud Storage para Firebase para hospedar um modelo. Para detalhes da implementação, consulte o guia de migração do Firebase ML para o Cloud Storage.

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 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, 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.

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 pode ser maior. Além disso, no 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 é preciso ou não classificar os objetos detectados usando o modelo de classificador personalizado fornecido. Para usar seu modelo de classificação personalizada, defina isso como true.

Limite de confiança de classificação

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

Número 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 de 10 será usado.

A API de detecção de objeto 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 em uma imagem estática.

Para configurar a API para esses casos de uso com um modelo agrupado localmente:

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.

Embora você só precise confirmar isso antes de executar o detector, se tiver um modelo hospedado remotamente e um agrupado localmente, talvez seja interessante realizar essa verificação ao instanciar o detector de imagens: crie um detector do modelo remoto se ele tiver sido baixado e do modelo local caso contrário.

Kotlin

val modelFile = File(context.cacheDir, "my_remote_model.tflite")

val model = if (modelFile.exists()) {
    // Use the downloaded model if available
    LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
} else {
    // Fall back to the bundled model
    LocalModel.Builder().setAssetFilePath("model.tflite").build()
}

val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(model)
        .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)

Java

File modelFile = new File(context.getCacheDir(), "my_remote_model.tflite");

LocalModel model;
if (modelFile.exists()) {
    // Use the downloaded model if available
    model = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
} else {
    // Fall back to the bundled model
    model = new LocalModel.Builder().setAssetFilePath("model.tflite").build();
}

CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(model)
                .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.

Kotlin

val localFile = File(context.cacheDir, "my_remote_model.tflite")
if (localFile.exists()) {
    // Model is already cached, initialize immediately
    initializeDetector(localFile)
} else {
    // Model is not yet available, show loading UI and start download
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            // Download complete, initialize the detector
            hideLoadingUI()
            initializeDetector(localFile)
        }
        .addOnFailureListener {
            // Handle download error
            showErrorUI()
        }
}

private fun initializeDetector(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val customObjectDetectorOptions = CustomObjectDetectorOptions.Builder(localModel)
            .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableClassification()
            .build()
    val objectDetector = ObjectDetection.getClient(customObjectDetectorOptions)
    // Enable ML-related UI features here
    enableMLFeatures(objectDetector)
}

Java

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    // Model is already cached, initialize immediately
    initializeDetector(localFile);
} else {
    // Model is not yet available, show loading UI and start download
    showLoadingUI();
    FirebaseStorage storage = FirebaseStorage.getInstance();
    StorageReference modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite");
    modelRef.getFile(localFile)
        .addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
                // Download complete, initialize the detector
                hideLoadingUI();
                initializeDetector(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                // Handle download error
                showErrorUI();
            }
        });
}

private void initializeDetector(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomObjectDetectorOptions customObjectDetectorOptions =
            new CustomObjectDetectorOptions.Builder(localModel)
                    .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                    .enableClassification()
                    .build();
    ObjectDetector objectDetector = ObjectDetection.getClient(customObjectDetectorOptions);
    // Enable ML-related UI features here
    enableMLFeatures(objectDetector);
}

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 um YUV_420_888 media.Image. É recomendável construir um InputImage dessas fontes se você tiver acesso direto a uma delas. Se você criar um InputImage de outras fontes, vamos processar a conversão internamente para você, e isso pode ser menos eficiente.

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

Como usar um media.Image

Para criar um objeto InputImage usando 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 vão calcular o valor de rotação para você.

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 calcular 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 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 ByteBuffer ou ByteArray

Para criar um objeto InputImage usando um ByteBuffer ou um ByteArray, primeiro calcule o grau de rotação da imagem conforme descrito anteriormente para a entrada de 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. Receber informações sobre 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 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 textual do rótulo. Só será retornado se os metadados do modelo LiteRT contiverem descrições de rótulos.
Índice de rótulos O índice do rótulo entre todos os rótulos aceitos pelo 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();
  }
}

Garantir uma ótima experiência do usuário

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

  • A detecção de objeto bem-sucedida depende da complexidade visual do objeto. Para serem detectados, objetos com poucos recursos visuais precisam 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, 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.

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.

  • Se você usar a API Camera ou camera2, limite as chamadas ao detector. Se um novo frame de vídeo for disponibilizado durante a execução do detector, descarte esse 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 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ê usar 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 classes CameraSourcePreview e GraphicOverlay no app de exemplo 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.