【程式碼吸貓】使用 Google MLKit 進行影象識別
theme: healer-readable highlight: atelier-sulphurpool-light
一起用程式碼吸貓!本文正在參與【喵星人徵文活動】 MLKit 是 Google 提供的移動端機器學習庫。工程師僅通過少量程式碼就能在 Andorid 或 iOS 上實現各種 AI 能力,例如影象、文字、人臉識別等等,藉助 TensorFlow Lite 其中很多能力可以在裝置上離線完成。
https://developers.google.com/ml-kit
本文帶大家在 Android 上體驗 MLKit 的以下功能:
- 影象識別(Image Labeling)
- 目標檢測(Object Detection)
- 目標追蹤(Object Tracking)
1. 影象識別(Image Labeling)
影象識別是計算機視覺的一個重要領域,簡單說就是幫你提取圖片中的有效資訊。 MLKit 提供了 ImageLabeling 功能,可以識別影象資訊並進行分類標註。
比如輸入一張包含貓的圖片,ImageLabeling 能識別出圖片中的貓元素,並給出一個貓的標註,除了最顯眼的貓 ImageLabeling還能識別出花、草等圖片中所有可識別的事物,並分別給出出現的概率和佔比,識別的結果以 List<ImageLabel>
返回。 基於預置的預設模型,ImageLabeling可以對影象元素進行超過 400 種以上的標註分類,當然你可以使用自己訓練的模型擴充更多分類。
預設模型當前支援的分類標註:
https://developers.google.com/ml-kit/vision/image-labeling/label-map
Android 中引入 MLKit 的 ImageLabeliing 很簡單,在 gradle 中新增相關依賴即可
groovy
implementation 'com.google.mlkit:image-labeling:17.0.5'
接下來寫一個 Android 的 Demo 來展示使用效果。我們使用 Compose 為 Demo 寫一個簡單的 UI:
```kotlin @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
kotlin
fun Context.assetsToBitmap(fileName: String): Bitmap? =
assets.open(fileName).use {
BitmapFactory.decodeStream(it)
}
```
點選 Button 後,對 Bitmap 進行識別,獲取識別後的資訊更新 imageLabel
。
看一下 onClick 內的內容:
```kotlin
val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
val image = InputImage.fromBitmap(bmp, 0)
labeler.process(image).addOnSuccessListener { labels : List
```
首先建立 ImageLabeler
處理器,InputImage.fromBitmap
將 Bitmap 處理為 ImageLabeler 可接受的資源型別,處理結果通過 Listener 返回。
處理成功,返回 ImageLabel
的列表,ImageLabel 代表每一個種類的標註資訊,影象經識別後獲得一組這樣de 標註,包含每一種類的名字以及其出現概率,這些資訊可以在影象檢索等場景中作為權重使用。
2. 目標檢測(Object Detection)
目標檢測也是計算機視覺的一個基礎研究方向。這裡需要注意 “檢測” 和 “識別” 的區別: - 檢測(Detecting):關注的是 Where is,即目標在哪裡 - 識別(Lebeling):關注的是 What is,即目標是什麼
ImageLebeling 可以識別影象中的事物分類,但是無法確定哪個事物在哪裡。而目標檢測可以確定有幾個事物分別在哪裡,但是事物的分類資訊不清晰。
ObjectDetection 雖然也提供了一定的識別能力,但是其預設的模型檔案只能識別有限的幾個種類,無法像 ImageLebeling 那樣精確分類。想要識別更準確的資訊需要藉助額外的模型檔案。但是我們可以將上述兩套 API 配合使用,各取所長以達到目標檢測的同時進行準確的識別和分類。
首先新增 ObjectDetection 依賴
groovy
implementation 'com.google.mlkit:object-detection:16.2.7'
接下來在上面例子中,增加一個 Button 用於點選後的目標檢測 ```kotlin @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 後進行目標檢測,具體實現如下:
```kotlin 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。
```kotlin
private fun getLabels(
bitmap: Bitmap,
objects: List
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 是一個非同步過程,為了呼叫方便,定義為一個掛起函式:
kotlin
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 更易用。
groovy
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 佈局如下:
```kotlin
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
是不變的,這便於在多目標中更好地鎖定個體。
```kotlin
@Composable
private fun CameraPreview(detectedObjects: SnapshotStateList
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 的實現
```kotlin
class ObjectAnalyzer(
private val coroutineScope: CoroutineScope,
private val detectedObjects: SnapshotStateList
@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([email protected]) {
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 技術的發展,未來在移動端上的應用場景也會越來越多。
本文程式碼:https://github.com/vitaviva/JetpackComposePlayground/tree/main/mlkit_exp
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!
- 面試必備:Kotlin 執行緒同步的 N 種方法
- Jetpack MVVM 七宗罪之六:ViewModel 介面暴露不合理
- CreationExtras 來了,建立 ViewModel 的新方式
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼
- 為什麼 RxJava 有 Single / Maybe 等單發資料型別,而 Flow 沒有?
- 使用整潔架構優化你的 Gradle Module
- 一道面試題:介紹一下 Fragment 間的通訊方式?
- 【程式碼吸貓】使用 Google MLKit 進行影象識別
- Kotlin 1.6 正式釋出,帶來哪些新特性?
- Android Dev Summit '21 精彩內容盤點