Как помечать изображения с помощью собственной модели на Android

С помощью ML Kit можно распознавать объекты на изображениях и добавлять к ним ярлыки. Этот API поддерживает широкий спектр пользовательских моделей классификации изображений. Информацию о требованиях к совместимости моделей, о том, где найти предварительно обученные модели, и о том, как обучать собственные модели, можно найти в разделе Пользовательские модели с ML Kit.

Добавить в приложение функцию распознавания объектов на изображениях можно двумя способами: встроить в него конвейер обработки данных или использовать внешний конвейер, который зависит от сервисов Google Play. Если вы выберете несвязанный конвейер, размер приложения будет меньше. Подробную информацию можно просмотреть в таблице ниже.

ПакетныйБез пакета
Название библиотекиcom.google.mlkit:image-labeling-customcom.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

Попробовать

Подготовка

  1. В файле build.gradle.kts на уровне проекта убедитесь, что репозиторий Maven от Google указан в разделах buildscript и allprojects.

  2. Добавьте зависимости для библиотек 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")
    }
    
  3. Если вы решите использовать конвейер в сервисах 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.

    Если вы не включите скачивание конвейера во время установки или не запросите явное скачивание, конвейер будет скачан при первом запуске разметчика. Запросы, отправленные до завершения скачивания, не принесут результатов.

  4. Если вы хотите скачать модель с помощью Cloud Storage для Firebase, убедитесь, что вы добавили Firebase в проект Android (если ещё этого не сделали). При добавлении модели в пакет это не требуется.

1. Загрузка модели

Модель можно загрузить из локального или удаленного источника.

Как настроить источник локальной модели

Чтобы встроить модель в приложение:

  1. Скопируйте файл модели (обычно с расширением .tflite или .lite) в папку assets/ приложения. (Возможно, вам сначала потребуется создать папку. Для этого нажмите правой кнопкой мыши на папку app/ и выберите New > Folder > Assets Folder (Создать > Папка > Папка объектов).)

  2. Создайте объект 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.