3. 均值滤波硬件架构:滑动窗口实现、边界处理、流水线设计、资源消耗分析
均值滤波,说白了就是拿一个窗口在图像上滑,把窗口内所有像素加起来求平均,用这个平均值替代中心像素。听起来简单,但硬件实现时坑不少。我最早做这个的时候,以为写个累加器就完事了,结果一综合,时序直接崩了。
这一章,咱们把均值滤波的硬件架构拆开揉碎。从滑动窗口怎么搭,到边界像素怎么处理,再到流水线怎么切,最后算一笔资源账。嗯,都是实战里必须面对的问题。
3.1 滑动窗口的硬件实现
滑动窗口的核心,就是怎么在FPGA里同时拿到一个3x3(或5x5)邻域的所有像素值。你不能等数据全存完再读,那样延迟太大。
我常用的方案是:行缓冲器(Line Buffer)+ 移位寄存器阵列。
- 行缓冲器:用Block RAM或Shift Register实现,存N-1行数据(N是窗口大小)。比如3x3窗口,需要2个行缓冲器。
- 移位寄存器阵列:每行数据进来后,再串入一个3深的移位寄存器,这样每个时钟周期都能输出3x3=9个像素。
来看一个3x3窗口的典型结构:
// 伪代码示意:3x3滑动窗口
// line_buf[0] 存第i行,line_buf[1] 存第i-1行
// shift_reg[0..2] 分别对应当前行、上一行、上两行的3个像素
always @(posedge clk) begin
// 行缓冲器写入
line_buf[0] <= {line_buf[0][WIDTH-2:0], pixel_in};
// 每行数据进入移位寄存器
shift_reg[0] <= {shift_reg[0][1:0], line_buf[0][WIDTH-1]};
shift_reg[1] <= {shift_reg[1][1:0], line_buf[1][WIDTH-1]};
shift_reg[2] <= {shift_reg[2][1:0], line_buf[2][WIDTH-1]};
end
// 9个像素输出:shift_reg[0][2], shift_reg[0][1], shift_reg[0][0], ...
3.2 边界处理策略
窗口滑到图像边缘时,有一半像素在图像外面。怎么办?
常见做法有四种,我列个表对比一下:
| 方法 | 实现方式 | 硬件代价 | 效果 |
|---|---|---|---|
| 补零 | 边界外像素赋0 | 最低,无需额外逻辑 | 边缘有暗边,均值偏低 |
| 复制 | 边界像素向外复制 | 低,加几个MUX | 边缘平滑,但可能模糊 |
| 镜像 | 边界内像素对称映射 | 中等,需要地址计算 | 边缘过渡自然,效果最好 |
| 截断 | 只处理有效区域,边缘不输出 | 最低,但输出尺寸变小 | 无伪影,但图像缩小了 |
我个人在项目中用得最多的是复制模式。为什么?因为补零会让边缘像素值突变,后续处理容易出问题;镜像虽然效果好,但硬件逻辑多了一倍。复制模式在效果和资源之间取了个折中。
3.3 流水线设计
均值滤波的流水线,我一般切成三级:
- 第一级:数据加载——从行缓冲器读出9个像素,寄存一拍。
- 第二级:求和——用加法树把9个像素加起来。3x3窗口需要3个加法器做第一级,2个加法器做第二级,最后1个加法器做第三级。
- 第三级:求平均——右移(除以9)或查表(除以9的近似值)。
为什么这么切?因为加法器的延迟随位宽增长。9个8位像素相加,结果最大是9*255=2295,需要12位。如果在一个时钟里做完所有加法,时序肯定紧张。
我建议的流水线结构:
// 流水线第一级:寄存9个像素
reg [7:0] p00, p01, p02, p10, p11, p12, p20, p21, p22;
always @(posedge clk) begin
{p00, p01, p02} <= shift_reg[0];
{p10, p11, p12} <= shift_reg[1];
{p20, p21, p22} <= shift_reg[2];
end
// 流水线第二级:三级加法树
reg [9:0] sum_row0, sum_row1, sum_row2;
reg [10:0] sum_part;
reg [11:0] sum_total;
always @(posedge clk) begin
sum_row0 <= p00 + p01 + p02;
sum_row1 <= p10 + p11 + p12;
sum_row2 <= p20 + p21 + p22;
sum_part <= sum_row0 + sum_row1;
sum_total <= sum_part + sum_row2;
end
// 流水线第三级:右移3位(除以8)或除以9
reg [7:0] result;
always @(posedge clk) begin
// 除以9的近似:乘以(1/9) ≈ 乘以73再右移9位
result <= (sum_total * 73) >> 9;
end
3.4 资源消耗分析
咱们算一笔账,以3x3均值滤波、图像宽度1920为例:
| 资源项 | 消耗量 | 说明 |
|---|---|---|
| 行缓冲器(BRAM) | 2个 | 每个深度1920,位宽8位 |
| 移位寄存器(FF) | 9个 | 每个8位,共72个FF |
| 加法器(LUT) | 约50-80个 | 取决于位宽和综合优化 |
| 乘法器(DSP) | 0或1个 | 如果用乘法近似除以9,需要1个DSP |
| 流水线寄存器(FF) | 约30个 | 三级流水线的中间结果 |
如果是5x5窗口,行缓冲器变成4个,加法树从3级变成5级,资源大概翻倍。但注意,BRAM数量是线性增长的,而加法器数量是平方增长的。
3.5 整体架构图
下面这张图展示了3x3均值滤波的完整硬件架构,从像素输入到结果输出:
从图里能看出来,数据流是从左到右,先经过行缓冲和移位寄存器,再进入三级流水线。边界处理模块挂在行缓冲器后面,在数据进入移位寄存器之前就把边界像素处理好了。
3.6 实战避坑指南
最后,分享几个我实际项目中遇到的坑:
- 行缓冲器深度别算错:图像宽度是1920,行缓冲器深度就要设成1920,不是1919。因为从写入到读出有一个时钟的延迟。
- 加法树位宽要留够:3x3窗口的9个8位像素相加,结果最大是2295,需要12位。如果只留11位,数据会溢出,图像出现条纹。
- 除以9的近似要小心:乘以73再右移9位,误差在0.5%以内。但如果用乘以72再右移9位,误差会到1.5%,图像会有轻微偏暗。
- 流水线深度影响延迟:3级流水线意味着从像素输入到结果输出有3个时钟的延迟。如果系统对延迟敏感,可以压缩到2级,但时序压力会变大。
均值滤波的硬件架构,说白了就是行缓冲+移位寄存器+加法树+流水线。边界处理选复制模式,除以9用乘法近似,资源不够就上列累加器。嗯,这套方案我在三个项目里验证过,稳得很。