1. 项目概述:当Compose遇上YOLO
在移动端实现实时图像识别一直是Android开发者的热门挑战。传统方案要么依赖云端API(延迟高、隐私差),要么使用笨重的原生视图(难以维护)。这次我们尝试用Jetpack Compose构建UI层,配合TensorFlow Lite部署YOLOv5模型,打造一个完全本地的轻量级识别方案。
实测在Pixel 4上能达到23FPS的识别速度,且APK体积仅增加8.7MB。这种组合特别适合需要快速响应和隐私保护的场景,比如:
- 工业质检中的缺陷实时检测
- 零售行业的智能货架管理
- 家庭安防监控的入侵识别
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与核心组件
2.1 为什么选择YOLOv5s-TFLite
经过对比测试多个模型后,最终选择YOLOv5s的TFLite版本主要基于:
- 模型尺寸:原始PyTorch模型86MB → 量化后TFLite仅23MB
- 推理速度:在移动CPU上比MobileNetSSD快3倍(测试数据见下表)
- 精度平衡:COCO数据集mAP@0.5达到56.8%,满足大部分场景
| 模型类型 | 输入尺寸 | AP@0.5 | 推理耗时(ms) |
|---|---|---|---|
| YOLOv5s-FP32 | 640x640 | 58.2% | 142 |
| YOLOv5s-INT8 | 640x640 | 56.8% | 89 |
| MobileNetSSD | 300x300 | 45.1% | 273 |
注意:量化会导致约1-2%的精度损失,但通过校准数据集可以优化
2.2 Compose的相机交互方案
传统Camera2 API与Compose的整合需要特殊处理:
kotlin复制@Composable
fun CameraPreview(
analyzer: ImageAnalysis.Analyzer,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
factory = { context ->
PreviewView(context).apply {
this.scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
},
update = { view ->
val cameraProviderFuture = ProcessCameraProvider.getInstance(view.context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(view.surfaceProvider)
}
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 640))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(ContextCompat.getMainExecutor(view.context), analyzer) }
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
}, ContextCompat.getMainExecutor(view.context))
},
modifier = modifier
)
}
关键配置点:
- 使用
AndroidView桥接传统视图 - 设置
STRATEGY_KEEP_ONLY_LATEST避免帧堆积 - 通过
TargetResolution控制输入尺寸
3. 模型集成实战
3.1 模型转换与优化
从PyTorch到TFLite的完整转换路径:
bash复制python export.py --weights yolov5s.pt --include tflite --int8 --data coco.yaml
需要特别注意:
- 校准数据集应包含典型场景样本(至少200张)
- Android端输入需要NHWC格式(PyTorch默认NCHW)
- 添加
--iou-thres 0.5参数保持与训练时一致
3.2 安卓端推理加速技巧
通过Benchmark测试发现三个优化点:
- 线程配置(效果提升40%)
kotlin复制val options = Interpreter.Options().apply {
numThreads = 4 // 大核线程数
setUseXNNPACK(true) // 启用ARM优化
}
- 内存复用(减少GC卡顿)
kotlin复制val inputBuffer = ByteBuffer.allocateDirect(640*640*3)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
fun detect(bitmap: Bitmap) {
inputBuffer.rewind()
convertBitmapToBuffer(bitmap, inputBuffer)
interpreter.run(inputBuffer, outputBuffer)
}
- 预热机制(首帧加速)
kotlin复制fun warmUp() {
val dummyInput = FloatArray(640*640*3) { 0.5f }
interpreter.run(dummyInput, dummyOutput)
}
4. 性能调优实录
4.1 帧率提升的五个关键点
在Galaxy S21上的优化过程:
-
分辨率分级处理:
- 预览流:1920x1080
- 分析流:640x640(通过
setTargetResolution控制)
-
动态跳帧策略:
kotlin复制var lastProcessTime = 0L
val analyzer = ImageAnalysis.Analyzer { image ->
val current = System.currentTimeMillis()
if (current - lastProcessTime > 50) { // 控制20FPS
processImage(image)
lastProcessTime = current
}
image.close()
}
- 结果缓存复用:
kotlin复制val lastResults = mutableStateOf(emptyList<Detection>())
LaunchedEffect(Unit) {
snapshotFlow { lastResults.value }
.distinctUntilChanged()
.collect { results ->
// 更新UI
}
}
4.2 内存泄漏排查案例
典型问题:CameraX的ImageProxy未关闭
kotlin复制// 错误示例
analyzer = ImageAnalysis.Analyzer { image ->
processImage(image) // 忘记close!
}
// 正确做法
analyzer = ImageAnalysis.Analyzer { image ->
try {
processImage(image)
} finally {
image.close()
}
}
通过Android Profiler发现的三个内存热点:
- 未复用的Bitmap对象
- 累积的ImageProxy实例
- 过大的中间Float数组
5. 效果展示与业务适配
5.1 自定义绘制方案
在Compose中实现检测框绘制:
kotlin复制@Composable
fun DetectionOverlay(
detections: List<Detection>,
modifier: Modifier = Modifier
) {
Canvas(modifier.fillMaxSize()) {
detections.forEach { detection ->
val rect = detection.boundingBox.toRect(size)
drawRect(
color = Color.Red,
topLeft = rect.topLeft,
size = rect.size,
style = Stroke(width = 2.dp.toPx())
)
drawText(
text = "${detection.label} ${"%.2f".format(detection.score)}",
topLeft = rect.topLeft + Offset(0f, -20f),
color = Color.White,
fontSize = 14.sp.toPx()
)
}
}
}
5.2 业务逻辑扩展示例
零售货架检测场景的特殊处理:
kotlin复制fun filterShelfItems(detections: List<Detection>): List<ShelfItem> {
return detections
.filter { it.label in setOf("bottle", "can", "box") }
.groupBy { it.boundingBox.center.y.toInt() / 50 } // 按垂直位置分组
.flatMap { (row, items) ->
items.sortedBy { it.boundingBox.center.x }
.mapIndexed { col, item ->
ShelfItem(row, col, item.label)
}
}
}
6. 常见问题解决方案
6.1 模型加载失败排查
现象:java.lang.IllegalArgumentException: Cannot convert to TensorFlowLite tensor
可能原因及修复:
- 输入维度不匹配 → 检查
inputShape是否为[1,640,640,3] - 量化模型使用浮点输入 → 改用
ByteBuffer输入 - 文件损坏 → 验证模型MD5值
6.2 画面卡顿优化
诊断步骤:
- 使用Android GPU Inspector查看渲染耗时
- 检查是否有主线程阻塞操作
- 分析
Systrace报告中的锁竞争
典型修复方案:
kotlin复制// 将图像转换移至IO线程
fun processImage(image: ImageProxy) = lifecycleScope.launch(Dispatchers.IO) {
val bitmap = image.toBitmap()
val rotated = bitmap.rotate(image.imageInfo.rotationDegrees.toFloat())
detector.detect(rotated)
image.close()
}
7. 进阶扩展方向
对于需要更高精度的场景,可以考虑:
- 动态模型切换:根据场景加载不同精度的模型
kotlin复制val modelType = when(networkType) {
NetworkType.WIFI -> "yolov5m-int8"
NetworkType.ETHERNET -> "yolov5l-fp16"
else -> "yolov5s-int8"
}
-
多模型级联:
- 先用轻量模型检测区域
- 再对ROI进行高精度识别
-
边缘计算协同:
kotlin复制val useCloud = detectionConfidence < 0.3f
if (useCloud) {
uploadRegionOfInterest(bitmap.crop(detection.boundingBox))
}
