4、去马赛克(Demosaic):双线性插值、边缘导向插值、硬件加速架构设计
去马赛克,说白了就是给每个像素补全RGB三个颜色通道。CMOS传感器上每个像素只采集一种颜色——R、G或B,这叫Bayer格式。你想想看,一个像素只有一种颜色,那画面能看吗?所以得靠周围像素把缺失的两个颜色“猜”出来。
我在项目中遇到过不少坑,最典型的就是:插值算法选不好,画面边缘全是彩色锯齿,客户一看就说“这摄像头是不是坏了”。嗯,今天咱们就把这事彻底讲透。
4.1 双线性插值:最朴素的方法
双线性插值,思路很简单——用相邻同色像素的平均值来填补缺失值。比如一个G像素位置缺R和B,那就取它上下左右四个R像素的平均值作为R,取四个B像素的平均值作为B。
具体来说,Bayer格式有四种常见排列:RGGB、BGGR、GRBG、GBRG。以RGGB为例:
- R位置:G取上下左右四个G的平均,B取对角四个B的平均
- G位置:R取左右(或上下)两个R的平均,B取上下(或左右)两个B的平均
- B位置:G取上下左右四个G的平均,R取对角四个R的平均
核心公式(以RGGB排列的R位置为例)
G_out = (G_up + G_down + G_left + G_right) / 4
B_out = (B_tl + B_tr + B_bl + B_br) / 4
双线性插值的优点是简单、资源少、延迟低。但缺点也很明显——边缘处会产生严重的伪彩色和锯齿。我早期做低端摄像头芯片时用过这个方案,结果拍出来的文字边缘全是红蓝条纹,后来再也不敢在要求高的产品上用了。
| 指标 | 双线性插值 | 边缘导向插值 |
|---|---|---|
| 计算复杂度 | 低(4次加法+1次移位) | 中(梯度计算+条件分支) |
| 伪彩色抑制 | 差 | 好 |
| 边缘保留 | 差(锯齿明显) | 好(沿边缘插值) |
| 硬件资源 | 少(几个加法器+行缓存) | 中等(梯度计算+比较器) |
| 适用场景 | 低端、实时性要求极高 | 中高端、画质优先 |
4.2 边缘导向插值:聪明一点的做法
双线性插值为什么在边缘处翻车?因为它不管图像内容,一律平均。边缘处像素值变化剧烈,平均就会把边缘“抹平”,产生伪彩色。
边缘导向插值的思路是:先判断当前像素处于水平边缘还是垂直边缘,然后只沿着边缘方向插值,不跨边缘。
具体做法分三步:
- 计算梯度:在G通道上计算水平和垂直方向的梯度
- 判断方向:梯度小的方向就是边缘方向
- 定向插值:只沿边缘方向取像素做平均
梯度计算示例(以G位置为例)
// 水平梯度
grad_h = |G_left - G_right|
// 垂直梯度
grad_v = |G_up - G_down|
// 判断方向
if (grad_h < grad_v) {
// 水平边缘,用左右两个R平均
R_out = (R_left + R_right) / 2
} else if (grad_v < grad_h) {
// 垂直边缘,用上下两个R平均
R_out = (R_up + R_down) / 2
} else {
// 平坦区域,用四个方向平均
R_out = (R_up + R_down + R_left + R_right) / 4
}
我曾经在一个4K视频项目中用过这个方案。效果确实比双线性好很多,边缘的彩色锯齿基本消失了。但有个坑——梯度计算对噪声很敏感。如果传感器噪声大,梯度方向会乱跳,导致插值结果不稳定。后来我加了一个小阈值:梯度差小于某个值时,按平坦区域处理,这才稳住。
注意:边缘导向插值虽然好,但硬件实现时要注意两点:
- 梯度计算需要值减法,资源比加法多
- 条件分支在流水线中可能引起气泡,需要做预测或并行化
4.3 硬件加速架构设计
讲完算法,咱们聊聊怎么在硬件里把它跑起来。我个人习惯把Demosaic模块分成三个子模块:行缓存模块、梯度计算模块、插值计算模块。
下面这张图是我在项目中常用的架构:
4.3.1 行缓存模块
行缓存是Demosaic的命脉。双线性插值需要3行数据(当前行+上下各1行),边缘导向需要5行。我建议用双端口SRAM实现,深度等于图像宽度,宽度等于Bayer数据位宽(通常10-12bit)。
为什么是双端口?因为要同时读写。一个端口写新数据,另一个端口读旧数据,这样才能流水不断。
行缓存配置参考
// 以1920x1080@12bit为例
// 双线性:3行 x 1920 x 12bit = 69,120bit ≈ 8.6KB
// 边缘导向:5行 x 1920 x 12bit = 115,200bit ≈ 14.4KB
// 推荐使用双端口SRAM,写端口带宽=像素时钟,读端口带宽=3~5倍像素时钟
4.3.2 梯度计算与插值计算流水线
这两个模块可以做成三级流水线:
- 第一级:从行缓存读出5x5窗口数据,做梯度计算
- 第二级:方向判断,生成插值系数
- 第三级:根据系数做加权平均,输出RGB
每级一个时钟周期,三个时钟周期后输出第一个像素,之后每个时钟输出一个像素。延迟固定,时序好控制。
硬件优化小技巧
我曾经在FPGA上实现时发现,梯度计算中的值减法可以用补码加法替代,省掉一个减法器。具体做法:先做减法,取结果的符号位,如果为负则取反加一。这样只用加法器和选择器,资源更省。
4.3.3 伪彩色校正
即使用了边缘导向插值,某些场景下还是会有残留伪彩色。我习惯在插值模块后面加一个后处理:
// 伪彩色校正:检查RGB通道的差异
if (abs(R - G) > threshold && abs(B - G) > threshold) {
// 疑似伪彩色,用邻域中值替换
R = median(R_neighbors);
B = median(B_neighbors);
}
这个校正逻辑在硬件里实现也不复杂——加一个3x3的中值滤波器,只对伪彩色像素做处理。阈值可以做成寄存器,方便调试时调整。
避坑指南
我曾经在一个项目中把阈值设得太小,结果正常边缘也被当成伪彩色处理了,画面变得模糊。后来我把阈值设成传感器噪声水平的3倍,效果才正常。记住:阈值要跟传感器噪声挂钩,不能拍脑袋定。
4.4 小结
去马赛克这个模块,说难不难,说简单也不简单。双线性插值适合资源紧张的低端产品,边缘导向插值是主流选择。硬件架构上,行缓存深度、流水线级数、伪彩色校正这三个点是最容易出问题的地方。
嗯,如果你正在做这个模块,我建议先从边缘导向插值入手,把梯度计算和方向判断调稳了,再考虑加伪彩色校正。别一上来就想搞个完美的方案,硬件这东西,得一步一步来。