Rotular imagens com um modelo treinado pelo AutoML no Android

Após treinar seu próprio modelo com o AutoML Vision Edge, use esse modelo no seu app para rotular imagens.

Antes de começar

  1. Adicione o Firebase ao seu projeto para Android, caso ainda não tenha feito isso.
  2. Adicione as dependências das bibliotecas do Android do Kit de ML ao arquivo Gradle do módulo (nível do aplicativo) (geralmente app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-automl:18.0.5'
    }
    

1. Carregar o modelo

O Kit de ML executa seus modelos gerados pelo AutoML no dispositivo. No entanto, é possível configurar o Kit para carregar seu modelo remotamente a partir do Firebase, do armazenamento local ou de ambos.

Com a hospedagem no Firebase, você pode atualizar o modelo sem liberar uma nova versão do app, além de usar a Configuração remota e os Testes A/B para exibir dinamicamente modelos diferentes para diversos conjuntos de usuários.

Se você optar por fornecer o modelo hospedando-o apenas com o Firebase, sem agrupá-lo com o app, é possível reduzir o tamanho do download inicial do aplicativo. No entanto, se o modelo não estiver agrupado com o app, nenhuma função relacionada a ele vai estar disponível até que o app faça o download do modelo pela primeira vez.

Ao agrupar o modelo e o aplicativo, é possível garantir que os recursos de ML do aplicativo ainda funcionem quando o modelo hospedado pelo Firebase não estiver disponível.

Configurar uma fonte de modelo hospedada no Firebase

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

Java

// Specify the name you assigned in the Firebase console.
FirebaseAutoMLRemoteModel remoteModel =
    new FirebaseAutoMLRemoteModel.Builder("your_remote_model").build();

Kotlin+KTX

// Specify the name you assigned in the Firebase console.
val remoteModel = FirebaseAutoMLRemoteModel.Builder("your_remote_model").build()

Depois inicie a tarefa de download do modelo, especificando as condições de acordo com 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 maneira assíncrona no Firebase:

Java

FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder()
        .requireWifi()
        .build();
FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Success.
            }
        });

Kotlin+KTX

val conditions = FirebaseModelDownloadConditions.Builder()
    .requireWifi()
    .build()
FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Success.
    }

Muitos apps iniciam a tarefa de download no código de inicialização, mas é possível fazer isso a qualquer momento antes de precisar usar o modelo.

Configurar uma fonte de modelo local

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

  1. Extraia o modelo e os metadados dele do arquivo ZIP que você salvou a partir do Console do Firebase. Recomendamos usar os arquivos da maneira como foram salvos no download, sem fazer alterações neles, inclusive nos nomes.
  2. Inclua seu modelo e os arquivos de metadados dele no pacote do aplicativo:

    1. Se você não tiver uma pasta de recursos no projeto, crie uma clicando com o botão direito do mouse na pasta app/ e, em seguida, em Novo > Pasta > Pasta de recursos.
    2. Crie uma subpasta dentro da pasta de recursos para armazenar os arquivos do modelo.
    3. Copie os arquivos model.tflite, dict.txt e manifest.json para a subpasta (todos os três arquivos precisam estar na mesma pasta).
  3. Adicione o seguinte ao arquivo build.gradle do seu app para garantir que o Gradle não compacte o arquivo de modelo ao criar o app:
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
    O arquivo de modelo será incluído no pacote do app e disponível para o Kit de ML como um recurso bruto.
  4. Crie um objeto FirebaseAutoMLLocalModel, especificando o caminho para o arquivo de manifesto do modelo:

    Java

    FirebaseAutoMLLocalModel localModel = new FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build();
    

    Kotlin+KTX

    val localModel = FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build()
    

Criar um rotulador de imagens usando o modelo

Depois de configurar as origens do modelo, crie um objeto FirebaseVisionImageLabeler a partir de uma delas.

Se você tem apenas um modelo agrupado localmente, basta criar um rotulador a partir do objeto FirebaseAutoMLLocalModel e configurar o limite de pontuação de confiança exigido. Consulte Avaliar seu modelo.

Java

FirebaseVisionImageLabeler labeler;
try {
    FirebaseVisionOnDeviceAutoMLImageLabelerOptions options =
            new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
                    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                   // to determine an appropriate value.
                    .build();
    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
} catch (FirebaseMLException e) {
    // ...
}

Kotlin+KTX

val options = FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0)  // Evaluate your model in the Firebase console
                                // to determine an appropriate value.
    .build()
val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)

Se você tem um modelo hospedado remotamente, confira se foi feito o download dele antes da execução. É possível verificar o status da tarefa de download do modelo usando o método isModelDownloaded() do gerenciador de modelos.

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.

Java

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

                FirebaseVisionImageLabeler labeler;
                try {
                    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
                } catch (FirebaseMLException e) {
                    // Error.
                }
            }
        });

Kotlin+KTX

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded -> 
    val optionsBuilder =
        if (isDownloaded) {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel)
        } else {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
        }
    // Evaluate your model in the Firebase console to determine an appropriate threshold.
    val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
    val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)
}

Se você tem apenas um modelo hospedado remotamente, desative a função relacionada ao modelo (por exemplo, ocultando ou esmaecendo parte da IU) até confirmar que o download do modelo foi concluído. Para fazer isso, anexe um listener ao método download() do gerenciador de modelos:

Java

FirebaseModelManager.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+KTX

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

2. Preparar a imagem de entrada

Em seguida, para cada imagem que você quer rotular, crie um objeto FirebaseVisionImage usando uma das opções descritas nesta seção e transmita-o para uma instância de FirebaseVisionImageLabeler (descrita na próxima seção).

É possível criar um objeto FirebaseVisionImage a partir de um objeto media.Image, um arquivo no dispositivo, uma matriz de bytes ou um objeto Bitmap:

  • Para criar um objeto FirebaseVisionImage com base em 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 FirebaseVisionImage.fromMediaImage().

    Se você usa a biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer calculam o valor de rotação para você. Basta converter a rotação em uma das constantes ROTATION_ do Kit de ML antes de chamar FirebaseVisionImage.fromMediaImage():

    Java

    private class YourAnalyzer implements ImageAnalysis.Analyzer {
    
        private int degreesToFirebaseRotation(int degrees) {
            switch (degrees) {
                case 0:
                    return FirebaseVisionImageMetadata.ROTATION_0;
                case 90:
                    return FirebaseVisionImageMetadata.ROTATION_90;
                case 180:
                    return FirebaseVisionImageMetadata.ROTATION_180;
                case 270:
                    return FirebaseVisionImageMetadata.ROTATION_270;
                default:
                    throw new IllegalArgumentException(
                            "Rotation must be 0, 90, 180, or 270.");
            }
        }
    
        @Override
        public void analyze(ImageProxy imageProxy, int degrees) {
            if (imageProxy == null || imageProxy.getImage() == null) {
                return;
            }
            Image mediaImage = imageProxy.getImage();
            int rotation = degreesToFirebaseRotation(degrees);
            FirebaseVisionImage image =
                    FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
    

    Kotlin+KTX

    private class YourImageAnalyzer : ImageAnalysis.Analyzer {
        private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
            0 -> FirebaseVisionImageMetadata.ROTATION_0
            90 -> FirebaseVisionImageMetadata.ROTATION_90
            180 -> FirebaseVisionImageMetadata.ROTATION_180
            270 -> FirebaseVisionImageMetadata.ROTATION_270
            else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
        }
    
        override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
            val mediaImage = imageProxy?.image
            val imageRotation = degreesToFirebaseRotation(degrees)
            if (mediaImage != null) {
                val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                // Pass image to an ML Kit Vision API
                // ...
            }
        }
    }
    

    Se você não usa uma biblioteca de câmera que ofereça a rotação da imagem, faça o cálculo usando a rotação do dispositivo e a orientação do sensor da câmera:

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

    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
    }

    Em seguida, transmita o objeto media.Image e o valor de rotação para FirebaseVisionImage.fromMediaImage():

    Java

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

    Kotlin+KTX

    val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
  • Para criar um objeto FirebaseVisionImage com base no URI de um arquivo, transmita o contexto do aplicativo e o URI do arquivo para FirebaseVisionImage.fromFilePath(). Isso é útil ao usar uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem no app de galeria dele.

    Java

    FirebaseVisionImage image;
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Kotlin+KTX

    val image: FirebaseVisionImage
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri)
    } catch (e: IOException) {
        e.printStackTrace()
    }
  • Para criar um objeto FirebaseVisionImage com base em um ByteBuffer ou uma matriz de bytes, primeiro calcule a rotação da imagem conforme descrito acima para a entrada media.Image.

    Em seguida, crie um objeto FirebaseVisionImageMetadata que contenha a altura, a largura, o formato de codificação de cores e a rotação da imagem:

    Java

    FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
            .setWidth(480)   // 480x360 is typically sufficient for
            .setHeight(360)  // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build();

    Kotlin+KTX

    val metadata = FirebaseVisionImageMetadata.Builder()
            .setWidth(480) // 480x360 is typically sufficient for
            .setHeight(360) // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build()

    Use o buffer ou a matriz e o objeto de metadados para criar um objeto FirebaseVisionImage:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
    // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
    // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
  • Para criar um objeto FirebaseVisionImage com base em um objeto Bitmap:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromBitmap(bitmap)
    A imagem representada pelo objeto Bitmap precisa estar na posição vertical, sem a necessidade de ser girada novamente.

3. Executar o rotulador de imagens

Para rotular objetos em uma imagem, transmita o objeto FirebaseVisionImage ao método processImage() do FirebaseVisionImageLabeler.

Java

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

Kotlin+KTX

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

Quando a rotulação de imagem é concluída, uma matriz de objetos FirebaseVisionImageLabel é transmitida para o listener de êxito. É possível obter informações sobre um atributo reconhecido na imagem em cada objeto.

Exemplo:

Java

for (FirebaseVisionImageLabel label: labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
}

Kotlin+KTX

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

Dicas para melhorar o desempenho em tempo real

  • Limite as chamadas ao detector. Se um novo frame de vídeo ficar disponível durante a execução do detector, descarte esse frame.
  • Se você estiver usando 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. Ao fazer isso, você renderiza a superfície de exibição apenas uma vez para cada frame de entrada.
  • 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.