4. 图像分析用例集成:ImageAnalysis.Analyzer接口、YUV_420_888格式转换、Compose中实时分析输出
4.1 原理概述
CameraX 的 ImageAnalysis 用例提供逐帧访问相机传感器的原始图像数据。其核心是 ImageAnalysis.Analyzer 接口,开发者需实现 analyze(imageProxy: ImageProxy) 方法。每一帧到来时,CameraX 通过分析器回调返回一个 ImageProxy 对象,其中封装了 Image(通常为 YUV_420_888 格式)和相关的元数据。
在 Android 平台上,YUV_420_888 是相机输出的标准图像格式,包含三个平面:Y (亮度)、U (Cb)、V (Cr)。为了在 Compose 中实时显示分析结果(例如人脸框、物体检测边框、颜色分析热力图),通常需要将 YUV 数据转换为 RGB 或 Bitmap,再通过 ImageBitmap 绘制到 Canvas 上。
关键挑战:
- 帧率匹配:分析器回调频率可能与 UI 刷新频率冲突,需要合理控制分析速率。
- YUV 转换效率:软件转换在低端设备上可能成为性能瓶颈。
- Compose 与生命周期:分析器通常运行在 CameraX 的后台线程,而 Compose 重组必须在主线程更新状态。
4.2 实现流程
- 配置 ImageAnalysis 用例:在
ProcessCameraProvider中创建ImageAnalysis实例,设置背压策略(STRATEGY_KEEP_ONLY_LATEST或STRATEGY_BLOCK_PRODUCER)和输出图像格式(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888或YUV_420_888)。 - 实现 Analyzer:在
analyze方法中提取imageProxy.image,获取 YUV 数据,进行转换(如 NV21 或自定义渲染),生成分析结果(如 bitmap、特征点列表)。 - 通过协程安全传递到 Compose 状态:使用
viewModel或mutableStateOf在后台线程更新 StateFlow,Compose 可感知变化并重组。 - 在 Composable 中显示:使用
Canvas或Image组件绘制分析结果(叠加层)。 - 释放资源:分析完成后务必调用
imageProxy.close(),否则会阻塞新帧。
关键提示: 推荐使用
ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST 避免分析积压,配合 coil 或 skiko 的 GPU 转换可大幅降低 CPU 负载。
4.3 关键代码与配置
4.3.1 创建 ImageAnalysis 用例
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
.build()
// 在 CameraX 绑定中
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
4.3.2 实现 Analyzer(YUV 转 Bitmap + 分析示例)
class YuvAnalyzer(
private val onResult: (Bitmap) -> Unit
) : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
val image = imageProxy.image ?: return
val bitmap = imageProxyToBitmap(image) // 自定义转换方法
if (bitmap != null) {
onResult(bitmap)
}
imageProxy.close()
}
private fun imageProxyToBitmap(image: Image): Bitmap? {
if (image.format != ImageFormat.YUV_420_888) return null
val planes = image.planes
val yPlane = planes[0]
val uPlane = planes[1]
val vPlane = planes[2]
val ySize = yPlane.rowStride * image.height
val uvSize = uPlane.rowStride * image.height / 2 // 近似值
val yuvData = ByteArray(ySize + uvSize * 2)
// 拷贝 Y 平面
yPlane.buffer.get(yuvData, 0, ySize)
// 拷贝 U 和 V 平面(需要处理行间距)
uPlane.buffer.get(yuvData, ySize, uPlane.buffer.remaining())
vPlane.buffer.get(yuvData, ySize + uPlane.buffer.remaining(), vPlane.buffer.remaining())
// 使用 RenderScript 或 libyuv 加速转换,此处简化使用 YuvImage
val yuvImage = YuvImage(yuvData, ImageFormat.NV21, image.width, image.height, null)
val outputStream = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 80, outputStream)
val jpegBytes = outputStream.toByteArray()
return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
}
}
4.3.3 Compose 中实时更新分析结果
@Composable
fun LiveAnalyzeOverlay(viewModel: AnalyzeViewModel) {
val currentBitmap by viewModel.analysisResult.collectAsState()
Box(modifier = Modifier.fillMaxSize()) {
// 背景预览由 CameraX Preview 提供(另一层)
// 分析结果叠加
currentBitmap?.let { bitmap ->
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit
)
}
}
}
// ViewModel 中
class AnalyzeViewModel : ViewModel() {
private val _analysisResult = MutableStateFlow<Bitmap?>(null)
val analysisResult: StateFlow<Bitmap?> = _analysisResult.asStateFlow()
fun onFrameAnalyzed(bitmap: Bitmap) {
viewModelScope.launch(Dispatchers.Main) {
_analysisResult.value = bitmap
}
}
}
4.4 调试与常见错误
| 常见问题 | 现象 | 解决方案 |
|---|---|---|
未调用 imageProxy.close() | 分析器只回调一次,后续帧被阻塞 | 在 analyze 方法末尾始终调用 close() |
| YUV 到 Bitmap 转换耗时过长 | UI 卡顿或分析帧率远低于相机帧率 | 使用 RenderScript 或 libyuv 原生库;减小分析分辨率(使用 setTargetResolution) |
| Compose 状态更新主线程冲突 | 重组异常或状态未更新 | 在 viewModelScope.launch(Dispatchers.Main) 中更新 mutableStateOf,不要在分析器线程直接赋值 |
| Bitmap 尺寸与预览不对齐 | 叠加图层偏移或拉伸 | 使用 Modifier.aspectRatio 或 Layout 对齐,确保分析后的 Bitmap 宽高比与相机预览一致 |
4.5 性能与稳定性优化
- 降低分析分辨率:通过
setTargetResolution(Size(640, 480))减少每帧数据量,YUV→Bitmap 速度可提升 2~3 倍。 - 使用 GPU 加速转换:利用 CameraX 内置的
ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888直接获取 RGBA 格式(不经过 YUV 转换),但需确认设备支持,兼容性略低。 - 限定分析帧率:在
analyze方法中通过时间戳控制,例如每 100ms 处理一帧,降低 CPU 占用。 - 复用 Bitmap:避免在每帧创建新 Bitmap 对象,使用
BitmapPool或reuse机制减少 GC 压力。 - 使用原生代码(NDK):对于复杂分析(如人脸识别、文字检测),将 YUV 数据直接传递给 C/C++ 库,避免 Java 层二次拷贝。
性能警示:在低端设备上(如 Android Go 或 API 26 以下),软件 YUV→JPEG→Bitmap 的路径会严重拖慢分析速度。建议优先使用
RenderScript 的 ScriptIntrinsicYuvToRGB(API 17+)或集成 libyuv。