简介:这份资源面向图像处理初学者与计算机视觉方向的学习者,聚焦红外与可见光图像融合这一经典课题,帮助读者理解如何借助Python与小波变换将两类传感器图像的优势结合起来。红外图像反映温度分布,可见光图像保留形状、颜色与纹理,融合后可服务于夜间监控、军事侦察、医学成像与遥感等场景。资源包共4个文件,全部为py脚本,压缩后约3KB,分别承担JPG与PNG图像的读取、批量读取以及融合主流程,便于按需调用与二次修改。核心思路是先对两幅图像做严格配准,再用pywt进行小波分解,在低频与高频系数上采用平均、加权平均或最大值等策略组合,最后重构并保存融合结果。已有5374人学习下载,读者可借此掌握从读取、分解、融合到批量处理的完整链路,并在此基础上尝试直方图均衡化、对比度增强等优化手段,快速搭建可复用的融合实验框架。
1. 红外与可见光图像融合到底在解决什么问题
夜里十一点,小区门口那台可见光摄像头拍到的画面几乎全黑,但同一机位的红外摄像头能清楚看到蹲在墙根的人形热源。问题来了:两路画面各看各的,值班的人得来回切屏,漏看是迟早的事。红外和可见光图像融合要干的事,就是把这两路信息合成一张图——既有可见光的纹理、颜色、路牌文字,又保留红外的热辐射目标。用 Python 做这件事,是当前最省事的路径:OpenCV、NumPy、PyTorch 生态齐全,从传统方法到深度学习模型都能跑。
这篇面向的是想真正把融合跑起来的人:做安防监控的、做电力巡检的、做多模态目标检测预处理的,以及刚学 Python 想找个能出图的实战项目的。我会按「先搞懂为什么融、再动手融、最后避开翻车点」的顺序讲,代码可直接抄,参数会逐个说明。红外可见光图像融合不是把两张图简单叠加,叠加会糊、会偏色、会丢细节,后面会讲清楚为什么。
2. 融合之前先想清楚:两类图像差在哪、目标是什么
2.1 红外与可见光的成像差异决定了融合策略
红外传感器响应的是目标的热辐射,波长通常在 8~14 微米的长波红外波段,成像特点是:温度高的目标亮,背景暗,不受光照影响,但空间分辨率普遍低于可见光,边缘模糊,纹理几乎为零。可见光传感器响应 400~700 纳米的反射光,分辨率高、纹理丰富、色彩真实,但一到夜间或强逆光就基本失效。
这两者的差异不是「清晰度不同」这么简单,而是信息维度不同。红外给的是「哪里有热目标」,可见光给的是「这个目标长什么样、周围环境是什么」。融合的核心目标就是在一张图里同时保住这两类信息,且不能引入虚假信息——比如把可见光的暗区硬提亮成红外那种亮块,那就是伪影。
常见的融合策略分两大流派。传统方法走的是数学变换路线:多尺度分解(拉普拉斯金字塔、小波)、稀疏表示、显著性检测。优点是无需训练、可解释、算力要求低;缺点是对复杂场景泛化差,参数得手调。深度学习方法是当前主流:用编码器-解码器结构分别提取两路特征,在特征层融合后重建。优点是效果好、能端到端训练;缺点是要数据、要显卡、要调参。
选哪条路,取决于你手上有没有配对数据集和 GPU。如果只是想把两路监控画面实时合一张图,传统方法几十行代码就能跑;如果是要做下游目标检测的预处理,且手上有配准好的红外可见光数据集,那深度学习方法值得投入。
2.2 融合质量的评价指标不能只看眼睛
很多人跑完融合只看图「好不好看」,这是不够的。融合图像最终要么给人看,要么给算法用,两种用途的评价标准不同。
给人看的场景,关注的是视觉保真度:有没有伪影、色彩是否自然、目标是否突出。给算法用的场景,关注的是下游任务指标:把融合图送进目标检测器,mAP 涨了没有。
常用的客观指标有这么几类,我一般会同时看两三个,不会只信一个:
| 指标 | 含义 | 数值方向 | 适用场景 |
|---|---|---|---|
| EN(信息熵) | 图像信息量 | 越大越好 | 通用 |
| MI(互信息) | 融合图从源图继承的信息量 | 越大越好 | 通用 |
| VIF(视觉信息保真度) | 与源图的视觉信息一致性 | 越大越好 | 人眼观察 |
| SD(标准差) | 对比度 | 适中偏大 | 通用 |
| AG(平均梯度) | 边缘清晰度 | 越大越好 | 细节保留 |
| SF(空间频率) | 整体活跃度 | 越大越好 | 纹理丰富场景 |
注意:EN 和 AG 高不代表图就好,过度增强会把噪声也放大,指标虚高但视觉很差。我踩过这个坑,后面避坑章节会细说。
2.3 配准是融合的前置条件,别跳过
红外和可见光摄像头即使装在同一云台,视场角和畸变也不同,直接融合会出现重影。配准这一步不能省。常见做法是:先用棋盘格或标定板分别标定两个相机的内参和外参,再做单应性变换对齐;如果两路分辨率差异大,先统一到同一尺寸再配准。
如果手上拿到的数据集已经配准好了(比如 TNO、RoadScene 这类公开数据集),那可以直接进入融合步骤。自己采数据的话,配准精度直接决定融合上限,配不准后面全白搭。
3. 用 Python 跑通传统融合:从读取到出图的最小闭环
3.1 环境准备与依赖安装
先把环境搭好。Python 版本建议 3.8~3.10,太新的版本有些视觉库轮子还没跟上。用 conda 或 venv 都行,我习惯 conda。
# 创建虚拟环境 conda create -n fusion python=3.9 -y conda activate fusion # 安装核心依赖 pip install opencv-python numpy matplotlib scikit-image # 如果要跑深度学习融合,再加 pip install torch torchvision参数说明:opencv-python 负责图像读写和基础变换,numpy 做矩阵运算,scikit-image 提供部分评价指标实现。装完用python -c "import cv2; print(cv2.__version__)"验证一下,能打印版本号就说明环境没问题。如果报 DLL 相关错误,多半是缺 Visual C++ 运行库,装一下即可。
3.2 基于拉普拉斯金字塔的融合实现
拉普拉斯金字塔融合是传统方法里最经典、最容易复现的一种。思路是:把两张源图分别做多尺度分解,低频层用加权平均保留整体亮度,高频层取绝对值最大保留边缘细节,最后重建。
import cv2 import numpy as np def laplacian_pyramid_fusion(img_ir, img_vis, levels=4): """ 红外与可见光图像拉普拉斯金字塔融合 img_ir: 红外图,灰度或三通道 img_vis: 可见光图,与红外同尺寸 levels: 金字塔层数,一般3-5 """ # 统一为 float32,归一化到 0-1 ir = img_ir.astype(np.float32) / 255.0 vis = img_vis.astype(np.float32) / 255.0 # 若红外是单通道,可见光是三通道,把红外复制成三通道 if len(ir.shape) == 2 and len(vis.shape) == 3: ir = cv2.cvtColor((ir * 255).astype(np.uint8), cv2.COLOR_GRAY2BGR).astype(np.float32) / 255.0 # 构建高斯金字塔 gp_ir = [ir] gp_vis = [vis] for i in range(levels): gp_ir.append(cv2.pyrDown(gp_ir[-1])) gp_vis.append(cv2.pyrDown(gp_vis[-1])) # 构建拉普拉斯金字塔 lp_ir = [gp_ir[levels]] lp_vis = [gp_vis[levels]] for i in range(levels, 0, -1): size = (gp_ir[i-1].shape[1], gp_ir[i-1].shape[0]) up_ir = cv2.pyrUp(gp_ir[i], dstsize=size) up_vis = cv2.pyrUp(gp_vis[i], dstsize=size) lp_ir.append(gp_ir[i-1] - up_ir) lp_vis.append(gp_vis[i-1] - up_vis) # 融合:低频加权平均,高频取绝对值最大 fused_pyramid = [] for i, (l_ir, l_vis) in enumerate(zip(lp_ir, lp_vis)): if i == len(lp_ir) - 1: # 顶层低频 fused = 0.5 * l_ir + 0.5 * l_vis else: # 高频层 mask = np.abs(l_ir) >= np.abs(l_vis) fused = np.where(mask, l_ir, l_vis) fused_pyramid.append(fused) # 重建 result = fused_pyramid[0] for i in range(1, len(fused_pyramid)): size = (fused_pyramid[i].shape[1], fused_pyramid[i].shape[0]) result = cv2.pyrUp(result, dstsize=size) + fused_pyramid[i] result = np.clip(result, 0, 1) return (result * 255).astype(np.uint8) # 调用 ir = cv2.imread('ir.png', cv2.IMREAD_GRAYSCALE) vis = cv2.imread('vis.png', cv2.IMREAD_COLOR) fused = laplacian_pyramid_fusion(ir, vis, levels=4) cv2.imwrite('fused_lap.png', fused)逻辑说明:pyrDown逐层降采样构建高斯金字塔,相邻层相减得到拉普拉斯层。融合时低频层用 0.5/0.5 加权,这是最保守的做法,能避免亮度突变;高频层用np.where按绝对值大小逐像素选择,保留边缘更强的那个源。重建时从顶层开始逐层pyrUp并累加。
参数说明:levels控制分解层数,层数越多保留的尺度越丰富,但计算量增大,且层数过高会导致低频信息过度平滑。我一般设 3~5,图像分辨率 640×480 用 4 层比较合适。低频加权系数 0.5/0.5 可以调,如果希望红外目标更突出,可以改成 0.6/0.4,但可见光纹理就会弱一些,需要权衡。
3.3 融合结果的保存与批量处理
单张跑通后,实际项目往往是批量处理。下面这段把整个文件夹的配对图像跑一遍,并统一命名输出。
import os import glob def batch_fusion(ir_dir, vis_dir, out_dir, levels=4): os.makedirs(out_dir, exist_ok=True) ir_files = sorted(glob.glob(os.path.join(ir_dir, '*.png'))) for ir_path in ir_files: name = os.path.basename(ir_path) vis_path = os.path.join(vis_dir, name) if not os.path.exists(vis_path): print(f'跳过,无配对可见光图: {name}') continue ir = cv2.imread(ir_path, cv2.IMREAD_GRAYSCALE) vis = cv2.imread(vis_path, cv2.IMREAD_COLOR) if ir is None or vis is None: print(f'读取失败: {name}') continue # 尺寸不一致时统一到可见光尺寸 if ir.shape[:2] != vis.shape[:2]: ir = cv2.resize(ir, (vis.shape[1], vis.shape[0])) fused = laplacian_pyramid_fusion(ir, vis, levels) cv2.imwrite(os.path.join(out_dir, f'fused_{name}'), fused) print(f'完成: {name}') batch_fusion('./ir', './vis', './output', levels=4)逻辑说明:按红外图文件名去可见光目录找同名文件配对,找不到就跳过并打印,避免中途报错中断。尺寸不一致时用cv2.resize对齐,但注意这只是应急,正式流程应该在配准阶段解决。
参数说明:levels同上。批量处理时建议先拿 5 张试跑,确认配对逻辑和输出正常再全量跑。如果数据量大,可以加多进程,但 OpenCV 本身部分操作已多线程,先测单进程耗时再决定。
4. 上深度学习:编码器-解码器融合的落地要点
4.1 网络结构选型与数据准备
传统方法在简单场景够用,但遇到复杂光照、多目标、需要下游检测时,深度学习方法优势明显。主流结构是编码器-解码器:两个独立编码器分别提红外和可见光特征,中间用融合模块(加法、拼接、注意力)合并,解码器重建融合图。
数据准备是关键。需要成对的、已配准的红外可见光图像。公开数据集有 TNO、RoadScene、M3FD 等,电力场景可以用电力红外数据集,但要注意这类数据集多是单模态,需要自己配对。数据量建议至少几百对起步,太少容易过拟合。
训练时的损失函数一般组合三项:像素级损失(保证整体亮度)、梯度损失(保边缘)、结构相似性损失(保结构)。权重需要调,常见起点是 1:10:1,梯度损失权重大是因为融合最怕边缘糊。
4.2 训练脚本骨架与关键参数
下面是一个简化的训练循环骨架,重点看参数怎么设。
import torch import torch.nn as nn from torch.utils.data import DataLoader # 假设已定义 FusionNet 和 FusionDataset model = FusionNet().cuda() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.999)) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) criterion_pixel = nn.L1Loss() criterion_grad = GradientLoss() # 自定义梯度损失 criterion_ssim = SSIMLoss() # 自定义结构相似性损失 loader = DataLoader(FusionDataset('./data'), batch_size=8, shuffle=True, num_workers=4) for epoch in range(100): model.train() for ir, vis, _ in loader: ir, vis = ir.cuda(), vis.cuda() fused = model(ir, vis) loss = (1.0 * criterion_pixel(fused, vis) + 10.0 * criterion_grad(fused, ir, vis) + 1.0 * criterion_ssim(fused, vis)) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(f'Epoch {epoch}, LR {scheduler.get_last_lr()[0]:.6f}') if epoch % 10 == 0: torch.save(model.state_dict(), f'fusion_epoch{epoch}.pth')逻辑说明:三个损失加权求和,梯度损失权重最高,因为边缘保留是融合的核心诉求。学习率用 Adam 默认的 1e-4 起步,每 20 个 epoch 衰减一半。每 10 个 epoch 存一次权重,方便回退。
参数说明:batch_size受显存限制,8GB 显存跑 256×256 的图大概能到 8;显存不够就降到 4 或 2,同时把学习率相应调小。num_workers设成 CPU 核数的一半左右,太多反而拖慢。损失权重不是固定的,如果发现融合图偏暗,把像素损失权重提到 2;如果边缘还是糊,梯度损失提到 15 试试。
4.3 推理与结果后处理
训练完拿单张图推理,注意模型要切到 eval 模式,且输入要做同样的归一化。
model.eval() with torch.no_grad(): ir_t = torch.from_numpy(ir).float().unsqueeze(0).unsqueeze(0).cuda() / 255.0 vis_t = torch.from_numpy(vis).float().permute(2,0,1).unsqueeze(0).cuda() / 255.0 fused = model(ir_t, vis_t) fused = fused.squeeze().permute(1,2,0).cpu().numpy() fused = np.clip(fused * 255, 0, 255).astype(np.uint8) cv2.imwrite('fused_dl.png', fused)逻辑说明:推理时不需要计算梯度,用torch.no_grad()省显存。输入维度和训练时保持一致,红外单通道加两个维度变成 (1,1,H,W),可见光三通道 permute 成 (1,3,H,W)。输出反归一化后裁剪到 0~255。
参数说明:如果推理结果整体偏灰,检查训练时的归一化方式是否一致;如果出现棋盘格伪影,多半是解码器上采样用了转置卷积且步长设置不当,换成双线性插值加卷积会好很多。
5. 避坑与排查:融合项目里最容易翻车的几件事
5.1 指标虚高但图很难看
现象:EN、AG 指标都很高,但融合图噪声明显、色彩失真。原因:过度增强把噪声也放大了,指标只反映信息量不反映质量。解决:别只看指标,一定要人眼看图;同时加 VIF、SSIM 这类保真度指标交叉验证;如果指标高但视觉差,优先信眼睛。
5.2 红外和可见光没对齐导致重影
现象:融合图里目标有双层轮廓,像重影。原因:两路图像没配准,或配准精度不够。解决:回到配准步骤,用标定板重新标定;如果数据集本身没配准,先做单应性变换对齐再融合。这个坑最隐蔽,因为单看融合图可能以为是算法问题。
5.3 深度学习训练损失不下降
现象:训练几十个 epoch,loss 几乎不动。原因:常见有三种——学习率太大导致震荡、损失权重失衡导致某一项主导、数据归一化不一致。解决:先把学习率降到 1e-5 试;再把三项损失分别打印出来看哪项异常;最后检查红外和可见光的归一化方式是否统一。
5.4 融合图整体偏暗或偏亮
现象:输出图比源图暗一大截或亮一大截。原因:低频融合权重不合理,或解码器最后一层激活函数把值域压错了。解决:传统方法调低频加权系数;深度学习方法检查最后一层是不是用了 sigmoid 导致值域被压到 0~1 但训练目标没对应归一化。
5.5 批量处理时文件名配对错乱
现象:融合结果和源图对不上,张冠李戴。原因:红外和可见光文件命名规则不一致,或排序方式不同。解决:配对前先打印两边文件名列表核对;统一用文件名而非序号配对;加一层校验,尺寸差异过大的直接跳过并记录。
6. 把融合图送进下游检测:验证价值的具体做法
融合图到底有没有用,最终要看下游任务。我一般的验证流程是:拿同一批测试图,分别用可见光原图、红外原图、融合图各跑一遍目标检测,比 mAP。如果融合图的 mAP 比单模态都高,说明融合确实带来了增益;如果只是和可见光持平,那融合的意义就要打问号。
具体操作上,可以用 YOLO 系列做检测器,把三种输入分别组织成三份数据配置,训练同样的 epoch,对比验证集指标。注意控制变量:检测器结构、超参、训练轮数必须完全一致,只换输入。下面是一个数据配置的示例片段。
# dataset_fused.yaml path: ./datasets/fused train: images/train val: images/val names: 0: person 1: vehicle三份配置只有path不同,其余完全一致。跑完后看mAP@0.5和mAP@0.5:0.95两个指标。我的血泪经验是:融合图在夜间和逆光场景增益最明显,白天正常光照下可能和可见光持平甚至略低,因为融合过程多少会引入一点伪影。所以评估时要分场景统计,别只看总体均值。
另一个技巧是看融合图对小目标的召回。红外对小目标热源敏感,融合后小目标召回率往往能涨几个点,这个增益比总体 mAP 更有说服力。如果做电力巡检,绝缘子、线夹这类目标的检测,融合图的价值通常体现在漏检率下降上。
最后说个习惯:我每次做完融合,都会把源图、融合图、检测框叠在一起存一张对比图,按场景分类归档。时间长了就能看出哪类场景融合有效、哪类无效,下次采数据或调参就有方向。这个方案值不值得做,答案不在论文指标里,在你自己的场景数据里。希望帮到你。
本文还有配套的精品资源,点击获取