Etiqueta imágenes con un modelo personalizado en Android

Puedes usar ML Kit para reconocer entidades en una imagen y etiquetarlas. Esta API admite una amplia variedad de modelos personalizados de clasificación de imágenes. Consulta Modelos personalizados con ML Kit para obtener orientación sobre los requisitos de compatibilidad de los modelos, dónde encontrar modelos previamente entrenados y cómo entrenar tus propios modelos.

Existen dos formas de integrar el etiquetado de imágenes con modelos personalizados: incluir la canalización como parte de tu app o usar una canalización no incluida que dependa de los Servicios de Google Play. Si seleccionas la canalización no agrupada, tu app será más pequeña. Consulta la siguiente tabla para obtener detalles.

Red de Búsqueda y Red de DisplaySin agrupar
Nombre de la bibliotecacom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom

Implementación
La canalización se vincula de forma estática a tu app en el tiempo de compilación.La canalización se descarga de forma dinámica con los Servicios de Google Play.
Tamaño de la appAumento de tamaño de aproximadamente 3.8 MBAumento de tamaño de aproximadamente 200 KB.
Tiempo de inicializaciónLa canalización está disponible de inmediato.Es posible que debas esperar a que se descargue la canalización antes de usarla por primera vez.
Etapa del ciclo de vida de la APIDisponibilidad general (DG)Beta

Hay dos formas de integrar un modelo personalizado: puedes empaquetarlo colocándolo dentro de la carpeta de recursos de tu app o descargarlo de forma dinámica desde Firebase. En la siguiente tabla, se comparan estas dos opciones.

Modelo agrupado Modelo alojado
El modelo es parte del APK de la app, lo que aumenta su tamaño. El modelo no forma parte de tu APK. Se aloja subiéndolo a Cloud Storage. Te recomendamos que uses Cloud Storage para Firebase.
El modelo está disponible de inmediato, incluso cuando el dispositivo Android está sin conexión Tu app debe incluir código para descargar el modelo a pedido
No se necesita un proyecto de Firebase Se requiere un proyecto de Firebase (si se usa Cloud Storage para Firebase).
Debes volver a publicar tu app para actualizar el modelo El modelo de envío se actualiza sin volver a publicar la app
No hay pruebas A/B integradas Pruebas A/B con Firebase Remote Config

Probar

Antes de comenzar

  1. En tu archivo build.gradle.kts de nivel de proyecto, asegúrate de incluir el repositorio de Maven de Google en las secciones buildscript y allprojects.

  2. Agrega las dependencias para las bibliotecas de Android del ML Kit al archivo Gradle a nivel de la app de tu módulo, que suele ser app/build.gradle.kts. Elige una de las siguientes dependencias según tus necesidades:

    Para empaquetar la canalización con tu app, haz lo siguiente:

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

    Para usar la canalización en los Servicios de Google Play, haz lo siguiente:

    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. Si eliges usar la canalización en los Servicios de Google Play, puedes configurar tu app para que descargue automáticamente la canalización en el dispositivo después de que se instale desde Play Store. Para ello, agrega la siguiente declaración al archivo AndroidManifest.xml de tu 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>
    

    También puedes verificar explícitamente la disponibilidad de la canalización y solicitar la descarga a través de la API de ModuleInstallClient de los Servicios de Google Play.

    Si no habilitas las descargas de canalizaciones en el momento de la instalación ni solicitas una descarga explícita, la canalización se descargará la primera vez que ejecutes el etiquetador. Las solicitudes que realices antes de que se complete la descarga no generarán resultados.

  4. Si quieres descargar un modelo con Cloud Storage para Firebase, asegúrate de agregar Firebase a tu proyecto de Android, en caso de que aún no lo hayas hecho. Esto no es obligatorio cuando se agrupa un modelo.

1. Carga el modelo

Puedes cargar el modelo desde una fuente incluida de forma local o desde una fuente alojada de forma remota.

Configura una fuente de modelo local

Sigue estos pasos para empaquetar el modelo con tu app:

  1. Copia el archivo del modelo (que generalmente termina en .tflite o .lite) en la carpeta assets/ de tu app. (Es posible que primero debas crear la carpeta. Para ello, haz clic con el botón derecho en la carpeta app/ y, luego, en New > Folder > Assets Folder).

  2. Crea un objeto LocalModel y especifica la ruta al archivo del 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();

Configura una fuente de modelo alojada de forma remota

Para usar el modelo alojado de forma remota, debes descargar el archivo del modelo en el almacenamiento local del dispositivo con la lógica de tu propia app y, luego, cargarlo como un modelo local. Te recomendamos que uses Cloud Storage para Firebase para alojar un modelo. Para obtener detalles de la implementación, consulta la guía de migración de Firebase ML a Cloud Storage.

Configura el etiquetador de imágenes

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

Están disponibles las siguientes opciones:

Opciones
confidenceThreshold

Es la puntuación de confianza mínima de las etiquetas detectadas. Si no se configura, se usará cualquier umbral del clasificador especificado por los metadatos del modelo. Si el modelo no contiene metadatos o estos no especifican un umbral del clasificador, se usará un umbral predeterminado de 0.0.

maxResultCount

Es la cantidad máxima de etiquetas que se devolverán. Si no se establece, se usará el valor predeterminado de 10.

Si solo tienes un modelo empaquetado a nivel local, crea un etiquetador a partir del 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);

Si tienes un modelo alojado de forma remota, comprueba si se descargó antes de ejecutarlo.

Si bien solo debes confirmar esto antes de ejecutar el etiquetador, si tienes un modelo alojado de forma remota y un modelo incluido de forma local, podría tener sentido realizar esta verificación cuando instancies el etiquetador de imágenes: crea un etiquetador a partir del modelo remoto si se descargó y a partir del modelo local en caso contrario.

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

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

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. Prepara la imagen de entrada

Luego, deberás crear un objeto InputImage a partir de tu imagen por cada imagen que quieras etiquetar. El etiquetador de imágenes se ejecuta más rápido cuando usas un Bitmap o, si usas la API de Camera2, un media.Image de YUV_420_888, que se recomienda cuando es posible.

Puedes crear un objeto InputImage a partir de diferentes fuentes, que se explican a continuación.

Usa un media.Image

Para crear un objeto InputImage 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 InputImage.fromMediaImage().

Si usas la biblioteca CameraX, las clases OnImageCapturedListener y ImageAnalysis.Analyzer calculan el valor de rotación por ti.

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

Si no usas una biblioteca de cámaras que te proporcione el grado de rotación de la imagen, puedes calcularlo a partir del grado de rotación del dispositivo y la orientación del sensor de la cámara en el 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;
}

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

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Usa un URI de archivo

Para crear un objeto InputImage a partir de un URI de archivo, pasa el contexto de la app y el URI del archivo a InputImage.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.

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

Usa ByteBuffer o ByteArray

Para crear un objeto InputImage a partir de un ByteBuffer o un ByteArray, primero calcula el grado de rotación de la imagen como se describió anteriormente para la entrada media.Image. Luego, crea el objeto InputImage con el búfer o el array, junto con la altura, el ancho, el formato de codificación de color y el grado de rotación de la imagen:

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

Usa un Bitmap

Para crear un objeto InputImage a partir de un objeto Bitmap, realiza la siguiente declaración:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

La imagen está representada por un objeto Bitmap junto con los grados de rotación.

3. Ejecuta el etiquetador de imágenes

Para etiquetar objetos de una imagen, pasa el objeto image al método process() de 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. Obtén información sobre las entidades etiquetadas

Si la operación de etiquetado de imágenes se ejecuta correctamente, se pasará una lista de objetos ImageLabel al objeto de escucha que detecta el resultado correcto. Cada objeto ImageLabel representa un elemento etiquetado en la imagen. Puedes obtener la descripción del texto de cada etiqueta (si está disponible en los metadatos del archivo de modelo de LiteRT), la puntuación de confianza y el índice. Por ejemplo:

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

Sugerencias para mejorar el rendimiento en tiempo real

Si quieres etiquetar imágenes en una aplicación en tiempo real, sigue estos lineamientos para lograr la mejor velocidad de fotogramas:

  • Si usas las APIs de Camera o camera2, limita las llamadas al etiquetador de imágenes. Si surge un fotograma de video nuevo mientras se ejecuta el etiquetador de imágenes, ignora ese fotograma. Consulta la clase VisionProcessorBase en la app de ejemplo de la guía de inicio rápido para ver un ejemplo.
  • Si usas la API de CameraX, asegúrate de que la estrategia de contrapresión esté establecida en su valor predeterminado ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Esto garantiza que solo se entregará una imagen para el análisis a la vez. Si se producen más imágenes cuando el analizador está ocupado, se descartarán automáticamente y no se pondrán en cola para su entrega. Una vez que se cierra la imagen que se está analizando llamando a ImageProxy.close(), se entregará la siguiente imagen más reciente.
  • Si usas la salida del etiquetador de imágenes para superponer gráficos en la imagen de entrada, primero debes obtener el resultado de ML Kit y, luego, renderizar la imagen y realizar la superposición en un solo paso. Esto renderiza la superficie de visualización solo una vez para cada fotograma de entrada. Consulta las clases CameraSourcePreview y GraphicOverlay en la app de ejemplo de inicio rápido para ver un ejemplo.
  • 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.