第15章:CameraX与ML Kit集成——让摄像头真正“看懂”世界

说实话,CameraX学到这儿,你已经能让手机拍出不错的照片了。但拍照只是第一步,真正的乐趣在于——让摄像头理解画面里有什么。这就是我们今天要聊的:CameraX与ML Kit的集成。

ML Kit是Google推出的移动端机器学习框架。它不需要你懂深度学习,也不需要你训练模型。几行代码,就能让人脸检测、文字识别、对象检测跑起来。我个人觉得,这是Android开发里最“爽”的体验之一。

核心思路:CameraX负责获取图像帧,ML Kit负责分析图像帧。两者通过ImageAnalysis用例连接,形成“采集-分析-反馈”的闭环。

15.1 整体架构:CameraX + ML Kit 怎么配合?

先看一张图,帮你快速建立整体认知。我每次做这类集成时,都会先在脑子里过一遍这个流程。

CameraX + ML Kit 集成架构图 CameraX 图像采集层 Preview(预览) | ImageCapture(拍照) | ImageAnalysis(分析) ImageProxy(图像帧) 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