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], ...
我的习惯:行缓冲器深度设成图像宽度+1,留一个时钟的余量。不然边界处容易丢像素,我吃过这个亏。

3.2 边界处理策略

窗口滑到图像边缘时,有一半像素在图像外面。怎么办?

常见做法有四种,我列个表对比一下:

方法 实现方式 硬件代价 效果
补零 边界外像素赋0 最低,无需额外逻辑 边缘有暗边,均值偏低
复制 边界像素向外复制 低,加几个MUX 边缘平滑,但可能模糊
镜像 边界内像素对称映射 中等,需要地址计算 边缘过渡自然,效果最好
截断 只处理有效区域,边缘不输出 最低,但输出尺寸变小 无伪影,但图像缩小了

我个人在项目中用得最多的是复制模式。为什么?因为补零会让边缘像素值突变,后续处理容易出问题;镜像虽然效果好,但硬件逻辑多了一倍。复制模式在效果和资源之间取了个折中。

我曾经踩过的坑:用补零做3x3均值滤波,结果边缘像素的均值比实际低了1/3,整张图边缘发黑。后来改成复制模式,问题解决。

3.3 流水线设计

均值滤波的流水线,我一般切成三级:

  1. 第一级:数据加载——从行缓冲器读出9个像素,寄存一拍。
  2. 第二级:求和——用加法树把9个像素加起来。3x3窗口需要3个加法器做第一级,2个加法器做第二级,最后1个加法器做第三级。
  3. 第三级:求平均——右移(除以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
关键点:除以9不能用简单的右移,因为9不是2的幂。我一般用乘法近似:乘以73再右移9位,误差在0.5%以内,完全够用。

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数量是线性增长的,而加法器数量是平方增长的。

我的建议:如果资源紧张,可以考虑用列累加器替代行缓冲器。每列单独累加,窗口滑动时只更新一列的数据,能省不少BRAM。但控制逻辑会复杂一些,适合对面积敏感的场景。

3.5 整体架构图

下面这张图展示了3x3均值滤波的完整硬件架构,从像素输入到结果输出:

3x3均值滤波硬件架构图 3x3均值滤波硬件架构 像素输入 行缓冲器 Line Buffer x2 移位寄存器 3x3 阵列 流水线级1 数据寄存 流水线级2 加法树求和 流水线级3 求平均输出 结果输出 边界处理 复制/镜像/补零 关键参数 • 窗口大小:3x3 • 图像宽度:1920像素 • 流水线深度:3级

从图里能看出来,数据流是从左到右,先经过行缓冲和移位寄存器,再进入三级流水线。边界处理模块挂在行缓冲器后面,在数据进入移位寄存器之前就把边界像素处理好了。

3.6 实战避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 行缓冲器深度别算错:图像宽度是1920,行缓冲器深度就要设成1920,不是1919。因为从写入到读出有一个时钟的延迟。
  • 加法树位宽要留够:3x3窗口的9个8位像素相加,结果最大是2295,需要12位。如果只留11位,数据会溢出,图像出现条纹。
  • 除以9的近似要小心:乘以73再右移9位,误差在0.5%以内。但如果用乘以72再右移9位,误差会到1.5%,图像会有轻微偏暗。
  • 流水线深度影响延迟:3级流水线意味着从像素输入到结果输出有3个时钟的延迟。如果系统对延迟敏感,可以压缩到2级,但时序压力会变大。
我曾经犯过的错:有一次做5x5均值滤波,我图省事直接用了3级流水线,结果加法树太长,时序没过。后来老老实实切成5级,频率从100MHz提到了200MHz。流水线深度不是越少越好,够用就行。

均值滤波的硬件架构,说白了就是行缓冲+移位寄存器+加法树+流水线。边界处理选复制模式,除以9用乘法近似,资源不够就上列累加器。嗯,这套方案我在三个项目里验证过,稳得很。