3、图像采集与预处理:摄像头驱动(V4L2)、图像格式转换(RGB/BGR/YUV)、缩放与归一化

好,咱们进入实战环节的第一个硬骨头——怎么把摄像头里的画面,变成神经网络能“吃”的数据。

很多人一上来就调模型,结果发现摄像头采集的画面全是花的,或者颜色不对,或者分辨率不匹配。嗯,这些坑我都踩过。今天咱们就把这层窗户纸捅破。

3.1 摄像头驱动:V4L2 到底在干什么?

V4L2,全称 Video for Linux 2。说白了,它就是 Linux 系统下操作摄像头的标准接口。你想想看,市面上的摄像头五花八门,有 USB 的、有 CSI 接口的、有 MIPI 的。如果没有一个统一的标准,咱们写驱动得累死。

V4L2 的作用,就是给上层应用提供一个统一的 API。你调用 open()ioctl()mmap()read() 这些函数,底层硬件怎么折腾,你不用管。

我个人习惯把 V4L2 的采集流程总结成四步:

  1. 打开设备open("/dev/video0", O_RDWR)。注意,有些嵌入式设备上 video0 可能是 ISP 处理后的,video1 才是原始 sensor。我遇到过好几次搞错节点,画面全黑的情况。
  2. 设置格式:通过 VIDIOC_S_FMT 设置像素格式(比如 V4L2_PIX_FMT_YUYV)、分辨率(比如 640x480)。
  3. 申请缓冲区:用 VIDIOC_REQBUFS 申请几块内存。我一般申请 3 到 4 块,形成环形缓冲区。这样 CPU 在处理一帧时,摄像头可以往另一块缓冲区写数据,不冲突。
  4. 启动采集VIDIOC_STREAMON,然后循环 VIDIOC_DQBUF(取出)和 VIDIOC_QBUF(放回)。

核心要点:V4L2 的缓冲区管理是性能关键。千万别用 read() 一帧一帧读,那效率太低了。一定要用 mmapDMABUF 方式,零拷贝才是嵌入式该有的样子。

3.2 图像格式转换:RGB、BGR、YUV 的“爱恨情仇”

摄像头出来的原始数据,绝大多数是 YUV 格式。为什么?因为人眼对亮度(Y)敏感,对色度(UV)不敏感。YUV 可以压缩色度信息,节省带宽。

但咱们的深度学习模型,通常吃的是 RGB 或 BGR。这就涉及到一个绕不开的环节——格式转换。

我曾经在一个项目里,直接拿 YUV 数据喂给模型,结果模型推理准确率直接掉了一半。后来才发现,模型训练时用的是 BGR,而摄像头出来的是 YUYV,中间没做转换。颜色通道全乱了。

3.2.1 YUV 到 RGB 的转换公式

这个公式其实不复杂,但你要注意系数。不同的标准(BT.601、BT.709)系数不一样。咱们嵌入式里常用 BT.601:

R = Y + 1.402 * (V - 128)
G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
B = Y + 1.772 * (U - 128)

嗯,这里要注意,浮点运算在嵌入式设备上很慢。我一般会转成定点运算,或者直接用查表法。比如把 1.402 * 256 ≈ 359,然后右移 8 位。

3.2.2 常见的 YUV 布局

格式 说明 常见场景
YUYV Y 和 UV 交错排列,每 2 个像素共享一组 UV USB 摄像头最常见
NV12 先存所有 Y,再交错存 UV 海思、瑞芯微等 SoC 的硬件编码器
NV21 先存所有 Y,再交错存 VU(顺序相反) Android 相机默认输出

我的小技巧:如果 SoC 有硬件 ISP,尽量用硬件做 YUV 到 RGB 的转换。比如瑞芯微的 RGA 模块,或者海思的 VGS 模块。硬件转换一帧 1080p 只需要几毫秒,软件转换可能要几十毫秒。

3.3 缩放与归一化:让模型“吃”得舒服

模型训练时用的图片大小是固定的,比如 224x224、320x320。但摄像头采集的分辨率可能是 1920x1080。这就必须做缩放。

另外,模型输入层的数值范围通常是 [0, 1] 或 [-1, 1],而原始像素值是 [0, 255]。所以还要做归一化。

3.3.1 缩放算法怎么选?

嵌入式设备上,常用的缩放算法有三种:

  • 最近邻插值:最快,但锯齿严重。适合做预览,不适合做模型输入。
  • 双线性插值:速度和质量的平衡点。我大部分项目都用这个。
  • 双三次插值:质量最好,但慢。除非你的芯片有硬件加速,否则别用。

我曾经在一个低功耗项目里,为了省电,用了最近邻插值。结果模型推理时,人脸边缘全是锯齿,关键点检测直接飘了。后来换成双线性,问题解决。

3.3.2 归一化的正确姿势

归一化公式很简单:

normalized_value = (pixel_value - mean) / std

但这里有个坑——mean 和 std 的顺序。模型训练时用的 mean 是 [123.675, 116.28, 103.53](BGR 顺序),你归一化时也得按 BGR 顺序来。如果你用的是 RGB 顺序,结果就全乱了。

我建议你把归一化和格式转换放在一起做。比如从 YUV 转到 BGR 时,直接做归一化,省掉一次内存拷贝。

注意:很多嵌入式芯片的 NPU(比如瑞芯微的 RKNN、算丰的 BMNNSDK)支持在硬件推理时自动做归一化。你只需要在模型转换时配置好 mean 和 std 参数。如果硬件支持,就别在 CPU 上做归一化,浪费算力。

3.4 实战建议:一条完整的预处理流水线

说了这么多,咱们串起来看看。一个典型的人脸检测预处理流水线是这样的:

  1. V4L2 采集一帧 YUYV 数据(比如 640x480)。
  2. 用硬件 ISP 或软件将 YUYV 转为 BGR(640x480)。
  3. 用双线性插值缩放到模型输入尺寸(比如 320x320)。
  4. 按模型要求的 mean/std 做归一化。
  5. 送入 NPU 或 CPU 做推理。

你想想看,如果每一步都做一次内存拷贝,那延迟就上去了。我一般会这样做:

  • V4L2 的缓冲区直接 mmap 到用户空间。
  • 用 NEON 指令集做 YUV 转 BGR 和归一化,一次循环搞定。
  • 缩放用硬件 RGA 模块,不占用 CPU。

这样整条流水线下来,从采集到推理,延迟可以控制在 30ms 以内。嗯,这才是嵌入式该有的样子。

总结一句话:V4L2 是敲门砖,格式转换是必经路,缩放归一化是最后一道关。每一步都有坑,但只要你理解了原理,踩过一次就记住了。

下一章,咱们聊聊怎么用 OpenCV 快速验证这些流程。但记住,OpenCV 只是工具,底层原理才是你吃饭的本事。