Когда вы передаете изображение в 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 |
Попробовать
- Пример использования встроенной модели можно найти в приложении Vision Quickstart, а пример использования размещенной модели – в приложении AutoML Quickstart.
- Пример реализации этого API можно найти в приложении Material Design showcase.
Подготовка
1. В файлеbuild.gradle.kts на уровне проекта убедитесь, что репозиторий Google Maven указан в разделах buildscript и allprojects.
Добавьте зависимости для библиотек 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") }Если вы хотите скачать модель из Cloud Storage для Firebase, убедитесь, что добавили Firebase в проект Android. Это не требуется, если вы используете модель в комплекте.
1. Загрузка модели
Модель можно загрузить из локального или удаленного источника.
Как настроить источник локальной модели
Чтобы добавить модель в приложение:
Скопируйте файл модели (обычно с расширением
.tfliteили.lite) в папкуassets/приложения. (Возможно, вам сначала потребуется создать папку. Для этого нажмите правой кнопкой мыши на папкуapp/и выберите New > Folder > Assets Folder (Создать > Папка > Папка объектов).)Создайте объект
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
В режиме В |
| Обнаружение и отслеживание нескольких объектов |
false (по умолчанию) | true
Нужно ли обнаруживать и отслеживать до пяти объектов или только самый заметный объект (по умолчанию). |
| Классификация объектов |
false (по умолчанию) | 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. | ||||||
| Ярлыки |
|
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.