Обнаружение и отслеживание объектов с помощью 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, указывающий положение объекта на изображении.
Идентификатор отслеживания Целое число, которое идентифицирует объект на изображениях. Null в режиме 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 и коллекцией шаблонов 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.