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

    Котлин

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

    Котлин

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

Котлин

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

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

Котлин

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

Котлин

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
)

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

Котлин

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

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 — значение равно null.
Метки
Описание метки Текстовое описание метки. Оно будет представлять собой одну из строковых констант, определенных в 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
    }
}

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