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:

    AutoMLImageLabelerLocalModel localModel =
       
    new AutoMLImageLabelerLocalModel.Builder()
           
    .setAssetFilePath("manifest.json")
           
    // or .setAbsoluteFilePath(absolute file path to manifest file)
           
    .build();
    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:

// 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();
// 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:

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

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

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

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

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

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

val image = InputImage.fromMediaImage(mediaImage, rotation)
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.

val image: InputImage
try {
    image
= InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e
.printStackTrace()
}
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:

val image = InputImage.fromByteBuffer(
        byteBuffer
,
       
/* image width */ 480,
       
/* image height */ 360,
        rotationDegrees
,
       
InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
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:

val image = InputImage.fromBitmap(bitmap, 0)
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 .

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

for (ImageLabel label : labels) {
   
String text = label.getText();
   
float confidence = label.getConfidence();
   
int index = label.getIndex();
}
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 .