简介:基于Python的小波变换红外与可见光图像融合算法项目包,面向毕业设计、课程设计与项目开发,针对夜间或低光环境下热目标信息与可见光纹理细节融合难题,提供完整可运行方案,可应用于智能监控、自动驾驶等领域。压缩包共31个文件,其中3个Python脚本实现主流程、融合算法与效果评估,12个PNG、4个JPG、2个BMP图像作为测试样例及融合结果图,6个TXT、1个CSV、1个MD文档承载算法原理解析、评价指标和项目说明,整体大小16.38MB。已有102人学习浏览,适合需要快速上手多尺度变换图像融合的开发者参考。项目包含完整源码与文档,系统讲解小波分解中低频近似系数保留轮廓、高频细节系数反映水平/垂直/对角边缘的原理,并附带多组图像融合评价报告,便于二次开发、扩展实验和结果复现。
1. 小波变换做红外与可见光图像融合:为什么多尺度分解成了标配
红外相机拍出来的是热辐射差异,目标亮、背景平;可见光相机拍出来的是反射光,纹理多、目标却可能淹没在场景里。小波变换做红外与可见光图像融合,核心思路是把图像通过多尺度分解拆成不同频率的子带,低频保留亮度与结构,高频保留边缘与细节,然后让红外和可见光分别贡献自己擅长的那部分。基于 python 实现这条路线,原理清晰、代码量适中、评价指标完备,是很多课程设计和毕业设计选多尺度图像融合的入门标配,也为后续拉普拉斯金字塔、引导滤波融合打好了地基。这篇文章顺着“原理—融合规则—完整代码—踩坑—参数验证”的顺序,把从拆图到重建的每一步讲透,适合第一次做图像融合课设、毕设的同学照着落地。
2. 图像在小波变换下变成了什么:四个子带、选型逻辑与分解层数
2.1 单层分解的四个子带:cA、cH、cV、cD 各装了什么信息
小波变换在图像处理里的直观解释,可以理解成对图像的行和列各做一次低通与高通滤波。一次二维小波分解,会把图像切分成四个子带:cA 是低频近似,cH 是水平方向的高频,cV 是垂直方向的高频,cD 是对角方向的高频。四个子带合在一起就是原图的完备表示,而且每个子带的尺寸各自减半。继续对 cA 做下一层分解,就得到一层比一层粗糙的多尺度结构。
从频谱语义上看,低频近似代表“这个区域整体亮不亮、是什么材质的底色”,对应天空、大块墙面、路面这类缓慢变化的信息;高频三个子带代表“哪里有边缘、哪里有纹理”,对应建筑物的边界、树叶的轮廓、行人的衣褶,也包含了传感器噪声。对融合任务来说,这个划分非常关键:红外图像的低频比较可靠,热目标在低频里就是一块突出亮斑;可见光图像则胜在高频细节丰富,砖缝和草木边缘都有。把两者按频率分开处理,就能做到“红外给亮度,可见光给纹理”,而不是简单在像素上平均。
小波基的选择也很影响后续融合表现。最基础的 haar 小波只有两个系数,分解快,但重建出来的图像容易出现方块状伪影,低频也不够平滑;db2、db4 这类 Daubechies 小波系数更长,频率局部性更好,对自然图像的压缩和重建都更干净。刚上手时直接用 db2,极少需要换别的。
2.2 用 python 拉起 PyWavelets:最小分解—重建代码与模式选择
python 生态里做小波变换不需要自己写滤波器组,PyWavelets 这个库把分解、重建、阈值去噪都封装好了,import 名是 pywt。安装它的时候会自动带上 numpy,所以只要你的 python 环境能把 numpy 装好,pywt 基本不会出问题。
import numpy as np import pywt import cv2 # 读灰度图,并先把尺寸改成偶数 ir = cv2.imread("ir.png", cv2.IMREAD_GRAYSCALE) ir = cv2.resize(ir, (512, 512)) # 三层小波分解,返回低频+每层三个高频子带 coeffs = pywt.wavedec2(ir, wavelet="db2", level=3, mode="periodization") cA3, (cH3, cV3, cD3), (cH2, cV2, cD2), (cH1, cV1, cD1) = coeffs # 从系数重建回原图 rec = pywt.waverec2(coeffs, wavelet="db2", mode="periodization") print("cA3 形状:", cA3.shape) # (64, 64) print("重建是否一致:", np.allclose(rec, ir.astype(np.float32)))逻辑说明:wavedec2 的返回值是一个列表,第一个元素是低频近似系数 cA,之后每一层是一个元组,分别保存水平、垂直、对角三个方向的高频系数。level=3 表示把低频近似连续分解三次,512 的原图最终会得到一个 64×64 的 cA3。waverec2 是它的逆操作,传入同样的 wavelet 和 mode 就能重建图像。
参数说明:小波基这里选了 db2,比 haar 平滑、比 db4 计算量小;mode 选 periodization 是为了让每个子带尺寸整齐减半,方便后面逐层融合。注意 periodization 要求输入尺寸是偶数,如果你的图是奇数尺寸,就先 resize,或者改用 symmetric 模式。另外 wavedec2 输入最好是 float32,否则重建后做差值比较时会有精度误差,上面 np.allclose 就是拿 float32 和重建结果比的。
2.3 分解层数怎么定:层数、尺寸变化与融合需求的关系
分解层数直接决定“多尺度”到底有多“多”。只用 1 层时,图像只被拆成 256×256 的低频和三个高频子带,中频细节还大量留在低频里,融合规则只能影响最锐利的边缘,效果很有限。用 5 层以上时,最高层低频只有 16×16,区域亮度信息被压得太狠,融合后背景光感容易失真,计算量却小不了多少。我一般从 3 层开始试,2 层用来快速验证流程通不通,4 层只在目标图像本身非常平滑时才考虑。
在 3 层分解下,512×512 输入对应的子带尺寸是:cA3 64×64,第三层细节 64×64,第二层 128×128,第一层 256×256。融合时,低频系数决定整幅图的亮度分布,第一层高频决定最细微的纹理,第二、三层高频决定中等尺度的结构边缘。这样每一层都有自己的职责,调参时也就有了明确方向:如果结果纹理不清晰,先检查第一层高频有没有被低通规则污染;如果背景亮度不自然,再回头看 cA3 的加权方式,而不是盲目改小波基。
3. 红外与可见光该怎么融合:低频加权、高频取大与路线选择
3.1 低频近似系数加权:让背景亮度自然过渡
低频近似在整幅图里占的能量最多,融合规则做得好不好,肉眼第一眼就看它。最简单的做法是线性加权:把红外低频和可见光低频按固定权重相加,权重 0.5 就是各取一半。这个方案对光照差异不大的图像有效,但红外热目标在低频里往往亮度很高,直接平均会把目标亮度摊薄,融合图看起来不那么“突出”。
常见做法是把固定权重升级成显著性自适应权重:先计算红外与可见光低频的差异,差异大的地方说明红外有更强的目标信号,就提高红外权重;差异小的地方说明两者背景接近,保持基准权重即可。
def lowpass_fuse(ir_cA, vis_cA, w=0.5, eps=1e-6): # 低频融合:差异越大,红外权重越高 diff = np.abs(ir_cA - vis_cA) sig = diff / (diff.max() + eps) w_ir = w + (1 - w) * sig fused = w_ir * ir_cA + (1 - w_ir) * vis_cA return fused.astype(np.float32)逻辑说明:sig 是归一化后的显著性图,数值范围在 0 到 1 之间;w_ir 是每个像素位置的红外权重,diff 大的地方权重接近 1,也就是以红外为主,diff 小的地方权重接近 w,保持基准比例。这样热目标区域不会被平均掉,背景区域又能保留可见光的自然光照。
参数说明:w 是基准红外权重,一般取 0.4~0.6。w 越大整体越偏红外,目标越亮但背景细节可能被红外平坦区带偏;w 越小整体越偏可见光,纹理保留好但热目标会变暗。diff.max() 加上 eps 是为了防止低频全是零时除零出错。这个函数输入必须是 float 数组,uint8 直接传入会在减法时出问题,调用前先 astype(np.float32)。
3.2 高频细节系数取大:用区域能量保住目标边缘
高频子带装的是边缘和纹理,融合目标相对直接:哪个源图在这个位置的细节更强,就把哪个源的系数拿过来。最朴素的实现是逐像素比较绝对值,谁大取谁,也就是绝对值取大规则。它对强边缘响应很好,但有一个天然缺点:噪声同样属于高频,单点比较时噪声系数可能刚好很大,结果把噪点也当成细节保留下来,融合图会出现白斑。
更稳妥的是区域能量取大,在像素周围开一个小窗口,先算窗口内系数的平方和作为能量,再看哪个源图在这个局部区域里细节更丰富。
def highpass_fuse(ir_h, vis_h, win=3): # 高频融合:局部能量取大,避免单点噪声干扰 k = np.ones((win, win), dtype=np.float32) / (win * win) e_ir = cv2.filter2D(ir_h**2, -1, k, borderType=cv2.BORDER_REFLECT_101) e_vis = cv2.filter2D(vis_h**2, -1, k, borderType=cv2.BORDER_REFLECT_101) mask = (e_ir >= e_vis).astype(np.float32) return mask * ir_h + (1 - mask) * vis_h逻辑说明:ir_h**2 把系数平方作为瞬时能量,cv2.filter2D 用 3×3 的均值核在邻域内求和,得到局部能量图。mask 在能量大的位置选红外系数,否则选可见光系数。由于 mask 是 0/1 硬切换,边缘会有一定硬朗感,但比单点取大稳定很多。
参数说明:win 是能量窗口尺寸,3 最常用,5 更平滑但会牺牲细边缘。输入必须是 float32,因为 uint8 的平方会溢出。如果噪声特别明显,可以先对高频子带做一次小波软阈值去噪,再把两边的系数送进 highpass_fuse,这也是小波变换图像去噪与融合结合的一种常见做法。
3.3 像素级、特征级与决策级融合:课设和毕设走哪条线
做多尺度图像融合的人还会遇到一个分类问题:图像融合按信息抽象程度分成像素级、特征级和决策级。像素级融合直接在原始像素或变换系数上做规则,信息保留最多,小波融合就属于这一类;特征级融合会先做目标检测、边缘提取或区域分割,再对特征图或区域加权,适合红外与可见光差异非常大的异源传感器;决策级融合则是各传感器先独立判断目标状态,再对判断结果做逻辑组合,信息损失最多但鲁棒性最高。
对课设和毕设来说,从像素级小波融合起步是最合理的,原理容易讲清楚,评价指标也可以定量计算。如果想在这个题目上做改进,可以在小波基础上加高频阈值去噪、加低频显著性权重、或者用引导滤波对融合系数后处理,形成自己的创新点,这些都比从零设计一套融合网络容易出成果。
4. 用 python 跑通三层小波融合全流程:配准、代码与批量输出
4.1 图像配准与预处理:融合前先回答“对齐了没”
很多人会在融合阶段问一句:图像配准和图像融合不是一回事吗?不是。配准解决的是两幅图的坐标系是否一致,融合解决的是对齐之后怎么合成。红外与可见光通常来自两个不同传感器,焦距、视场和光轴中心都不一样,直接把两张图送进小波分解,目标边缘会出现重影,后续融合规则调的再好也没用。
毕业设计里最常见的数据集是已经配准过的公共测试图,比如 TNO 数据集里尺寸统一的裁剪图,这时只需要做尺寸对齐和灰度化。如果你用的是自己拍的图,就得先做配准。
import cv2 # 红外一般是单通道,可见光一般是三通道 ir = cv2.imread("ir.png", cv2.IMREAD_GRAYSCALE) vis = cv2.imread("vis.jpg", cv2.IMREAD_COLOR) vis_gray = cv2.cvtColor(vis, cv2.COLOR_BGR2GRAY) # 统一到红外图的尺寸 if vis_gray.shape != ir.shape: vis_gray = cv2.resize(vis_gray, (ir.shape[1], ir.shape[0]))逻辑说明:先把可见光转成灰度,因为小波融合一般在亮度域进行;再用 resize 把可见光对齐到红外的宽高。如果两幅图目标有明显旋转或位移,resize 解决不了,要先用 ORB 或 SIFT 提取特征点,计算单应矩阵后做 cv2.warpPerspective,或者手动选取三组以上对应点做仿射变换。
参数说明:cv2.resize 默认插值是 INTER_LINEAR,放大时够用;如果是缩小,改用 INTER_AREA 能减少锯齿感。配准后建议把两图都转成 float32 再进入融合函数,避免后续平方计算时溢出。
4.2 三层小波分解—融合—重建主函数:可直接复制的实现
把第 3 章的两个融合函数和配准流程组合起来,就得到整个融合项目的主函数。所有参数都暴露在函数入参里,后面做批量实验时不用改函数体。
import numpy as np import pywt import cv2 def fuse_ir_vis(ir, vis, wavelet="db2", level=3, w=0.5, win=3, mode="periodization"): # 预处理:尺寸对齐、转浮点 if ir.shape != vis.shape: vis = cv2.resize(vis, (ir.shape[1], ir.shape[0])) ir = ir.astype(np.float32) vis = vis.astype(np.float32) # 多尺度分解 coef_ir = pywt.wavedec2(ir, wavelet, level=level, mode=mode) coef_vis = pywt.wavedec2(vis, wavelet, level=level, mode=mode) fused = [] for ir_c, vis_c in zip(coef_ir, coef_vis): if isinstance(ir_c, tuple): # 三个高频子带分别做能量取大 fused.append(tuple(highpass_fuse(h_i, h_v, win) for h_i, h_v in zip(ir_c, vis_c))) else: # 低频子带做显著性加权 fused.append(lowpass_fuse(ir_c, vis_c, w)) # 重建融合图 out = pywt.waverec2(fused, wavelet, mode=mode) out = cv2.normalize(out, None, 0, 255, cv2.NORM_MINMAX) return out.astype(np.uint8)逻辑说明:对红外和可见光分别做 level 层 wavedec2,然后用 zip 逐层对应融合。isinstance(ir_c, tuple) 用来区分低频数组和每层的高频元组:低频走 lowpass_fuse,高频三个方向走 highpass_fuse。最后 waverec2 重建,再用 NORM_MINMAX 把浮点结果拉伸到 0~255,转成 uint8 输出。
参数说明:wavelet 默认 db2,level 默认 3,低频权重 w 默认 0.5,能量窗口 win 默认 3,延拓 mode 默认 periodization。如果换了数据集、图像尺寸不是偶数,把 mode 改成 symmetric 即可。这个函数接收的是灰度图,输入彩色图前要先调用 cvtColor 或按下一小节的方式处理。
4.3 批量测试与保存:把参数暴露成入参而不是写死在过程里
课设和毕设经常要跑多组参数对比。批量跑实验时,我习惯把输出目录固定下来,并把关键参数拼进文件名,这样后面看结果时一眼就知道这组图是用什么参数生成的,不用每次翻记录。
from pathlib import Path out_dir = Path("out") out_dir.mkdir(exist_ok=True) for w in [0.4, 0.5, 0.6]: for win in [3, 5]: out = fuse_ir_vis(ir, vis, level=3, w=w, win=win) name = f"db2_l3_w{w:.1f}_win{win}.png" cv2.imwrite(str(out_dir / name), out) print("已生成:", name)逻辑说明:外层循环遍历低频权重 w,内层循环遍历能量窗口 win,每个组合都调用一次 fuse_ir_vis,生成 6 组融合结果。文件名把 w 和 win 都编码进去,便于后续用文件列表配合指标脚本排序。
参数说明:批量跑时建议先用小尺寸图把流程跑通,再换成完整分辨率,否则十几组实验叠加起来会很慢。输出目录用 pathlib 创建,跨平台行为一致,避免在 Windows 上拼字符串路径出错。
另外补充一个彩色可见光处理技巧:完整融合项目里,可见光往往是彩色的,直接把三通道图丢进 fuse_ir_vis 会报错。常见做法是把可见光转到 YCrCb 颜色空间,只对亮度通道 Y 与红外做融合,Cr 和 Cb 色度通道原样保留,最后再合并转回 BGR。这样能在保留颜色信息的同时让融合算法只作用于亮度域,结果比整幅灰度融合自然很多。
5. 小波融合避坑指南:条纹伪影、边缘发白、糊图和图像发灰
小波融合的坑看着像玄学,其实每一个都有明确的参数来源。下面四条是我自己踩过、也帮别人排查过的典型问题,按“现象—原因—解决”写清楚。
5.1 融合图出现横竖条纹或棋盘格:延拓模式与尺寸不一致
现象:重建出来的融合图上有一层淡淡的横竖条纹,严重时像棋盘格,肉眼很容易看出来,但指标计算可能显示不出异常。
原因:wavedec2 和 waverec2 使用的 mode 不一致。比如分解时用 periodization,重建时默认用 symmetric,边界处的系数没法正确还回去,就会在边缘位置留下规则误差。另一种情况是输入图像宽度或高度是奇数,还强行用 periodization,导致子带尺寸不一致,重建时错位。
解决:把分解和重建的 mode 统一,建议在项目里用一个变量保存,不要在每个调用点重复写。同时保证输入是偶数尺寸:
if ir.shape[0] % 2 == 1 or ir.shape[1] % 2 == 1: ir = cv2.resize(ir, (ir.shape[1] - ir.shape[1] % 2, ir.shape[0] - ir.shape[0] % 2))这段逻辑说明:先把宽高修正为偶数,再进 wavedec2。参数说明:resize 在这里只做尺寸裁剪式修正,如果因为去掉一行导致图像偏移,可以先整体缩放到偶数尺寸,再用同一个尺寸去对齐可见光图。
5.2 高频绝对值取大后边缘发白:噪声被放大了
现象:融合图里可见光的平缓区域出现零星白点,红外目标边缘周围有一圈亮边,整体画面显得很“脏”。
原因:绝对值取大规则只看单点系数,红外和可见光里边的噪声也被当成高频细节选进来了。噪声的系数可能不大,但在某个像素上刚好比另一个源图的大,于是被取进来;目标边缘处如果两个源图都有强响应,硬取大还容易让边缘宽度增加,形成光晕感。
解决:不要用单点绝对值取大,改用第 3 章的 highpass_fuse 区域能量取大。如果噪声仍然明显,先对高频子带做软阈值去噪,再融合。软阈值可以直接用 pywt.threshold:
ir_h1 = pywt.threshold(ir_h1, sigma=5.0, mode="soft") vis_h1 = pywt.threshold(vis_h1, sigma=5.0, mode="soft")逻辑说明:pywt.threshold 会把绝对值小于阈值的系数置零,大于阈值的系数向零收缩,从而压掉小幅噪声。参数说明:sigma 阈值建议从噪声标准差估计,实际调试时看 5~15 这个范围;阈值太大,细小纹理也会被抹掉,融合图会变平。
5.3 可见光的纹理被低频平均糊掉:层数与低频权重都要调
现象:融合图大块目标看得清,但砖墙、树叶这些细节像被磨过皮,一开始以为是高频规则没生效。
原因:高频规则通常没问题,真正的问题在低频。分解层数太少时,中频纹理还残留在低频系数里;低频权重如果朝红外倾斜,可见光的背景纹理又被平均稀释,结果就是整幅图有种“红外背景叠在可见光上面”的塑料感。
解决:先把层数提到 3 或 4,再把低频权重 w 往可见光方向调。比如原来是 0.6,改成 0.4,让背景亮度主要由可见光贡献;红外目标靠显著性机制去凸显,而不是靠整体权重去压。一条经验是:目标不明显时优先调显著性曲线,而不是把 w 一味调大。
out = fuse_ir_vis(ir, vis, level=3, w=0.4, win=3)这段代码说明:同样的融合函数,只用 level 和 w 两组参数就能明显改变纹理观感。参数说明:w=0.4 意味着低频里可见光占 60%,适合背景纹理本来就很重要的场景;如果红外目标在低频里特别亮,w 可以回落 0.5 再观察。
5.4 融合输出灰蒙蒙:数值域、对比度拉伸与 Y 通道处理
现象:融合图指标算起来不差,肉眼看却灰蒙蒙一片,目标不突出,亮部最多到 200,暗部降不到 0。
原因:小波系数经过去噪、加权和重建后,浮点数值范围往往缩窄,直接转 uint8 会把大量中间灰度集中在一起,直方图像一根柱子顶在中间。另一个原因是用整幅彩色图直接转灰度做融合,把可见光里的颜色区分度丢了,视觉上也会更平。
解决:重建后做对比度拉伸,最简单的是 NORM_MINMAX;如果希望更可控,用百分位截断:
out = cv2.normalize(out, None, 0, 255, cv2.NORM_MINMAX) # 或者用百分位拉伸,避免被极端亮点拉偏 p_low, p_high = np.percentile(out, (2, 98)) out = np.clip((out - p_low) / (p_high - p_low + 1e-6) * 255, 0, 255)逻辑说明:NORM_MINMAX 把 min 映射到 0、max 映射到 255,速度最快;百分位拉伸则忽略最亮最暗的 2% 像素,防止个别过曝点把整体亮度压低。参数说明:2 和 98 是一组常用百分位,想要更强烈的对比度可以改成 (1, 99),但要注意别把高光细节截没了。彩色图像发灰的问题,则要回到第 4 章的 YCrCb 融合流程,把色度通道保留下来再参与显示。
6. 融合结果怎么算合格:指标验证、参数整定表与进阶方向
6.1 写一个五行的信息熵计算,配合平均梯度看结果
信息熵反映融合图像携带细节的丰富程度,平均梯度反映边缘清晰度。信息熵计算不依赖参考图,是课设里最常用的指标。
def entropy(img): hist = np.bincount(img.ravel(), minlength=256) / img.size hist = hist[hist > 0] return -np.sum(hist * np.log2(hist))逻辑说明:灰度直方图归一化成概率分布后,取非零概率计算信息熵。参数说明:img 必须是 uint8 灰度图,如果是浮点图先转 uint8。平均梯度可以配合 np.gradient 使用:np.mean(np.abs(np.gradient(out.astype(np.float32)))),数值越高代表细节越锐利。只看熵容易把噪声当成信息量,所以要和平均梯度一起看。
6.2 参数整定表与十六组实验的批量跑法
下面这组参数是我在多个数据集上调试后觉得比较稳的起始范围,每项调整都有明确目的。
| 参数 | 常见取值 | 作用 | 调试建议 |
|---|---|---|---|
| 小波基 | db2 / db4 / sym4 | 子带分解质量 | 纹理复杂用 db2,想要平滑用 db4 |
| 分解层数 | 2~3 | 中频细节的多尺度程度 | 从 3 开始,纹理不足再降 |
| 低频红外权重 w | 0.4~0.6 | 背景亮度来源 | 目标不明显可加到 0.6 |
| 高频规则 | energy 3×3 / absmax | 边缘保留与噪声抑制 | 噪声大用 energy |
| 延拓模式 | periodization / symmetric | 边界伪影 | 偶数尺寸用 periodization |
批量跑实验时,把这些参数做成列表,遍历组合后把信息熵、平均梯度、SSIM 打印到 CSV,再肉眼筛前面几张图,效率会高很多。SSIM 可以调用 skimage.metrics.structural_similarity,两张源图分别算,取均值作为参考。
6.3 下一步:拉普拉斯金字塔与引导滤波融合
这条小波融合路线跑通后,再往进阶走就很顺:把小波换成拉普拉斯金字塔,融合规则不变,对比一下两种多尺度分解的差异;或者在小波融合系数上做引导滤波,让高频融合的 mask 从硬切换变成软过渡,边缘会温和很多。灰度融合没问题后,再补上 YCrCb 的彩色处理,项目文档里就能多写出一节“基于亮度域的多尺度彩色融合”。
我最早做这个项目时也只在灰度上跑,结果颜色全丢,后来把 YCrCb 加到流程里才真正把可见光的色彩信息保留下来。指标和肉眼一定要一起看,光看熵值容易被噪声骗过去;配准不到位的图,融合规则调再细都是白费。小波融合这条路,很值得把每一组规则和参数都跑一遍,它几乎是后续所有多尺度融合算法的共同底盘。希望帮到你。
本文还有配套的精品资源,点击获取