深入 Android ML Kit 全链路实战:从视觉检测 Pipeline 到 CameraX 集成的端侧智能工程落地
去年在做一款工业巡检 App,需求是在产线不停机的前提下,用手机摄像头实时检测零件表面缺陷。当时团队里所有人都在聊大模型(LLM),但遇到这种纯视觉任务时,LLM 完全使不上劲——延迟太高、模型太大、推理成本扛不住。最终转向了 ML Kit 的端侧视觉能力,配合 CameraX 构建了一套实时分析链路。
记录一下当时的工程选型和落地细节。
ML Kit 的定位:端侧视觉的”瑞士军刀”
ML Kit 本质是 Google 把移动端视觉能力封装成了一套统一 API。它的核心优势不是”最强精度”,而是 开箱即用 + 零服务端依赖。
它提供两大类能力:
- 基础视觉 API:人脸检测、文字识别(OCR)、条码扫描、图像标记、目标检测、姿态检测、自拍分割。这些模型已内置在 Play Services 中,不需要额外下载。
- 自定义模型 API:对接 TensorFlow Lite 模型,适合企业级定制场景,比如我们当时的缺陷检测模型。
容易忽略的一点:ML Kit 的检测管线(Detection Pipeline)不是简单的一帧一帧独立处理。它在内部维护了帧间状态追踪,这对连续视频流的稳定性很关键——下面展开讲这个机制。
检测管线的工作机制
以目标检测(Object Detection)为例,ML Kit 的处理链路分三步:
CameraX 输出帧 → InputImage 封装 → 检测器推理 → 结果回调
InputImage 是整个管线的入口数据格式。它需要从 CameraX 的 ImageProxy 转换而来,这里有个容易踩坑的细节。
创建 InputImage 有两种方式:
// 方式一:从 ByteBuffer(推荐,零拷贝)
val image = InputImage.fromByteBuffer(
buffer,
width, height, rotation,
InputImage.IMAGE_FORMAT_YUV_420_888
)
// 方式二:从 Bitmap(会做格式转换,有额外开销)
val image = InputImage.fromBitmap(bitmap, rotation)
实际项目中 必须用方式一。CameraX 输出的 YUV 数据直接传入,避免 RGB 转换带来的 15-25ms 延迟。高帧率场景下,这点优化累积起来帧率能差一档。
拿到 InputImage 后,检测器的运行很简单:
objectDetector.process(image)
.addOnSuccessListener { results ->
for (obj in results) {
val box = obj.boundingBox
val labels = obj.labels
// 拿到检测框和分类标签
}
}
.addOnFailureListener { e -> /* 处理错误 */ }
回调里的 DetectedObject 包含四个关键字段:边界框(boundingBox)、分类标签(labels)、追踪 ID(trackingId)、以及四个角点坐标。追踪 ID 是 ML Kit 自动维护的,同一物体在连续帧中保持相同 ID,省去了自己写追踪逻辑的麻烦。
CameraX 集成:帧率与检测节奏的平衡
CameraX 的 ImageAnalysis 用例是连接相机取流和 ML Kit 的桥梁。官方推荐的做法是用 setAnalyzer 注册回调:
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(640, 480))
.build()
imageAnalysis.setAnalyzer(executor) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
val inputImage = InputImage.fromMediaImage(
mediaImage, imageProxy.imageInfo.rotationDegrees
)
objectDetector.process(inputImage)
.addOnCompleteListener {
imageProxy.close() // 必须手动关闭
}
}
}
三个关键决策:
背压策略选 STRATEGY_KEEP_ONLY_LATEST。当检测耗时超过帧间隔时(比如一帧处理耗时 150ms,但摄像头以 30fps 生产帧),CameraX 会直接丢弃旧帧,只保留最新一帧给分析器。这避免了帧积压导致的内存暴涨,代价是可能跳过某些帧——对实时检测场景是可接受的取舍。
分辨率设 640×480(VGA)。当时测试过 1080p 输入,检测准确率提升不到 2%,但推理延迟从 60ms 飙升到 180ms。对端侧模型来说,输入分辨率是性价比最高的调优杠杆。
imageProxy.close() 必须调用。漏了这行会导致 ImageProxy 不归还到共享缓冲区,30 帧左右 CameraX 就抛异常停止出帧。这个问题在日志里表现为 “ImageProxy is already closed” 或缓冲区耗尽,排查起来非常痛苦。
构建完整 Pipeline:检测 + 分类 + 反馈
工业场景光有检测不够,需要串联多个能力。以缺陷检测为例,我们的 Pipeline 是这样的:
CameraX 帧 → 目标检测(定位缺陷区域)→ 图像分类(判断缺陷类型)→ 结果聚合 → UI 反馈
核心代码结构:
class InspectionPipeline(private val context: Context) {
private val detector = ObjectDetection.getClient(...)
private val classifier = ImageClassification.getClient(...)
private var lastResults: InspectionResult? = null
fun process(imageProxy: ImageProxy) {
val inputImage = InputImage.fromMediaImage(
imageProxy.image!!, imageProxy.imageInfo.rotationDegrees
)
detector.process(inputImage).addOnSuccessListener { detections ->
if (detections.isEmpty()) {
lastResults = InspectionResult.Normal
imageProxy.close()
return@addOnSuccessListener
}
// 对每个检测到的区域做分类
val detection = detections.first()
val cropImage = cropToDetectionBox(inputImage, detection.boundingBox)
classifier.process(cropImage).addOnSuccessListener { labels ->
lastResults = when (labels.firstOrNull()?.text) {
"scratch" -> InspectionResult.Scratch(detection.boundingBox)
"dent" -> InspectionResult.Dent(detection.boundingBox)
else -> InspectionResult.Unknown
}
imageProxy.close()
}
}
}
}
Pipeline 设计上我做了两个取舍:
检测到目标后才触发分类,而非每帧都跑分类器。这减少了 70% 的分类器调用次数——大部分生产画面其实是正常的,不需要分类判断。
只取第一个检测结果做分类。需求上检测到任意缺陷就应该报警,不需要对多个目标逐一分类。如果场景需要多目标分析,这个简化不适用。
端侧推理的工程落地经验
踩过的三个坑:
第一个坑:模型加载时机。 检测器和分类器加起来约 15MB,如果在 onCreate 里同步加载,首帧延迟超过 2 秒。正确做法是异步预加载:
// 在 Application 或 Splash 阶段完成
lifecycleScope.launch(Dispatchers.IO) {
ObjectDetection.getClient(options) // 触发下载
}
ML Kit 的模型是懒加载的,首次调用 process() 时才真正初始化。提前调 getClient() 只是创建配置对象,需要先跑一次空推理(warm-up)来触发模型加载。我的做法是传一张 1×1 像素的空白帧进去,消耗约 200ms,换来了后续帧的零等待。
第二个坑:线程模型。 setAnalyzer 的回调在 CameraX 内部线程上执行,如果在这里做同步耗时操作,会阻塞出帧。用 addOnSuccessListener 的异步回调没有这个问题,但如果 Pipeline 逻辑很重,应该把结果处理放到独立的 HandlerThread:
val analysisThread = HandlerThread("ml-inference").apply { start() }
val analysisHandler = Handler(analysisThread.looper)
不过实际项目中我发现,ML Kit 本身的推理已经在内部线程池执行,额外再开线程往往收益不大。重点反而是控制并发帧数——用 AtomicBoolean 做单帧保护:
private val isProcessing = AtomicBoolean(false)
setAnalyzer { imageProxy ->
if (isProcessing.compareAndSet(false, true)) {
pipeline.process(imageProxy) {
isProcessing.set(false)
}
} else {
imageProxy.close() // 跳过,但要关掉
}
}
第三个坑:设备兼容性。 ML Kit 的基础模型依赖 Google Play Services,部分国产手机(尤其海外版)的 Play Services 版本过低会导致模型不可用。用 GoogleApiAvailability 做运行时检查,降级策略是引导用户更新或切换到 TFLite 自定义模型。
端侧智能的适用边界
做完这个项目后我对”端侧该做什么”有了更清晰的认识。
ML Kit 擅长的领域:规则明确的视觉任务(检测、分类、OCR)、对延迟敏感的场景(<50ms 推理)、离线环境。不适合的领域:需要语义理解的任务(交给 LLM)、高精度需求(考虑云端大模型)、模型频繁更新的场景。
当时技术选型讨论中,产品经理想直接用 GPT-4V 的 API 做缺陷分析。我坚持端侧方案,理由很简单:工厂网络不稳定、延迟要求 <200ms、数据不能出园区。这三个约束直接把云端方案排除掉了。
如果场景也有类似约束,ML Kit + CameraX 的组合值得放进工具箱。它不是最”前沿”的方案,但在端侧实时视觉分析这件事上,它的成熟度目前还没有替代品。