双边滤波器(Bilateral Filter)FPGA 实现详解
一、公式在说什么
1.1 双边滤波想干什么
图像里有噪声,我们希望把噪声抹掉,但又不想把图像边缘也抹糊。
最简单的想法是:把一个像素和周围几个像素取平均。但这样做的问题是------如果当前像素在边缘上,比如左边是白墙、右边是黑衣服,取平均后边缘会变灰,边缘就糊掉了。
双边滤波的想法是:
平均的时候,不光看"你离我近不近",还要看"你和我像不像"。
- 如果邻居离我近,可以参与平均;
- 如果邻居灰度和我差很多(像边缘两侧),就不让它参与。
这样噪声被平均掉了,边缘被保留下来。
1.2 数学公式
双边滤波输出一个像素的值,公式如下:
O ( i , j ) = ∑ k , l w ( i , j , k , l ) ⋅ I ( k , l ) ∑ k , l w ( i , j , k , l ) O(i,j) = \frac{\sum_{k,l} w(i,j,k,l) \cdot I(k,l)}{\sum_{k,l} w(i,j,k,l)} O(i,j)=∑k,lw(i,j,k,l)∑k,lw(i,j,k,l)⋅I(k,l)
先别慌,拆开看:
| 符号 | 意思 |
|---|---|
| O ( i , j ) O(i,j) O(i,j) | 中心像素 ( i , j ) (i,j) (i,j) 滤波后的输出值 |
| I ( k , l ) I(k,l) I(k,l) | 邻居像素 ( k , l ) (k,l) (k,l) 的原始灰度值 |
| w ( i , j , k , l ) w(i,j,k,l) w(i,j,k,l) | 中心 ( i , j ) (i,j) (i,j) 和邻居 ( k , l ) (k,l) (k,l) 之间的"重要程度",叫权重 |
| ∑ k , l \sum_{k,l} ∑k,l | 对中心周围某个范围内的所有邻居求和 |
| 分子 | 所有"权重 × 像素灰度"加起来的总和 |
| 分母 | 所有权重的总和 |
这其实就是一个加权平均:
输出 = 权重 × 灰度的总和 权重的总和 \text{输出} = \frac{\text{权重} \times \text{灰度的总和}}{\text{权重的总和}} 输出=权重的总和权重×灰度的总和
如果你把每个权重都当成 1,就是普通的算术平均。
1.3 权重 w w w 由哪两部分组成
w ( i , j , k , l ) = exp ( − ( i − k ) 2 + ( j − l ) 2 2 σ d 2 ) ⏟ 空间权重 g ⋅ exp ( − ( I ( i , j ) − I ( k , l ) ) 2 2 σ r 2 ) ⏟ 相似度权重 s w(i,j,k,l) = \underbrace{\exp\left(-\frac{(i-k)^2 + (j-l)^2}{2\sigma_d^2}\right)}{\text{空间权重 } g} \cdot \underbrace{\exp\left(-\frac{(I(i,j) - I(k,l))^2}{2\sigma_r^2}\right)}{\text{相似度权重 } s} w(i,j,k,l)=空间权重 g exp(−2σd2(i−k)2+(j−l)2)⋅相似度权重 s exp(−2σr2(I(i,j)−I(k,l))2)
空间权重 g g g
( i − k ) 2 + ( j − l ) 2 (i-k)^2 + (j-l)^2 (i−k)2+(j−l)2 是中心像素和邻居像素的距离平方。邻居离得越远,这个值越大;前面有个负号,所以整个指数就越小。
意思就是:离得越近,越应该参考。
这和普通高斯滤波一样。
相似度权重 s s s
I ( i , j ) − I ( k , l ) I(i,j) - I(k,l) I(i,j)−I(k,l) 是中心像素和邻居像素的灰度差。灰度差越大,指数越小。
意思就是:颜色越像,越应该参考;颜色差太多,就滚蛋。
这就是双边滤波和普通高斯滤波的区别------普通高斯滤波只有 g g g,没有 s s s。
1.4 举个生活例子
假设你站在操场中央,要判断"这里是不是草地"。你会问周围的同学:
- 离你 1 米的同学(空间距离近)→ 参考意见大;
- 离你 50 米的同学(空间距离远)→ 参考意见小;
- 穿同样校服的同学(灰度相似)→ 他说的话可信;
- 穿一身红衣服的老师(灰度差大)→ 虽然他也在旁边,但他可能站在跑道上,他的话对"草地判断"权重降低。
双边滤波就是同时用"距离"和"穿着 similarity"来决定听谁的。
二、从公式到硬件:每一步是怎么来的
公式本身只定义了数学运算。要在 FPGA 上实现,必须把每个数学操作翻译成电路。翻译过程中会遇到问题,每个问题对应一个步骤。
问题 1:公式里有 ∑ k , l \sum_{k,l} ∑k,l,意味着要同时拿到很多像素
公式不是只算中心像素自己,而是要把中心像素和周围一堆像素一起算。FPGA 接收图像是串行的------一次来一个像素,像水龙头流水一样。
解决:先存几行像素,等凑够一个局部窗口再一起算。代码选了 3×3 窗口,就是存上一行、当前行、下一行,每次取 9 个像素。
→ 对应 Step 1:生成 3×3 矩阵
注意:公式没指定 3×3,5×5、7×7 都可以。选 3×3 是因为资源少、延迟低,是工程选择。
问题 2:公式里需要 I ( i , j ) − I ( k , l ) I(i,j) - I(k,l) I(i,j)−I(k,l),即中心像素减每个邻居的灰度差
有了 3×3 矩阵,中心是中间那个像素,周围 8 个是邻居。要算 9 个差值。
解决:放 9 个减法器,大减小,得到 9 个绝对值差。
→ 对应 Step 2:9 路绝对值差
问题 3:公式里有 exp ( ⋯ ) \exp(\cdots) exp(⋯),FPGA 不会算指数函数
FPGA 只会加、减、乘、移位,不会算 exp \exp exp、 ln \ln ln、 sin \sin sin 这些。
解决:把指数函数的结果提前算好,做成一张表。输入灰度差,查表直接输出相似度权重。
→ 对应 Step 3:LUT 查表
只有灰度相似度权重 s s s 需要 LUT。空间权重 g g g 只和位置有关,3×3 里每个位置到中心的距离固定,可以提前算好写成常量。
问题 4:公式里 w = g × s w = g \times s w=g×s,两个权重要乘起来
每个像素都有一个空间权重 g g g(常量)和一个查表得到的相似度权重 s s s,总权重要把它们相乘。
解决:9 个乘法器,每个像素一路。
→ 对应 Step 4:9 路乘法,算总权重 w w w
问题 5:公式分母是 ∑ w \sum w ∑w,所有权重要加起来
加权平均需要分母。9 个权重加起来。
解决:两级加法树,先三三相加,再把三个和相加。
→ 对应 Step 5:两级加法树求和
问题 6:公式里有除法 ∑ ( w ⋅ I ) ∑ w \frac{\sum(w \cdot I)}{\sum w} ∑w∑(w⋅I),FPGA 除法很慢
FPGA 的除法器不是一拍出来的。这段代码里一个除法要 34 个时钟周期。
解决 :先算 w / ∑ w w / \sum w w/∑w 得到归一化权重,再和原始像素相乘。9 个权重各除一次,可以并行做。
→ 对应 Step 7:9 个并行除法器
也可以先算 w ⋅ I w \cdot I w⋅I 再加起来最后除一次,但代码选了"先除后乘":9 个除法器可以并行、流水线更顺、单个除法器位宽更小。
问题 7:等除法器算出结果时,原来的像素已经没了
从输入像素到除法结果出来要 34 拍。34 拍后,原始像素早就被新进来的数据覆盖了。
解决:给原始像素数据也建一条 34 拍的延迟链,让它和除法结果在同一拍对齐,然后再相乘相加。
→ 对应 Step 6 + Step 8:延迟对齐
问题 8:公式分子是 ∑ ( w ⋅ I ) \sum(w \cdot I) ∑(w⋅I),归一化权重要和像素相乘再加总
现在有了归一化权重和延迟后的原始像素,把它们相乘,9 路结果加起来。
解决:9 个乘法器 + 两级加法树。
→ 对应 Step 9:9 路乘法 + 加法树
问题 9:图像边缘凑不出完整 3×3 窗口
公式假设每个像素都有完整邻居。但图像第一行、最后一行、第一列、最后一列没有。
解决:边缘直接输出原始中心像素,不做滤波。
→ 对应 Step 10:边界处理
问题 10:除法结果是定点小数,要回到 8 位灰度
为了避免小数,代码里把被除数先左移 10 位(相当于乘 1024)再除,结果变成 0~1024 的整数。最后要除以 1024 并四舍五入。
解决:取高 8 位 + 第 9 位做进位,完成四舍五入。
→ 对应 Step 10:四舍五入输出
三、推导总表
| 推导 | 公式里出现的东西 | 硬件实现遇到的问题 | 解决方案 | 对应 Step |
|---|---|---|---|---|
| 1 | ∑ k , l \sum_{k,l} ∑k,l 需要多个像素 | 图像是串行输入的,一次只有一个像素 | 加行缓存,生成 3×3 窗口 | Step 1 |
| 2 | I ( i , j ) − I ( k , l ) I(i,j) - I(k,l) I(i,j)−I(k,l) | 需要算中心与每个邻居的灰度差 | 9 路绝对值减法 | Step 2 |
| 3 | exp ( − Δ I 2 / 2 σ r 2 ) \exp(-\Delta I^2 / 2\sigma_r^2) exp(−ΔI2/2σr2) | FPGA 不会算指数 | LUT 查表 | Step 3 |
| 4 | w = g ⋅ s w = g \cdot s w=g⋅s | 空间权重和相似度权重要相乘 | 9 路乘法器 | Step 4 |
| 5 | ∑ w \sum w ∑w | 9 个权重要加起来当分母 | 两级加法树 | Step 5 |
| 6 | 除法延迟 | 除法器要 34 拍,原始像素会丢失 | 给原始像素建 34 拍延迟链 | Step 6/8 |
| 7 | w / ∑ w w / \sum w w/∑w | 需要权重归一化 | 9 个并行除法器 | Step 7 |
| 8 | ∑ ( w ⋅ I ) \sum(w \cdot I) ∑(w⋅I) | 归一化权重和像素相乘再加总 | 9 路乘法 + 加法树 | Step 9 |
| 9 | 3×3 窗口 | 边缘像素没有完整邻居 | 边缘直接输出原值 | Step 10 |
| 10 | 定点数还原 | 结果要回到 8 位灰度 | 取高位 + 四舍五入 | Step 10 |
四、实现步骤(代码对应)
Step 1:生成 3×3 滑窗矩阵
双边滤波的前提是每一个像素位置都要有完整的 3×3 邻域。但 RAW 图像是逐行逐列流式进入 FPGA 的,某个时刻只有一个像素。所以必须用行缓存 + 列缓存把前后两行、前后两列的数据暂存下来,凑成 3×3 矩阵。
verilog
Matrix_Generate_3X3_8Bit #(
.IMG_HDISP (IMG_HDISP), // 图像宽
.IMG_VDISP (IMG_VDISP) // 图像高
) u_Matrix_Generate_3X3_8Bit (
.clk (clk),
.rst_n (rst_n),
.din_vsync (per_img_vsync),
.din_href (per_img_href),
.din (per_img_Y),
.dout_vsync(matrix_img_vsync),
.dout_href (matrix_img_href),
.dout (matrix_img_data)
);
Matrix_Generate_3X3_8Bit 内部做了两件事:
- 用两个 FIFO/行缓存存前两行;
- 每行用移位寄存器存相邻三列。
输出端同时给出 9 个像素:matrix_p11 ~ matrix_p33。
Step 2:算 9 路绝对值差
中心像素是 matrix_p22,其余 8 个是邻居。相似度权重需要知道每个邻居和中心的灰度差。
verilog
// 中心像素
assign matrix_p22 = matrix_p12_r0;
// 9 路绝对值差
assign abs_p11_p22 = (matrix_p11 >= matrix_p22) ? (matrix_p11 - matrix_p22) : (matrix_p22 - matrix_p11);
assign abs_p12_p22 = (matrix_p12 >= matrix_p22) ? (matrix_p12 - matrix_p22) : (matrix_p22 - matrix_p12);
// ... 共 9 路
Step 3:LUT 查相似度权重
用灰度差查表,得到相似度权重 s s s。
verilog
// 9 个 LUT,输入是灰度差,输出是相似度权重
rom_bilateral_filter_8bit u0 (.clka(clk), .addra(abs_p11_p22), .douta(similarity11));
rom_bilateral_filter_8bit u1 (.clka(clk), .addra(abs_p12_p22), .douta(similarity12));
// ... 共 9 个
LUT 的内容提前由 s = exp ( − Δ I 2 / 2 σ r 2 ) s = \exp(-\Delta I^2 / 2\sigma_r^2) s=exp(−ΔI2/2σr2) 计算好。
Step 4:总权重 w = g × s w = g \times s w=g×s
空间权重 g g g 是预计算的常量,和 LUT 输出的 s s s 相乘。
verilog
// 9 路乘法
assign weight11 = g11 * similarity11;
assign weight12 = g12 * similarity12;
// ... 共 9 路
Step 5:权重求和
把 9 个权重加起来,得到分母 ∑ w \sum w ∑w。
verilog
assign weight_sum_stage1_1 = weight11 + weight12 + weight13;
assign weight_sum_stage1_2 = weight21 + weight22 + weight23;
assign weight_sum_stage1_3 = weight31 + weight32 + weight33;
assign weight_sum = weight_sum_stage1_1 + weight_sum_stage1_2 + weight_sum_stage1_3;
Step 6 + Step 8:延迟对齐
除法器要 34 拍。为了让像素数据和权重在同一拍相遇,给它们都建延迟链。
verilog
// 原始像素延迟 34 拍
always @(posedge clk) matrix_p11_r1[0] <= matrix_p11;
always @(posedge clk) matrix_p11_r1[1] <= matrix_p11_r1[0];
// ... 直到 [33]
同步信号 matrix_img_vsync、matrix_img_href 也要延迟同样拍数,保证输出时序正确。
Step 7:9 个并行除法器
每个权重除以权重和,得到归一化权重。高位补 10 个 0 是为了把结果放大 1024 倍,避免小数。
verilog
// 9 个除法器并行
// dividend = {10'd0, weightXX},相当于 weightXX * 1024
// divisor = weight_sum
// 商 norm_weightXX 范围约 0~1024
Step 9:归一化权重乘像素再加总
verilog
mult_p11_w11 <= matrix_p11_r1[C_CLK_LATENCY-1] * norm_weight11;
mult_p12_w12 <= matrix_p12_r1[C_CLK_LATENCY-1] * norm_weight12;
// ... 共 9 路
sum_result <= mult_p11_w11 + mult_p12_w12 + ... + mult_p33_w33;
Step 10:边界处理 + 四舍五入
verilog
always @(posedge clk) begin
if (matrix_img_href_d) begin
if (edge_flag_d) // 图像边界,不滤波
post_img_data <= matrix_p22_r1[C_CLK_LATENCY-1];
else // 有效区域,四舍五入
post_img_data <= sum_result[17:10] + sum_result[9];
end
end
五、关键参数说明
verilog
parameter IMG_HDISP = 11'd640; // 图像宽度 640
parameter IMG_VDISP = 11'd480; // 图像高度 480
localparam C_CLK_LATENCY = 34; // 除法器延迟(拍),用于对齐数据延迟链
3×3 高斯空间权重(预计算好的常量,不是 LUT):
verilog
// [g11, g12, g13] [109, 115, 109]
// [g21, g22, g23] = [115, 122, 115]
// [g31, g32, g33] [109, 115, 109]
localparam g11=7'd109, g12=7'd115, g13=7'd109;
localparam g21=7'd115, g22=7'd122, g23=7'd115;
localparam g31=7'd109, g32=7'd115, g33=7'd109;
这些值来源于 σ d = 1.5 \sigma_d=1.5 σd=1.5 的高斯核,7 位定点。中心 g22=122 最大,四角 g11=109 最小,符合"离中心越远权重越小"。