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 Display | Sin agrupar | |
|---|---|---|
| Nombre de la biblioteca | com.google.mlkit:image-labeling-custom | com.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 app | Aumento de tamaño de aproximadamente 3.8 MB | Aumento de tamaño de aproximadamente 200 KB. |
| Tiempo de inicialización | La 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 API | Disponibilidad 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
- Consulta la app de inicio rápido de Vision para ver un ejemplo del uso del modelo incluido y la app de inicio rápido de AutoML para ver un ejemplo del uso del modelo alojado.
Antes de comenzar
En tu archivo
build.gradle.ktsde nivel de proyecto, asegúrate de incluir el repositorio de Maven de Google en las seccionesbuildscriptyallprojects.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") }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.xmlde 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.
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:
Copia el archivo del modelo (que generalmente termina en
.tfliteo.lite) en la carpetaassets/de tu app. (Es posible que primero debas crear la carpeta. Para ello, haz clic con el botón derecho en la carpetaapp/y, luego, en New > Folder > Assets Folder).Crea un objeto
LocalModely 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 objetoInputImage 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 objetosImageLabel 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
Cameraocamera2, 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 claseVisionProcessorBaseen 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 predeterminadoImageAnalysis.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
CameraSourcePreviewyGraphicOverlayen 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 formatoImageFormat.NV21.