4、视频帧采集与预处理:BGR/YUV/RGB格式转换、图像缩放(resize)、ROI裁剪、镜像翻转
各位同学,欢迎来到第四讲。上一章我们聊了摄像头驱动和V4L2采集,拿到了原始帧数据。但说实话,那些数据直接扔给编码器?不行,根本不行。为什么?因为摄像头出来的数据格式五花八门,分辨率也未必是你想要的。今天我们就来解决这个问题——视频帧的预处理。
我个人习惯把预处理叫做「给编码器喂饭」。你想想看,编码器是个挑食的家伙,你得把食材(原始帧)切好、调味、摆盘,它才愿意好好干活。这一章我们就把这四道工序讲透:格式转换、缩放、裁剪、镜像翻转。
4.1 色彩空间转换:BGR、YUV、RGB 到底怎么选?
先说个我踩过的坑。几年前做一款USB摄像头产品,采集到的数据明明是BGR格式,我直接当RGB送进了编码器。结果画面颜色全偏了,人脸跟阿凡达似的。排查了半天,才发现是色彩通道顺序搞反了。
所以,理解色彩空间转换,是预处理的第一步。
4.1.1 为什么会有这么多格式?
说白了,不同硬件和算法对色彩敏感度不一样。
- RGB/BGR:显示器、OpenCV 喜欢用。每个像素用三个通道表示红绿蓝。
- YUV:视频编码器的最爱。它把亮度(Y)和色度(U、V)分开。人眼对亮度敏感,对色度不敏感,所以色度可以压缩采样,节省带宽。
我建议你记住一个原则:采集用啥格式,编码用啥格式,中间尽量少转。每转一次,就有精度损失。
4.1.2 常用转换公式与代码
OpenCV 里默认是 BGR 顺序。如果你从摄像头读到的是 BGR,要送给编码器(通常要 YUV),就得转。代码很简单:
// C++ 示例:BGR 转 YUV420(NV12)
cv::Mat bgr_frame; // 假设这是采集到的帧
cv::Mat yuv_frame;
cv::cvtColor(bgr_frame, yuv_frame, cv::COLOR_BGR2YUV_YV12);
嗯,这里要注意:COLOR_BGR2YUV_YV12 和 COLOR_BGR2YUV_NV12 是不同的。YV12 是 planar 格式,NV12 是 semi-planar。编码器通常要 NV12,你最好查一下编码器的文档。
4.1.3 查表法加速
如果你在嵌入式设备上做实时转换,CPU 资源很宝贵。cvtColor 虽然方便,但性能一般。我建议用查表法(Look-Up Table, LUT)来加速。
原理很简单:把 RGB 到 YUV 的转换系数预先算好,存成表格。转换时直接查表,省去浮点运算。代码大概长这样:
// 伪代码:查表法 BGR 转 Y
uint8_t lut_y[256][256][256]; // 这太大了,实际用分通道查表
// 实际做法:分别对 R、G、B 通道查表
uint8_t y = lut_y_r[r] + lut_y_g[g] + lut_y_b[b];
当然,256x256x256 的表太夸张了。实际项目中,我们通常把 RGB 每个通道的系数拆开,用三个一维表加起来。这样内存占用小,速度也快。
4.2 图像缩放(Resize):别小看这个操作
摄像头采集的分辨率,比如 1920x1080,编码器可能只支持 1280x720。或者你想降低码率,缩小分辨率。这时候就要 resize。
但 resize 不是简单地把像素扔掉。不同的插值算法,效果和性能天差地别。
4.2.1 插值算法怎么选?
| 算法 | 质量 | 速度 | 适用场景 |
|---|---|---|---|
| 最近邻插值 | 低(有锯齿) | 极快 | 实时预览、低功耗设备 |
| 双线性插值 | 中等 | 快 | 大多数场景,平衡之选 |
| 双三次插值 | 高 | 慢 | 离线处理、高质量需求 |
| Lanczos | 极高 | 极慢 | 专业图像处理 |
我个人习惯:实时视频流用双线性。质量够用,速度也跟得上。如果你用 ARM 的 NEON 指令集优化,双线性缩放可以做到几乎不占 CPU。
4.2.2 OpenCV 的 resize 用法
cv::Mat src, dst;
cv::resize(src, dst, cv::Size(1280, 720), 0, 0, cv::INTER_LINEAR);
这里有个细节:fx 和 fy 参数可以传 0,表示由目标尺寸自动计算缩放比例。如果你要固定宽高比,记得手动算一下。
4.3 ROI 裁剪:只处理你关心的区域
有时候,整个画面太大了,我只想处理其中一小块。比如人脸识别,我只关心人脸区域。这时候就用 ROI(Region of Interest)。
ROI 裁剪的好处很明显:
- 减少计算量:只处理小区域,速度更快。
- 降低码率:编码器只编码 ROI 区域,背景不变,码率自然低。
- 提高精度:算法只关注目标区域,减少背景干扰。
4.3.1 如何实现 ROI 裁剪?
OpenCV 里很简单,直接用矩形区域索引:
cv::Rect roi(100, 100, 200, 200); // x, y, width, height
cv::Mat cropped = src(roi);
注意:cropped 和 src 共享内存。如果你修改 cropped,src 也会变。要独立副本的话,用 .clone()。
if (roi.x + roi.width <= src.cols && roi.y + roi.height <= src.rows)。
4.3.2 动态 ROI 的坑
如果你做目标跟踪,ROI 是动态变化的。这时候要注意:ROI 的边界不能太靠近图像边缘,否则目标一移动,ROI 就出界了。我建议给 ROI 留一些「安全边距」。
4.4 镜像翻转:解决摄像头安装方向问题
这个操作看起来简单,但实际项目中经常被忽略。你想想看,摄像头装在设备上,可能倒着、侧着、甚至旋转了 180 度。如果不做翻转,画面就是颠倒的。
镜像翻转有三种:
- 水平翻转:左右镜像,像照镜子。
- 垂直翻转:上下颠倒。
- 旋转 180 度:水平+垂直同时翻转。
4.4.1 OpenCV 实现
cv::flip(src, dst, 0); // 0: 垂直翻转
cv::flip(src, dst, 1); // 1: 水平翻转
cv::flip(src, dst, -1); // -1: 同时翻转(旋转180度)
代码就一行,没什么难度。但我要说的是:翻转操作一定要放在预处理的最前面。为什么?因为翻转之后,图像的坐标系统变了。如果你先裁剪再翻转,裁剪的区域就错了。
4.5 预处理流水线的整体设计
好了,四个操作都讲完了。实际项目中,它们不是独立执行的,而是串成一个流水线。我建议的顺序是:
- 镜像翻转(如果需要)
- ROI 裁剪(先确定要处理的区域)
- 图像缩放(缩到目标分辨率)
- 色彩空间转换(转成编码器需要的格式)
为什么这个顺序?你想想看:先翻转,坐标系统就统一了;再裁剪,只保留感兴趣区域;然后缩放,减少后续处理的数据量;最后转格式,直接喂给编码器。每一步都减少了后续步骤的计算量。
当然,如果你用硬件加速(比如 ISP、GPU),顺序可能会变。但软件层面,这个顺序是最合理的。
好了,这一章的内容就到这里。下一章我们会讲编码器的配置和码率控制,那才是真正的硬核内容。各位先把预处理练熟,后面才能跟上节奏。
公众号:蓝海资料掘金营,微信deep3321