简介:频率域滤波是图像处理中的经典方法,通过傅里叶变换将图像分解为低频与高频分量,再对频域谱进行修改,从而完成平滑、去噪等操作。一份基于OpenCV的示例工程面向图像处理初学者和OpenCV使用者,以高斯、理想、巴特沃斯三种低通滤波器为主线,展示从离散傅里叶变换、滤波核构造、频谱修改到逆变换与归一化的完整过程。包内共8个文件,一个cpp源文件为核心代码,附有dsp、dsw、ncb、plg、opt等Visual C++工程文件,可快速用VC编译运行;两幅bmp测试图像方便观察滤波前后的效果,整个压缩包仅86KB,非常轻量。代码中对三种滤波器的截止频率和过渡特性做了差异化处理,便于对比高斯滤波的平滑效果、理想滤波器陡峭截断可能引发的振铃现象,以及巴特沃斯滤波器的平坦过渡带表现。通过该示例,读者能直观理解频率域滤波与空间域滤波的关系,掌握OpenCV中DFT相关函数用法;已有2149人学习下载,适合作为频域低通滤波教学演示、图像去噪实验或OpenCV二次开发的参考。
1. 频率域滤波不是炫技:OpenCV 里高斯、理想、巴特沃斯到底解决什么问题
在工业视觉里,我常被问:频率域滤波能做的,cv2.GaussianBlur 十行就做完了,何必折腾?这个问题成立,直到你碰见摩尔纹、周期性噪点、玻璃划痕这些“有规律”的噪声——空间域里你要为它们手工设计一个几十像素宽的卷积核,而频率域里只需要在频谱图上找到对应亮点,用高斯、理想、巴特沃斯三种滤波器之一,把那个频率“摘掉”。这份用 OpenCV 实现的频率域滤波方案,核心链路只有三步:cv2.dft 把图像送到频域,按传递函数整形频谱,cv2.idft 逆变换回来。它适合做图像平滑、去周期噪声、细节增强,依赖只有 opencv-python 和 numpy,新手照着步骤能跑通,熟手可以直接抄参数和避坑经验。
2. 用 cv2.dft 把图像送进频域:从灰度图到居中频谱的完整数据流
2.1 为什么一定要 fftshift:低频不在中心,滤波器就没法设计
OpenCV 的 cv2.dft 做完正变换后,低频分量(直流 DC)默认放在矩阵的四个角上,而不是中心。这不是 bug,而是 DFT 数学定义里零频在第一个样本位置的直接结果。于是你会看到一张“四个角各有一团亮斑”的频谱图,低频在角落、高频在中间,这和我们脑子里“中心是低频、往外是高频”的直觉完全相反。
工程上做频率域滤波时,几乎所有滤波器都以“中心为圆心、D0 为半径”来构造,比如理想低通的圆形区域、高斯低通的指数衰减。如果你不先把频谱中心化,滤波器的圆心就得挪到四个角上,一个完整圆盘被切成四块分布在角落,边界处的处理会非常别扭,代码也难读。所以标准做法是补一步 np.fft.fftshift。
这里有个容易搜错的关键点:中心化函数是 numpy 的,不是 cv2 的。OpenCV 本身没有提供 fftshift/ifftshift 接口,网上很多教程直接写 np.fft.fftshift(dft),你搜“opencv fftshift”反而找不到结果。它做的事情很简单:把矩阵的前半行和后半行互换,再把前几列和后几列互换,纯粹是数据重排,不改变任何数学值。
对偶数尺寸图像,fftshift 后中心和几何中心完全重合;对奇数尺寸图像,中心点会偏移不到一个像素,实际构造滤波器时用 rows//2 和 cols//2 来定位即可,肉眼和绝大多数应用都察觉不到这零点几个像素的偏差。整套流程里 shift 是出现频率最高的翻车点,后面避坑章节还会专门讲。
2.2 最小数据流:从灰度图到居中频谱,代码与参数说明
先装好依赖:pip install opencv-python numpy。下面的代码把一张灰度图读进来,转 float32,做 DFT,再把频谱中心化并输出一张可视化用的幅值谱。
import cv2 import numpy as np # 1. 灰度图 + 转 float32,cv2.dft 只接受浮点输入 img = cv2.imread("board_gray.png", cv2.IMREAD_GRAYSCALE) img_f = img.astype(np.float32) # 2. 填充到“DFT 友好尺寸”,提高计算效率,也降低周期延拓的突变程度 rows, cols = img_f.shape nrows = cv2.getOptimalDFTSize(rows) # 返回包含 2、3、5 因子的高效尺寸 ncols = cv2.getOptimalDFTSize(cols) img_pad = np.zeros((nrows, ncols), np.float32) img_pad[:rows, :cols] = img_f # 3. 正变换 + 频谱居中 dft = cv2.dft(img_pad, flags=cv2.DFT_COMPLEX_OUTPUT) dft_shift = np.fft.fftshift(dft) # 4. 幅值谱可视化,注意这只是用来“看”的,不要拿它做滤波 mag = cv2.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]) mag_log = np.log(1.0 + mag) # 压缩超大动态范围 mag_vis = cv2.normalize(mag_log, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) cv2.imwrite("spectrum.png", mag_vis)第三步的 cv2.dft 输出的不是单通道矩阵,而是双通道——第 0 通道是实部,第 1 通道是虚部。这是复数频谱的存储方式,后面做滤波时也要记住这个布局。flags=cv2.DFT_COMPLEX_OUTPUT 就是告诉 OpenCV 按复数格式输出,如果不传这个 flag,默认行为在不同版本里会有差异,建议总是显式写上。
第二步先做 padding,主要为了性能:cv2.getOptimalDFTSize 返回的尺寸对 FFT 算法更友好,通常由小质数因子组成。另一个更实际的原因是,DFT 假设图像是无限周期延拓的,图像边界在延拓时会出现强度跳变,频谱里会多出一条与边界垂直的亮线,padding 能稍微缓解这种边界泄漏。padding 区域本身是黑边,后续滤波完成后需要裁剪掉,否则恢复图四周会有一圈黑框。
第四步的 log(1+mag) 是频谱可视化的标准操作:原始频谱里中心直流分量的值可能是四周高频分量的几万倍,直接显示只有中心一个白点,取对数后才能看到从中心向外辐射的纹理结构。注意,可视化时取对数没问题,但如果把取过对数的频谱拿去做滤波再 idft,结果会完全错乱——对数是非线性变换,只用于显示。
提示:如果只是验证流程,不追求速度,可以跳过 getOptimalDFTSize,直接在原始尺寸上做 dft。滤波器尺寸跟着原始尺寸走,代码更简单,但频谱边缘的泄漏会比 padding 后明显一些。
2.3 频谱亮斑和空间周期的对应关系:判断 D0 用的直觉
做频率域滤波最值钱的能力不是写代码,而是“看懂频谱图”。周期为 T 像素的条纹,在频谱上会产生一对对称亮斑,这对亮斑到中心的距离大约是 N / T,其中 N 是图像宽度。反过来,你看到频谱上在距离中心 16 像素处有一对亮点,说明原图里存在周期约 16 像素的重复纹理。
这个换算关系直接决定滤波器的 D0 取值。比如图像是 256 像素宽,有一组周期 16 像素的摩尔纹,那频谱亮斑离中心约 16 个像素,低通滤波器只要把 D0 设在 8~10,就能把这对亮点压掉,同时保留更靠近中心的真实图像能量。D0 的单位是“像素”,但它衡量的是频率样本的距离,不是什么抽象比例。
另外,对实数图像做 DFT,频谱是共轭对称的——亮点总是成对出现且关于中心对称。这一点可以作为自检工具:如果你的频谱图出现了一个孤零零的亮点,多半是数据读取出了问题,比如把复数频谱当成单通道图像显示,或者 padding 时把图像放歪了。
3. 构造三种低通传递函数:理想、高斯、巴特沃斯的公式与 D0、n 参数
3.1 理想低通滤波器:一行 np.where,但它带来的振铃最凶
理想低通的传递函数是硬截断:距离中心小于等于 D0 的频点全部保留,大于 D0 的频点全部置零。用代码表达就是一句话:
def ideal_lp(shape, D0): rows, cols = shape y, x = np.mgrid[0:rows, 0:cols] D = np.sqrt((x - cols // 2) ** 2 + (y - rows // 2) ** 2) return np.where(D <= D0, 1.0, 0.0).astype(np.float32)np.mgrid 生成坐标网格后,D 就是每个频点到中心的欧氏距离。np.where 把距离小于 D0 的位置填 1、其余填 0,得到一张 0/1 二值圆盘。这里有个隐藏前提:滤波器尺寸应当与 padding 后的频谱尺寸一致,也就是调用时传入 (nrows, ncols),而不是原始图像尺寸。
理想低通在数学上很漂亮,工程上却几乎是“只可演示、不可交付”的典型。原因是频域的矩形窗逆变换到空间域对应的是 sinc 函数,它带有明显的正负旁瓣。用这张二值圆盘去过滤频谱,相当于让图像和 sinc 核做卷积,结果就是图像边缘处出现一圈一圈明暗相间的光晕,专业说法叫吉布斯振铃。D0 越小、截断越狠,振铃越明显。
如果非得用理想滤波器做效果对比,我的习惯是只在演示“频率域能做什么”的时候用,真正要落地去噪或锐化时,一律换高斯或低阶巴特沃斯。振铃不是参数没调好,而是滤波器形状的固有属性,换 D0 只能换振铃位置,不能消除。
3.2 高斯低通:平滑过渡、没有振铃,参数只有一个 D0
高斯低通的传递函数是 H(u,v) = exp(-D² / (2 * D0²))。它的特点是频谱里没有“一刀切”的边界,所有频点都按距离指数衰减,所以不会产生 sinc 旁瓣,振铃基本为零。这也是工业场景默认首选它的原因。
def gaussian_lp(shape, D0): rows, cols = shape y, x = np.mgrid[0:rows, 0:cols] D = np.sqrt((x - cols // 2) ** 2 + (y - rows // 2) ** 2) return np.exp(-(D * D) / (2 * D0 * D0)).astype(np.float32)注意代码里写的是 D * D,这是距离的平方,对应公式里的 D²。很多第一次写高斯滤波器的人会漏掉平方,把衰减写成了 exp(-D / (2D0²)),结果是高频衰减为线性、低频也被过度压掉,平滑效果变得很怪。另一个常见问题是 D 和 D0 的单位混用:D 是像素距离,D0 也必须用像素,不要拿空间域的 sigma 直接填进来。
高斯低通滤波器在数学上和空间域高斯模糊有对偶关系:高斯函数的傅里叶变换仍是高斯函数。所以空间域里 cv2.GaussianBlur 做的事情,本质上就是在频域乘了一个高斯形状的低通滤波器。差别主要在边界处理——OpenCV 空间域卷积默认做反射填充,而频率域是周期延拓,所以同一个参数下两种实现的结果不会像素级一致,但肉眼看差别很小。实际项目里选一条路走通就好,不必来回换算 sigma 和 D0。
D0 的调试起点,我一般取图像短边的 1/20 到 1/10。比如 1024x1024 的图,D0 从 50 开始试;效果不够平滑就加大,平滑过头丢细节就减小。这个范围比固定取 30、50 这种“魔法数字”收敛快得多。
3.3 巴特沃斯低通:用阶数 n 控制过渡带的陡峭度,振铃和保留细节的旋钮
巴特沃斯低通的传递函数是 H = 1 / (1 + (D / D0)^(2n))。它介于理想和高斯之间:n 小的时候曲线平缓,接近高斯;n 大的时候过渡带变陡,逼近理想低通。工程价值在于,你可以通过 n 精确控制“滤波器的锐度”。
def butterworth_lp(shape, D0, n=2): rows, cols = shape y, x = np.mgrid[0:rows, 0:cols] D = np.sqrt((x - cols // 2) ** 2 + (y - rows // 2) ** 2) return (1.0 / (1.0 + (D / D0) ** (2 * n))).astype(np.float32)n 取 1 时,这个公式就是一阶系统最常见的响应形态,幅频曲线和 RC 低通滤波器是同一种形状,衰减非常平缓;n 取 2 时过渡带变陡一些,振铃还不太明显,是我最常用的起点;n 到 4 以上,行为已经接近理想低通,振铃随之明显,基本失去了用巴特沃斯的理由。所以参数组合一般固定为:D0 决定截止位置,n 决定过渡带陡峭程度,先在 n=2 上把 D0 调好,再考虑要不要动 n。
巴特沃斯代码里有个容易被忽略的点:D=0 时,(0/D0)^(2n)=0,分母是 1,所以直流分量增益为 1,这是正确的低通行为。如果 D0 传 0 会导致除零,因此 D0 至少要给 1 以上。
和理想低通相比,巴特沃斯的优势是“留有余地”:在 D0 之外的频点不是立刻归零,而是逐渐衰减,过渡带里的能量仍然部分保留。这对真实图像很重要,因为图像细节和噪声通常不会严格分居两个半径之外,平滑的过渡带能减少细节的突然丢失。
3.4 一张表选型:三种滤波器的行为差异和推荐参数
| 滤波器 | 传递函数 | 过渡带 | 振铃表现 | 推荐参数 |
|---|---|---|---|---|
| 理想低通 | D <= D0 取 1,否则 0 | 最陡,硬截断 | 边缘有明显光晕,严重 | 仅用于效果演示 |
| 高斯低通 | exp(-D²/2D0²) | 平滑,无旁瓣 | 基本无振铃 | D0 = 图像短边/20 ~ /10 |
| 巴特沃斯低通 | 1 / (1 + (D/D0)^(2n)) | 由 n 控制陡峭度 | n>=4 时明显,n<=2 轻微 | D0 同上,n=2 起步 |
补充一个实用细节:上面三个滤波器都以“到中心的距离”为变量,所以天然各向同性。如果你要保留水平方向的纹理、只滤垂直方向,需要把 D 从欧氏距离换成椭圆距离,比如 D = sqrt((dx * scale_x)² + (dy * scale_y)²)。这个扩展在工业瑕疵检测里很常见,但先要把圆形版本跑通再改。
4. 从滤波到恢复:复数乘法、idft 逆变换与结果验证的完整流程
4.1 让滤波器和频谱对齐:尺寸一致、通道扩展、乘法方式
滤波器 H 是单通道二维矩阵,而 dft_shift 是双通道复数矩阵。直接拿 H 去乘 dft_shift 会报维度不一致。常见的处理方式是把 H 在最后一个维度上复制成两通道,让实部和虚部乘同一个增益:
H = gaussian_lp((nrows, ncols), D0=40) # H 是 (nrows, ncols) 单通道,频谱是 (nrows, ncols, 2) 双通道 H2 = np.stack([H, H], axis=-1) # 变成 (nrows, ncols, 2) filtered_shift = dft_shift * H2 # 实部、虚部分别乘同一个滤波器系数这段乘法是逐元素进行的,复数乘法的规则是:实部乘实部、虚部乘虚部,滤波器系数是实数,所以不存在交叉项。H2 的两个通道数值一样,这是“实数滤波器作用于复数频谱”的标准姿势。
也可以用 cv2.mulSpectrums(dft_shift, H2, 0) 达到同样效果,它内部做了复数乘法优化,但要求两个输入都是双通道且类型一致,反而多一层约束。我的经验是直接 numpy 乘法更直观,出问题时也好排查。真正要检查的只有两件事:H.shape 是否等于 dft_shift.shape[:2];H 的数据类型是否和频谱一致,频谱是 float32,H 也要先转 float32。
如果这里出现形状对不上,最常见的原因是滤波器用了原始图像尺寸、频谱用了 padding 后尺寸。写代码时我习惯把 shape 直接传进滤波器构造函数,而不是在函数内部读取 image.shape。
4.2 逆变换、裁剪与归一化:把复数频谱还原成能看的图像
滤波做完后,逆变换的顺序和正变换正好相反:先 ifftshift 把低频从中心挪回四角,再做 cv2.idft,取实部,裁掉 padding,最后归一化到 0~255。下面是一个可以直接复制的完整函数:
import cv2 import numpy as np def frequency_filter(img, H): # img: 单通道灰度图 (uint8 或 float32) # H: 与 padding 后频谱同尺寸的 float32 滤波器 img_f = img.astype(np.float32) rows, cols = img_f.shape nrows = cv2.getOptimalDFTSize(rows) ncols = cv2.getOptimalDFTSize(cols) img_pad = np.zeros((nrows, ncols), np.float32) img_pad[:rows, :cols] = img_f if H.shape != (nrows, ncols): raise ValueError("滤波器尺寸 %s 与填充后频谱尺寸 %s 不一致" % (H.shape, (nrows, ncols))) dft = cv2.dft(img_pad, flags=cv2.DFT_COMPLEX_OUTPUT) dft_shift = np.fft.fftshift(dft) H2 = np.stack([H, H], axis=-1) filtered_shift = dft_shift * H2 filtered = np.fft.ifftshift(filtered_shift) # 关键:把低频移回四角 img_back = cv2.idft(filtered)[:, :, 0] # 取实部通道 result = img_back[:rows, :cols] # 裁掉 padding 黑边 result = cv2.normalize(result, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) return resultnp.fft.ifftshift 这一步很多人会漏掉。fftshift 把低频移到中心,滤波操作在中心坐标系里完成,逆变换前必须再用 ifftshift 把低频搬回四角。如果少这一步,idft 的结果会把频谱的四个象限错位拼接,图像看起来像被切成了四块重新摆放,亮度还可能出现反转。
cv2.idft 返回的同样是双通道复数格式,[ :, :, 0] 取实部,虚部理论上接近 0。DFT 本身没有归一化,正变换乘了 N 倍的缩放会留在 idft 结果里,所以恢复图的像素值范围通常是原始值的几倍大,必须 normalize 到 0~255。也可以给 cv2.idft 传 flags=cv2.DFT_SCALE,让它内部除以 N,但我更习惯先裁剪再手动 normalize,这样可选范围和控制点都更清楚。
注意:调这个函数时,先用 H 全为 1 的滤波器跑一遍,结果应该和原图几乎一致。如果这一步都不对,问题一定出在 shift 或 padding,和滤波器参数无关。
4.3 用一张测试图验证:叠加周期条纹,再把它滤掉
验证频率域滤波最直接的方法,是自己造一张含周期噪声的图。下面生成 256x256 的垂直条纹背景,中间放一个暗色方块模拟真实缺陷:
yy, xx = np.mgrid[0:256, 0:256] base = 128.0 + 40.0 * np.sin(2 * np.pi * xx / 16.0) # 周期 16 px 的条纹 base[64:192, 64:192] = 80.0 # 中间的暗方块 test_img = np.clip(base, 0, 255).astype(np.uint8) # 构造高斯低通,D0 取 8:条纹周期 16px,亮斑约在距离中心 16px 处 H = gaussian_lp((cv2.getOptimalDFTSize(256), cv2.getOptimalDFTSize(256)), D0=8) clean = frequency_filter(test_img, H)周期 16 像素的条纹,在频谱图上对应距离中心约 256/16=16 像素位置的一对亮点。D0 取 8 时,这对亮点落在滤波器截止半径以外,会被显著压制;暗方块的边缘属于宽谱信号,大部分低频能量保留,所以方块轮廓还在,只是边缘过渡变宽了。
跑完看一眼频谱图和滤波后的图,验证标准就两条:条纹是否明显变淡甚至消失;方块边缘是否还清晰可辨。如果条纹还在,说明 D0 太大或滤波器截止不够陡;如果方块边缘变得特别糊,说明 D0 太小,连低频都被砍掉了。造这种合成图的好处是,你明确知道噪声频率在哪,调参不再是盲试。
5. 频率域滤波避坑指南:振铃、黑屏、网格纹与黑边的排查记录
5.1 理想低通滤波后边缘出现一圈一圈的光晕
现象:滤波结果图像里的文字、物体边缘附近出现明暗交替的涟漪,像加了劣质柔焦效果。
原因:理想低通在频域是硬截断的矩形窗,逆变换到空间域对应 sinc 函数核,它有强烈的负旁瓣。图像边缘这种阶跃信号和 sinc 核卷积,就会产生过冲和振铃。D0 越小,截断半径越窄,振铃越往边缘外扩散;D0 越大,振铃越紧贴在边缘上,但不会消失。
解决:不要用理想低通做实际交付。换成高斯低通直接消除振铃,或换巴特沃斯 n=2 明显减弱振铃。如果项目考核点就是理想滤波,那至少要把 D0 取得相对大一些,让截断不那么生硬,同时接受一定程度的边缘光晕。
5.2 高通滤波后整张图变黑,只有轮廓是亮的
现象:按 H = 1 - H_low 构造高通滤波器,滤波结果几乎全黑,只在强边缘处有一层亮线。
原因:H_low 的中心(直流分量位置)是 1,所以高通滤波器的直流增益是 1 - 1 = 0。图像的平均亮度被完全滤掉,所有像素值围绕 0 波动,显示出来自然是一张黑底轮廓图。这不是 OpenCV 的问题,而是高通滤波的数学本质——它把所有平缓信息都归零了。
解决:如果只想增强边缘而不是把图像变成边缘图,用高频提升:把高通结果乘以增益再加回原图,或者更朴素地用非锐化掩蔽 result = cv2.addWeighted(img, 1, img - lowpassed, 1.5, 0)。如果确实需要完整的边缘图做分析,那当前结果反而是对的,直接 normalize 到 0~255 再用即可。
5.3 滤波结果像被切成四块、亮度翻转
现象:逆变换回来的图像被分成四个象限错位拼接,某些区域明暗关系反了,整体看起来像拼图错位。
原因:正变换时做了 np.fft.fftshift,滤波在中心坐标系操作,但逆变换前忘记做 np.fft.ifftshift,直接把中心化的频谱交给 cv2.idft。频谱的象限布局和 idft 期望的布局不一致,导致空间域信号错位。
解决:在 cv2.idft 之前严格执行 np.fft.ifftshift。自检方法是构造一个全 1 滤波器跑完整流程,结果应当和原图一致;一旦出现错位或亮度异常,优先检查 shift 是否成对出现。这个 bug 很难靠肉眼看滤波器代码找出来,最好把正逆变换封装成函数,强制性配对。
5.4 恢复图四周有一圈黑边,或边缘出现亮线
现象:结果图像边缘有一圈明显的黑框,或者图像四条边界出现异常的亮线。
原因:黑边来自 getOptimalDFTSize 引入的 padding 区域没有裁剪干净;亮线来自 DFT 的周期延拓假设。图像左右边界在延拓时被硬生生拼在一起,原本不连续的强度产生了一个虚假的阶跃,频谱里就多出一条十字亮线。
解决:黑边直接裁剪 result[:rows, :cols] 就行。如果裁剪后边缘亮线仍然明显,说明输入图像本身边界太陡,可以在 dft 之前先用 cv2.copyMakeBorder 做反射扩展,或者说在 padding 前把图像边界做镜面延拓,让延拓后的信号周期性更平滑。工业应用里还有一个习惯做法:先对整幅图做轻度的边界平滑,再进频率域。
5.5 彩色图直接调用 cv2.dft 报错,或滤波后色调怪异
现象:对 BGR 三通道彩色图直接调用 cv2.dft 抛异常;拆成三个通道分别滤波再合并后,整张图颜色偏得厉害。
原因:cv2.dft 只接受单通道浮点图或双通道复数图,三通道输入直接报错。彩色图逐通道滤波后,R、G、B 三个通道各自做了独立的归一化,原本的比例关系被破坏,色调自然偏移。
解决:工程上最常见的做法是先转 YCrCb,只对 Y(亮度)通道做频率域滤波,Cr、Cb 保持不变,再合并转回 BGR。因为人眼对亮度细节最敏感,色度通道做滤波既慢又容易产生伪彩。代码如下:
ycrcb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2YCrCb) y, cr, cb = cv2.split(ycrcb) y_filtered = frequency_filter(y, H) # 对亮度通道滤波 res = cv2.merge([y_filtered, cr, cb]) res = cv2.cvtColor(res, cv2.COLOR_YCrCb2BGR)如果项目要求必须在 RGB 空间处理,另一个可用方案是逐通道滤波但不做归一化,只在最后合并时统一做一次全局拉伸。不过这样容易在色度通道里留下振铃伪影,YCrCb 方案明显省心。
6. 把低通改造成高通与带阻:两个实用变体和参数经验
6.1 高通:用 1 - H_low,但一定保住直流分量
高通滤波器的构造公式很朴素:H_hp = 1 - H_low。问题在上面避坑章节提过——这样会把直流归零,结果变成黑底边缘图。做边缘增强时,正确的改造是把直流分量恢复为 1:
H_lp = gaussian_lp((nrows, ncols), D0=20) H_hp = 1.0 - H_lp H_hp[nrows // 2, ncols // 2] = 1.0 # 保住直流,避免整图均值归零这个操作付出的是均匀区域不再全黑,换来的是原图整体亮度被保留、边缘和纹理被叠加突出。想要更强的锐化效果,直接用 result = np.clip(img + 1.5 * (img - lowpassed), 0, 255) 这种非锐化掩蔽写法更直观,增益系数 1.5 可以在 0.5~3 之间调。
6.2 带阻:用高斯环挖掉周期噪声,不对全图动手
周期噪声在频谱里是成对亮点,低通会把整个高频区都压掉,太浪费。更精准的方案是构造一个只围绕目标亮点的阻带环:
def gaussian_bandstop(shape, D0, width): y, x = np.mgrid[0:shape[0], 0:shape[1]] D = np.sqrt((x - shape[1] // 2) ** 2 + (y - shape[0] // 2) ** 2) band = np.exp(-((D - D0) ** 2) / (2 * width ** 2)) return (1.0 - band).astype(np.float32)D0 直接取频谱中亮斑到中心的距离,先用第一步的频谱可视化把它“量”出来,width 从 D0 的 1/4 开始试。这个滤波器对亮斑附近一小圈做压制,其余频段几乎不受影响,是处理摩尔纹和扫描波纹的首选方案。
6.3 参数不是调出来的,是量出来的
我现在处理新图像,第一件事永远是显示频谱,第二件事才动手选滤波器。D0 的单位是频域样本点,不是无量纲的魔法数字。同一张图在不同尺寸下,同一个 D0 效果完全不同,只有先看频谱亮斑的位置,你才知道该滤哪里。这套流程跑熟之后,频率域滤波的“玄学感”会消失大半,剩下的都是可量化的工程判断。希望帮到你。
本文还有配套的精品资源,点击获取