
简介面向FPGA与MATLAB联合开发的数字图像处理场景中值滤波Verilog实现方案完整覆盖从算法仿真到硬件部署的链路。中值滤波作为典型非线性滤波技术能有效抑制椒盐噪声并保持边缘锐度在FPGA上利用并行处理能力可满足实时图像预处理需求。压缩包共2个文件、大小约10KBTo_txt.m脚本负责将图像数据转换为文本格式便于FPGA端读取与仿真midfilter.zip则封装了中值滤波器的Verilog源码及配套设计/测试文件可直接导入工程复用。已有227人学习下载适合具备一定Verilog基础、正着手FPGA图像处理项目的读者。通过资源可掌握数据流控制、窗口缓存、排序网络等核心技巧也可结合MATLAB HDL Coder快速生成硬件代码避开从零搭建的繁琐过程。1. 中值滤波Verilog实现为什么一个“排序”电路比“平均”电路更值得做图像传感器在弱光或长曝光条件下经常冒出孤立的亮暗噪点——椒盐噪声。均值滤波会把噪点“抹开”像涂了一层灰中值滤波则是把窗口内像素排序后取中间值噪点本身极难成为中位数所以它既去噪又保边缘。做FPGA图像处理时很多人第一个想到的是均值滤波因为它只需要一个加法器和移位寄存器但真正落地后发现边界糊、细节丢才回头用中值滤波。中值滤波的Verilog实现难点不在滤波思想而在“如何在硬件里快速排出9个数的中位数”——这不是软件里的冒泡排序而是面积换速度的排序网络设计。这篇文章适合正在做图像预处理IP核、视频采集链路降噪、或者准备把算法搬进FPGA的开发者。你会看到从3x3窗口构建、纯组合逻辑排序、流水线切割到多bit数据位宽与彩色图像适配的完整路径。我会把每个能抄作业的模块贴出来包括参数怎么改、时序怎么约束、仿真里常见的翻车点。目标只有一个让你看完能独立写出一版能综合、能跑到100MHz以上的中值滤波模块而不是停留在概念理解。2. 中值滤波的硬件本质从像素窗口到排序网络2.1 排序才是核心矛盾中值滤波为何是“非线性”软件里做中值滤波步骤无非是取窗口、排序、选中值但在硬件里每一步都要重新思考。取窗口意味着你要缓存两行像素——这需要行存储排序意味着你要比较——这需要大量比较器。为什么说中值滤波“非线性”因为它的输出不是输入像素的线性组合而是依赖于数据的相对大小关系。均值滤波可以写成乘法累加中值滤波做不到。一个3x3窗口需要9个像素同时参与运算。这9个像素从哪儿来答案是一个像素时钟进来一个灰度值你必须把前两行对应位置的像素也同时准备好。这就是行缓存line buffer的由来。常见做法是例化两个FIFO或移位寄存器链每一个延迟恰好一个行周期。对于1920x1080的图像一行有1920个像素如果你用移位寄存器链会消耗巨量寄存器——一个8bit像素乘1920乘2行大约3万个触发器这在小规模FPGA上相当可观。我的经验是行缓存用Block RAM或FIFO更省资源。但FIFO有个坑读延迟不确定需要你精确控制读写时序。如果你只是做原型验证用移位寄存器链最直观如果做正式IP改成Xilinx或Altera的FIFO原语更靠谱。下面给出用移位寄存器组做行缓存的写法因为它最容易理解和排错。// 3x3窗口行缓存模块化设计参数化行宽和位宽 module line_buffer( input wire clk, input wire rst_n, input wire [7:0] din, // 当前像素输入 input wire valid_in, // 输入有效信号 output wire [7:0] row0, // 第0行最旧 output wire [7:0] row1, // 第1行中间 output wire [7:0] row2 // 第2行最新当前输入 ); parameter WIDTH 1920; // 行宽 parameter DW 8; // 像素位宽 parameter DEPTH WIDTH * DW; // 用移位寄存器实现两行延迟 reg [DEPTH-1:0] shift0; // 第一行缓存 reg [DEPTH-1:0] shift1; // 第二行缓存 always (posedge clk or negedge rst_n) begin if (!rst_n) begin shift0 {DEPTH{1b0}}; shift1 {DEPTH{1b0}}; end else if (valid_in) begin shift0 {shift0[DEPTH-DW-1:0], din}; // 最新像素进第一行缓存 shift1 {shift1[DEPTH-DW-1:0], shift0[DEPTH-1:DEPTH-DW]}; // 第一行溢出像素进第二行缓存 end end assign row2 din; // 当前行 assign row1 shift0[DEPTH-1:DEPTH-DW]; // 上一行 assign row0 shift1[DEPTH-1:DEPTH-DW]; // 上上行 endmodule这段代码的关键在于两个移位寄存器的级联关系。每个时钟周期当前像素din被推入shift0的头部shift0的最低位即最早进来的像素被推入shift1。这样row1就是延迟一行后的像素row0就是延迟两行后的像素。参数WIDTH是行宽DW是灰度位宽。当你在1080p图像上使用时整个移位寄存器组的资源消耗是1920x8x2≈30K触发器占一个小规模FPGA的10%左右——这也是我不推荐用纯寄存器做大型行缓存的原因。2.2 排序网络的选型全并行比较器换来单周期输出取到9个像素之后下一步就是排序。软件里用sort函数、用快排硬件里你只能老老实实做比较器网络。中值滤波不需要完整排序只需要第5大的数——也就是中位数。因此排序网络可以裁剪不需要排完9个数排到能确定中位数即可。硬件排序网络有几种常见拓扑插入排序网络、冒泡排序网络、以及基于“最大值-最小值”法的中值选择网络。我最常用的是“三分组取中值再汇总”的结构9个像素分成3组每组3个像素先用3个比较器全排序两两比较即可得到最大值、中值、最小值然后从3个中值里再取一次中值。这个结构的比较器数量最少每组需要3个比较器做全排序共9个三组的中值再做一次三点排序需要3个比较器。总计12个比较器。相比全排序网络所需的几十个比较器资源省了很多而且延迟只有两个比较器级联的深度。逻辑表达为中值 各组中值的中值。这个结论在数学上是成立的因为全局中值必然存在于各组中值的区间内而三个中值的中位恰好就是全局中位。我不展开证明你如果感兴趣可以用穷举法验证——随手写个Python脚本枚举9个数的所有排列这个结论在全部排列下都成立。下面给出三点排序器的Verilog实现它是整个排序网络的基本单元。// 三点排序器输入a,b,c输出max,mid,min module sort3( input wire [7:0] a, input wire [7:0] b, input wire [7:0] c, output wire [7:0] max, output wire [7:0] mid, output wire [7:0] min ); wire [7:0] ab_min (a b) ? a : b; wire [7:0] ab_max (a b) ? b : a; wire [7:0] abc_min; wire [7:0] abc_mid; wire [7:0] abc_max; assign abc_min (ab_min c) ? ab_min : c; assign abc_max (ab_max c) ? ab_max : c; assign abc_mid (ab_max c) ? ((ab_min c) ? ab_min : c) : ab_max; assign max abc_max; assign mid abc_mid; assign min abc_min; endmodule这个模块用了两级比较先比a和b得到较小值和较大值再分别与c比较得出三者的最大、中间、最小。注意mid的赋值逻辑是我常用的写法——它避免了额外例化比较器用嵌套三元表达式实现综合后仍然是两级的比较器结构。时序上从输入到输出经过两个比较器深度在常规工艺下8bit比较器延迟在2-3ns左右两级大约5ns对应200MHz的极限——这是纯组合逻辑的最快速度。2.3 顶层拼接从9个像素到1个中值的完整数据通路行缓存输出3行像素但每行只有1个像素——我们需要在同一时刻得到3x3窗口的9个像素。这需要再对每行的像素做二级移位每一行延迟2拍、1拍、0拍形成3列。硬件上就是再级联两个寄存器和行缓存类似只不过延迟单元数量从1920变成了2。整个数据通路的时序关系是像素持续输入行缓存打两行列寄存器打两拍。当第N个像素到达时窗口内9个像素分别是第N-2行的第N-2、N-1、N个像素第N-1行的对应三个第N行的对应三个。映射到Verilog里就是把行缓存输出的row0、row1、row2分别再接2级寄存器得到3行x3列的矩阵。这个“行列同步”的设计是整条数据通路不出错的关键——很多人写到这里数据对不齐问题几乎都出在行缓存与列寄存器之间的时序配合上。// 3x3窗口捕获与中值计算组合逻辑全并行版 module median_filter3x3( input wire clk, input wire rst_n, input wire [7:0] din, input wire valid_in, output reg [7:0] dout, output reg valid_out ); // 行缓存实例 wire [7:0] row0, row1, row2; line_buffer #(.WIDTH(1920), .DW(8)) u_lb( .clk(clk), .rst_n(rst_n), .din(din), .valid_in(valid_in), .row0(row0), .row1(row1), .row2(row2) ); // 列缓存每行打两拍 reg [7:0] row0_d1, row0_d2; reg [7:0] row1_d1, row1_d2; reg [7:0] row2_d1, row2_d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin row0_d1 8d0; row0_d2 8d0; row1_d1 8d0; row1_d2 8d0; row2_d1 8d0; row2_d2 8d0; end else if (valid_in) begin {row0_d2, row0_d1} {row0_d1, row0}; // 合并移位 {row1_d2, row1_d1} {row1_d1, row1}; {row2_d2, row2_d1} {row2_d1, row2}; end end // 9像素窗口 wire [7:0] p00 row0_d2, p01 row0_d1, p02 row0; wire [7:0] p10 row1_d2, p11 row1_d1, p12 row1; wire [7:0] p20 row2_d2, p21 row2_d1, p22 row2; // 三组三点排序 wire [7:0] max0, mid0, min0; sort3 u_s0(.a(p00), .b(p01), .c(p02), .max(max0), .mid(mid0), .min(min0)); wire [7:0] max1, mid1, min1; sort3 u_s1(.a(p10), .b(p11), .c(p12), .max(max1), .mid(mid1), .min(min1)); wire [7:0] max2, mid2, min2; sort3 u_s2(.a(p20), .b(p21), .c(p22), .max(max2), .mid(mid2), .min(min2)); // 三个中值再取中值 wire [7:0] final_mid; sort3 u_s3(.a(mid0), .b(mid1), .c(mid2), .max(), .mid(final_mid), .min()); // 输出一拍延迟valid同步 always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout 8d0; valid_out 1b0; end else begin dout final_mid; valid_out valid_in; end end endmodule这段代码把整个中值滤波主体串起来了。行缓存延迟两行列缓存延迟两拍组合逻辑计算出中值输出寄存器打一拍对齐时序。注意valid_out也必须打一拍和dout保持同步——否则下游模块会采到无效数据。sort3的max和min接口在最后一组排序中悬空这没问题综合工具会自动优化掉无用逻辑。参数说明行宽1920和位宽8是默认值你如果做640x480的VGA图像把WIDTH改成640即可。位宽如果从8bit灰度换成10bit或12bit的RAW图把DW改掉所有sort3比较器的输入位宽会自动跟随。这个模块在Xilinx 7系列上综合后纯组合逻辑路径大约5-6ns能轻松跑在100MHz-150MHz。如果你要上200MHz需要做流水线分割——这是后面要讲的重点。3. 从组合逻辑到流水线把100MHz瓶颈推到200MHz3.1 为什么纯组合逻辑跑不快关键路径在排序网络里上面贴的代码是纯组合逻辑计算中值dout直接由final_mid打了一拍输出。但真正的关键路径不在输出寄存器而在组合逻辑本身从窗口像素进入sort3到final_mid稳定输出中间经过了两级比较器每一级比较器内部又可能被综合工具展开成多级LUT。8bit比较器如果用运算符综合器通常会把它变成4-6级LUT链因为8bit比较本质上是逐位比较有进位链。两条sort3级联关键路径大约是10-12级LUT再加上布线延迟在7系列FPGA上跑到150MHz已经是极限。如果你想处理4K60fps——像素时钟大约594MHz——这远远不够。解决办法就是流水线化把两级排序拆开中间插入寄存器让每一级只做一个比较器的延迟。这样时钟频率上限直接翻倍。流水线化的代价是输出延迟增加一个时钟周期但吞吐量不减。对图像处理来说流水线延迟几个周期完全无所谓因为像素是连续流入的只要valid信号跟着打拍下游就会自动对齐。下面是一个流水线版的三点排序器我把两级比较拆成了两个时钟周期完成。// 流水线三点排序器2级延迟适合高频时钟 module sort3_pipe( input wire clk, input wire rst_n, input wire [7:0] a, input wire [7:0] b, input wire [7:0] c, output reg [7:0] max, output reg [7:0] mid, output reg [7:0] min ); reg [7:0] ab_min_r, ab_max_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ab_min_r 8d0; ab_max_r 8d0; max 8d0; mid 8d0; min 8d0; end else begin // 第一级比较a和b ab_min_r (a b) ? a : b; ab_max_r (a b) ? b : a; // 第二级与c比较得出最终结果 max (ab_max_r c) ? ab_max_r : c; min (ab_min_r c) ? ab_min_r : c; mid (ab_max_r c) ? ((ab_min_r c) ? ab_min_r : c) : ab_max_r; end end endmodule这个模块用了两个寄存器级。第一个时钟沿采样a、b的比较结果第二个时钟沿再与c比较得出完整的三点排序。注意第二级的输入用的是ab_min_r、ab_max_r寄存器输出而不是第一级组合逻辑的毛刺——这就是切割关键路径的核心每一级之间用寄存器隔离。把顶层模块里的sort3全部换成sort3_pipe其余逻辑不变整体流水深度变为行缓存不变列缓存不变排序网络从2级组合变成4级寄存输出寄存器再打一拍总延迟比原来多3拍左右。3.2 行缓存是否也要流水线化Block RAM的读写延迟坑做了排序网络流水线化之后新瓶颈很容易转移到行缓存上。如果你用移位寄存器组做行缓存延迟恰好是1个时钟周期没有任何问题。但如果换用Block RAM或FIFO原语读写延迟可能有2-3个周期并且不同厂商的FIFO读延迟不一致——写入数据后要等几个周期才能在读端口看到数据。这个延迟如果不补偿行缓存输出的row0、row1、row2在时间上就错位了整个窗口采集到的像素根本不在同一列。我踩过的坑用Xilinx FIFO的First-Word Fall-Through模式读延迟是0但那是“看起来是0”实际上第一个字在写进去之后立即可见后续数据每个时钟沿更新这没问题。用Standard模式读延迟是1拍你需要把读使能提前一拍拉高。如果你把换FIFO后的行缓存直接接到原有列缓存上画面上会出现锯齿状边缘——那就是行列错位了。解决办法是把列缓存的级数从2拍增加或减少补偿FIFO的额外延迟。比如Standard模式FIFO多了1拍读延迟列缓存就从2拍减少到1拍这样窗口内仍然是对齐的。更稳妥的方案是给所有行都经过统一的延迟链——即不用3个不同的FIFO而是用一个FIFO延迟两行再用寄存器延迟若干拍让三条路径的总延迟完全一致。我一般建议初期验证用移位寄存器组资源够用且不会出时序问题等算法稳定了、要考虑资源优化时再换FIFO换的时候务必用testbench检查窗口数据对齐关系不要直接上板。3.3 关键路径优化技巧位宽缩减与比较器结构选择如果你追求极致频率排序网络的比较器还有优化空间。8bit比较器在FPGA上如果写成(a b) ? a : b综合工具会生成一个减法器结构——即先算a-b再看符号位。减法器有进位链8bit大约8级LUT延迟。更好的写法是拆成高位比较和低位比较两级但这并不能减少延迟反而增加了逻辑量。真正有效的手段是直接给比较器加输出寄存器并利用FPGA的专用进位链。Xilinx 7系列的LUT有专用的快速进位链综合工具会自动把比较器映射上去所以不要手动优化比较器内部的逻辑结构。你该做的是用(* use_dsp no *)或类似的综合属性把比较器保持在LUT中不要让它进入DSP48。比较器带DSP是资源浪费而且延迟不一定更小。如果你的位宽超过12bit比如做16bit的医学图像建议把排序网络拆成两级先排高8位只在高位相等时才比较低位。这样可以减少大部分比较器的有效位宽。但代价是逻辑复杂度上升。我的倾向是12bit以内直接用全位宽比较器流水线切割后频率足够。4. 参数设计与彩色图像适配不止是改一个位宽的事4.1 窗口尺寸由噪声密度决定为什么3x3不够时要上5x53x3中值滤波能去掉单像素椒盐噪声但当噪声密度超过10%时窗口内可能出现两个或更多噪点3x3的中值就有概率落在噪点上。此时需要5x5或7x7窗口。5x5窗口需要缓存4行行缓存数量翻倍排序网络从9个数变25个数——全并行排序网络直接做25输入比较器数量会是上百个级联组合逻辑延迟爆炸。工程上的做法是5x5窗口不直接排25个数而是先做行方向的5点中值再做列方向的5点中值。这是“十字形中值滤波”效果和全窗口有差异但在硬件上只需要两阶段排序比较器数量少一个数量级。如果你的算法允许这种近似我强烈建议用十字形。如果必须严格5x5中值可以用奇偶分解的排序网络但那属于另一个层面的研究了。参数选择上我的经验公式是窗口边长取噪声块的最大尺寸加1。比如脉冲噪声如果是单像素孤立点窗口3x3足够如果噪声粘连成2x2的块窗口至少要5x5。代价是分辨率损失——5x5窗口会让边缘额外模糊两个像素在文字识别场景里会很致命。因此窗口不是越大越好选刚好能覆盖噪声尺寸的最小奇数值。4.2 位宽适配8bit灰度到10bit/12bit RAW图的统一处理图像传感器输出的RAW图通常是10bit或12bit特别是CMOS传感器在高增益模式下的噪声处理必须保留足够位宽否则量化误差会重新引入伪轮廓。中值滤波对位宽的敏感度低于均值滤波——中值不受极端值影响量化阶梯带来的误差不会像求和那样累积但这不意味着你可以随便截位。如果你把12bit数据直接截成8bit再滤波等于提前丢失信息。正确做法是保持全位宽滤波输出再按需求截取或映射。上面代码里的DW参数就是干这个的从8改成12所有寄存器和比较器宽度自动扩展。需要注意的是行缓存的内存消耗和位宽成正比12bit比8bit多50%的存储开销。代码里唯一需要额外处理的是line_buffer里的DEPTH WIDTH * DW——12bit x 1920 23040bit约23K寄存器。如果在Zynq-7020上这会占将近一半的Slice。这个规模还是可控的。如果上4K行宽3840x12x4行184K寄存器直接爆掉。这时必须换FIFO/BRAM实现。4.3 彩色图像YUV分量复用还是RGB独立滤波RGB三通道各自做中值滤波一个模块例化三份资源乘3。虽然直观但有个生理学问题人眼对亮度的噪声最敏感对色度的噪声容忍度更高。更聪明的做法是把RGB转YUV只对Y分量做中值滤波UV分量做均值甚至不滤波。这样资源只增加一个色彩空间转换器滤波模块维持一份。在Verilog里做RGB转YUV需要乘法器8bit全精度转换需要3个乘法器和若干加法器。在FPGA上可以用移位加法近似但因为涉及彩色图像我不建议过度压缩精度——色度偏移比噪声更难看。如果做实时HDMI输入处理带宽允许的话直接三通道独立滤波最省心逻辑简单且不会引入色偏风险。我的建议是监控或工业视觉场景用RGB独立滤波保证每通道都有降噪消费级视频后处理用YUV方案只对Y滤波。下面的代码实现Y通道单独滤波时需要把Y从像素流里抽取出来滤波后再合并回UV。也就是在顶层把像素拆开、滤波、重组这个流程在数据通路上是直通的。// 彩色图像Y通道中值滤波的顶层数据流 // 输入像素为YCbCr422一个时钟进来一个Y和一个C分量 module y_median_filter( input wire clk, input wire rst_n, input wire [7:0] y_in, // Y分量 input wire [7:0] c_in, // Cb或Cr偶奇交替 input wire valid_in, output wire [7:0] y_out, output wire [7:0] c_out, output wire valid_out ); // Y通道进3x3中值滤波 wire y_filtered; median_filter3x3 u_mf( .clk(clk), .rst_n(rst_n), .din(y_in), .valid_in(valid_in), .dout(y_filtered), .valid_out(valid_out) ); // 色度通道打同样拍数的延迟链保持同步 reg [7:0] c_delay; always (posedge clk or negedge rst_n) begin if (!rst_n) c_delay 8d0; else c_delay c_in; end assign y_out y_filtered; assign c_out c_delay; endmodule这里注意一个细节valid_out来自中值滤波模块它内部已经有输出寄存器的延迟。色度通道的c_delay必须打同样拍数——这里我打了一拍因为median_filter3x3的输出来自输出寄存器等效延迟1拍。如果你把中值滤波改成2级流水线这里的c_delay就要改成2级。延迟链拍数必须和滤波器的流水深度严格一致这是彩色图像处理里最常见的时序坑——Y和C对不齐画面会出现颜色错位比噪声更刺眼。5. 中值滤波Verilog实战避坑5个最容易翻车的地方5.1 窗口数据错位画面出现斜向条纹现象是滤波后的图像边缘有锯齿状斜纹像是窗口“扫歪”了。原因几乎都是行缓存和列缓存的延迟不匹配。检查方法很简单在testbench里输入一个竖向跳变的图像——比如左半屏全黑右半屏全白——观察窗口内9个像素是否在水平方向对齐。如果p02和p22之间的相位关系不对就是行缓存延迟出了问题。解决办法不要凭感觉调列缓存级数。写一个断言在仿真里检查p00、p01、p02是否来自同一行且相邻列。具体做法是给line_buffer的输出加一个row_tag位每个像素进来时打上行号标记然后检查窗口内的行号是否连续。5.2 复位后前两行输出全黑行缓存的初始化问题现象是图像顶部出现两行黑边或白边然后才恢复正常。原因是行缓存在复位后全部清零但前两行数据尚未填满窗口内出的是无效零值。中值滤波器对零值不敏感吗不零值是有效的极值它会参与排序。解决办法不是改滤波器而是门控输出使能。在计数器中统计已输入的像素行数只有填满2行后才开始输出滤波结果。通常在行同步信号有效后用行计数器和像素计数器联合判断。注意不要只等行计数器——如果你是连续无消隐的像素流必须自己维护行状态否则会出现在前两个有效行内就启动滤波导致两条黑线。5.3 排序结果在中值附近抖动比较器输入存在X态现象是仿真波形里final_mid在某些时钟沿出现不定值X。原因是sort3的输入在复位期间为未知高阻态或X组合逻辑传播导致输出X。这在功能仿真里经常翻车但上板后反而正常——因为FPGA上电后寄存器会初始化为0或1不是X。解决办法testbench的复位信号释放前不要使能valid_in。或者在sort3的输入端口接一个复位抹零的寄存器。最保险的是把顶层模块里所有寄存器的复位值都设置成全0并且复位时间至少拉长10个时钟周期确保所有流水级都被清干净。5.4 资源占用比预期高出一倍比较器被综合成了除法器现象是综合报告里DSP48和LUT的消耗量异常偏高。原因是写比较器时不小心用了/或%运算符——比如把中值计算写成(abc)/3这种均值公式。另一个可能sort3里的嵌套三元表达式写法不规范综合工具无法识别为比较器网络生成了查找表ROM。解决办法综合后查看原理图确认比较器被映射为LUT而不是乘法器或ROM。代码里避免在排序网络附近使用任何算术运算符只用关系运算符、和三元选择。同时加一行综合属性(* keep true *)标注关键比较器信号防止被优化掉或改变实现方式。5.5 时序收敛不了流水线切了但关键路径还在排序网络里现象是时序报告中WNS为负路径定位到sort3的输出端口。原因是顶层模块换了流水线sort3_pipe但行缓存和列缓存的数据路径没有同步加寄存器——也就是说p00到sort3_pipe的路径上有大量布线延迟因为p00来自行缓存的长路径。解决办法在窗口捕获和排序网络之间插一级寄存器。也就是把p00~p22先打一拍再送进sort3_pipe这样输入侧的时序彻底隔离。代价是输出多1拍延迟但这一拍对于吞吐量没有影响。我一般会在顶层的数据通路里预留这级寄存器便于后期调整时序。6. 进阶验证方法testbench里拷问你的中值滤波模块写testbench验证中值滤波最大的误区是随机输入、肉眼比对波形。正确的做法是构造可预测的图案配合Python或MATLAB生成的参考模型做逐像素比对。我习惯构造三类测试图案单像素脉冲、水平边缘、渐变斜线。单像素脉冲验证去噪效果——输入一个全0背景上单个255的亮点滤波后这个亮点应该被周围0值“吃掉”。水平边缘验证保边性——左半屏0右半屏255窗口内的中值在边缘处应该是255或0不会出现中间灰度。下面给出一个参考用的testbench框架。它用$readmemh读入像素激励滤波结果写入文件之后导入Python和软件中值滤波输出做差分比对。// 中值滤波testbench输出逐像素比对用数据 timescale 1ns / 1ps module tb_median_filter; reg clk 0; reg rst_n 0; reg [7:0] din 8d0; reg valid_in 0; wire [7:0] dout; wire valid_out; // 待测模块 median_filter3x3 u_dut( .clk(clk), .rst_n(rst_n), .din(din), .valid_in(valid_in), .dout(dout), .valid_out(valid_out) ); // 时钟10ns周期 always #5 clk ~clk; // 激励读取 reg [7:0] mem [0:9999]; integer i; integer fd; initial begin // 读取测试图像假设为100x100灰度图 $readmemh(test_image.hex, mem); // 复位 rst_n 0; #20; rst_n 1; #10; // 输出文件 fd $fopen(filtered_out.hex, w); // 送入像素流 for (i 0; i 10000; i i 1) begin (posedge clk); din mem[i]; valid_in 1; end // 收尾等流水线排空 repeat(10) (posedge clk); valid_in 0; #50; $fclose(fd); $finish; end // 监视输出并写入文件 always (posedge clk) begin if (valid_out) $fwrite(fd, %02x\n, dout); end endmodule这个testbench的局限在于10000个像素太小而且没有处理行同步信号——valid_in是连续拉高的相当于一张无消隐区的裸图像流。实际使用时你需要按行构造数据每行100个像素行与行之间插入行消隐并用行计数器通知滤波器。如果你拿这个testbench直接测带行缓存的模块会发现在前两行输出窗口数据根本不对。正确做法是激励里包含行同步标志模块里也把行同步纳入状态控制。验证的最终指标是峰值信噪比。把testbench输出的filtered_out.hex读进Python与原始图像和加噪图像分别计算PSNR。中值滤波的PSNR通常比均值滤波高3-5dB并且边缘能量保留率更高。我在实际项目中会额外用主观评价——把滤波前后图像并列显示观察边缘和纹理区域的细节保留程度。客观指标达标但主观发糊说明窗口选大了反过来说PSNR不高但主观干净可能是噪声和细节的权衡适合这个场景。还有一个容易被忽略的验证点奇偶行交错。如果你用的图像传感器是隔行扫描或Bayer RAW输出行缓存的延迟必须考虑两行之间的相位——有些CMOS传感器输出的有效像素会有1个像素的偏移这会让长宽比为1920x1080的行宽参数差1个数。我会在testbench里故意使用1921个像素的“错误行宽”确认模块输出发生错位从而验证宽度参数是否真的被正确配置。这个习惯救了我两次——一次是行宽算错一位一次是奇偶场接反。中值滤波Verilog实现看起来是一个“排序电路”的小事但它串起了行缓存、流水线、时序收敛、测试验证的整条链路——比均值滤波多走的每一步都会在仿真和时序报告里给你找出来。我不能说这套方案在所有场景下都最优但至少对绝大多数实时图像降噪任务它比去噪神经网络更可控、延迟更低、资源可预测。希望这篇笔记能帮你少走几步弯路。本文还有配套的精品资源点击获取