Обнаружение и отслеживание объектов с помощью ML Kit на Android

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

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

Попробовать

Подготовка

  1. В файле build.gradle на уровне проекта убедитесь, что репозиторий Maven от Google указан в разделах buildscript и allprojects.
  2. Добавьте зависимости для библиотек ML Kit для Android в файл Gradle на уровне приложения, который обычно называется app/build.gradle:
    dependencies {
      // ...
    
      implementation 'com.google.mlkit:object-detection:17.0.2'
    
    }

1. Как настроить детектор объектов

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

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

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

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

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

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

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

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

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

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

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

    Чтобы настроить API для этих вариантов использования:

    Kotlin

    // Live detection and tracking
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
            .enableClassification()  // Optional
            .build()
    
    // Multiple object detection in static images
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableMultipleObjects()
            .enableClassification()  // Optional
            .build()

    Java

    // Live detection and tracking
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
                    .enableClassification()  // Optional
                    .build();
    
    // Multiple object detection in static images
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
                    .enableMultipleObjects()
                    .enableClassification()  // Optional
                    .build();
  2. Получите экземпляр ObjectDetector:

    Kotlin

    val objectDetector = ObjectDetection.getClient(options)

    Java

    ObjectDetector objectDetector = ObjectDetection.getClient(options);

2. Подготовьте исходное изображение

Чтобы обнаружить и отследить объекты, передайте изображения методу process() экземпляра ObjectDetector.

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

3. Обработка изображения

Передайте изображение методу process():

Kotlin

objectDetector.process(image)
    .addOnSuccessListener { detectedObjects ->
        // Task completed successfully
        // ...
    }
    .addOnFailureListener { e ->
        // Task failed with an exception
        // ...
    }

Java

objectDetector.process(image)
    .addOnSuccessListener(
        new OnSuccessListener<List<DetectedObject>>() {
            @Override
            public void onSuccess(List<DetectedObject> detectedObjects) {
                // Task completed successfully
                // ...
            }
        })
    .addOnFailureListener(
        new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

4. Получение информации о распознанных объектах

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

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

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

Kotlin

for (detectedObject in detectedObjects) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
        val text = label.text
        if (PredefinedCategory.FOOD == text) {
            ...
        }
        val index = label.index
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : detectedObjects) {
    Rect boundingBox = detectedObject.getBoundingBox();
    Integer trackingId = detectedObject.getTrackingId();
    for (Label label : detectedObject.getLabels()) {
        String text = label.getText();
        if (PredefinedCategory.FOOD.equals(text)) {
            ...
        }
        int index = label.getIndex();
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        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.