Rotular imagens com um modelo personalizado no Android

Use o Kit de ML para reconhecer entidades em uma imagem e rotulá-las. Essa API oferece suporte a uma ampla variedade de modelos de classificação de imagens personalizados. 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 a rotulagem de imagens com modelos personalizados: agrupando o pipeline como parte do app ou usando um pipeline não agrupado que depende do Google Play Services. Se você selecionar o pipeline não agrupado, o app será menor. Consulte a tabela a seguir para detalhes.

AgrupadasNão agrupadas
Nome da bibliotecacom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom

Implementação
O pipeline é vinculado estaticamente ao app no tempo de build.O pipeline é baixado dinamicamente usando o Google Play Services.
Tamanho do appAumento de tamanho de cerca de 3,8 MB.Aumento de tamanho de cerca de 200 KB.
Tempo de inicializaçãoO pipeline está disponível imediatamente.Talvez seja necessário aguardar o download do pipeline antes do primeiro uso.
Estágio do ciclo de vida da APIDisponibilidade geral (GA)Beta

Há duas maneiras de integrar um modelo personalizado: agrupe o modelo colocando-o na pasta de recursos do app ou faça o download dele dinamicamente usando o Firebase. A tabela a seguir compara essas duas opções.

Modelo agrupado Modelo hospedado
O modelo faz parte do APK do app, o que aumenta o tamanho dele. O modelo não faz parte do APK. Ele é hospedado fazendo upload para o Cloud Storage. Recomendamos o uso do 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 baixar o modelo sob demanda
Não é necessário ter um projeto do Firebase Requer um projeto do Firebase (se 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 para envolvidos no 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.kts. Escolha uma das seguintes dependências com base nas suas necessidades:

    Para agrupar o pipeline com o app:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation("com.google.mlkit:image-labeling-custom:17.0.3")
    }
    

    Para usar o pipeline no Google Play Services:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded pipeline in Google Play services
      implementation("com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5")
    }
    
  3. Se você optar por usar o pipeline no Google Play Services, poderá configurar o app para baixar automaticamente o pipeline para o dispositivo depois que o app for instalado na Play Store. Para fazer isso, adicione a seguinte declaração ao arquivo AndroidManifest.xml do app:

    <application ...>
        ...
        <meta-data
            android:name="com.google.mlkit.vision.DEPENDENCIES"
            android:value="custom_ica" />
        <!-- To use multiple downloads: android:value="custom_ica,download2,download3" -->
    </application>
    

    Você também pode verificar explicitamente a disponibilidade do pipeline e solicitar o download pela API ModuleInstallClient do Google Play Services.

    Se você não ativar os downloads do pipeline no momento da instalação ou solicitar o download explícito, o pipeline será baixado na primeira vez em que você executar o rotulador. As solicitações feitas antes da conclusão do download não produzirão resultados.

  4. Se você quiser fazer o download de um modelo usando o Cloud Storage para Firebase, verifique se você adicionou 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 aplicativo, siga estas etapas:

  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 o 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, é necessário baixar o arquivo modelo para o armazenamento local do dispositivo usando a lógica do app e carregá-lo como um modelo local. Recomendamos o uso do 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.

Configurar o rotulador de imagens

Depois de configurar as origens do modelo, crie um objeto ImageLabeler usando uma delas.

As seguintes opções estão disponíveis:

Opções
confidenceThreshold

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

maxResultCount

Número máximo de rótulos a serem retornados. Se não for definido, o valor padrão de 10 será usado.

Se você tiver apenas um modelo agrupado localmente, basta criar um rotulador usando o objeto LocalModel:

Kotlin

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

Java

CustomImageLabelerOptions customImageLabelerOptions =
        new CustomImageLabelerOptions.Builder(localModel)
            .setConfidenceThreshold(0.5f)
            .setMaxResultCount(5)
            .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

Se você tiver um modelo hospedado remotamente, será necessário verificar se foi feito o download dele antes de executá-lo.

Embora essa confirmação seja necessária apenas antes de executar o rotulador, se você tem um modelo hospedado remotamente e um modelo empacotado localmente, o ideal é fazer a verificação ao instanciar o rotulador de imagens: crie um rotulador usando o modelo remoto se ele tiver sido transferido por download. Caso contrário, use o modelo local.

Kotlin

val modelFile = File(context.cacheDir, "my_downloaded_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 options = CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(options)

Java

File modelFile = new File(context.getCacheDir(), "my_downloaded_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();
}
CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build();
ImageLabeler labeler = ImageLabeling.getClient(options);

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()) {
    initializeLabeler(localFile)
} else {
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            hideLoadingUI()
            initializeLabeler(localFile)
        }
        .addOnFailureListener {
            showErrorUI()
        }
}

private fun initializeLabeler(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val options = CustomImageLabelerOptions.Builder(localModel).build()
    val labeler = ImageLabeling.getClient(options)
    enableMLFeatures(labeler)
}

Java

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    initializeLabeler(localFile);
} else {
    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) {
                hideLoadingUI();
                initializeLabeler(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                showErrorUI();
            }
        });
}

private void initializeLabeler(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(localModel).build();
    ImageLabeler labeler = ImageLabeling.getClient(options);
    enableMLFeatures(labeler);
}

2. Preparar a imagem de entrada

Em seguida, para cada imagem que você quer rotular, crie um InputImage objeto usando sua imagem. O rotulador de imagens é executado mais rapidamente quando você usa um Bitmap ou, se você usa a API camera2, um media.Image YUV_420_888. Essas opções são recomendadas quando possível.

Você pode criar um InputImage objeto a partir 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 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 usando 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 usando 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. Executar o rotulador de imagens

Para rotular objetos em uma imagem, transmita o objeto image para o método process() do ImageLabeler.

Kotlin

labeler.process(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

Java

labeler.process(image)
        .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() {
            @Override
            public void onSuccess(List<ImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

4. Receber informações sobre entidades rotuladas

Se a operação de rotulagem de imagem for bem-sucedida, uma lista de ImageLabel objetos será transmitida ao listener de êxito. Cada objeto ImageLabel representa algo que foi rotulado na imagem. É possível receber a descrição de texto de cada rótulo (se disponível nos metadados do arquivo modelo do LiteRT), a pontuação de confiança e o índice. Exemplo:

Kotlin

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
    val index = label.index
}

Java

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

Dicas para melhorar o desempenho em tempo real

Caso você queira rotular imagens em um aplicativo em tempo real, siga estas diretrizes para ter as melhores taxas de frames:

  • Se você usar a Camera ou camera2 API, limite as chamadas para o rotulador de imagens. Se um novo frame de vídeo estiver disponível enquanto o rotulador de imagens estiver em execução, elimine o frame. Consulte a classe VisionProcessorBase no app de exemplo do guia de início rápido para ver 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 rotulador de imagens para sobrepor elementos gráficos em a 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 CameraSourcePreview e GraphicOverlay classes no app de exemplo do guia de início rápido para ver 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.