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

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

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

Попробуйте это

Прежде чем начать

  1. В файле build.gradle на уровне проекта обязательно включите репозиторий Maven от Google в разделы buildscript и allprojects .
  2. Добавьте зависимости для библиотек Android ML Kit в файл 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 для этих вариантов использования:

    Котлин

    // 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()

    Ява

    // 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 :

    Котлин

    val objectDetector = ObjectDetection.getClient(options)

    Ява

    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 вычисляют значение поворота автоматически.

Котлин

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
            // ...
        }
    }
}

Ява

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
          // ...
        }
    }
}

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

Котлин

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
}

Ява

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() :

Котлин

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

Использование URI файла

Чтобы создать объект InputImage из URI файла, передайте контекст приложения и URI файла в InputImage.fromFilePath() . Это полезно, когда вы используете намерение ACTION_GET_CONTENT , чтобы предложить пользователю выбрать изображение из своего приложения галереи.

Котлин

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 с буфером или массивом, вместе с высотой изображения, шириной, форматом кодировки цвета и степенью поворота:

Котлин

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
)

Ява

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 , сделайте следующее объявление:

Котлин

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

Изображение представлено объектом Bitmap вместе с градусами поворота.

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

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

Котлин

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

Ява

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.
Этикетки
Описание этикетки Текстовое описание метки. Это будет одна из констант String, определенных в PredefinedCategory .
Индекс этикетки Индекс метки среди всех меток, поддерживаемых классификатором. Это будет одна из целочисленных констант, определенных в PredefinedCategory .
Уверенность в этикетке Значение достоверности классификации объекта.

Котлин

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
    }
}

Ява

// 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 в примере приложения быстрого запуска для примера.
  • Если вы используете API Camera2, захватывайте изображения в формате ImageFormat.YUV_420_888 . Если вы используете старый API Camera, захватывайте изображения в формате ImageFormat.NV21 .