4. 图像分析用例集成

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 实现流程

  1. 配置 ImageAnalysis 用例:在 ProcessCameraProvider 中创建 ImageAnalysis 实例,设置背压策略(STRATEGY_KEEP_ONLY_LATEST 或 STRATEGY_BLOCK_PRODUCER)和输出图像格式(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888 或 YUV_420_888)。
  2. 实现 Analyzer:在 analyze 方法中提取 imageProxy.image,获取 YUV 数据,进行转换(如 NV21 或自定义渲染),生成分析结果(如 bitmap、特征点列表)。
  3. 通过协程安全传递到 Compose 状态:使用 viewModel 或 mutableStateOf 在后台线程更新 StateFlow,Compose 可感知变化并重组。
  4. 在 Composable 中显示:使用 Canvas 或 Image 组件绘制分析结果(叠加层)。
  5. 释放资源:分析完成后务必调用 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。

内联 SVG:图像分析数据流与转换架构

ImageAnalysis 分析数据流 CameraX 传感器帧 ImageAnalysis 用例 Analyzer.analyze() ImageProxy (YUV) YUV→RGB/Bitmap 转换 Compose Canvas 叠加 关键瓶颈:YUV 转换应在后台线程完成,并通过协程传递到主线程更新 Compose 状态 建议使用 ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST 避免帧堆积 图示:从传感器到 Compose 显示的完整数据流