Как обнаруживать, отслеживать и классифицировать объекты с помощью собственной модели классификации на устройстве Android

С помощью ML Kit можно обнаруживать и отслеживать объекты в последовательных кадрах видео.

Когда вы передаете изображение в ML Kit, он обнаруживает на нем до пяти объектов и определяет их положение. При обнаружении объектов в видеопотоках каждому объекту присваивается уникальный идентификатор, который можно использовать для отслеживания объекта от кадра к кадру.

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

Интегрировать специальную модель можно двумя способами. Вы можете включить модель в пакет, поместив ее в папку объектов приложения, или динамически скачать ее из Cloud Storage. В таблице ниже сравниваются эти два варианта.

Модель в комплекте Размещенная модель
Модель входит в APK-файл приложения, поэтому его размер увеличивается. Модель не входит в состав APK-файла. Загрузка в Cloud Storage. Мы рекомендуем использовать Cloud Storage for Firebase.
Модель доступна сразу, даже если устройство Android не подключено к интернету. В приложении должен быть код для скачивания модели по запросу
Проект Firebase не требуется Проект Firebase (если вы используете Cloud Storage for Firebase).
Чтобы обновить модель, необходимо повторно опубликовать приложение. Как отправлять обновления модели без повторной публикации приложения
Нет встроенного A/B-тестирования A/B-тестирование с помощью Firebase Remote Config

Попробовать

Подготовка

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

  1. Добавьте зависимости для библиотек ML Kit для Android в файл Gradle на уровне приложения, который обычно называется app/build.gradle.kts:

    Для добавления модели в приложение:

    dependencies {
      // ...
      // Object detection & tracking feature with custom bundled model
      implementation("com.google.mlkit:object-detection-custom:17.0.2")
    }
    
  2. Если вы хотите скачать модель из 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.

2. Настройте детектор объектов

После того как вы настроите источники модели, настройте детектор объектов для своего варианта использования с помощью объекта CustomObjectDetectorOptions. Вы можете изменить следующие настройки:

Настройки детектора объектов
Режим обнаружения STREAM_MODE (по умолчанию) | SINGLE_IMAGE_MODE

В режиме STREAM_MODE (по умолчанию) детектор объектов работает с низкой задержкой, но при первых нескольких вызовах может выдавать неполные результаты (например, не указывать ограничивающие рамки или ярлыки категорий). Кроме того, в STREAM_MODE детектор присваивает объектам идентификаторы отслеживания, которые можно использовать для отслеживания объектов в разных кадрах. Используйте этот режим, если вам нужно отслеживать объекты или если важна низкая задержка, например при обработке видеопотоков в реальном времени.

В SINGLE_IMAGE_MODE детектор объектов возвращает результат после того, как определена ограничительная рамка объекта. Если вы также включите классификацию, результат будет возвращен после того, как будут доступны ограничительная рамка и ярлык категории. В результате задержка обнаружения может быть выше. Кроме того, в SINGLE_IMAGE_MODE идентификаторы отслеживания не назначаются. Используйте этот режим, если задержка не критична и вы не хотите иметь дело с частичными результатами.

Обнаружение и отслеживание нескольких объектов false (по умолчанию) | true

Нужно ли обнаруживать и отслеживать до пяти объектов или только самый заметный объект (по умолчанию).

Классификация объектов false (по умолчанию) | true

Нужно ли классифицировать обнаруженные объекты с помощью предоставленной модели собственного классификатора. Чтобы использовать собственную модель классификации, задайте значение true.

Порог достоверности классификации

Минимальный уровень достоверности обнаруженных ярлыков. Если не задано, будет использоваться пороговое значение классификатора, указанное в метаданных модели. Если модель не содержит метаданных или в метаданных не указан порог классификатора, будет использоваться порог по умолчанию, равный 0,0.

Максимальное количество ярлыков для объекта

Максимальное количество ярлыков на объект, которое будет возвращать детектор. Если правило не настроено, используется значение по умолчанию (10).

API обнаружения и отслеживания объектов оптимизирован для двух основных вариантов использования:

  • Обнаружение и отслеживание наиболее заметного объекта в видоискателе камеры в реальном времени.
  • Распознавание нескольких объектов на статичном изображении.

Чтобы настроить API для этих вариантов использования с локально упакованной моделью:

Kotlin

// Live detection and tracking
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

// Multiple object detection in static images
val customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(localModel)
        .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
        .enableMultipleObjects()
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)

Java

// Live detection and tracking
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

// Multiple object detection in static images
CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(localModel)
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableMultipleObjects()
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

ObjectDetector objectDetector =
    ObjectDetection.getClient(customObjectDetectorOptions);

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

Хотя подтверждать это нужно только перед запуском детектора, если у вас есть и модель, размещенная удаленно, и модель, встроенная локально, может быть целесообразно выполнять эту проверку при создании экземпляра детектора изображений: создать детектор на основе удаленной модели, если она была скачана, и на основе локальной модели в противном случае.

Kotlin

val modelFile = File(context.cacheDir, "my_remote_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 customObjectDetectorOptions =
        CustomObjectDetectorOptions.Builder(model)
        .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
        .enableClassification()
        .setClassificationConfidenceThreshold(0.5f)
        .setMaxPerObjectLabelCount(3)
        .build()

val objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions)

Java

File modelFile = new File(context.getCacheDir(), "my_remote_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();
}

CustomObjectDetectorOptions customObjectDetectorOptions =
        new CustomObjectDetectorOptions.Builder(model)
                .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                .enableClassification()
                .setClassificationConfidenceThreshold(0.5f)
                .setMaxPerObjectLabelCount(3)
                .build();

ObjectDetector objectDetector =
        ObjectDetection.getClient(customObjectDetectorOptions);

Если у вас есть только модель, размещенная удаленно, вам следует отключить функции, связанные с моделью, например, сделать серым или скрыть часть пользовательского интерфейса, пока вы не подтвердите, что модель загружена.

Kotlin

val localFile = File(context.cacheDir, "my_remote_model.tflite")
if (localFile.exists()) {
    // Model is already cached, initialize immediately
    initializeDetector(localFile)
} else {
    // Model is not yet available, show loading UI and start download
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            // Download complete, initialize the detector
            hideLoadingUI()
            initializeDetector(localFile)
        }
        .addOnFailureListener {
            // Handle download error
            showErrorUI()
        }
}

private fun initializeDetector(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val customObjectDetectorOptions = CustomObjectDetectorOptions.Builder(localModel)
            .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableClassification()
            .build()
    val objectDetector = ObjectDetection.getClient(customObjectDetectorOptions)
    // Enable ML-related UI features here
    enableMLFeatures(objectDetector)
}

Java

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    // Model is already cached, initialize immediately
    initializeDetector(localFile);
} else {
    // Model is not yet available, show loading UI and start download
    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) {
                // Download complete, initialize the detector
                hideLoadingUI();
                initializeDetector(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                // Handle download error
                showErrorUI();
            }
        });
}

private void initializeDetector(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomObjectDetectorOptions customObjectDetectorOptions =
            new CustomObjectDetectorOptions.Builder(localModel)
                    .setDetectorMode(CustomObjectDetectorOptions.SINGLE_IMAGE_MODE)
                    .enableClassification()
                    .build();
    ObjectDetector objectDetector = ObjectDetection.getClient(customObjectDetectorOptions);
    // Enable ML-related UI features here
    enableMLFeatures(objectDetector);
}

3. Как подготовить входное изображение

Создайте объект InputImage на основе изображения. Детектор объектов работает непосредственно с Bitmap, NV21 ByteBuffer или YUV_420_888 media.Image. Если у вас есть прямой доступ к одному из этих источников, рекомендуем создать InputImage на его основе. Если вы создадите InputImage из других источников, мы выполним преобразование самостоятельно, но оно может быть менее эффективным.

Объект 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 вместе с градусами поворота.

4. Запустите детектор объектов.

Kotlin

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (detectedObject in results) {
          // ...
        }
    });

Java

objectDetector
    .process(image)
    .addOnFailureListener(e -> {...})
    .addOnSuccessListener(results -> {
        for (DetectedObject detectedObject : results) {
          // ...
        }
    });

5. Как получать информацию о помеченных объектах

Если вызов process() будет выполнен успешно, список объектов DetectedObject будет передан слушателю успешного выполнения.

Каждый объект DetectedObject содержит следующие свойства:

Граничная рамка Объект Rect, указывающий положение объекта на изображении.
Идентификатор отслеживания Целое число, которое идентифицирует объект на разных изображениях. Нулевое значение в SINGLE_IMAGE_MODE.
Ярлыки
Описание ярлыка Текстовое описание ярлыка. Возвращается, только если метаданные модели LiteRT содержат описания ярлыков.
Индекс ярлыка Индекс ярлыка среди всех ярлыков, поддерживаемых классификатором.
Достоверность ярлыка Уровень достоверности классификации объекта.

Kotlin

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (detectedObject in results) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
      val text = label.text
      val index = label.index
      val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : results) {
  Rect boundingBox = detectedObject.getBoundingBox();
  Integer trackingId = detectedObject.getTrackingId();
  for (Label label : detectedObject.getLabels()) {
    String text = label.getText();
    int index = label.getIndex();
    float confidence = label.getConfidence();
  }
}

Как обеспечить удобство для пользователей

Чтобы обеспечить удобство для пользователей, следуйте этим рекомендациям:

  • Успешное обнаружение объектов зависит от их визуальной сложности. Чтобы объекты с небольшим количеством визуальных признаков были обнаружены, они должны занимать большую часть изображения. Вам следует предоставить пользователям инструкции по съемке объектов, которые вы хотите распознавать.
  • Если вы используете классификацию и хотите обнаруживать объекты, которые не попадают ни в одну из поддерживаемых категорий, реализуйте специальную обработку для неизвестных объектов.

Также рекомендуем ознакомиться с приложением ML Kit Material Design и коллекцией шаблонов для функций на основе машинного обучения.

Повышение эффективности

Если вы хотите использовать обнаружение объектов в приложении, работающем в реальном времени, следуйте приведенным ниже рекомендациям, чтобы добиться оптимальной частоты кадров.

  • При использовании режима потоковой передачи в приложении реального времени не применяйте функцию обнаружения нескольких объектов, поскольку большинство устройств не смогут обеспечить достаточную частоту кадров.

  • Если вы используете 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.