Добавить в приложение функцию распознавания объектов на изображениях можно двумя способами: встроить в него конвейер обработки данных или использовать внешний конвейер, который зависит от сервисов Google Play. Если вы выберете несвязанный конвейер, размер приложения будет меньше. Подробную информацию можно просмотреть в таблице ниже.
| Пакетный | Без пакета | |
|---|---|---|
| Название библиотеки | com.google.mlkit:image-labeling-custom | com.google.android.gms:play-services-mlkit-image-labeling-custom |
Реализация | Конвейер статически связан с приложением во время сборки. | Конвейер динамически скачивается с помощью сервисов Google Play. |
| Размер приложения | Размер увеличился примерно на 3,8 МБ. | Увеличение размера примерно на 200 КБ. |
| Время инициализации | Pipeline доступен сразу. | Перед первым использованием может потребоваться подождать, пока скачается конвейер. |
| Этап жизненного цикла API | Общая доступность | Бета |
Интегрировать собственную модель можно двумя способами: упаковать модель, поместив ее в папку ресурсов вашего приложения, или динамически загрузить ее из Firebase. В таблице ниже сравниваются эти два варианта.
| Модель в комплекте | Размещенная модель |
|---|---|
| Модель входит в APK-файл приложения, поэтому его размер увеличивается. | Модель не входит в состав APK-файла. Он размещается в Cloud Storage. Мы рекомендуем использовать Cloud Storage для Firebase. |
| Модель доступна сразу, даже если устройство Android не подключено к интернету. | В приложении должен быть код для скачивания модели по запросу. |
| Проект Firebase не требуется | Требуется проект Firebase (если используется Cloud Storage for Firebase). |
| Чтобы обновить модель, необходимо повторно опубликовать приложение. | Обновляйте модели, не публикуя приложение заново |
| Нет встроенного A/B-тестирования | A/B-тестирование с помощью Firebase Remote Config |
Попробовать
- Пример использования встроенной модели можно найти в приложении для быстрого начала работы с Vision API, а размещенной – в приложении для быстрого начала работы с AutoML API.
Подготовка
В файле
build.gradle.ktsна уровне проекта убедитесь, что репозиторий Maven от Google указан в разделахbuildscriptиallprojects.Добавьте зависимости для библиотек ML Kit для Android в файл gradle на уровне приложения вашего модуля, который обычно называется
app/build.gradle.kts. Выберите одну из следующих зависимостей в соответствии со своими потребностями:Для объединения конвейера с приложением:
dependencies { // ... // Use this dependency to bundle the pipeline with your app implementation("com.google.mlkit:image-labeling-custom:17.0.3") }Для использования конвейера в сервисах Google Play:
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") }Если вы решите использовать конвейер в сервисах Google Play, то сможете настроить приложение так, чтобы конвейер автоматически скачивался на устройство после установки приложения из Google Play. Для этого добавьте в файл
AndroidManifest.xmlприложения следующую декларацию:<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>Вы также можете явно проверить доступность конвейера и запросить скачивание через ModuleInstallClient API сервисов Google Play.
Если вы не включите скачивание конвейера во время установки или не запросите явное скачивание, конвейер будет скачан при первом запуске разметчика. Запросы, отправленные до завершения скачивания, не принесут результатов.
Если вы хотите скачать модель с помощью Cloud Storage для Firebase, убедитесь, что вы добавили Firebase в проект Android (если ещё этого не сделали). При добавлении модели в пакет это не требуется.
1. Загрузка модели
Модель можно загрузить из локального или удаленного источника.
Как настроить источник локальной модели
Чтобы встроить модель в приложение:
Скопируйте файл модели (обычно с расширением
.tfliteили.lite) в папкуassets/приложения. (Возможно, вам сначала потребуется создать папку. Для этого нажмите правой кнопкой мыши на папкуapp/и выберите New > Folder > Assets Folder (Создать > Папка > Папка объектов).)Создайте объект
LocalModel, указав путь к файлу модели: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();
Как настроить удаленный источник модели
Чтобы использовать модель, размещенную на удаленном сервере, вам нужно скачать ее в локальное хранилище устройства с помощью логики приложения, а затем загрузить как локальную модель. Мы рекомендуем размещать модель в Cloud Storage для Firebase. Подробные инструкции по реализации приведены в руководстве по переходу с Firebase ML на Cloud Storage.
Как настроить инструмент для разметки изображений
После того как вы настроите источники модели, создайте объект ImageLabeler на основе одного из них.
Доступны описанные ниже параметры.
| Параметры | |
|---|---|
confidenceThreshold
|
Минимальный уровень достоверности обнаруженных ярлыков. Если не задано, будет использоваться пороговое значение классификатора, указанное в метаданных модели. Если модель не содержит метаданных или в них не указан порог классификатора, будет использоваться порог по умолчанию, равный 0.0. |
maxResultCount
|
Максимальное количество возвращаемых ярлыков. Если правило не настроено, используется значение по умолчанию (10). |
Если у вас есть только локально встроенная модель, создайте ярлык на основе объекта 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);
Если модель размещена удаленно, перед ее запуском необходимо убедиться, что она скачана.
Хотя подтверждать это нужно только перед запуском инструмента, если у вас есть и удаленная, и локальная модель, может быть целесообразно выполнять эту проверку при создании экземпляра инструмента: создать инструмент на основе удаленной модели, если она скачана, и на основе локальной модели в противном случае.
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);
Если у вас есть только модель, размещенная на удаленном сервере, вам следует отключить связанные с ней функции, например заблокировать или скрыть часть интерфейса, пока модель не будет скачана.
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. Как подготовить входное изображение
Затем для каждого изображения, которое вы хотите пометить, создайте объектInputImage. Распознавание изображений выполняется быстрее всего, если вы используете Bitmap или, если вы используете camera2 API, YUV_420_888 media.Image, которые рекомендуются по возможности.
Объект InputImage можно создать из разных источников. Ниже описаны все способы.
Использование media.Image
Чтобы создать объект InputImage из объекта media.Image, например при съемке изображения камерой устройства, передайте объект media.Image и поворот изображения в InputImage.fromMediaImage().
Если вы используете библиотеку
CameraX, классы OnImageCapturedListener и ImageAnalysis.Analyzer рассчитают значение поворота за вас.
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 // ... } } }
Если вы не используете библиотеку камеры, которая предоставляет угол поворота изображения, вы можете рассчитать его на основе угла поворота устройства и ориентации датчика камеры в устройстве:
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; }
Затем передайте объект media.Image и значение угла поворота в InputImage.fromMediaImage():
Kotlin
val image = InputImage.fromMediaImage(mediaImage, rotation)
Java
InputImage image = InputImage.fromMediaImage(mediaImage, rotation);
С помощью URI файла
Чтобы создать объект InputImage на основе URI файла, передайте контекст приложения и URI файла в InputImage.fromFilePath(). Это полезно, когда вы используете намерение ACTION_GET_CONTENT, чтобы предложить пользователю выбрать изображение из галереи.
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(); }
Как использовать ByteBuffer или ByteArray
Чтобы создать объект InputImage из ByteBuffer или ByteArray, сначала рассчитайте угол поворота изображения, как описано выше для входных данных media.Image.
Затем создайте объект InputImage с буфером или массивом, а также высотой, шириной, форматом кодирования цвета и углом поворота изображения:
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 );
Используется Bitmap
Чтобы создать объект InputImage на основе объекта Bitmap, добавьте следующее объявление:
Kotlin
val image = InputImage.fromBitmap(bitmap, 0)
Java
InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);
Изображение представлено объектом Bitmap вместе с градусами поворота.
3. Запустите разметку изображений
Чтобы добавить ярлыки к объектам на изображении, передайте объект image методу process() объекта 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. Как получить информацию об объектах с ярлыками
Если операция по распознаванию объектов на изображении выполнена успешно, слушателю успешного выполнения передается список объектовImageLabel. Каждый объект ImageLabel представляет собой то, что было помечено на изображении. Вы можете получить текст описания каждой метки (если он доступен в метаданных файла модели LiteRT), оценку достоверности и индекс. Пример:
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(); }
Советы по повышению эффективности в реальном времени
Если вы хотите помечать изображения в приложении, работающем в реальном времени, следуйте приведенным ниже рекомендациям, чтобы достичь оптимальной частоты кадров.
- Если вы используете API
Cameraилиcamera2, ограничьте количество вызовов к сервису распознавания изображений. Если новый видеокадр становится доступным во время работы средства разметки изображений, удалите кадр. Пример можно найти в классеVisionProcessorBaseкраткого руководства по работе с примером приложения. - Если вы используете API
CameraX, убедитесь, что для стратегии противодавления задано значение по умолчаниюImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Это гарантирует, что для анализа будет отправлено только одно изображение за раз. Если во время работы анализатора будут созданы другие изображения, они будут автоматически удалены и не будут поставлены в очередь на доставку. После того как анализируемое изображение будет закрыто вызовом ImageProxy.close(), будет доставлено следующее последнее изображение. - Если вы используете выходные данные средства разметки изображений для наложения графики на входное изображение, сначала получите результат из ML Kit, а затем выполните рендеринг изображения и наложения за один шаг. Оно отрисовывается на экране только один раз для каждого входного кадра. Пример можно найти в классах
CameraSourcePreviewиGraphicOverlayв кратком руководстве по работе с примером приложения. - Если вы используете Camera2 API, делайте снимки в формате
ImageFormat.YUV_420_888. Если вы используете старый Camera API, делайте снимки в форматеImageFormat.NV21.