
简介面向FPGA与数字图像处理开发者这份资源提供中值滤波算法的Verilog实现适用于去除椒盐噪声、保留边缘细节的实时图像预处理场景。内容以压缩包形式包含中值滤波器的Verilog工程代码与设计文件另有一个MATLAB脚本用于将图像数据转换为文本格式方便后续仿真与FPGA调试交互整体10KB、共2个文件结构精简适合初学者快速开展算法验证与硬件部署。描述中提到的方法路径较为清晰先借助MATLAB完成中值滤波算法原型设计与仿真再通过HDL Coder自动生成Verilog代码配合FPGA并行流水线、存储器管理实现实时处理有助于缩短从算法到硬件落地的迁移周期。目前已有227人学习代码量不大但涉及数据流组织、边界处理、接口设计等FPGA开发要点可作为入门数字图像处理硬件加速的参考。1. 中值滤波的Verilog实现难的不是滤波而是排序做图像采集卡或者实时视频预处理的时候脉冲噪声椒盐噪声是最让人头疼的干扰之一一幅正常画面里突然冒出来几十个纯白或纯黑的像素点均值滤波会把噪声“抹开”成一片灰斑形态学滤波又对噪声形状敏感唯独中值滤波能把这些孤立噪点直接摘掉几乎不伤边缘。于是很多做FPGA图像链路的工程师都会在某个版本里加入一个“中值滤波Verilog实现”的需求真正动手之后才发现滤波本身连乘法器都用不到难点全在“9个像素在一个时钟周期里挑出第5大的值”这件事上。我之前接过一个实时视频预处理模块客户要求三行三列窗口、每像素时钟出一个结果不能丢帧。一开始以为用几个比较器就能糊弄过去结果在时序收敛和数据对齐上连续翻车最后才把全比较法、排序网络和行列缓存的配合彻底理清。这篇笔记就按我实际做过的方案来讲先讲清楚硬件里中值滤波长什么样再给可直接拿走的Verilog实现和仿真参数最后把那些让综合工具“玄学报错”的坑一个一个说透。2. 先搞清楚硬件里的中值滤波从滑动窗口到排序操作2.1 中值滤波到底在算什么不是平均是排序取中中值滤波的数学定义很简单对目标像素周围邻域内的所有像素值排序取中间那个值作为输出。对应到3×3窗口就是把9个灰度值排好序取第5个。这个操作在软件里用一句“sort然后取中间”就完了在FPGA里却不能这么写——综合工具不会给你生成一个通用的排序电路它只会把你写的比较逻辑翻译成LUT和触发器。换个角度看中值滤波就是三个子问题的组合第一怎么在像素流里形成3×3窗口第二怎么对这9个值做并行排序第三怎么保证排序结果和输入像素流严格对齐不丢一拍、不错一位。其中第二个问题是核心前两个问题其实是标准的数据通路设计。在实际硬件实现里我习惯把中值滤波看成“滑动窗口内的排序网络”。排序网络的特点是输入和输出不经过反馈完全是组合逻辑展开只要给定9个输入输出就是排序后的序列。这意味着中值滤波的延迟可以做到固定拍数非常适合视频流这种每时钟周期进一个像素的场景。2.2 窗口怎么形成三行缓存与列对齐是关键先解决数据来源的问题。视频流通常是一个像素接一个像素串行进来按行扫描。要拿到当前像素左上、正上、右上这些邻域值必须先把整幅图像的前面两行缓存住。常见做法是用两个行缓存FIFO每个FIFO深度等于图像宽度第1个FIFO存第N-1行输入是当前像素流第2个FIFO存第N-2行它的输入来自第1个FIFO的输出当前像素、第1个FIFO输出、第2个FIFO输出三者按同一个节拍取数就凑成了三行同一列的数据。然后把这3个列数据分别和它前两个周期的列数据拼接就得到3×3窗口的9个像素。用Verilog描述时窗口里的9个寄存器我习惯命名为v0 ~ v8其中v4是当前中心像素v0 v1 v2 v3 v4 v5 v6 v7 v8这9个信号必须来自同一个时钟节拍否则排序网络算出来的中值对应的窗口位置就错了。每来一个有效像素9个值整体向左上平移一次这就是滑动窗口的硬件本质。2.3 三种中值提取方案选型全比较法、排序网络、近似算法拿到9个像素之后怎么取中值我实际对比过三种方案方案比较器数量逻辑级数资源占用适用场景全比较法计数选通369选22~3级最低适合9个输入3×3窗口首选冒泡排序网络每轮9路交换约4轮4~5级中等寄存器多需要同时输出多个排序值时双调/奇偶归并排序网络约19~24个比较交换器3~4级较高窗口扩大到5×5之后更合适近似中值行排序取中9个以内2级最低对精度要求不高的降噪预处理全比较法的思路非常直接对9个值两两比较统计每个值比其它几个值大的次数。因为9个值各不相同或者有相等次数为4的那个值就是中值。这个方案在9输入时最省资源逻辑级数也浅是3×3中值滤波里最成熟的工程做法。排序网络则更适合“我需要的不只是中值还有最大值、最小值”这种场景比如后面接自适应阈值分割时。如果只滤波排序网络属于杀鸡用牛刀不仅寄存器多时序收敛也更费劲。近似算法就是网上常见的“三行分别取中值再取中值”——我明确说这不是严格3×3中值滤波输出有概率和真实中值不一样。除非你的算法验证阶段就接受了这种近似否则别用在医疗影像或工业检测这类对像素一致性要求高的地方。3. 全比较法实现3×3中值滤波模块代码与参数说明3.1 模块接口与时序约定下面这段代码是我在实际视频预处理模块里用过的简化版本去掉了行缓存FIFO只保留核心的窗口数据输入和中值输出。这样更容易看懂排序逻辑接真实FIFO时只需要把三个行缓存的输出按节拍接进来即可。module median_3x3_full_compare #( parameter DATA_W 8, parameter IDX_W 4 ) ( input wire clk, input wire rst_n, input wire in_valid, input wire [DATA_W-1:0] v0, v1, v2, v3, v4, v5, v6, v7, v8, output reg [DATA_W-1:0] median_out, output reg out_valid ); // 9个输入两两比较生成36个比较结果 // cmp[i][j] 1 表示 v[i] v[j] reg cmp [0:8][0:8]; genvar i, j; generate for (i 0; i 9; i i 1) begin : cmp_i for (j i 1; j 9; j j 1) begin : cmp_j always (*) begin if (v[i] v[j]) begin cmp[i][j] 1b1; cmp[j][i] 1b0; end else begin cmp[i][j] 1b0; cmp[j][i] 1b1; end end end end endgenerate这里比较结果用二维reg数组来表示。注意比较时v[i] v[j]和v[j] v[i]是互斥的也就是说每一对像素比较结果必定有一个1、一个0这样后面统计“比它大的个数”时每个值的秩之和必然是8不会出现漏计。两个值相等时上面代码会把cmp[i][j]置0、cmp[j][i]置1等于把“大”的定义偏向了后一个输入。这并不影响中值选取的正确性——重复值在窗口里占多个坑位秩为4的那个值依然存在只是如果整个窗口全相等时会选到靠后的输入值本身不变。3.2 统计秩与选通输出核心的5行RTL比较结果生成之后下一步就是统计每个输入“赢过”多少次。// 统计每个值的秩rank[k] 它比多少个其它值大 reg [3:0] rank [0:8]; always (*) begin for (int k 0; k 9; k k 1) begin rank[k] 4d0; for (int m 0; m 9; m m 1) begin if (m ! k) rank[k] rank[k] cmp[k][m]; end end end // 选出秩等于4的那个值就是中值 always (*) begin median_out v4; // 兜底赋值 for (int k 0; k 9; k k 1) begin if (rank[k] 4d4) median_out v[k]; end end这个选通逻辑综合出来是一个多路选择器树9选1大约用8个LUT级别对时序压力很小。rank在0~8之间4位二进制够用。最后打一拍输出保证输出端不和组合逻辑直接相连方便外部时序约束always (posedge clk or negedge rst_n) begin if (!rst_n) begin out_valid 1b0; end else begin out_valid in_valid; end end endmodule整个中值计算部分是纯组合逻辑第一级是比较器第二级是秩统计加法树第三级是选通。以8bit灰度为例合成后在主流FPGA中大约消耗180~240个LUT时序跑200MHz以上没问题。如果综合工具报告时序不收敛优先检查是不是把输出寄存器去掉导致组合逻辑直连到很长的数据通路上。3.3 参数怎么调位宽、窗口尺寸与吞吐率的关系上面模块里暴露的DATA_W是灰度位宽常见8bit如果是12bit或16bit的RAW图像需要把位宽参数改大比较器资源会线性增长但秩统计、选通逻辑几乎不受影响。更值得关注的是吞吐率参数这个模块每时钟周期处理一个像素输出也是每周期一个。换句话说吞吐率等于输入像素时钟频率。处理1080p60fps的视频像素时钟大约148.5MHz所以模块需要能跑到148.5MHz以上处理4K60则大约需要594MHz这时候单模块很难收敛可选的方案是把图像分成四路并行的条带每个条带一个滤波模块。行缓存FIFO的深度跟图像宽度相关。如果只是单独仿真这个核心模块不接FIFO输入可以把9个颜色值直接拼接成72bit总线valid信号约束好就行。我在做行为仿真时通常会把输入写成任务函数随机生成带脉冲噪声的图像逐像素送入模块再和Matlab参考模型对比输出。4. 气泡排序网络与双调网络什么时候值得换一种写法4.1 气泡排序网络适合同时输出最大、最小、中值的场景有一种常见需求是滤波之后紧接着要做阈值分割不仅想要中值还想知道窗口内的最大值和最小值用来估算局部对比度。全比较法虽然也可以读出最大最小但要多加两级树形比较器。这时候换成气泡排序网络反而更直接。气泡排序网络的核心思想是“相邻比较交换”一组数据通过多级比较交换器后大的向一端移动小的向另一端移动。在Verilog里可以全部展开成组合逻辑// 单级交换单元交换 a、b使 min_out 为较小值 function automatic void swap_minmax( input [7:0] a, b, output [7:0] min_out, max_out ); if (a b) begin min_out b; max_out a; end else begin min_out a; max_out b; end endfunction然后按轮次把9个值两两配对第一轮比较 (0,1)、(2,3)、(4,5)、(6,7)(8)等待第二轮比较 (1,2)、(3,4)、(5,6)、(7,8)如此交错进行经过4~5轮之后9个值基本有序。最终sorted[4]就是中值sorted[0]、sorted[8]就是最小和最大值。这种写法比较器数量多但每一级交换都很规整适合FPGA布局布线不容易出现莫名其妙的延迟过大的路径。代价是寄存器资源比全比较法多出两到三倍。我只在“确实要同时用最大最小中值”时选它。4.2 双调排序网络从3×3走向5×5的跳板窗口从3×3扩大成5×5以后输入变成25个值全比较法需要300个比较器综合出来逻辑级数很深时序非常难看。这时候可以用双调排序网络它把一个长度非2的幂的序列补零/复制到2的幂然后分阶段比较交换。双调排序网络里有两个操作bitonicMerge和bitonicSort。在Verilog里通常用generate循环展开成固定级数。5×5窗口取中值先对每行5个值做小规模全排序得到每行的最小值、中间值、最大值然后利用行与行之间的有序性对9个“候选值”做第二轮排序取中。这不是精确的25点中值但很多图像处理论文里把它当快速近似方案用误差通常在1~2个灰度级以内肉眼完全看不出区别。我实际在资源紧张的工程里用过这个近似方案效果能接受。但如果产品规格书里写的是“25点严格中值”那就必须上完整双调网络或者更复杂的基于直方图的方案。直方图法在FPGA上是另一条路线统计窗口内灰度值分布从低到高累加直方图累加到13个像素时对应的灰度就是中值。这个方法在8bit灰度下需要256级累加资源比排序网络多但灰度位宽变大时扩展性好。4.3 排序网络的综合参数级数、位宽、时序优化给一组我常用综合参数参考参数全比较法冒泡网络双调网络比较器数量9输入36约72约40~50组合逻辑级数8bit3级5级4级寄存器数量约100约300约260最大时钟频率7系列级别280MHz220MHz240MHz代码可读性最好一般较差频率数据以8bit灰度、普通speed grade为参考实际值取决于具体芯片和布局。一个经验是如果综合后时序违例不到2ns优先优化组合逻辑级数把比较结果、相等标志用寄存器打一拍不要试图让工具“硬挤”。5. 中值滤波Verilog实现的避坑指南5个常见翻车点5.1 窗口数据没对齐中值结果全是花的现象仿真波形里每个输出像素单独看都对但和参考图像对比时整幅图出现斜向条纹或边缘错位。原因3个行缓存FIFO的读延迟不一致或者列拼接寄存器没有同步打拍。比如第一个FIFO输出的行数据和当前像素流的行数据本来就相差一行如果第二个FIFO深度设错窗口的9个寄存器根本不是同一行同一列的组合。解决严格按“当前像素从FIFO0读出→打入FIFO1”的顺序两个FIFO都用FWFT模式并且在进入窗口拼接模块之前对三路数据各打一拍同步。仿真时用一张有明显的横竖线的测试图逐帧查比对着波形图数丰富多。5.2 FIFO深度设成了2的幂而不是图像宽度现象图像右侧出现一整条竖线错位或者每行的最后几个像素被错误滤波。原因行缓存FIFO的深度必须至少等于一行有效像素数。如果图像宽度是1920配置FIFO深度为2048读指针清零时机不对行尾数据就会残留到下一行开头。解决把FIFO深度写死为“图像有效宽度 4消隐余量”或者在行同步有效信号到来时强制重同步读指针。更稳妥的做法是外部直接把行有效信号接入FIFO的读使能用行同步清零计数器完成行对齐。5.3 相等像素处理出错导致中值偏向异常值现象把滤波器接在字符叠加模块后面字符区域和背景交界处出现一排错误的灰点。原因两个像素相等时如果比较逻辑同时输出cmp[i][j]0且cmp[j][i]0那么两个值的秩都会偏小可能没有任何一个值的秩等于4选通过程掉进兜底赋值输出了固定的v4中心像素导致局部输出抖动。解决比较时保证每对输出互补见3.1节代码。用if (v[i] v[j])代替if (v[i] v[j])让相等时结果偏向固定一侧这样秩总和永远为8选通不会落空。5.4 只验证了功能没验证时序上板后图像偶发闪线现象ModelSim仿真完全正确上板跑一会儿出现单帧闪线或者某个分辨率下固定位置有噪点。原因滤波模块组合逻辑延迟接近时钟周期遇到温度升高或电压波动时序余量不足出现亚稳态偶尔一拍采到错误比较结果。解决把输出寄存器补上组合路径加(* keep true *)保留中间信号用综合工具的时序报告看最差路径落在哪个比较器上。如果还抖把排序逻辑拆成两级流水第一级算前18个比较结果第二级算剩余比较和秩选择中间插入寄存器代价是多一拍延迟换来稳定收敛。5.5 综合工具把排序网络优化没了现象资源报告里LUT数量异常少但仿真结果输出恒等于某个输入值。原因工具发现排序网络的输入输出存在“等价”关系比如某些输入常年恒为0灰度0优化掉了整块逻辑。这属于优化过了头。解决检查激励里是否所有灰度值都有覆盖别只喂全灰图。另外在综合属性里设置max_fanout或者preserve保住关键节点的比较逻辑。这类问题本质上不算bug但排查起来极其耗时间所以测试向量一定要包含随机噪声图。6. 从3×3走向5×5一个你必须知道的中值陷阱与我的选择最后聊一个常见的进阶需求把窗口扩大到5×5让降噪能力更强。很多人的第一反应是“把3×3的中值模块复制5份每行一个最后再取中值”——这个方案我明确告诉你结果不是严格5×5中值滤波。原因很直观5×5窗口有25个值按行分组得到5个行中值这5个行中值的中值并不等于25个值的全局中值。举个例子某行的中值可能整体偏高它依然能挤进最终候选但全局排序里它根本排不到第13位。这个近似在噪声密度低时问题不大噪声密度高时滤波结果会偏亮或偏暗。我实际处理5×5窗口时有两个选择如果芯片资源够就用26输入双调排序网络补一个恒为0的输入凑成16不25个输入补到32成本太高可以直接用非2的幂版本奇偶归并排序网络比较器约160个5级流水跑150MHz没问题。如果资源紧张我会退而求其次用“十字形5×5中值”——只取中心十字上的9个值做中值虽不是完整25点滤波但实时性和资源占用和3×3几乎一致对脉冲噪声的抑制在视觉上也不输完整5×5多少。这块我用过不止一次代价是竖条纹噪声会残留适合对边缘保护要求高的场景不适合强噪声环境。如果做医疗或工业检测类项目我建议严格25点中值别省资源。验证方式是用Python生成一张带2%椒盐噪声的标准测试图分别用完整25点中值和近似方案跑一遍肉眼比对差值和结构相似度你立刻就能看出差异。还有一个工程习惯我会在中值滤波模块前保留原始图像的一路延时对齐通路方便后续做“差值检测”——滤波结果和原图相减差值大的像素就是疑似噪声点这个信息可以送给后面的坏像素校正或压缩预处理。中值滤波模块本身只花几十行代码但它接出来的这条差分通路往往才是整个预处理链路里价值最高的部分。希望这次讲的这些实现细节和踩坑经验能帮你少走一段我当年走过的弯路。本文还有配套的精品资源点击获取