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 边缘导向插值:聪明一点的做法

双线性插值为什么在边缘处翻车?因为它不管图像内容,一律平均。边缘处像素值变化剧烈,平均就会把边缘“抹平”,产生伪彩色。

边缘导向插值的思路是:先判断当前像素处于水平边缘还是垂直边缘,然后只沿着边缘方向插值,不跨边缘。

具体做法分三步:

  1. 计算梯度:在G通道上计算水平和垂直方向的梯度
  2. 判断方向:梯度小的方向就是边缘方向
  3. 定向插值:只沿边缘方向取像素做平均

梯度计算示例(以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模块分成三个子模块:行缓存模块、梯度计算模块、插值计算模块。

下面这张图是我在项目中常用的架构:

Demosaic硬件加速架构 Bayer输入 行缓存模块 5行Line Buffer 梯度计算模块 H/V梯度 + 方向判断 插值计算模块 定向平均 + 伪彩色校正 RGB输出 控制模块 流水线控制 架构要点: 1. 行缓存深度取决于插值窗口大小,边缘导向需要5行(2行上+1行当前+2行下) 2. 梯度计算和插值计算可流水并行,每时钟周期输出一个像素 3. 控制模块负责帧同步、行同步、像素有效信号生成

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 小结

去马赛克这个模块,说难不难,说简单也不简单。双线性插值适合资源紧张的低端产品,边缘导向插值是主流选择。硬件架构上,行缓存深度、流水线级数、伪彩色校正这三个点是最容易出问题的地方。

嗯,如果你正在做这个模块,我建议先从边缘导向插值入手,把梯度计算和方向判断调稳了,再考虑加伪彩色校正。别一上来就想搞个完美的方案,硬件这东西,得一步一步来。