3. 去噪算法基础:空间域滤波原理与RTL架构设计

图像去噪,说白了就是给传感器拍出来的原始数据“搓澡”。CMOS传感器天生带噪,热噪声、读出噪声、光子散粒噪声……这些乱七八糟的东西混在一起,如果不处理,后面的图像质量根本没法看。

空间域滤波是ISP流水线里最基础、也最常用的一类去噪手段。它不依赖频域变换,直接在像素的“邻居”里做文章。我刚开始做ISP的时候,觉得这玩意儿太简单了,不就是算个平均嘛。后来踩了坑才发现——简单的东西,想做好反而最难。

3.1 三种经典滤波器:原理与对比

空间域滤波的核心思想就一句话:用目标像素周围邻域的像素值,重新计算当前像素的值。不同的滤波器,区别在于“怎么算”和“权重怎么给”。

滤波器类型 核心思想 去噪能力 边缘保留 硬件代价
均值滤波 邻域像素取算术平均 中等 差(边缘模糊严重) 极低(只需加法器+除法器)
中值滤波 邻域像素取中间值 强(尤其对椒盐噪声) 好(几乎不模糊边缘) 高(需要排序网络)
高斯滤波 按高斯分布加权平均 强(对高斯噪声最优) 中等(权重平滑过渡) 中等(需要乘法器+查表)

我的经验:实际项目中,很少只用一种滤波器。通常是“中值+高斯”组合拳——中值干掉椒盐噪声,高斯平滑高斯噪声。我在一个安防摄像头项目里试过纯均值滤波,结果画面像蒙了一层雾,客户直接拒收。

3.2 均值滤波:最简单,但坑最多

均值滤波的公式,大家应该都熟:

// 3x3均值滤波
output = (p00 + p01 + p02 + p10 + p11 + p12 + p20 + p21 + p22) / 9

RTL实现确实简单——9个像素求和,然后右移3位(除以8)或者用除法器。但这里有个大坑:除以9不是2的幂次。

我曾经踩过的坑:为了省资源,直接用右移3位代替除以9。结果图像整体偏暗,因为除以8比除以9大了12.5%。后来我改用乘法近似:乘以(2^16/9)再右移16位,精度就够了。

均值滤波的RTL架构,说白了就是一个滑动窗口+累加器:

// 伪代码:3x3均值滤波RTL架构
always @(posedge clk) begin
    // 行缓冲:3行数据
    line_buf[0] <= {line_buf[0][(WIDTH-1):0], pixel_in};
    line_buf[1] <= line_buf[0];
    line_buf[2] <= line_buf[1];
    
    // 3x3窗口寄存器
    window[0][0] <= line_buf[0][(WIDTH-1):WIDTH-2];
    // ... 其他窗口寄存器类似
    
    // 求和树
    sum = window[0][0] + window[0][1] + ... + window[2][2];
    
    // 输出(近似除以9)
    output <= sum * 7282;  // 7282 = 2^16 / 9
end

3.3 中值滤波:排序网络的艺术

中值滤波对椒盐噪声简直是“天敌”。原理很简单:把邻域像素排个序,取中间那个值。但硬件实现排序,成本可不低。

3x3窗口需要排序9个数,取第5大的。最直接的办法是冒泡排序网络,但9个数全排序需要36个比较器,面积太大。

我推荐的做法:用“行排序+列排序+对角线排序”的三级流水线结构。3x3窗口先对每行3个数排序,得到3个中值;再对这3个中值排序,取中间值。这样只需要9个比较器,面积缩小了4倍。

中值滤波的RTL架构,核心是排序单元:

// 3输入排序单元
module sort3(
    input [7:0] a, b, c,
    output [7:0] min, mid, max
);
    assign {min, max} = (a < b) ? {a, b} : {b, a};
    // 再与c比较,得到最终排序结果
endmodule

// 3x3中值滤波流水线
// Stage 1: 行排序
sort3 row0(.a(w[0][0]), .b(w[0][1]), .c(w[0][2]), .mid(mid0));
sort3 row1(.a(w[1][0]), .b(w[1][1]), .c(w[1][2]), .mid(mid1));
sort3 row2(.a(w[2][0]), .b(w[2][1]), .c(w[2][2]), .mid(mid2));

// Stage 2: 列排序(取三个中值的中值)
sort3 col(.a(mid0), .b(mid1), .c(mid2), .mid(median_out));

3.4 高斯滤波:权重分配有讲究

高斯滤波的权重,是根据二维高斯函数算出来的。离中心越近,权重越大。3x3的高斯核长这样:

// 3x3高斯核(σ≈0.85)
// 归一化系数:1/16
[1  2  1]
[2  4  2]  * (1/16)
[1  2  1]

为什么选这个核?因为权重都是2的幂次,硬件实现只需要移位和加法,不需要乘法器。你看,4就是左移2位,2就是左移1位,1就是原值。

高斯滤波的RTL架构,核心是加权求和树:

// 3x3高斯滤波RTL(无乘法器版本)
// 权重:1,2,1 / 2,4,2 / 1,2,1

// 第一级:按权重移位
w_1x = (w[0][0] + w[0][2] + w[2][0] + w[2][2]);        // 权重1
w_2x = (w[0][1] + w[1][0] + w[1][2] + w[2][1]) << 1;  // 权重2
w_4x = w[1][1] << 2;                                     // 权重4

// 第二级:求和
sum = w_1x + w_2x + w_4x;

// 第三级:归一化(右移4位,即除以16)
output <= sum >> 4;

关键点:高斯核的大小和σ值直接影响去噪效果。核越大,去噪越强,但边缘越模糊。我在一个医疗图像项目里试过7x7高斯核,结果血管边缘都糊了,医生直接说“这图没法看”。后来改成5x5,σ=1.2,效果就好多了。

3.5 三种滤波器的RTL架构对比

从硬件实现角度看,三种滤波器的资源消耗差异很大:

资源项 均值滤波 中值滤波 高斯滤波
加法器 8个 0个(排序用比较器) 7个
比较器 0个 9个(3x3窗口) 0个
乘法器 0个 0个 0个(移位实现)
行缓冲 2行 2行 2行
延迟 3个时钟 5个时钟 3个时钟

选型建议:如果资源紧张,选均值滤波;如果噪声类型是椒盐噪声为主,选中值滤波;如果追求图像质量,选高斯滤波。我个人的习惯是——默认用高斯,遇到椒盐噪声再切中值。

3.6 整体架构与SVG流程图

三种滤波器在ISP流水线中的位置,以及它们之间的切换逻辑,可以用下面这张图来表示:

空间域滤波器架构与切换逻辑 空间域滤波器RTL架构与切换逻辑 原始像素流 行缓冲(3行) 3x3 滑动窗口 均值滤波 求和树 + 移位 中值滤波 排序网络 高斯滤波 加权求和树 MUX 选择器 sel[1:0] 去噪后像素流 sel[1:0] = 00: 均值滤波 sel[1:0] = 01: 中值滤波 sel[1:0] = 10: 高斯滤波

这张图展示了三种滤波器的统一架构。核心思路是:共享行缓冲和滑动窗口,通过MUX选择不同的滤波算法。这样做的好处是——资源复用,面积小。

3.7 边界处理:容易被忽略的细节

图像边缘的像素,没有完整的3x3邻域。怎么处理?有三种常见方案:

  1. 补零(Zero Padding):边缘补0。最简单,但边缘会有暗边。
  2. 复制(Replicate):边缘像素复制到外面。效果最好,但需要额外逻辑。
  3. 镜像(Mirror):边缘像素镜像对称。效果也不错,但控制逻辑复杂。

我曾经犯过的错:在一个项目中偷懒用了补零,结果图像四周出现了一圈黑边,客户以为是传感器坏了。后来改成复制模式,问题解决。记住——边界处理不是小事,直接影响图像质量。

好了,空间域滤波的基础就讲到这里。均值、中值、高斯这三种滤波器,是ISP去噪的基石。理解它们的原理和RTL架构,后面学更复杂的去噪算法(比如双边滤波、非局部均值)就会轻松很多。