第15章:CameraX与ML Kit集成——让摄像头真正“看懂”世界
说实话,CameraX学到这儿,你已经能让手机拍出不错的照片了。但拍照只是第一步,真正的乐趣在于——让摄像头理解画面里有什么。这就是我们今天要聊的:CameraX与ML Kit的集成。
ML Kit是Google推出的移动端机器学习框架。它不需要你懂深度学习,也不需要你训练模型。几行代码,就能让人脸检测、文字识别、对象检测跑起来。我个人觉得,这是Android开发里最“爽”的体验之一。
核心思路:CameraX负责获取图像帧,ML Kit负责分析图像帧。两者通过ImageAnalysis用例连接,形成“采集-分析-反馈”的闭环。
15.1 整体架构:CameraX + ML Kit 怎么配合?
先看一张图,帮你快速建立整体认知。我每次做这类集成时,都会先在脑子里过一遍这个流程。
流程其实很简单:CameraX的ImageAnalysis不断输出图像帧,你把帧交给ML Kit的检测器,检测器返回结果,你再把结果绘制到Preview上或者做其他处理。
我的习惯:在ImageAnalysis的analyze方法里,我会先判断当前帧是否正在处理中。如果上一帧还没分析完,就直接跳过当前帧。这样可以避免积压,保证流畅度。
15.2 集成前的准备工作
嗯,这里要注意。ML Kit的依赖分两种:一种是内置模型(直接下载),一种是动态模型(运行时下载)。我建议初学者先用内置模型,省心。
在build.gradle里加上这些:
// CameraX 核心依赖
def camerax_version = "1.3.1"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
// ML Kit 人脸检测
implementation 'com.google.mlkit:face-detection:16.1.6'
// ML Kit 文字识别
implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
// ML Kit 对象检测
implementation 'com.google.mlkit:object-detection:17.0.1'
避坑提醒:我曾经在项目里同时集成了三个ML Kit模块,结果APK体积直接增加了30MB。如果你只需要人脸检测,就别加文字识别的依赖。ML Kit的模型文件都不小,按需添加才是正道。
15.3 人脸检测:最常用的功能
人脸检测是ML Kit里最成熟的功能之一。我最早接触它是在2018年,那时候准确率就已经很高了。现在更是稳定得让人放心。
先初始化检测器:
// 创建人脸检测器
val faceDetector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.build()
)
然后和CameraX的ImageAnalysis绑定:
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
faceDetector.process(inputImage)
.addOnSuccessListener { faces ->
// faces 就是检测到的人脸列表
for (face in faces) {
val bounds = face.boundingBox
val smileProb = face.smilingProbability
val leftEyeOpen = face.leftEyeOpenProbability
Log.d("FaceDetect", "人脸位置: $bounds, 微笑概率: $smileProb")
}
}
.addOnCompleteListener {
imageProxy.close() // 必须关闭,否则会卡死
}
} else {
imageProxy.close()
}
}
关键点:imageProxy.close() 一定要调用。我见过太多新手忘记关这个,结果CameraX的ImageAnalysis卡住不动,画面直接冻住。每次analyze回调结束,不管有没有结果,都要close。
15.4 文字识别:让摄像头变成扫描仪
文字识别在中文场景下特别实用。你想想看,扫描名片、识别菜单、提取文档,这些场景都能用上。
ML Kit的中文文字识别模型效果不错。我测试过印刷体,准确率在95%以上。手写体稍微差一些,但也够用。
// 初始化中文文字识别器
val recognizer = TextRecognition.getClient(ChineseTextRecognizerOptions.Builder().build())
// 在ImageAnalysis中使用
imageAnalysis.setAnalyzer(executor) { imageProxy ->
val mediaImage = imageProxy.image ?: return@setAnalyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
recognizer.process(inputImage)
.addOnSuccessListener { visionText ->
val resultText = visionText.text
Log.d("OCR", "识别结果: $resultText")
// 遍历每个文字块
for (block in visionText.textBlocks) {
val blockText = block.text
val blockRect = block.boundingBox
// 可以在这里绘制框选区域
}
}
.addOnCompleteListener {
imageProxy.close()
}
}
我的经验:文字识别对图像质量要求比较高。如果画面模糊或者光线太暗,识别率会直线下降。我建议在识别前加一个简单的亮度判断,太暗的话提示用户补光。
15.5 对象检测:识别画面中的物体
对象检测比人脸检测更通用。它能识别出画面里的杯子、手机、书本、人等常见物体。ML Kit提供了两种模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 单图模式 | 检测一张图片中的多个物体 | 拍照后分析 |
| 流式模式 | 连续检测,跟踪物体位置 | 实时预览 |
流式模式更适合和CameraX配合。代码如下:
// 创建对象检测器(流式模式)
val objectDetector = ObjectDetection.getClient(
ObjectDetectorOptions.Builder()
.setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
.enableClassification() // 启用分类,可以知道是什么物体
.build()
)
// 在ImageAnalysis中使用
imageAnalysis.setAnalyzer(executor) { imageProxy ->
val mediaImage = imageProxy.image ?: return@setAnalyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
objectDetector.process(inputImage)
.addOnSuccessListener { detectedObjects ->
for (obj in detectedObjects) {
val box = obj.boundingBox
val trackingId = obj.trackingId // 跟踪ID,用于跨帧追踪
if (obj.classificationCategory == ObjectDetector.CATEGORY_PERSON) {
Log.d("ObjectDetect", "检测到人,位置: $box")
}
}
}
.addOnCompleteListener {
imageProxy.close()
}
}
注意:流式模式下,trackingId可以帮助你跨帧追踪同一个物体。但如果你切换了摄像头(比如从后置切到前置),trackingId会重置。我曾经在这个坑里爬了半天,后来才发现是切换摄像头导致的。
15.6 在Preview上绘制检测结果
光检测还不够,你得把结果画出来让用户看到。我一般用自定义OverlayView来实现。
class OverlayView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint().apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 4f
}
private val textPaint = Paint().apply {
color = Color.GREEN
textSize = 40f
}
var detectedRects: List<Rect> = emptyList()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (rect in detectedRects) {
canvas.drawRect(rect, paint)
canvas.drawText("检测到", rect.left.toFloat(), rect.top - 10f, textPaint)
}
}
}
然后在Activity里,把检测结果传给OverlayView:
// 在analyze的成功回调里
overlayView.detectedRects = faces.map { it.boundingBox }
activity.runOnUiThread {
overlayView.invalidate()
}
注意坐标系转换:ML Kit返回的坐标是图像坐标系,而Preview显示的是屏幕坐标系。两者宽高比可能不同,需要做映射。我习惯用Matrix来做这个转换,省心。
15.7 性能优化建议
说实话,ML Kit跑在手机上还是挺吃性能的。尤其是对象检测,CPU占用率不低。我总结了几条优化经验:
- 降低分辨率:ImageAnalysis的targetResolution不要设太高,640x480就够用。分辨率越高,处理越慢。
- 跳帧处理:不是每一帧都需要分析。我一般每3帧分析一次,或者用时间间隔控制,比如每200ms分析一次。
- 异步处理:analyze方法里不要做UI操作,全部放到回调里处理。否则会阻塞CameraX的流水线。
- 及时释放:imageProxy.close() 必须调用,否则内存会暴涨。
我的习惯:在analyze方法开头加一个时间戳判断。如果距离上次分析不到150ms,就直接close并返回。这样既保证了流畅度,又不会漏掉关键帧。
15.8 完整示例:人脸检测 + 绘制框选
最后,给你一个完整的Activity骨架。把前面讲的内容串起来:
class FaceDetectActivity : AppCompatActivity() {
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var previewView: PreviewView
private lateinit var overlayView: OverlayView
private val faceDetector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.build()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_face_detect)
previewView = findViewById(R.id.previewView)
overlayView = findViewById(R.id.overlayView)
startCamera()
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(640, 480))
.build()
imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
val inputImage = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
faceDetector.process(inputImage)
.addOnSuccessListener { faces ->
val rects = faces.map { it.boundingBox }
runOnUiThread {
overlayView.detectedRects = rects
overlayView.invalidate()
}
}
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis
)
} catch (e: Exception) {
Log.e("CameraX", "绑定失败", e)
}
}, ContextCompat.getMainExecutor(this))
}
override fun onDestroy() {
super.onDestroy()
cameraProvider.unbindAll()
faceDetector.close()
}
}
这个例子跑起来后,你就能看到摄像头画面里,人脸被绿色框圈出来了。是不是挺有成就感的?
ML Kit和CameraX的集成,说白了就是“采集-分析-反馈”三个步骤。你掌握了这个模式,换成文字识别、对象检测,无非是换一个检测器的事。嗯,今天就到这儿,去试试吧。
公众号:蓝海资料掘金营,微信deep3321