
做图像处理的朋友应该都听过这样一句话拉普拉斯滤波器是一个高通滤波器。道理谁都能讲两句——拉普拉斯是二阶导数导数反映变化变化就是高频。但如果你被追问一句你能不能把它的频率响应画出来让我亲眼看到低频被压到接近零、高频被放大的变化趋势很多人就卡住了。我前段时间做边缘检测实验恰好需要对比均值、高斯、Sobel、拉普拉斯几类滤波器的频域特性。用OpenCV把它们的空间域核和幅频响应图都画了一遍之后之前那种模糊的理解变得非常直观一张图摆在那里谁是低通谁是高通一目了然。这篇内容就把我的完整做法写出来。你会看到怎么用OpenCV生成多种滤波器的二维图像怎么把空间域核转换成频域响应以及最终用图像和数值两条路径证明拉普拉斯就是高通滤波器。适合对图像处理有基础、但想亲手验证和理解滤波器本质的读者。1. 判断一个滤波器是高通还是低通先看它的频谱1.1 从“核元素之和”到直流增益滤波器的空间域实现就是一个卷积核。对一个图像做滤波等价于在频域把图像频谱和核的频谱逐点相乘。卷积定理告诉我们核的频谱决定了哪些频率分量被保留、哪些被衰减。在离散情况下频域响应 H(u,v) 是核 h[m,n] 的离散傅里叶变换。这里有个特别直观的检验指标就是直流增益 H(0,0)它等于对核的所有元素求和。把核元素加起来等于 0 意味着什么说明这个滤波器对恒定灰度区域完全不响应因为恒定灰度区域正是直流分量也就是零频。一个滤波器如果对直流无响应它绝对不可能是低通滤波器。拉普拉斯核的元素和恒为 0这一点几乎不用算就知道正中心 4上下左右各 -1加起来正好是 0。所以它的频谱在正中心必然是一个暗点这个暗点就是高通滤波器的第一张身份证。1.2 为什么二维频域图能一眼分辨需要先处理一个认知问题DFT 算出来的频谱零频分量在数组的四个角上如果不做移位看着非常别扭。习惯做法是用 fftshift 把零频移到数组中心这样频谱图中心代表低频离中心越远频率越高。低频在中心、高频在外围这个布局非常符合直觉就像把一张图像里的“缓慢变化”放在圆心“急剧变化”放在外圈。低通滤波器的响应图就是中心亮、四周暗高通滤波器则正好相反中心暗、外围亮。方向性滤波器比如 Sobel它的响应图不是同心圆而是集中在某个方向上的亮斑和条纹这正好体现了它对特定方向高频的选择性。制作二维响应图的时候有一个坑必须注意频谱的动态范围极大。均值滤波器的直流增益可能是 1而高频分量可能只有 0.001。直接 imshow 的话除了中心一个亮点其余部分全部是黑色。所以显示时必须做 log 缩放也就是计算 log(1 |H|)1 是为了防止 log(0) 导致负无穷。这个小细节在后面代码里会反复出现。2. 用OpenCV把几种常见滤波器的二维图像画出来2.1 先写一个通用的频域响应计算函数核心操作其实很短把卷积核放进一个大零数组的中心对整张数组做 DFT然后 fftshift 并取幅度谱。这一步的关键是把核放在数组中心不能直接拿一个小尺寸矩阵去算。我见过不少新手直接对一个 3x3 的核做 DFT结果频谱一团糟。原因是 DFT 隐含的周期性会把核的边界截断当成跳变产生严重的频谱泄漏。把核放到大零数组中心本质上是为核提供了“背景”让 DFT 计算出的结果更接近连续域的情况也方便后续用图像坐标解读频率位置。代码如下import cv2 import numpy as np import matplotlib.pyplot as plt def compute_frequency_response(kernel, size512): # 将核放到 (size, size) 零数组的正中心 pad np.zeros((size, size), dtypenp.float32) h, w kernel.shape cy, cx size // 2, size // 2 pad[cy-h//2:cy-h//2h, cx-w//2:cx-w//2w] kernel # OpenCV 的 DFT输出双通道实部 虚部 dft cv2.dft(pad, flagscv2.DFT_COMPLEX_OUTPUT) dft np.fft.fftshift(dft, axes(0, 1)) # 取幅值并做 log 缩放否则图上看不清细节 mag cv2.magnitude(dft[:, :, 0], dft[:, :, 1]) mag np.log1p(mag) # 等价于 log(1 mag) return pad, mag为什么用 cv2.dft 而不是 np.fft.fft2项目标题既然点名 OpenCV那我们就用 OpenCV 的接口它也正好是很多工程代码里的标准写法。cv2.dft 和 np.fft.fft2 在数学上等价只是 cv2 这边输入必须是 float32 单通道输出是双通道复数格式。配合 cv2.magnitude取幅度的效率也还可以。实际项目里如果用 OpenCV 的 dft 做频谱分析代码迁移成本会更低一些。2.2 准备低通和高通的典型核需要同时准备低通、高通和方向性滤波器的核才有对比价值。我用四类核均值、高斯、Sobel、拉普拉斯。# 均值滤波核5x5 mean_kernel np.ones((5, 5), dtypenp.float32) / 25.0 # 高斯核5x5sigma1.2 gx cv2.getGaussianKernel(5, 1.2, cv2.CV_32F) gaussian_kernel gx gx.T # Sobel X 方向梯度核3x3归一化 sobel_kernel np.array([ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1] ], dtypenp.float32) / 8.0 # 拉普拉斯核3x3 四邻域版本 lap_kernel np.array([ [0, -1, 0], [-1, 4, -1], [0, -1, 0] ], dtypenp.float32)注意几个细节。高斯核用 cv2.getGaussianKernel 生成一维列向量再与自己外积得到二维核这是标准的可分离滤波核构造方式。Sobel 核除以 8 是为了让梯度响应保持在一个合理的数值范围不影响高通特性。拉普拉斯核我用的中心 4、上下左右 -1 的版本和 OpenCV 内部使用的 [0,1,0;1,-4,1;0,1,0] 只差一个负号频率响应的形状完全一致。2.3 空域核与频域响应的成图效果绘制时我习惯用两行子图第一行是空间域核第二行是对应的频域响应。空间域核使用灰度显示频域响应使用 jet 伪彩色因为伪彩色对微弱变化更敏感。kernels { Mean 5x5: mean_kernel, Gaussian 5x5: gaussian_kernel, Sobel X 3x3: sobel_kernel, Laplacian 3x3: lap_kernel, } fig, axes plt.subplots(2, len(kernels), figsize(14, 7)) for i, (name, k) in enumerate(kernels.items()): pad_img, resp compute_frequency_response(k) axes[0, i].imshow(pad_img, cmapgray) axes[0, i].set_title(Spatial: name) axes[0, i].axis(off) axes[1, i].imshow(resp, cmapjet) axes[1, i].set_title(Frequency: name) axes[1, i].axis(off) plt.tight_layout() plt.show()实际画出来之后均值和高斯的频域响应就是中心一团亮斑向外快速衰减典型的低通形态。Sobel X 的响应明显呈现在水平方向两侧的亮斑说明它主要让水平方向的高频通过垂直方向上保留一定低频方向选择性一目了然。拉普拉斯的响应图正中心是一个接近黑色的暗点向外四圈逐渐变亮像一圈一圈泛开的涟漪——这就是高通滤波器的经典长相。3. 拉普拉斯是高通不止看图还要算得明白3.1 频域响应图里的两个决定性特征只看图可能会觉得“嗯确实有点像高通”但严谨一点最好把证据拆成两条第一条证据是中心暗点。拉普拉斯核所有元素和为 0所以它在零频处的响应是 0。这代表图像中完全平滑的区域经过拉普拉斯滤波后输出为 0灰度不变的区域被彻底滤除。只有存在变化的地方才有非零输出。第二条证据是亮度从中心向外递增。响应强度随着频率升高而增大说明频率越高的分量通过拉普拉斯后得到的增益越大。高频对应图像中的边缘、纹理、噪声所以拉普拉斯能提取边缘也能让噪声变得更刺眼。这不正是高通滤波器的定义吗——对高频敏感对低频免疫。3.2 用离散DFT解析式做数值证明可视化只能说明“看起来像高通”真正让人心里踏实的还得是数学表达式。对 3x3 四邻域拉普拉斯核做离散时间傅里叶变换可以得到解析式。设核为 h[0,0]4, h[±1,0]h[0,±1]-1。它的频率响应是H(u,v) 4 - 2cos(2πu/M) - 2cos(2πv/N)也可以改写成H(u,v) 2(2 - cos(2πu/M) - cos(2πv/N))这个式子就是完整的证据链。当 uv0 时cos(0)1H(0,0)0这就是中心暗点的来由。当频率很低时cos 做二阶泰勒展开1 - x²/2代入后近似得到 H ≈ (2πu/M)² (2πv/N)²。也就是说在低频段响应和频率的平方成正比频率越高增益越大。在最大频率处也就是 uM/2、vN/2 时cos -1H 达到最大值 8。用 MN512 举几个具体的数值点趋势会更清楚坐标 (u, v)频率H(u, v)(0, 0)直流0(1, 0)低频约 0.00015(128, 0)中低频约 2(256, 0)中高频约 4(256, 256)最大频率8从 0 到 8单调攀升这和高通滤波器的定义完全吻合。有人可能会纠结这个“单调”是否严格成立实际上沿着从中心出发的任意径向方向响应都是单调不减的。原因很简单余弦函数在频率从 0 增加到 Nyquist 的过程中从 1 单调下降到 -1而响应是余弦的负系数线性组合。3.3 剖面曲线从中心到边缘的增长趋势数值表格能说明问题但曲线更直观。提取频域响应图的中心行剖面线横轴是离中心的距离纵轴是 log(1|H|)把拉普拉斯和均值滤波器放在一起对比。_, lap_resp compute_frequency_response(lap_kernel) _, mean_resp compute_frequency_response(mean_kernel) # 取中心行右半部分避免左右对称重复 center lap_resp.shape[0] // 2 profile_lap lap_resp[center, center:] profile_mean mean_resp[center, center:] plt.figure(figsize(8, 4)) plt.plot(profile_lap, labelLaplacian 3x3) plt.plot(profile_mean, labelMean 5x5) plt.xlabel(distance from DC (in pixels)) plt.ylabel(log(1 |H|)) plt.legend() plt.show()两条曲线的走势完全相反。均值滤波器的剖面线从中心向两边一路下滑最终贴近 0拉普拉斯滤波器的剖面线从 0 出发一路向上爬升直到边缘两者形成鲜明的镜像对比。这张图放在任何汇报场合都比嘴说“拉普拉斯是高通”有说服力得多。4. 完整示例一个脚本画出全部对比结果4.1 完整代码前面把函数和核分开讲了这里给出一份可以整体运行的脚本。复制到本地或者在线环境跑完就能看到全部的对比图、剖面线和数值输出。import cv2 import numpy as np import matplotlib.pyplot as plt def compute_frequency_response(kernel, size512): pad np.zeros((size, size), dtypenp.float32) h, w kernel.shape cy, cx size // 2, size // 2 pad[cy-h//2:cy-h//2h, cx-w//2:cx-w//2w] kernel dft cv2.dft(pad, flagscv2.DFT_COMPLEX_OUTPUT) dft np.fft.fftshift(dft, axes(0, 1)) mag cv2.magnitude(dft[:, :, 0], dft[:, :, 1]) mag np.log1p(mag) return pad, mag mean_kernel np.ones((5, 5), dtypenp.float32) / 25.0 gx cv2.getGaussianKernel(5, 1.2, cv2.CV_32F) gaussian_kernel gx gx.T sobel_kernel np.array([ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1] ], dtypenp.float32) / 8.0 lap_kernel np.array([ [0, -1, 0], [-1, 4, -1], [0, -1, 0] ], dtypenp.float32) kernels { Mean 5x5: mean_kernel, Gaussian 5x5: gaussian_kernel, Sobel X 3x3: sobel_kernel, Laplacian 3x3: lap_kernel, } fig, axes plt.subplots(2, len(kernels), figsize(14, 7)) for i, (name, k) in enumerate(kernels.items()): pad_img, resp compute_frequency_response(k) axes[0, i].imshow(pad_img, cmapgray) axes[0, i].set_title(Spatial: name) axes[0, i].axis(off) axes[1, i].imshow(resp, cmapjet) axes[1, i].set_title(Frequency: name) axes[1, i].axis(off) plt.tight_layout() plt.show() _, lap_resp compute_frequency_response(lap_kernel) _, mean_resp compute_frequency_response(mean_kernel) center lap_resp.shape[0] // 2 profile_lap lap_resp[center, center:] profile_mean mean_resp[center, center:] plt.figure(figsize(8, 4)) plt.plot(profile_lap, labelLaplacian 3x3) plt.plot(profile_mean, labelMean 5x5) plt.xlabel(distance from DC (in pixels)) plt.ylabel(log(1 |H|)) plt.legend() plt.show() print(Laplacian kernel sum:, np.sum(lap_kernel)) print(Mean kernel sum:, np.sum(mean_kernel)) print(Laplacian DC response:, lap_resp[center, center]) print(Mean DC response:, mean_resp[center, center])运行后会输出Laplacian kernel sum: 0.0 Mean kernel sum: 1.0 Laplacian DC response: 0.0 Mean DC response: 0.6931472说明拉普拉斯核的和为 0直流响应为 0均值核的和为 1直流响应经过 log1p 后是 log(2)≈0.693。这一组数据本身就是“拉普拉斯抑制低频”的铁证。4.2 输出结果解读图的第一行是空间域核第二行是频域响应。从右往左看拉普拉斯的频域响应中心是全图最暗的区域色调从中心向外由深蓝过渡到亮黄说明增益随频率递增。均值和高斯则是中心一大片亮黄色边缘迅速变暗。Sobel X 的响应别有趣味。它的亮斑不在正中心而是在水平方向两侧说明它对 x 方向的高频分量敏感对 y 方向的平坦区域响应较弱。这是带方向性的高通滤波器和拉普拉斯这种各项同性的高通滤波器有本质区别。4.3 换一种滤波器验证结论是否成立单一核容易让人担心是特例。稳妥起见可以换 5x5 的拉普拉斯核、带对角线的八邻域拉普拉斯核甚至换高斯拉普拉斯算子再画一遍。# 5x5 拉普拉斯近似核 lap_5x5 np.array([ [-1, -1, -1, -1, -1], [-1, -1, -1, -1, -1], [-1, -1, 24, -1, -1], [-1, -1, -1, -1, -1], [-1, -1, -1, -1, -1] ], dtypenp.float32) / 8.0 # 八邻域拉普拉斯 lap_8 np.array([ [-1, -1, -1], [-1, 8, -1], [-1, -1, -1] ], dtypenp.float32)这两个核的元素和也全为 0画出来的频域响应图依然是中心暗、四周亮。区别只在增益曲线的陡峭程度和具体数值。你可以把这两个核传给同一个函数对比它们和 3x3 版本的剖面曲线——趋势完全一致。高通特性是拉普拉斯族滤波器共有的本质不是某一个特定核的巧合。5. 画频谱图容易踩的坑我的排错记录5.1 高频翻车现场第一个坑是直接对小核做 DFT 不 padding。我第一次画拉普拉斯频谱时图省事直接 np.fft.fft2(laplacian_kernel)结果出来的频谱图像一条脏兮兮的条纹中心的暗点完全看不见。原因就是核在左上角边界跳变被当成了强信号。第二个坑是数据类型。cv2.dft 对 uint8 类型会直接报错或给出不可预料的输出必须转成 float32。而 numpy 的 fft 则不管所以许多人混着用时容易忽略这一点。第三个坑是忘记 log 缩放。拉普拉斯频域响应中中心是 0周围逐渐变大但最大值也就 8 左右。如果按线性尺度 imshow整个图几乎全黑只有边缘一圈微弱的亮线。视觉上完全看不出递增趋势。第四个方向是剖面线选错。对于 Sobel 这种方向性滤波器只看中心横剖面会把方向特征截断得出“好像也是低通”的误判。需要分别画水平和垂直两个方向的剖面或者直接看二维响应图。5.2 常见问题速查表现象原因处理方法频谱图出现奇怪的条纹和伪影核没有放到大零数组中心边界截断造成频谱泄漏把核插值到 512x512 零数组中央再 DFT图像几乎全黑只有边缘亮频域动态范围过大没有做 log 缩放使用 np.log1p(mag) 后再显示中心亮点不在图像中间忘记 fftshift对 DFT 结果做 np.fft.fftshiftcv2.dft 报错或输出乱码输入数组不是 float32用 np.float32 转换后再传入拉普拉斯剖面线感觉前后波动用了径向平均或者横切方向不对建议分别取中心行剖面和中心列剖面均值核和拉普拉斯核曲线差距巨大一个响应是 1 附近另一个是 0-8画在同一坐标轴时注意纵轴范围必要时双 y 轴5.3 给新手的三个建议第一先看核元素和再画图。核对拉普拉斯类型滤波器元素和必然是 0核和大于 0 的一般是低通或带通。这是一个完全不需要代码就能完成的初步判断能让你少走很多弯路。第二统一用对数谱加 fftshift 的组合来做可视化。不要今天用线性尺度、明天用对数尺度否则两次实验之间的图形可能差异巨大影响你的直觉判断。第三对比多种滤波器时把低通、高通、方向高通放在同一张图的同一行里。参照物一旦固定你的眼睛会很快学会区分不同响应形态。我试过只画单个滤波器理解速度远不如并排对比来得快。最后分享一个我养成的习惯现在看任何卷积核第一反应不再是背它的名字而是先问三个问题——核的元素和是不是 0频响中心是亮点还是暗点从中心向外的剖面是上扬还是下降三个问题回答完这个滤波器的性格基本就摸清了。如果想再往下延伸可以试试把这套可视化方法用在可分离滤波器核、小波核、Gabor 核上。画过的频谱越多你对空域和频域之间那种对应关系的直觉就会越准。希望这篇内容也能帮你把“拉普拉斯是高通滤波器”这句话从嘴里变成亲眼验证过的结论。