Rotular imagens com um modelo treinado em AutoML no Android

Depois de treinar seu próprio modelo usando o AutoML Vision Edge , você poderá usá-lo em seu aplicativo para rotular imagens.

Há duas maneiras de integrar modelos treinados no AutoML Vision Edge: você pode agrupar o modelo colocando-o na pasta de ativos do seu aplicativo ou baixá-lo dinamicamente do Firebase.

Opções de agrupamento de modelos
Incluído em seu aplicativo
  • O modelo faz parte do APK do seu app
  • O modelo está disponível imediatamente, mesmo quando o dispositivo Android está offline
  • Não há necessidade de um projeto Firebase
Hospedado com Firebase
  • Hospede o modelo fazendo upload dele para o Firebase Machine Learning
  • Reduz o tamanho do APK
  • O modelo é baixado sob demanda
  • Envie atualizações de modelo sem republicar seu aplicativo
  • Teste A/B fácil com o Firebase Remote Config
  • Requer um projeto Firebase

Antes de você começar

  1. Adicione as dependências das bibliotecas Android do ML Kit ao arquivo gradle no nível do aplicativo do seu módulo, que geralmente é app/build.gradle :

    Para agrupar um modelo com seu aplicativo:

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

    Para baixar dinamicamente um modelo do Firebase, adicione a dependência linkFirebase :

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. Se você deseja baixar um modelo , certifique-se de adicionar o Firebase ao seu projeto Android , caso ainda não tenha feito isso. Isso não é necessário quando você agrupa o modelo.

1. Carregue o modelo

Configurar uma origem de modelo local

Para agrupar o modelo com seu aplicativo:

  1. Extraia o modelo e seus metadados do arquivo zip baixado do console do Firebase. Recomendamos que você use os arquivos conforme os baixou, sem modificação (incluindo os nomes dos arquivos).

  2. Inclua seu modelo e seus arquivos de metadados no pacote do seu aplicativo:

    1. Se você não tiver uma pasta de ativos em seu projeto, crie uma clicando com o botão direito na pasta app/ e clicando em Novo > Pasta > Pasta de ativos .
    2. Crie uma subpasta na pasta de ativos para conter os arquivos de modelo.
    3. Copie os arquivos model.tflite , dict.txt e manifest.json para a subpasta (todos os três arquivos devem estar na mesma pasta).
  3. Adicione o seguinte ao arquivo build.gradle do seu aplicativo para garantir que o Gradle não compacte o arquivo de modelo ao criar o aplicativo:

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

    O arquivo de modelo será incluído no pacote do aplicativo e disponível para o ML Kit como um ativo bruto.

  4. Crie o objeto LocalModel , especificando o caminho para o arquivo de manifesto do modelo:

    Java

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    Kotlin

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

Configurar uma origem de modelo hospedado no Firebase

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

Java

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

Kotlin

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

Em seguida, inicie a tarefa de download do modelo, especificando as condições sob as quais deseja 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 assíncrono do modelo do Firebase:

Java

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

Kotlin

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

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

Crie um rotulador de imagem a partir do seu modelo

Depois de configurar as fontes do modelo, crie um objeto ImageLabeler a partir de uma delas.

Se você tiver apenas um modelo empacotado localmente, basta criar um rotulador a partir do seu objeto CustomImageLabelerOptions e configurar o limite de pontuação de confiança que deseja exigir (consulte Avaliar seu modelo ):

Java

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

Kotlin

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

Se você tiver um modelo hospedado remotamente, precisará verificar se ele foi baixado antes de executá-lo. Você pode verificar o status da tarefa de download do modelo usando o método isModelDownloaded() do gerenciador de modelo.

Embora você só precise confirmar isso antes de executar o rotulador, se você tiver um modelo hospedado remotamente e um modelo empacotado localmente, pode fazer sentido realizar esta verificação ao instanciar o rotulador de imagem: crie um rotulador a partir do modelo remoto se ele foi baixado e, caso contrário, do modelo local.

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                CustomImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(localModel);
                }
                CustomImageLabelerOptions options = optionsBuilder
                        .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                                       // to determine an appropriate threshold.
                        .build();

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

Kotlin

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

Se você tiver apenas um modelo hospedado remotamente, desative a funcionalidade relacionada ao modelo (por exemplo, esmaecer ou ocultar parte da interface do usuário) até confirmar que o download do modelo foi feito. Você pode fazer isso anexando um ouvinte ao método download() do gerenciador de modelo:

Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @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.
            }
        });

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

2. Prepare a imagem de entrada

Então, para cada imagem que você deseja rotular, crie um objeto InputImage a partir da sua imagem. O rotulador de imagem é executado mais rapidamente quando você usa um Bitmap ou, se você usa a API camera2, um YUV_420_888 media.Image , que são recomendados quando possível.

Você pode criar um InputImage de diferentes fontes, cada uma delas explicada abaixo.

Usando uma media.Image

Para criar um objeto InputImage a partir de um objeto media.Image , como quando você captura uma imagem da câmera de um dispositivo, passe 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 para você.

Kotlin+KTX

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) {
        if (imageProxy == null || imageProxy.getImage() == null) {
            return;
        }
        Image mediaImage = imageProxy.getImage();
        InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees);
        // Pass image to an ML Kit Vision API
        // ...
    }
}

Se você não usar uma biblioteca de câmeras que forneça o grau de rotação da imagem, poderá calculá-lo a partir do grau de rotação do dispositivo e da orientação do sensor da câmera no dispositivo:

Kotlin+KTX

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 90)
    ORIENTATIONS.append(Surface.ROTATION_90, 0)
    ORIENTATIONS.append(Surface.ROTATION_180, 270)
    ORIENTATIONS.append(Surface.ROTATION_270, 180)
}
/**
 * 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, context: Context): 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)

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
        .getCameraCharacteristics(cameraId)
        .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    val result: Int
    when (rotationCompensation) {
        0 -> result = FirebaseVisionImageMetadata.ROTATION_0
        90 -> result = FirebaseVisionImageMetadata.ROTATION_90
        180 -> result = FirebaseVisionImageMetadata.ROTATION_180
        270 -> result = FirebaseVisionImageMetadata.ROTATION_270
        else -> {
            result = FirebaseVisionImageMetadata.ROTATION_0
            Log.e(TAG, "Bad rotation value: $rotationCompensation")
        }
    }
    return result
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 90);
    ORIENTATIONS.append(Surface.ROTATION_90, 0);
    ORIENTATIONS.append(Surface.ROTATION_180, 270);
    ORIENTATIONS.append(Surface.ROTATION_270, 180);
}

/**
 * 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, Context context)
        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);

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    int result;
    switch (rotationCompensation) {
        case 0:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            break;
        case 90:
            result = FirebaseVisionImageMetadata.ROTATION_90;
            break;
        case 180:
            result = FirebaseVisionImageMetadata.ROTATION_180;
            break;
        case 270:
            result = FirebaseVisionImageMetadata.ROTATION_270;
            break;
        default:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            Log.e(TAG, "Bad rotation value: " + rotationCompensation);
    }
    return result;
}

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

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Usando um URI de arquivo

Para criar um objeto InputImage a partir de um URI de arquivo, passe o contexto do aplicativo e o URI do arquivo para InputImage.fromFilePath() . Isso é útil quando você usa uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do aplicativo de galeria.

Kotlin+KTX

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

Usando um ByteBuffer ou ByteArray

Para criar um objeto InputImage a partir de 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 array, juntamente com a altura, largura, formato de codificação de cores e grau de rotação da imagem:

Kotlin+KTX

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* 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
);

Usando um Bitmap

Para criar um objeto InputImage a partir de um objeto Bitmap , faça a seguinte declaração:

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

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

3. Execute o rotulador de imagens

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

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

Kotlin

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

4. Obtenha informações sobre objetos rotulados

Se a operação de rotulagem de imagem for bem-sucedida, uma lista de objetos ImageLabel será passada para o ouvinte de sucesso. Cada objeto ImageLabel representa algo que foi rotulado na imagem. Você pode obter a descrição de texto de cada rótulo, a pontuação de confiança da correspondência e o índice da correspondência. Por exemplo:

Java

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

Kotlin

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

Dicas para melhorar o desempenho em tempo real

Se você deseja rotular imagens em um aplicativo em tempo real, siga estas diretrizes para obter as melhores taxas de quadros:

  • Limite as chamadas para o rotulador de imagens. Se um novo quadro de vídeo ficar disponível enquanto o rotulador de imagens estiver em execução, descarte o quadro. Consulte a classe VisionProcessorBase no aplicativo de exemplo de início rápido para ver um exemplo.
  • Se você estiver usando a saída do rotulador de imagens para sobrepor gráficos na imagem de entrada, primeiro obtenha o resultado e, em seguida, renderize a imagem e sobreponha em uma única etapa. Ao fazer isso, você renderiza na superfície de exibição apenas uma vez para cada quadro de entrada. Consulte as classes CameraSourcePreview e GraphicOverlay no aplicativo de exemplo 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 .