【Android】实战图像识别:Compose + MLKit + CameraX

【Android】实战图像识别:Compose + MLKit + CameraX

MLKit 是 Google 提供的移动端机器学习库,可以在 Andorid 或 iOS 上低成本地实现各种 AI 能力,例如图像、文字、人脸识别等等,而且很多能力可以在手机端离线完成。

https://developers.google.com/ml-kit

下面通过代码示例展示 MLKit 的以下功能:

图像识别(Image Labeling)目标检测(Object Detection)目标追踪(Object Tracking)

1. 图像识别(Image Labeling)

图像识别是计算机视觉的一个重要领域,简单说就是帮你提取图片中的有效信息。

MLKit 提供了 ImageLabeling 功能可以识别图像信息并对图像进行分类标注。

比如输入一张包含猫的图片,ImageLabeling 能识别出图片中的猫元素,并给出一个猫的标注,除了图片中最显眼的猫,ImageLabeling 还能识别出花、草等图片中所有可识别的事物,并分别给出出现的概率和占比,识别的结果以 List 返回。 基于预置的默认模型,ImageLabeling可以对图像元素进行超过 400 种以上的标注分类,当然你可以使用自己训练的模型扩充更多分类。

ImageLabeling 当前支持的标注分类:

https://developers.google.com/ml-kit/vision/image-labeling/label-map

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

// 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 代表每一个种类的标注,包含种类的名字以及其出现概率,这些信息可以在图像检索等场景中作为权重使用。

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

//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 绘制目标边界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

) = 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 =

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"

CameraX 的预览需要使用 androidx.camera.view.PreviewView,我们通过 AndroidView 集成到 Composable 中,AndroidView 上方覆盖 Canvas ,Canvas 绘制目标边框。

整个 UI 布局如下:

val detectedObjects = mutableStateListOf()

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) {

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

) : 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 还提供了其他一些实用功能,比如人脸检测、二维码识别、文字识别等,开发者可以免费使用这些能力,让计算机视觉技术更好地服务于我们的移动应用。

本文代码:

https://github.com/vitaviva/JetpackComposePlayground/tree/main/mlkit_exp

相关推荐

眼的成语
365bet线上注册

眼的成语

📅 07-01 👁️ 5104
诗词吟诵知识
bst365.com

诗词吟诵知识

📅 06-30 👁️ 7324
手机滤镜怎么调出来(华为手机滤镜在哪里找)
365亚洲体育投注

手机滤镜怎么调出来(华为手机滤镜在哪里找)

📅 09-01 👁️ 9002