27. Camera与机器学习:Camera帧数据预处理、TensorFlow Lite集成、实时目标检测
各位同学,欢迎来到第27章。说实话,走到这一步,咱们的摄像头采集已经玩得挺溜了。但采集只是手段,用数据干点实事才是目的。今天我们就来聊聊,怎么把Camera采集到的每一帧数据,喂给机器学习模型,让它帮我们“看懂”这个世界。
说白了,就是让手机摄像头变成一双“会思考的眼睛”。我最早接触这个方向,是在做一个智能安防项目。那时候TensorFlow Lite还没这么成熟,踩了不少坑。今天我把这些经验都揉碎了讲给你听。
27.1 帧数据预处理:从YUV到模型能吃的“菜”
Camera回调给我们的数据,通常是YUV_420_888格式。但大多数机器学习模型,尤其是图像分类和目标检测模型,吃的是RGB格式的Bitmap或者ByteBuffer。所以,第一步就是做格式转换。
为什么会这样?因为YUV是摄像头硬件直接输出的格式,压缩率高,适合传输。但模型训练时用的都是RGB,所以必须转。
核心流程:
- 从ImageReader的onImageAvailable回调中拿到Image对象
- 提取Y、U、V三个平面的数据
- 将YUV数据转换为RGB或NV21格式
- 缩放、裁剪到模型要求的输入尺寸(比如224x224或320x320)
- 归一化:将像素值从[0,255]映射到[-1,1]或[0,1]
我个人习惯用RenderScript来做YUV转RGB,性能比纯Java快很多。不过要注意,RenderScript在Android 12以后被标记为废弃,建议改用CameraX的ImageAnalysis配合OpenGL或者直接使用libyuv的JNI封装。
// 一个简单的YUV转RGB核心逻辑(伪代码)
public Bitmap yuv420ToRgb(Image image, int width, int height) {
// 拿到三个平面
ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
ByteBuffer vBuffer = image.getPlanes()[2].getBuffer();
// 这里省略了具体的像素排列计算
// 实际项目中建议用RenderScript或libyuv
// 我当年手写了一个转换函数,结果发现性能瓶颈在内存拷贝上
// 后来改用Native层处理,帧率直接翻倍
return convertToRgbBitmap(yBuffer, uBuffer, vBuffer, width, height);
}
小技巧:如果你用CameraX,可以直接用ImageAnalysis的setAnalyzer方法,它会给你一个ImageProxy对象。配合TensorFlow Lite的ImageProcessor,可以省去不少样板代码。
27.2 TensorFlow Lite集成:把模型塞进App
TensorFlow Lite是Google为移动端和嵌入式设备量身打造的轻量级推理引擎。说白了,就是把训练好的模型压缩、量化,然后塞进手机里跑。
集成步骤其实不复杂,但有几个坑我必须提前告诉你。
27.2.1 添加依赖
在build.gradle里加上TFLite的依赖。注意,如果你要用GPU加速,还需要额外加一个委托库。
dependencies {
// 核心库
implementation 'org.tensorflow:tensorflow-lite:2.14.0'
// GPU加速委托(可选)
implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
// 支持XNNPACK加速
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
}
注意:我曾经在一个项目里直接用了最新版的TFLite,结果发现和某个旧版NDK不兼容,编译报错。建议先确认你的AGP版本和NDK版本是否匹配。如果遇到so库加载失败,多半是abiFilters没配置对。
27.2.2 加载模型并创建解释器
模型文件通常放在assets目录下。加载时要注意,模型文件不要太大,否则首次加载会卡顿。我建议在应用启动时异步加载,或者在后台线程里初始化。
// 加载模型
Interpreter tflite = new Interpreter(loadModelFile(context));
// 设置输入输出格式
int[] inputShape = {1, 224, 224, 3}; // batch, height, width, channels
int[] outputShape = {1, 10}; // 假设有10个类别
// 分配内存
ByteBuffer inputBuffer = ByteBuffer.allocateDirect(4 * inputShape[0] * inputShape[1] * inputShape[2] * inputShape[3]);
ByteBuffer outputBuffer = ByteBuffer.allocateDirect(4 * outputShape[0] * outputShape[1]);
// 运行推理
tflite.run(inputBuffer, outputBuffer);
嗯,这里要注意,inputBuffer的字节序一定要设置成跟模型一致。我遇到过有人用默认的BIG_ENDIAN,结果模型输出全是NaN,排查了半天才发现是字节序问题。
27.3 实时目标检测:让摄像头“看见”物体
实时目标检测,说白了就是每一帧都跑一次模型,然后画出检测框。听起来简单,但要做到流畅运行,需要做不少优化。
我建议使用MobileNet SSD或YOLOv5-Tiny这类轻量级模型。它们能在手机上达到30fps以上的推理速度。
27.3.1 检测流程
- 从Camera拿到一帧数据
- 预处理:缩放、归一化、转RGB
- 喂给TFLite模型,执行推理
- 解析输出:拿到检测框坐标、置信度、类别ID
- 用NMS(非极大值抑制)过滤掉重叠的框
- 在预览画面上绘制检测结果
性能关键点:
- 尽量复用ByteBuffer和Bitmap对象,避免频繁GC
- 使用GPU委托加速推理(如果设备支持)
- 降低输入分辨率:320x320比640x640快4倍
- 跳帧处理:如果模型跑不过来,可以每2帧或3帧检测一次
// 解析SSD模型输出的示例
private List<Recognition> decodeOutput(float[][][] outputLocations, float[][] outputClasses, float threshold) {
List<Recognition> detections = new ArrayList<>();
for (int i = 0; i < outputClasses[0].length; i++) {
float confidence = outputClasses[0][i];
if (confidence > threshold) {
// 解析边界框坐标
float top = outputLocations[0][i][0];
float left = outputLocations[0][i][1];
float bottom = outputLocations[0][i][2];
float right = outputLocations[0][i][3];
// 这里要映射回原始图像尺寸
detections.add(new Recognition(top, left, bottom, right, confidence));
}
}
return detections;
}
27.4 知识体系总览
下面这张图,是我梳理的本章核心知识脉络。你可以把它当作一个快速索引。
27.5 避坑指南与性能调优
这部分是我最想跟你分享的。因为理论大家都懂,但实际跑起来,问题一个接一个。
我曾经踩过的坑:
- 内存泄漏:Image对象用完没有及时close,导致Camera预览卡死。记得在onImageAvailable回调里,处理完数据后立即调用image.close()。
- 模型加载慢:第一次加载模型时,如果模型文件超过10MB,主线程会卡顿。建议用异步任务加载,或者使用MMAP方式加载。
- GPU委托兼容性:不是所有手机都支持GPU加速。我建议先检测设备是否支持,如果不支持就回退到CPU。
- 帧率与精度平衡:输入分辨率越高,精度越好,但帧率越低。我一般推荐320x320作为起点,如果设备性能好再往上调。
我的个人建议:
- 使用TensorFlow Lite的Support库,它提供了ImageProcessor和TensorProcessor,可以帮你自动处理归一化和数据类型转换。
- 如果追求极致性能,可以考虑用NNAPI委托。它利用手机的NPU或DSP进行推理,功耗更低。
- 调试时,先把模型在PC上用Python跑一遍,确认输出正确,再移植到Android端。这样可以排除模型本身的问题。
27.6 实战要点总结
好了,我们来捋一捋今天讲的核心内容。你想想看,其实整个流程就是一个“采集-预处理-推理-后处理”的流水线。每一步都有优化空间,但最关键的还是预处理和模型选择。
| 环节 | 关键点 | 常见问题 |
|---|---|---|
| 帧采集 | 使用ImageReader,设置合适的帧率 | 帧率过高导致CPU过载 |
| 预处理 | YUV转RGB,缩放,归一化 | 格式转换慢,内存拷贝频繁 |
| 模型推理 | 加载TFLite模型,设置输入输出 | 模型加载慢,GPU兼容性差 |
| 后处理 | 解析输出,NMS过滤,绘制 | 检测框抖动,置信度阈值设置不当 |
嗯,今天就到这里。说实话,实时目标检测这块内容,光看文档是不够的。我建议你动手写一个Demo,从CameraX的ImageAnalysis开始,一步步把模型跑起来。遇到问题很正常,关键是学会怎么排查和优化。
记住,机器学习的落地,七分在数据预处理,三分在模型本身。把帧数据处理好了,你的应用就成功了一大半。