尧图精选

Python实现混合图像:傅里叶变换与频域滤波的计算机视觉课程设计

🕒 发布时间:2026/10/2 18:48:33 📁 来源:尧图网络
简介面向图像处理与计算机视觉学习者的Python课程设计资源实现Oliva、Torralba和Schyns在SIGGRAPH 2006论文中提出的混合图像简化版本。核心理念是将一幅图像的高频细节与另一幅图像的低频平滑部分融合使同一静态画面随观看距离远近呈现不同解读兼顾实验趣味性与频域理论深度。资源包含完整可运行的Python源码、实验报告模板与成品PDF、tex和docx报告文档、多张测试图像及说明文件共29个文件以jpg测试图、py脚本、tex/docx报告、md说明为主压缩包整体3.08MB已有326人学习下载。源码内部模块划分清晰工具函数、过滤实现与混合图像生成相互独立目录结构便于对照学习。读者可借此理解高斯/拉普拉斯金字塔、频域过滤与图像融合原理直接参考代码并复用预处理流程同时借助报告模板高效整理实验结论适合课程设计、期末项目或入门实践。1. 图像过滤与混合图像课程设计里的视觉错觉实战做课程设计时你会发现图像处理方向的项目几乎被 OCR、人脸识别、车牌识别这三样占满了想找个视觉冲击力强、原理清楚、答辩好讲的选题并不容易。混合图像Hybrid Image是个冷门但极有辨识度的方案它利用人眼对高频细节和低频轮廓的感知距离差异把两张不同的照片叠在同一个人眼视觉系统里——近看看到的是甲退后两三米看到的却是乙。这个项目正是围绕这一原理用 Python 从零实现空间域过滤、频率域过滤以及混合图像合成适合作为计算机视觉、数字图像处理课程的独立设计项目也适合想系统理解傅里叶变换到底怎么用在图像上的开发者拿来练手。整套资源的核心脉络是先用 OpenCV 读取图像通过延拓和边缘处理解决傅里叶变换的边界泄漏再用高斯低通和高斯高通分别在频率域提取低频和高频分量最后按照经典的 Oliva-Torralba 混合图像算法做加权合成。代码只依赖 numpy、opencv-python、matplotlib 三个库环境配置压力很小VSCode 或 PyCharm 都能直接跑。接下来我把整个实现拆开讲从滤波原理到参数调节再到我实际踩过的几个坑一步步说明白。2. 滤波算法的选型时域卷积与频率域乘积的取舍2.1 空间域过滤的实现方式与局限空间域过滤的核心思想是卷积。对图像上任一点以它为中心取一个核把核覆盖的像素加权求和结果就落在该点的输出值上。均值模糊、高斯模糊、中值滤波都属于这个范畴OpenCV 里对应的函数是cv2.filter2D、cv2.GaussianBlur、cv2.medianBlur。我在这套代码里没有采用空间域实现原因有两个一是混合图像需要精准控制截止频率空间域做高斯核虽然直观但核的尺寸和方差需要反复试二是频率域处理可以直接观察频谱能确认过滤是否正确这对课程设计答辩非常有用——你把频谱图贴出来老师就知道你不是调包调出来的。import cv2 import numpy as np # 空间域高斯模糊核大小与sigma共同决定截止频率 img cv2.imread(input.jpg, cv2.IMREAD_GRAYSCALE) blurred cv2.GaussianBlur(img, (31, 31), 5.0) # 频率域等价操作先做傅里叶变换再乘高斯核最后逆变换 F np.fft.fft2(img) F_shift np.fft.fftshift(F) rows, cols img.shape crow, ccol rows // 2, cols // 2 y np.arange(-crow, rows - crow) x np.arange(-ccol, cols - ccol) X, Y np.meshgrid(x, y) sigma 20.0 gauss_low np.exp(-(X**2 Y**2) / (2 * sigma**2)) F_filtered F_shift * gauss_low img_filtered np.fft.ifft2(np.fft.ifftshift(F_filtered)).real空间域高斯卷积和频率域乘高斯核在数学上是等价的区别只在实现路径。前者对每个像素都要做核宽度的乘加运算后者一次傅里叶变换加一次点乘即可。对 500×500 以上的图像频率域在计算量上有优势但注意傅里叶变换的周期延拓性质会带来边界伪影空间域卷积则没有这个问题。代码里的gauss_low是以图像中心为原点的二维高斯函数sigma越大保留的低频越多图像越模糊。空间域过滤的边界问题相对好处理cv2.GaussianBlur默认的 BORDER_REFLECT_101 模式已经够用。频率域的边界问题则必须单独解决我一般先用cv2.copyMakeBorder做反射延拓过滤完再裁掉延拓部分这个方法后面避坑章里会细说。2.2 频率域过滤的关键步骤与参数影响频率域过滤的标准流程是读图转灰度 → 扩展到偶数尺寸 → 延拓消除边界泄漏 →np.fft.fft2变换 →np.fft.fftshift把零频移到中心 → 构造滤波核做点乘 →ifftshift还原 → 逆变换取实部 → 裁剪延拓区 → 归一化到 0-255。每一步都有坑。np.fft.fft2的输出是复数数组直接abs()取模后值域极大中心 DC 分量可能比周围大几个数量级显示频谱时需要用np.log(1 abs(F))压缩动态范围否则看到的只是一个白点。fftshift和ifftshift必须成对使用前者把零频挪到中心便于构造滤波器后者在逆变换前把零频挪回角落漏掉任何一步都会导致输出图像被旋转或错位。高斯低通滤波核的参数主要是sigma它直接决定截止频率。sigma以像素为单位表示频率域中标准差的大小。对一张 512×512 的图像sigma取 15 到 30 之间时保留的是画面的大面积明暗结构人脸的轮廓、背景的形状还在细节纹理被压掉。sigma太小则滤波后图像发灰、边缘出现振铃太大则高频也留下来了混合图像就失去近看换图的视觉差。高通滤波器我习惯用1 - low_pass构造不直接从高斯谱导出。原因是1 - low_pass刚好把直流分量附近压掉同时保留中高频且对灰度图的能量分布更直观。注意不要直接把高通核乘上去就完事逆变换后的图像会有负值直接astype(np.uint8)会把负值截断成黑色必须做线性拉伸或加 128 偏移。def get_low_pass(shape, sigma): rows, cols shape crow, ccol rows // 2, cols // 2 y np.arange(-crow, rows - crow) x np.arange(-ccol, cols - ccol) X, Y np.meshgrid(x, y) return np.exp(-(X**2 Y**2) / (2 * sigma**2)) def get_high_pass(shape, sigma): return 1 - get_low_pass(shape, sigma)get_low_pass里的坐标向量经过np.arange构造后是不对称的原因是傅里叶变换的尺寸不一定都是偶数用np.fft.fftfreq生成频率坐标更稳妥。这段代码返回的核是 float64 类型与变换后的复数数组相乘时不会触发类型提升异常工程上比用cv2的GaussianBlur抠核更省事。高通滤波后的图像通常灰度值偏低因为零频被滤掉了图像的平均亮度消失。此时显示需要把像素值整体平移或拉伸我一般用cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)它等价于线性拉伸效果比加固定偏移更稳定。3. 混合图像合成低频分支与高频分支的处理流程3.1 经典 Oliva-Torralba 算法的工程化拆解混合图像的原理说来简单一张图像做低通滤波保留低频轮廓另一张图像做高通滤波保留高频细节两图叠加后近看眼睛聚焦细节看到的是高频图的内容退远后眼睛对高频不敏感画面只剩低频图的内容于是产生了“近看一个人、远看另一个人”的视觉切换。麻省理工的那套经典实验图和这套课程设计代码原理一致区别只在滤波器的实现细节。工程化实现时核心是两条分支的独立性。低频分支和高频分支的输入必须是同一尺寸最好先做一次对齐裁剪。因为后续叠加是逐像素相加尺寸不一致时不能直接广播。一般做法是取两张图像的中心区域做中心裁剪统一到相同的宽度和高度再用cv2.resize保证尺寸完全一致。def hybrid_image(img1, img2, sigma_low25, sigma_high25): gray1 cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) gray2 cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) # 尺寸对齐以较小尺寸为准做中心裁剪 h min(gray1.shape[0], gray2.shape[0]) w min(gray1.shape[1], gray2.shape[1]) gray1 center_crop(gray1, h, w) gray2 center_crop(gray2, h, w) # 延拓消除傅里叶变换的周期边界效应 p1 pad_reflect(gray1) p2 pad_reflect(gray2) # 低频分支 low frequency_filter(p1, get_low_pass(p1.shape, sigma_low)) # 高频分支 high frequency_filter(p2, get_high_pass(p2.shape, sigma_high)) # 逆变换后裁掉延拓区 low crop_back(low, h, w) high crop_back(high, h, w) # 归一化并叠加 low cv2.normalize(low, None, 0, 255, cv2.NORM_MINMAX) high cv2.normalize(high, None, 0, 255, cv2.NORM_MINMAX) img_hybrid cv2.addWeighted(low, 0.5, high, 0.5, 0) return img_hybridhybrid_image汇总了全流程灰度化、中心裁剪、反射延拓、频率域滤波、裁剪还原、归一化叠加。sigma_low和sigma_high是两套独立参数不一定要相等。经典论文里两个 sigma 相同但实际做课程设计时建议低频 sigma 略大、高频 sigma 略小这样低频保留更完整、高频边缘更锐利远距离观看时身份切换更明显。cv2.addWeighted的权重系数可以按图调整常用 0.5/0.5但某些图对低频更敏感时可以调到 0.6/0.4。3.2 中心裁剪与尺寸对齐的细节处理中心裁剪在整个流程里容易被人忽略但它直接决定合成质量。两张图宽高比例不一致时直接resize拉伸会让脸形畸变混合后高频图与低频图的五官对不上远看像叠影。正确做法是先保持比例缩放到相同面积再做中心裁剪。我的处理方式是先把较大的图按较小图的比例缩放然后对长边多余部分做中心裁剪。这样能保证两张图的核心人脸区域位置大致重合。实现中心裁剪时要注意start_y和start_x的取值必须是非负整数否则cv2.resize或切片时下标越界运行时报错很难排查。def center_crop(img, h, w): cur_h, cur_w img.shape[:2] start_y max((cur_h - h) // 2, 0) start_x max((cur_w - w) // 2, 0) return img[start_y:start_y h, start_x:start_x w]// 2后取整是向下取整如果原图尺寸减去目标尺寸得到的是奇数裁剪框会偏移一个像素对最终结果没有可感知影响。但后续流程要保证裁剪后的图像与另一分支的滤波结果尺寸一致否则cv2.addWeighted会因为shape不匹配直接抛异常。因此建议裁完后再断言一次两个图的 shape 相等能提前拦截大量低级错误。数据预处理时还有个容易踩的坑。图像是 BGR 读入的cv2.cvtColor转灰度时必须显式指定COLOR_BGR2GRAY写反了程序不会报错但会输出颜色通道错误的灰度图看起来像底片效果后续滤波全白费。4. 参数调节与效果验证sigma、权重和观看距离的关系4.1 sigma 对混合效果的影响规律混合图像的核心参数就两个低频的sigma_low和高频的sigma_high。两者共同决定混合图在不同观看距离下的呈现效果。人眼对高频信息的敏感度随距离下降极快对低频信息的感知却几乎不衰减这是混合图像能生效的生理基础。sigma_low偏大时低频图保留更多结构信息远看时画面清晰身份识别容易sigma_low偏小时低频图已经模糊到看不出轮廓远看是一团灰雾混合效果失败。sigma_high偏大时高频图残留较多的低频分量近看会产生类似“两张脸叠在一起”的鬼影sigma_high偏小时高频图只剩边缘线近看细节严重不足。我一般先用sigma_low20, sigma_high20起步然后调出频谱图观察两图的截止频率位置。频率域滤波的好处是可视化非常直接低通后的频谱中心亮、四周黑高通反之。如果低通后的频谱中心还有明显的亮斑向四周扩散说明 sigma 太大低频保留过度如果高通后的频谱中心几乎全黑但四周也偏暗说明 sigma 太小信息丢得太多。def inspect_spectrum(img): F np.fft.fft2(img) F_shift np.fft.fftshift(F) magnitude np.log(1 np.abs(F_shift)) return magnitudeinspect_spectrum返回的是对数压缩后的频谱幅度图。np.log(1 abs(...))是必须的因为原始傅里叶谱的 DC 分量可能是几十万甚至上百万直接显示只有中心一个白点周边全部纯黑。这里的1 防止 log(0) 出现负无穷数值上是为了显示稳定。频谱图能帮你判断参数之外的另一个问题——图像是否被正确零填充到偶数尺寸如果频谱中心不在正中央说明前面某个环节尺寸判断出错。4.2 权重系数与归一化的相互作用cv2.addWeighted(low, 0.5, high, 0.5, 0)里最后的参数 0 是亮度偏移。很多人不理解为什么需要归一化后再叠加原因是低通和高通输出的像素值域范围差异巨大低通后的图像经过逆变换基本落在 0-255而高通后的图像因为零频被滤掉均值可能是 120 或更低直接线性叠加后高通的负值没有被正确处理导致输出图像暗部发黑、亮部过曝。实际执行中cv2.normalize默认用cv2.NORM_MINMAX时会把最小值和最大值分别映射到 0 和 255这等价于在频域后做一次全局对比度拉伸。两个分支各自归一化后再加权叠加能避免幅值差异带来的通道失衡。权重系数不建议固定。低频和高频分支的权重和为 1改变的是对视觉系统的刺激强度分配。如果两张图有一张本身对比度偏低比如夜景图那它对应分支的权重可以调到 0.6另一张降到 0.4。我测试下来0.5/0.5 对多数白天场景够用但夜景与白天的组合图往往需要微调。# 推荐先归一化再做加权避免负值截断 low_norm cv2.normalize(low, None, 0, 255, cv2.NORM_MINMAX) high_norm cv2.normalize(high, None, 0, 255, cv2.NORM_MINMAX) alpha 0.5 blend cv2.addWeighted(low_norm, alpha, high_norm, 1 - alpha, 0)alpha越接近 1结果越像低频图越接近 0越像高频图。参数调优时先以 0.5 为基准然后以 0.05 的步长递增。判断标准是把混合图在电脑屏幕上缩小到 20% 左右观看如果能看到清晰的主体轮廓低频权重就是够的。放大到 100% 检查眼部、发丝边缘如果边缘有过强的重影则减小高频的权重或增大高频的 sigma。5. 常见问题与避坑指南五个频率域处理的典型报错5.1 输出图像出现四角错位或旋转错位现象混合后的图像像被切成四块重新拼接左上角的内容跑到右下角或者图像整体转了 180 度。原因fftshift和ifftshift调用配对错误。fft2输出低频在四个角fftshift把它们移到中心。逆变换前必须先ifftshift把中心移回角落再交给ifft2。很多人只做了fftshift忘记ifftshift或者两个都用fftshift就会输出旋转或错位的图。解决强制绑定流程——F_shift fftshift(fft2(img))处理完后img_back ifft2(ifftshift(F_shift))。这一对操作连在一起写不要留间隔。调试时可以在频谱图上标一个亮点验证变换和逆变换的路径上点是否回到原位。5.2 图像边缘出现明显的垂直或水平条纹现象滤波后的图像左右或上下边缘出现规则纹理像帘子一样垂下来。原因傅里叶变换假设图像是周期延拓的图像左边界和右边界、上边界和下边界被强行拼接形成虚假的高频跳变。这些跳变在频率域表现为贯穿整个频谱的亮线高通滤波后尤其明显等于给高频分支白白加了一道边缘噪声。解决滤波前用cv2.copyMakeBorder做反射延拓延拓宽度一般取图像短边的 10% 到 15%处理完逆变换后直接裁掉。反射延拓优于零填充零填充同样会引入边界跳变反射延拓能保持边界的连续性。代码里我封装了pad_reflect和crop_back就是为了处理这一环。5.3 混合图近看全是噪点图像像蒙了一层雾现象高频分支的图像细节不仅包含边缘还包含大量的颗粒感噪声叠加后整张图灰蒙蒙的。原因高通滤波保留的不只是目标图像的边缘结构还包括传感器噪声和 JPEG 压缩伪影。这些噪声集中在高频段被高通滤波器完整保留导致混合图近看时细节里全是噪点。解决先对输入图像做一次轻微的高斯模糊cv2.GaussianBlur(img, (3, 3), 0.8)左右把带噪的高频压掉一部分再做高通。或者在高通滤波后对结果做一次 3×3 中值滤波只保留连续的边缘线不保留孤立噪点。实际操作中我两种方法都试过前者更省时间后者效果更稳。5.4 uint8 类型转换导致图像变成纯黑或纯白现象滤波输出的图像显示时要么全黑要么全白中间没有任何灰度过渡。原因ifft2的返回值是复数取实部后数值范围包含负值。直接调用.astype(np.uint8)会把负值截断为 0正值大于 255 的截断为 255相当于把灰度分布硬生生压成了二值图。imshow默认对 float 型图像也按 0~1 解释值域不对时显示完全失真。解决逆变换取实部后先检查值域print(img.min(), img.max())确认最小值确实存在负值后再归一化。归一化必须放在类型转换之前不能转换后再拉伸。正确顺序是normalize → astype(np.uint8) → imshow。记住一个原则uint8是终点不是中间态。5.5 混合图效果不明显远看仍是高频图内容现象整张图看起来正常但退远后依旧能看到近看图的脸部细节没有预期的身份切换。原因低频分支和高频分支使用的 sigma 差距过小导致两图的频段重叠严重。视觉上高频图的低频成分与低频图的低频成分叠加远看时两套信息同时可见无法形成清晰的切换。解决把sigma_low和sigma_high拉开差距。低通保留更多低频高通滤得更狠即增大sigma_low、减小sigma_high。用频谱检查低通后的频谱半径明显大于高通保留的半径二者重叠区越小混合效果越清晰。但注意sigma_high不要小于 5否则边缘信息也滤没了。6. 验证混合效果的三种可量化方法如果你做完混合图像后只靠肉眼判断效果在课程设计答辩时容易被追问到细节。更稳的做法是用三种可量化的验证方式把主观感受变成客观指标。第一种是频谱占比分析法。对合成的混合图做傅里叶变换计算低频分量占全图能量的比例。混合图应该同时包含显著的直流分量和充足的高频能量。用np.sum(np.abs(F_shift[center_region])) / np.sum(np.abs(F_shift))可以算出中心区域的能量占比记录在不同观看距离假设下该比例的合理性。中心区域半径取图像短边的 5% 左右。h, w hybrid.shape[:2] F np.fft.fftshift(np.fft.fft2(hybrid)) ch, cw h // 2, w // 2 radius int(min(h, w) * 0.05) mask np.zeros((h, w), dtypenp.uint8) mask[ch-radius:chradius, cw-radius:cwradius] 1 low_energy np.sum(np.abs(F) * mask) / np.sum(np.abs(F)) print(f低频能量占比: {low_energy:.3f})这个比例不要低于 0.3如果低于 0.3 说明直流分量损失过多图像整体偏暗混合效果受限。第二种是金字塔差分法。把混合图逐级下采样每次缩小一半生成五层金字塔。在每一层上计算相邻层之间的结构相似度 SSIM。理想情况下金字塔底层原尺寸应显示高频图的特征顶部极小尺寸应显示低频图的特征中间层应该在某个尺度发生突变。SSIM 曲线出现明显的拐点说明混合效果成立。用skimage.metrics.structural_similarity可以直接算如果没有这个库用 OpenCV 的matchTemplate做归一化相关性分析也能看出趋势。# 多尺度观看模拟标题图大小决定眼睛聚焦的频段 scales [1.0, 0.5, 0.25, 0.12] for s in scales: resized cv2.resize(hybrid, None, fxs, fys, interpolationcv2.INTER_AREA) # 用拉普拉斯方差衡量细节清晰度 lap cv2.Laplacian(resized, cv2.CV_64F).var() print(fscale{s:.2f}, detail{lap:.2f})cv2.Laplacian的方差是图像细节丰富度的经典度量。原尺寸下细节值高、小尺寸下细节值低这是正常现象。异常情况是原尺寸下细节值偏低说明高频图内容不足小尺寸下细节值仍然很高说明低频图混入过多高频成分。对课程设计来说这种量化结果可以直接贴进实验报告比三张效果图更有说服力。第三种是 A/B 对比验证。把低频分支和高频分支分开输出成两张图再做像素级差图可视化。差图的高亮区域应该集中在纹理变化剧烈的区域眼睛、发丝、衣物边缘如果差图大面积发白说明两图的对齐或加权有问题。差分图适合写进答辩 PPT直观展示算法的中间产物。从那以后我每次做混合图像都会先跑一遍完整的频谱检查再调参数。这个习惯帮我避免了大半的调参玄学。这套课程设计资源的代码结构足够清晰直接跑通不难真正有价值的部分是改参数、看频谱、调权重的过程建议你拿到代码后先换自己的两张照片试再回头读滤波函数理解会比只看示例图深得多。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →