第四章:图像采集与预处理
4.1 V4L2框架:让摄像头开口说话
做仿生眼的第一步是什么?
你得先把图像数据拿到手。说白了,摄像头就是仿生眼的视网膜,而V4L2(Video for Linux 2)就是Linux下让摄像头开口说话的通用接口。我个人习惯把V4L2理解成一套标准协议——不管你是USB摄像头、MIPI接口的传感器,还是工业相机,只要它支持V4L2,上层应用就能用同一套API去操作。
我在项目中遇到过最头疼的事:换了块开发板,摄像头驱动不兼容,图像采集直接崩了。后来我学乖了,移植前一定先确认V4L2的设备节点是否存在——通常是/dev/video0。你可以用v4l2-ctl --list-devices快速检查。
核心流程:
- 打开设备:
fd = open("/dev/video0", O_RDWR) - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap) - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt) - 申请缓冲区:
ioctl(fd, VIDIOC_REQBUFS, &req) - 入队出队:
QBUF和DQBUF循环 - 关闭设备:
close(fd)
嗯,这里要注意:缓冲区管理是V4L2的精华。我建议用MMAP方式映射到用户空间,省去数据拷贝的开销。你想想看,仿生眼每秒要处理30帧甚至60帧图像,每帧少拷贝一次,CPU就少累一点。
// 典型的V4L2采集循环
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
while (1) {
ioctl(fd, VIDIOC_DQBUF, &buf); // 取出已填满的缓冲区
process_image(buffers[buf.index].start); // 处理图像
ioctl(fd, VIDIOC_QBUF, &buf); // 放回缓冲区
}
避坑指南:我曾经在RK3399平台上调试MIPI摄像头,发现采集到的图像全是条纹。查了两天才发现是V4L2的像素格式没对齐——传感器输出的是SBGGR10,我设成了YUYV。记住:VIDIOC_S_FMT之前,一定要先调用VIDIOC_ENUM_FMT确认支持的格式。
4.2 RAW图转RGB:从拜耳阵列到彩色世界
大多数CMOS传感器输出的原始数据是RAW格式——说白了就是拜耳阵列(Bayer Pattern)。每个像素只记录一种颜色(R、G或B),绿色占一半,红蓝各占四分之一。为什么?因为人眼对绿色最敏感,传感器也这么设计。
RAW图转RGB,核心就是去马赛克(Demosaicing)。最简单的办法是双线性插值:缺失的绿色像素用周围四个绿色取平均,红蓝类似。但这样做出来的图像边缘会有锯齿,仿生眼要是用这种图去做特征提取,效果会很差。
我在项目中用过几种插值算法,给你列个对比:
| 算法 | 质量 | 速度 | 适用场景 |
|---|---|---|---|
| 双线性插值 | 低 | 极快 | 预览、低功耗 |
| 边缘导向插值 | 中 | 快 | 一般视觉处理 |
| 自适应插值(如AHD) | 高 | 较慢 | 仿生眼、高精度 |
我个人习惯在嵌入式平台上用边缘导向插值。它先判断像素所在区域是水平边缘还是垂直边缘,然后沿着边缘方向插值。效果比双线性好不少,计算量又比AHD小。你想想看,仿生眼要实时处理,不能为了画质把帧率降到10fps。
// 简化的边缘导向插值(绿色通道)
uint8_t demosaic_green(uint16_t *raw, int x, int y, int width) {
int idx = y * width + x;
int g_h = (raw[idx-1] + raw[idx+1]) / 2; // 水平方向
int g_v = (raw[idx-width] + raw[idx+width]) / 2; // 垂直方向
int dh = abs(raw[idx-1] - raw[idx+1]);
int dv = abs(raw[idx-width] - raw[idx+width]);
return (dh < dv) ? g_h : g_v; // 沿梯度小的方向插值
}
小技巧:如果你的传感器支持10位或12位RAW,别直接转成8位。我建议先做黑电平校正和镜头阴影校正,再转RGB。否则暗部细节全丢了,仿生眼在弱光下就瞎了。
4.3 色彩空间转换:RGB到YUV的魔法
为什么仿生眼要用YUV而不是RGB?
原因很简单:人眼对亮度(Y)比对色度(UV)敏感得多。YUV把亮度和颜色分开,压缩时可以多扔点色度信息,图像质量下降不明显。我做过测试,YUV420比RGB888节省一半带宽,但视觉质量几乎看不出区别。
色彩空间转换的公式是固定的,但实现方式有讲究。标准BT.601转换公式:
Y = 0.299 * R + 0.587 * G + 0.114 * B
U = -0.169 * R - 0.331 * G + 0.500 * B + 128
V = 0.500 * R - 0.419 * G - 0.081 * B + 128
嗯,这里要注意:浮点运算在嵌入式平台上太慢了。我建议用整数查表法——提前算好所有256种R、G、B组合对应的Y、U、V值,存成三个256x256的查找表。运行时直接查表,速度能快10倍以上。
我在项目中踩过的坑:有一次用NEON指令集做YUV转换,发现结果偏色。查了半天,原来是RGB的存储顺序是BGR而不是RGB。很多摄像头驱动默认输出BGR,你按RGB公式去算,颜色当然不对。所以转换前一定要确认通道顺序。
另外,如果你用OpenCV,cvtColor()函数很方便,但它在ARM上默认用浮点。我建议自己写一个NEON优化版本,或者用cv::UMat开启OpenCL加速。仿生眼对实时性要求高,能省则省。
4.4 图像缩放与裁剪:让数据量可控
仿生眼采集到的原始图像分辨率通常很高——比如200万像素甚至500万像素。但后续的视觉算法(比如特征提取、目标跟踪)往往不需要这么高的分辨率。你想想看,处理一张500万像素的图,和一张30万像素的图,计算量差了一个数量级。
所以,图像缩放和裁剪是预处理的关键步骤。我个人习惯分两步走:
- 先裁剪:去掉图像边缘的无效区域(比如镜头畸变严重的部分)
- 再缩放:用双线性或双三次插值降到目标分辨率
为什么先裁剪?因为裁剪不涉及插值计算,只是指针偏移,几乎不花时间。先裁剪掉20%的无效像素,后续缩放的计算量也减少20%。
缩放算法选择上,我建议:
- 最近邻插值:最快,但锯齿严重。只适合做缩略图预览。
- 双线性插值:速度和质量平衡。仿生眼首选。
- 双三次插值:质量最好,但慢。适合离线处理或高精度场景。
避坑指南:我曾经在缩放时忽略了对齐要求。有些硬件加速器要求输入图像的宽度是16的倍数,否则会报错。所以裁剪时最好把宽度和高度对齐到16或32的倍数。另外,缩放后的图像尺寸最好也是2的幂次,方便后续FFT等算法处理。
// 双线性缩放的简化实现
void resize_bilinear(uint8_t *src, int sw, int sh,
uint8_t *dst, int dw, int dh) {
float scale_x = (float)sw / dw;
float scale_y = (float)sh / dh;
for (int y = 0; y < dh; y++) {
for (int x = 0; x < dw; x++) {
float sx = x * scale_x;
float sy = y * scale_y;
int ix = (int)sx, iy = (int)sy;
float fx = sx - ix, fy = sy - iy;
// 四个邻近像素加权平均
dst[y * dw + x] =
(1-fx)*(1-fy)*src[iy*sw+ix] +
fx*(1-fy)*src[iy*sw+ix+1] +
(1-fx)*fy*src[(iy+1)*sw+ix] +
fx*fy*src[(iy+1)*sw+ix+1];
}
}
}
硬件加速提示:很多嵌入式平台(比如NVIDIA Jetson、Rockchip RK3588)内置了ISP(图像信号处理器),可以直接在硬件层面完成RAW转RGB、缩放、裁剪。我建议优先使用硬件ISP,CPU只做后续的算法处理。这样整个流水线能跑到60fps以上。
4.5 本章知识体系总览
下面这张图总结了图像采集与预处理的完整流程。你可以把它当作仿生眼预处理模块的架构图:
整个流程走下来,你会发现:V4L2负责把原始数据从传感器搬到内存,RAW转RGB把拜耳阵列变成彩色图像,色彩空间转换把RGB变成更适合压缩和处理的YUV,最后缩放裁剪把数据量降到算法能承受的范围。每一步都有坑,但每一步也都有优化空间。
我个人觉得,预处理做得好不好,直接决定了后续算法的上限。你想想看,如果输入图像都是偏色、模糊、分辨率不对的,再牛的深度学习模型也白搭。所以,花时间把预处理调好,绝对是值得的。