Etiqueta imágenes con un modelo entrenado por AutoML en Android

Después de entrenar tu propio modelo con AutoML Vision Edge, puedes usarlo en tu app para etiquetar imágenes.

Antes de comenzar

  1. Si aún no lo hiciste, agrega Firebase a tu proyecto de Android.
  2. Agrega las dependencias para las bibliotecas de Android del ML Kit al archivo Gradle (generalmente app/build.gradle) de tu módulo (nivel de app):
    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. Carga el modelo

El ML Kit ejecuta los modelos generados con AutoML en el dispositivo. Sin embargo, puedes configurar ML Kit para cargar tu modelo, ya sea de forma remota desde Firebase, desde el almacenamiento local, o ambos.

Si alojas un modelo en Firebase, puedes actualizarlo sin lanzar una versión nueva de la app. Además, puedes usar Remote Config y A/B Testing para entregar de forma dinámica diferentes modelos a distintos conjuntos de usuarios.

Si eliges proporcionar el modelo únicamente mediante el alojamiento con Firebase y no empaquetarlo con tu app, puedes reducir el tamaño de descarga inicial de la app. Sin embargo, ten en cuenta que, si el modelo no se empaqueta con la aplicación, las funcionalidades relacionadas con el modelo no estarán disponibles hasta que la app lo descargue por primera vez.

Si empaquetas el modelo con la app, puedes asegurarte de que las funciones de AA de tu app estén activas cuando el modelo alojado en Firebase no esté disponible.

Configura una fuente de modelo alojada en Firebase

Para usar el modelo alojado de forma remota, crea un objeto FirebaseAutoMLRemoteModel y especifica el nombre que le asignaste al modelo cuando lo publicaste:

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

Luego, inicia la tarea de descarga del modelo y especifica las condiciones bajo las que deseas permitir la descarga. Si el modelo no está en el dispositivo o si hay una versión más reciente de este, la tarea descargará el modelo de Firebase de forma asíncrona:

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

Muchas apps comienzan la tarea de descarga en su código de inicialización, pero puedes hacerlo en cualquier momento antes de usar el modelo.

Configura una fuente de modelo local

Sigue estos pasos para empaquetar el modelo con tu app:

  1. Extrae el modelo y sus metadatos del archivo ZIP que descargaste desde Firebase console. Te recomendamos usar los archivos tal como los descargaste, sin modificarlos (incluidos los nombres de archivos).
  2. Incluye tu modelo y sus metadatos en el paquete de tu app:

    1. Si no tienes una carpeta de elementos en tu proyecto, debes crear una. Para ello, haz clic con el botón derecho en la carpeta app/ y, luego, haz clic en Nuevo > Carpeta > Carpeta de elementos.
    2. Crea una subcarpeta en la carpeta Elementos para que contenga los archivos del modelo.
    3. Copia los archivos model.tflite, dict.txt y manifest.json a la subcarpeta (los tres archivos deben estar en la misma carpeta).
  3. Agrega lo siguiente al archivo build.gradle de tu app para asegurarte de que Gradle no comprima el archivo del modelo cuando se compile la app:
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
    El archivo del modelo se incluirá en el paquete de la app y estará disponible para ML Kit como elemento sin procesar.
  4. Crea un objeto FirebaseAutoMLLocalModel y especifica la ruta de acceso al archivo de manifiesto del modelo:

    Java

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

    Kotlin+KTX

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

Crea un etiquetador de imágenes a partir de tu modelo

Después de configurar las fuentes de tu modelo, crea un objeto FirebaseVisionImageLabeler a partir de una de ellas.

Si solo tienes un modelo empaquetado localmente, crea un etiquetador desde el objeto FirebaseAutoMLLocalModel y configura el umbral de puntuación de confianza que deseas solicitar (consulta Evalúa tu 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)

Si tienes un modelo alojado de forma remota, comprueba si se descargó antes de ejecutarlo. Puedes verificar el estado de la tarea de descarga del modelo con el método isModelDownloaded() del administrador del modelo.

Aunque solo tienes que confirmar que se descargó antes de ejecutar el etiquetador, si tienes un modelo alojado de forma remota y uno empaquetado localmente, sería correcto realizar esta verificación cuando se crea una instancia de etiquetador de imágenes: crea un etiquetador desde el modelo remoto si se descargó o, en su defecto, desde el 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)
}

Si solo tienes un modelo alojado de forma remota, debes inhabilitar la funcionalidad relacionada con el modelo, por ejemplo, ocultar o inhabilitar parte de tu IU, hasta que confirmes que el modelo se descargó. Puedes hacerlo si adjuntas un objeto de escucha al método download() del administrador 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. Prepara la imagen de entrada

Luego, para cada imagen que quieras etiquetar, crea un objeto FirebaseVisionImage con una de las opciones que se describen en esta sección y pásalo a una instancia de FirebaseVisionImageLabeler (que se describe en la sección siguiente).

Puedes crear un objeto FirebaseVisionImage a partir de un objeto media.Image, un archivo en el dispositivo, un arreglo de bytes o un objeto Bitmap:

  • Para crear un objeto FirebaseVisionImage a partir de un objeto media.Image, como cuando se captura una imagen con la cámara de un dispositivo, pasa el objeto media.Image y la rotación de la imagen a FirebaseVisionImage.fromMediaImage().

    Si usas la biblioteca CameraX, las clases OnImageCapturedListener y ImageAnalysis.Analyzer calculan el valor de rotación por ti, así que solo tienes que convertir la rotación en una de las constantes ROTATION_ de ML Kit antes de llamar a 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
                // ...
            }
        }
    }
    

    Si no usas una biblioteca de cámaras que te proporcione la rotación de la imagen, puedes calcularla a partir de la rotación del dispositivo y la orientación del sensor de la cámara en el dispositivo:

    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
    }

    Luego, pasa el objeto media.Image y el valor de rotación a FirebaseVisionImage.fromMediaImage():

    Java

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

    Kotlin+KTX

    val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
  • Para crear un objeto FirebaseVisionImage a partir de un URI de archivo, pasa el contexto de la app y el URI de archivo a FirebaseVisionImage.fromFilePath(). Esto es útil cuando usas un intent ACTION_GET_CONTENT para solicitarle al usuario que seleccione una imagen de su app de galería.

    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 crear un objeto FirebaseVisionImage a partir de un ByteBuffer o un array de bytes, primero calcula la rotación de la imagen como se describió anteriormente para la entrada media.Image.

    Luego, crea un objeto FirebaseVisionImageMetadata que contenga la altura, el ancho, el formato de codificación de color y la rotación de la imagen:

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

    Usa el búfer o array, y el objeto de metadatos, para crear un 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 crear un objeto FirebaseVisionImage a partir de un objeto Bitmap, haz lo siguiente:

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromBitmap(bitmap)
    La imagen que representa el objeto Bitmap debe estar en posición vertical, sin que sea necesario rotarla.

3. Ejecuta el etiquetador de imágenes

Para etiquetar objetos de una imagen, pasa el objeto FirebaseVisionImage al método processImage() de 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
            // ...
        }

Si el etiquetado de imágenes se realiza correctamente, se pasará un array de objetos FirebaseVisionImageLabel al objeto de escucha que detecta el resultado correcto. De cada objeto, puedes obtener la información sobre una característica reconocida en la imagen.

Por ejemplo:

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
}

Sugerencias para mejorar el rendimiento en tiempo real

  • Regula las llamadas al detector. Si hay un fotograma de video nuevo disponible mientras se ejecuta el detector, ignora ese fotograma.
  • Si estás usando la salida del detector para superponer gráficos en la imagen de entrada, primero obtén el resultado de la detección de ML Kit y, luego, procesa la imagen y la superposición en un solo paso. De esta manera procesas la superficie de visualización solo una vez por cada fotograma de entrada.
  • Si usas la API de Camera2, captura imágenes en formato ImageFormat.YUV_420_888.

    Si usas la API de Camera más antigua, captura imágenes en formato ImageFormat.NV21.