本文带大家在 Android 上体验 MLKit 的以下功能:
- 1. 图像识别(Image Labeling)
- 2. 目标检测(Object Detection)
- 3. 目标追踪(Object Tracking)
1. 图像识别(Image Labeling)
图像识别是计算机视觉的一个重要领域,简单说就是帮你提取图片中的有效信息。 MLKit 提供了 ImageLabeling 功能,可以识别图像信息并进行分类标注。
比如输入一张包含猫的图片,ImageLabeling 能识别出图片中的猫元素,并给出一个猫的标注,除了最显眼的猫 ImageLabeling还能识别出花、草等图片中所有可识别的事物,并分别给出出现的概率和占比,识别的结果以 List<ImageLabel>
返回。 基于预置的默认模型,ImageLabeling可以对图像元素进行超过 400 种以上的标注分类,当然你可以使用自己训练的模型扩充更多分类。
默认模型当前支持的分类标注:
Android 中引入 MLKit 的 ImageLabeliing 很简单,在 gradle 中添加相关依赖即可
implementation 'com.google.mlkit:image-labeling:17.0.5'
接下来写一个 Android 的 Demo 来展示使用效果。我们使用 Compose 为 Demo 写一个简单的 UI:
@Composable fun MLKitSample() { Column { var imageLabel by remember { mutableStateOf("") } //Load Image val context = LocalContext.current val bmp = remember(context) { context.assetsToBitmap("cat.png")!! } Image(bitmap = bmp.asImageBitmap(), contentDescription = "") val coroutineScope = rememberCoroutineScope() Button( onClick = { //TODO : 图像识别具体逻辑,见后文 }) { Text("Image Labeling") } Text(imageLabel, Modifier.fillMaxWidth(), textAlign = TextAlign.Center) } }
将图片资源放入 /assets,并加载为 Bitmap
fun Context.assetsToBitmap(fileName: String): Bitmap? = assets.open(fileName).use { BitmapFactory.decodeStream(it) }
点击 Button 后,对 Bitmap 进行识别,获取识别后的信息更新 imageLabel
。
看一下 onClick 内的内容:
val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS) val image = InputImage.fromBitmap(bmp, 0) labeler.process(image).addOnSuccessListener { labels : List<ImageLabel> -> // Task completed successfully imageLabel = labels.scan("") { acc, label -> acc + "${label.text} : ${label.confidence}\n" }.last() }.addOnFailureListener { // Task failed with an exception }
首先创建 ImageLabeler
处理器,InputImage.fromBitmap
将 Bitmap 处理为 ImageLabeler 可接受的资源类型,处理结果通过 Listener 返回。
处理成功,返回 ImageLabel
的列表,ImageLabel 代表每一个种类的标注信息,图像经识别后获得一组这样de 标注,包含每一种类的名字以及其出现概率,这些信息可以在图像检索等场景中作为权重使用。
2. 目标检测(Object Detection)
目标检测也是计算机视觉的一个基础研究方向。这里需要注意 “检测” 和 “识别” 的区别:
- 检测(Detecting):关注的是 Where is,即目标在哪里
- 识别(Lebeling):关注的是 What is,即目标是什么
ImageLebeling 可以识别图像中的事物分类,但是无法确定哪个事物在哪里。而目标检测可以确定有几个事物分别在哪里,但是事物的分类信息不清晰。
ObjectDetection 虽然也提供了一定的识别能力,但是其默认的模型文件只能识别有限的几个种类,无法像 ImageLebeling 那样精确分类。想要识别更准确的信息需要借助额外的模型文件。但是我们可以将上述两套 API 配合使用,各取所长以达到目标检测的同时进行准确的识别和分类。
首先添加 ObjectDetection 依赖
implementation 'com.google.mlkit:object-detection:16.2.7'
接下来在上面例子中,增加一个 Button 用于点击后的目标检测
@Composable fun MLKitSample() { Column(Modifier.fillMaxSize()) { val detctedObject = remember { mutableStateListOf<DetectedObject>() } //Load Image val context = LocalContext.current val bmp = remember(context) { context.assetsToBitmap("dog_cat.jpg")!! } Canvas(Modifier.aspectRatio( bmp.width.toFloat() / bmp.height.toFloat())) { drawIntoCanvas { canvas -> canvas.withSave { canvas.scale(size.width / bmp.width) canvas.drawImage( // 绘制 image image = bmp.asImageBitmap(), Offset(0f, 0f), Paint() ) detctedObject.forEach { canvas.drawRect( //绘制目标检测的边框 it.boundingBox.toComposeRect(), Paint().apply { color = Color.Red.copy(alpha = 0.5f) style = PaintingStyle.Stroke strokeWidth = bmp.width * 0.01f }) if (it.labels.isNotEmpty()) { canvas.nativeCanvas.drawText( //绘制物体识别信息 it.labels.first().text, it.boundingBox.left.toFloat(), it.boundingBox.top.toFloat(), android.graphics.Paint().apply { color = Color.Green.toArgb() textSize = bmp.width * 0.05f }) } } } } } Button( onClick = { //TODO : 目标检测具体逻辑,见后文 }) { Text("Object Detect") } } }
由于我们要在图像上绘制目标边界的信息,所以这次采用 Canvas
绘制 UI,包括以下内容:
- drawImage:绘制目标图片
- drawRect:MLKit 检测成功后会返回
List<DetectedObject>
信息,基于 DetectedObject 绘制目标边界 - drawText:基于 DetectedObject 绘制目标的分类标注
点击 Button 后进行目标检测,具体实现如下:
val options = ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE) .enableMultipleObjects() .enableClassification() .build() val objectDetector = ObjectDetection.getClient(options) val image = InputImage.fromBitmap(bmp, 0) objectDetector.process(image) .addOnSuccessListener { detectedObjects -> // Task completed successfully coroutineScope.launch { detctedObject.clear() detctedObject.addAll(getLabels(bmp, detectedObjects).toList()) } } .addOnFailureListener { e -> // Task failed with an exception // ... }
通过 ObjectDetectorOptions
我们对检测处理进行配置。可使用 Builder 进行多个配置:
- setDetectorMode : ObjectDetection 有多种目标检测方式,这里使用的是最简单的一种
SINGLE_IMAGE_MODE
即针对单张图片的检测。此外还有针对视频流的检测等其他方式,后文介绍。 - enableMultipleObjects:可以只检测最突出的事物或是检测所有可事物,我们这里启动多目标检测,检测所有可检测的事物。
- enableClassification: ObjectDetection 在图像识别上的能力有限,默认模型只能识别 5 个种类,且都是比较宽泛的分类,比如植物、动物等。enableClassification 可以开启图像识别能力。开启后,其识别结果会存入
DetectedObject.labels
。由于这个识别结果没有意义,我们在例子中会替换为使用 ImageLebeling 识别后的标注信息
基于 ObjectDetectorOptions
创建 ObjectDetector
处理器,传入图片后开始检测。getLabels
是自定义方法,基于 ImageLebeling 添加图像识别信息。检测的最终结果更新至 detctedObject
这个 MutableStateList
,刷新 Compose UI。
private fun getLabels( bitmap: Bitmap, objects: List<DetectedObject> ) = flow { val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS) for (obj in objects) { val bounds = obj.boundingBox val croppedBitmap = Bitmap.createBitmap( bitmap, bounds.left, bounds.top, bounds.width(), bounds.height() ) emit( DetectedObject( obj.boundingBox, obj.trackingId, getLabel(labeler, croppedBitmap).map { //转换为 DetectedObject.Label DetectedObject.Label(it.text, it.confidence, it.index) }) ) } }
首先根据 DetectedObject
的边框信息 boundingBox
将 Bitmap 分解为小图片,然后对其调用 getLabel
获取标注信息补充进 DetectedObject 实例(这里实际是重建了一个实例)
getLabel
中的 ImageLebeling 是一个异步过程,为了调用方便,定义为一个挂起函数:
suspend fun getLabel(labeler: ImageLabeler, image: Bitmap): List<ImageLabel> = suspendCancellableCoroutine { cont -> labeler.process(InputImage.fromBitmap(image, 0)) .addOnSuccessListener { labels -> // Task completed successfully cont.resume(labels) } }
3. 目标追踪(Object Tracking)
目标追踪就是通过对视频逐帧进行 ObjectDetection ,以达到连续捕捉的效果。接下来的例子中我们启动一个相机预览,对拍摄到图像进行 ObjectTracking。
我们使用 CameraX 启动相机,因为 CameraX 封装的 API 更易用。
implementation "androidx.camera:camera-camera2:1.0.0-rc01" implementation "androidx.camera:camera-lifecycle:1.0.0-rc01" implementation "androidx.camera:camera-view:1.0.0-alpha20" implementation "com.google.accompanist:accompanist-permissions:0.16.1"
如上,引入 CameraX 相关类库,同时引入 accompanist-permissions 用来动态申请相机权限。
CameraX 的预览需要使用 androidx.camera.view.PreviewView
,我们通过 AndroidView
集成到 Composable 中,AndroidView 上方覆盖 Canvas ,Canvas 绘制目标边框。
整个 UI 布局如下:
val detectedObjects = mutableStateListOf<DetectedObject>() Box { CameraPreview(detectedObjects) Canvas(modifier = Modifier.fillMaxSize()) { drawIntoCanvas { canvas -> detectedObjects.forEach { canvas.scale(size.width / 480, size.height / 640) canvas.drawRect( //绘制边框 it.boundingBox.toComposeRect(), Paint().apply { color = Color.Red style = PaintingStyle.Stroke strokeWidth = 5f }) canvas.nativeCanvas.drawText( // 绘制文字 "TrackingId_${it.trackingId}", it.boundingBox.left.toFloat(), it.boundingBox.top.toFloat(), android.graphics.Paint().apply { color = Color.Green.toArgb() textSize = 20f }) } } } }
detectedObjects
是 ObjectDetection 逐帧实时检测的结果。CameraPreview
中集成了相机预览的 AndroidView,并实时更新 detectedObjects
。drawRect 和 drawText 在前面例子中也出现过,但需要注意这里 drawText 绘制的是 trackingId
。 视频的 ObjectDetection 会为 DetectedObject
添加 trackingId
信息, 视频目标的边框位置会不断变换,但是 trackingId
是不变的,这便于在多目标中更好地锁定个体。
@Composable private fun CameraPreview(detectedObjects: SnapshotStateList<DetectedObject>) { val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } val coroutineScope = rememberCoroutineScope() val objectAnalyzer = remember { ObjectAnalyzer(coroutineScope, detectedObjects) } AndroidView( factory = { ctx -> val previewView = PreviewView(ctx) val executor = ContextCompat.getMainExecutor(ctx) val imageAnalyzer = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(executor, objectAnalyzer) } cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build() cameraProvider.unbindAll() cameraProvider.bindToLifecycle( lifecycleOwner, cameraSelector, preview, imageAnalyzer ) }, executor) previewView }, modifier = Modifier.fillMaxSize(), ) }
CameraPreview
主要是关于 CameraX 的使用,本文不会逐行说明 CameraX 的使用,只关注与主题相关的代码: CameraX 可以设置 ImageAnalyzer
用于对视频帧进行解析,这正是用于我们的需求,这里自定义了 ObjectAnalyzer
做目标检测。
最后看一下 ObjectAnalyzer 的实现
class ObjectAnalyzer( private val coroutineScope: CoroutineScope, private val detectedObjects: SnapshotStateList<DetectedObject> ) : ImageAnalysis.Analyzer { private val options = ObjectDetectorOptions.Builder() .setDetectorMode(ObjectDetectorOptions.STREAM_MODE) .build() private val objectDetector = ObjectDetection.getClient(options) @SuppressLint("UnsafeExperimentalUsageError") override fun analyze(imageProxy: ImageProxy) { val frame = InputImage.fromMediaImage( imageProxy.image, imageProxy.imageInfo.rotationDegrees ) coroutineScope.launch { objectDetector.process(frame) .addOnSuccessListener { detectedObjects -> // Task completed successfully with(this@ObjectAnalyzer.detectedObjects) { clear() addAll(detectedObjects) } } .addOnFailureListener { e -> // Task failed with an exception // ... } .addOnCompleteListener { imageProxy.close() } } } }
ObjectAnalyzer
中获取相机预览的视频帧对其进行 ObjectDetection,检测结果更新至 detectedObjects
。注意此处 ObjectDetectorOptions
设置为 STREAM_MODE
专门处理视频检测。虽然把每一帧都当做 SINGLE_IMAGE_MODE
处理理论上也是可行的,但只有 STREAM_MODE
的检测结果才带有 trackingId
的值,而且 STREAM_MODE
下的边框位置经过防抖处理,位移更加顺滑。
最后
本文为了参加平台的活动,以喵为例介绍了 MLKit 图像识别的能力, MLKit 还有很多实用功能,比如人脸检测相较于 Android 自带的 android.media.FaceDetector 无论性能还是识别率都有质的飞跃。此外国内不少 AI 大厂也有很多不错的解决方案,比如旷视 。相信随着 AI 技术的发展,未来在移动端上的应用场景也会越来越多。