É possível usar o kit de aprendizado de máquina para detectar e rastrear objetos em frames de vídeo sucessivos.
Quando você passa uma imagem para o kit de ML, ele detecta até cinco objetos na imagem com a posição de cada objeto na imagem. Ao detectar objetos em streams de vídeo, cada objeto tem um ID exclusivo que pode ser usado para rastrear o objeto de quadro para quadro. Também é possível ativar a classificação aproximada de objetos, que rotula objetos com descrições de categorias amplas.
Testar
- Explore o app de amostra para ver um exemplo de uso dessa API.
- Consulte o app de demonstração do Material Design (em inglês) para ver uma implementação completa dessa API.
Antes de começar
- No arquivo
build.gradle
no nível do projeto, inclua o repositório Maven do Google nas seçõesbuildscript
eallprojects
. - Adicione as dependências das bibliotecas Android do kit de ML ao arquivo Gradle do módulo no nível do app, que geralmente é
app/build.gradle
:dependencies { // ... implementation 'com.google.mlkit:object-detection:17.0.0' }
1. Configurar o detector de objetos
Para detectar e rastrear objetos, primeiro crie uma instância de ObjectDetector
e,
opcionalmente, especifique as configurações do detector que você quer alterar do
padrão.
Configure o detector de objetos para seu caso de uso com um objeto
ObjectDetectorOptions
. É possível alterar as seguintes configurações:Configurações do detector de objetos Modo de detecção STREAM_MODE
(padrão) |SINGLE_IMAGE_MODE
Em
STREAM_MODE
(padrão), o detector de objetos é executado com baixa latência, mas pode produzir resultados incompletos, como caixas delimitadoras ou rótulos de categoria não especificados, nas primeiras chamadas do detector. Além disso, noSTREAM_MODE
, o detector atribui IDs de rastreamento a objetos, que podem ser usados para rastrear objetos em frames. Use esse modo quando você quiser rastrear objetos ou quando a baixa latência for importante, como ao processar streams de vídeo em tempo real.Em
SINGLE_IMAGE_MODE
, o detector de objetos retorna o resultado após a caixa delimitadora do objeto ser determinada. Se você também ativar a classificação, o resultado será retornado depois que a caixa delimitadora e o rótulo da categoria estiverem disponíveis. Como consequência, a latência de detecção é potencialmente maior. Além disso, emSINGLE_IMAGE_MODE
, os IDs de acompanhamento não são atribuídos. Use esse modo se a latência não for crítica e você não quiser lidar com resultados parciais.Detectar e rastrear vários objetos false
(padrão) |true
Se for preciso detectar e rastrear até cinco objetos ou apenas o objeto mais proeminente (padrão).
Classificar objetos false
(padrão) |true
Se é necessário classificar objetos detectados em categorias não específicas. Quando ativado, o detector de objetos os classifica nas seguintes categorias: artigos de moda, alimentos, artigos para casa, lugares e plantas.
A API de rastreamento e detecção de objetos é otimizada para estes dois casos de uso principais:
- Detecção ao vivo e rastreamento do objeto mais proeminente no visor da câmera.
- Detecção de vários objetos em uma imagem estática.
Para configurar a API nestes casos de uso:
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();
Consiga uma instância de
ObjectDetector
:Kotlin
val objectDetector = ObjectDetection.getClient(options)
Java
ObjectDetector objectDetector = ObjectDetection.getClient(options);
2. Preparar a imagem de entrada
Para detectar e rastrear objetos, transmita imagens para o métodoprocess()
da instância ObjectDetector
.
O detector de objetos é executado diretamente de um Bitmap
, ByteBuffer
NV21 ou um media.Image
YUV_420_888. É recomendável criar um InputImage
a partir dessas fontes
se você tiver acesso direto a uma delas. Se você criar um InputImage
a partir de outras origens, processaremos a conversão internamente para você, e ela pode ser menos eficiente.
Para cada frame de vídeo ou imagem em uma sequência, faça o seguinte:
É possível criar um objeto InputImage
a partir de diferentes origens, cada um explicado abaixo.
Como usar um media.Image
Para criar um objeto InputImage
a partir de um objeto media.Image
, como ao capturar uma imagem da câmera de um dispositivo, transmita o objeto media.Image
e a rotação da imagem para InputImage.fromMediaImage()
.
Se você usar a biblioteca
CameraX, as classes OnImageCapturedListener
e
ImageAnalysis.Analyzer
calcularão o valor de rotação
automaticamente.
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 // ... } } }
Se você não usar uma biblioteca de câmera que ofereça o grau de rotação da imagem, você poderá calculá-lo usando o grau de rotação do dispositivo e a orientação do sensor da câmera:
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; }
Em seguida, transmita o objeto media.Image
e o valor do grau de rotação para InputImage.fromMediaImage()
:
Kotlin
val image = InputImage.fromMediaImage(mediaImage, rotation)
Java
InputImage image = InputImage.fromMediaImage(mediaImage, rotation);
Como usar um URI de arquivo
Para criar um objeto InputImage
a partir de um URI de arquivo, transmita o contexto do app e o URI do arquivo para InputImage.fromFilePath()
. Isso é útil ao usar um intent ACTION_GET_CONTENT
para solicitar que o usuário selecione uma imagem do app de galeria dele.
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(); }
Como usar um ByteBuffer
ou ByteArray
Para criar um objeto InputImage
a partir de ByteBuffer
ou ByteArray
, primeiro calcule o grau de rotação de imagem conforme descrito anteriormente para a entrada media.Image
.
Em seguida, crie o objeto InputImage
com o buffer ou a matriz, junto com a altura, a largura, o formato de codificação de cores e o grau de rotação da imagem:
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 );
Como usar um Bitmap
Para criar um objeto InputImage
a partir de um objeto Bitmap
, faça a seguinte declaração:
Kotlin
val image = InputImage.fromBitmap(bitmap, 0)
Java
InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);
A imagem é representada por um objeto Bitmap
com os graus de rotação.
3. Processar a imagem
Transmita a imagem para o métodoprocess()
:
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. Receber informações sobre objetos detectados
Se a chamada para process()
for bem-sucedida, uma lista de DetectedObject
s será transmitida para o listener de êxito.
Cada DetectedObject
contém as seguintes propriedades:
Caixa delimitadora | Um Rect que indica a posição do objeto na imagem. |
||||||
ID de acompanhamento | Um número inteiro que identifica o objeto nas imagens. Nulo em SINGLE_IMAGE_MODE. | ||||||
Rótulos |
|
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(); } }
Garantir uma ótima experiência do usuário
Para ter a melhor experiência do usuário, siga estas diretrizes no seu app:
- A detecção bem-sucedida de objetos depende da complexidade visual deles. Para ser detectado, objetos com um pequeno número de recursos visuais podem precisar ocupar uma parte maior da imagem. É necessário fornecer aos usuários orientações sobre como capturar entradas que funcionem bem com o tipo de objeto que você quer detectar.
- Ao usar a classificação, se você quiser detectar objetos que não se enquadrem nas categorias compatíveis, implemente um tratamento especial para objetos desconhecidos.
Além disso, confira o app de demonstração do Kit de ML com Material Design e a coleção de Padrões para recursos com tecnologia de machine learning do Material Design.
Melhoria de performance
Se você quiser usar a detecção de objetos em um aplicativo em tempo real, siga estas diretrizes para ter as melhores taxas de frames:
Ao usar o modo de streaming em um aplicativo em tempo real, não use a detecção de vários objetos, já que a maioria dos dispositivos não conseguirá produzir taxas de frames adequadas.
Desative a classificação se ela não for necessária.
- Se você usar a API
Camera
oucamera2
, as chamadas para o detector serão limitadas. Se um novo frame de vídeo for disponibilizado enquanto o detector estiver em execução, descarte esse frame. Consulte a classeVisionProcessorBase
no app de amostra do guia de início rápido para ver um exemplo. - Se você usar a API
CameraX
, verifique se a estratégia de pressão de retorno está definida como o valor padrãoImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
. Isso garante que apenas uma imagem será exibida por vez para análise. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não ficarão na fila para entrega. Quando a imagem que está sendo analisada é fechada chamando ImageProxy.close(), a próxima imagem mais recente é entregue. - Se você usar a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do kit de ML e, em seguida, renderize a imagem e a sobreposição em uma única etapa. Isso renderiza a superfície de exibição apenas uma vez para cada frame de entrada. Consulte as classes
CameraSourcePreview
eGraphicOverlay
no app de amostra do guia de início rápido para ver um exemplo. - Se você usar a API Camera2, capture imagens no
formato
ImageFormat.YUV_420_888
. Se você usar a API Camera mais antiga, capture imagens no formatoImageFormat.NV21
.