简介:面向需要完成课程设计或期末大作业的高校学生,这是一份基于深度学习的红外与可见光图像融合Python源码。项目已通过导师指导并获得97分高分,压缩包下载后可直接运行,无需修改。资源体积非常精简,仅7KB,包含3个Python文件,分别承担图像预处理、直方图均衡化和Otsu阈值分割等关键任务:预处理脚本完成图像配准与归一化,直方图均衡化增强对比度,Otsu分割生成显著目标掩膜,为后续深度融合网络提供高质量输入。红外与可见光图像融合常用于夜间监控、目标识别等场景,该源码可作为入门级参考模板,也可在此基础上替换或增加深度网络结构,便于二次开发。目前已有601人学习下载,代码结构清晰、注释完整,尤其适合课程设计、期末大作业的快速启动与对比学习。
1. 基于深度学习的红外与可见光图像融合,课程设计的正确打开方式
夜间监控摄像头拍到的红外图像里有清晰的人形热源,但背景路牌、车牌、墙面纹理全是糊的;可见光图恰好相反,细节锐利却看不清暗处的目标。这两种图像的融合不是把两张图叠起来,而是在像素层面决定"哪里信红外、哪里信可见光"。基于深度学习的方案把这套决策交给了网络自己去学,这也是红外与可见光图像融合在课程设计里被反复选择的原因:模型结构不复杂,训练数据不需要标注,而且用 Python 源码复现时灵活度高。下面这套从模型架构、训练管道到损失函数调整的路径,能让新手按步骤跑通,也能让有经验的人看出哪些参数值得改。
2. 融合模型的架构选型:DenseFuse 自编码器的设计逻辑与代码骨架
2.1 为什么编码器-解码器结构适合红外与可见光融合
从主流论文和公开代码仓库看,基于深度学习的红外与可见光图像融合实现路径主要分为端到端生成式和自编码器式。端到端生成式以 FusionGAN 为代表,用生成器和判别器互相对抗来生成融合图;自编码器式则以 DenseFuse、NestFuse 为代表,用编码器提取特征、融合层合并特征、解码器重建图像。课程设计选自编码器式的理由非常实际:训练稳定、显存占用低、可解释性好,融合策略可以很方便地替换和对比。
自编码器把融合任务拆成"压缩—合并—重建"三段,编码器分别处理红外图和可见光图,融合层在特征空间做信息选择,解码器只负责把特征恢复到图像域。训练时不需要成对的有监督标签,让解码器重建原图就够了。这意味着只需要用可见光图完成编码器和解码器的训练,红外图纯粹在推理阶段参与融合计算。
2.2 密集连接编码器的设计意图
编码器里用的密集块,是把每一层卷积的输出都拼接到后续层的输入,每一层都能看到前面所有层的特征。这个设计在图像融合里特别有用,因为红外特征与可见光特征的形态差异很大:前者是热辐射梯度,后者是反射纹理。如果用普通卷积串联,浅层细节经过几层非线性变换后衰减明显;密集连接保证在第3层输出里依然保留第1、2层的边缘和纹理成分,融合层拿到的是更完整的特征集。
此外,密集块在增加网络容量的同时控制了参数量。每个卷积输出固定16通道,三层共48个新增通道,参数量约等于一个普通三层网络,但特征复用的效率高得多。
2.3 融合层的两种策略与取舍
融合层放在编码器和解码器之间,是决定融合质量最关键的部分。常见做法有两种:加法融合和L1范数加权融合。
加法融合是直接把特征相加,计算极简,但会把红外特征和可见光特征平均化,当边缘强度与热辐射强度差异大时,弱特征容易被强特征掩盖。L1加权融合先用每个位置的特征绝对值之和衡量活跃度,再对两组特征加权合成,本质上是逐像素的注意力机制。推理时只要有差别的区域,权重会自动向特征更突出的一侧倾斜。
加法融合的实现非常简单,一句feat_fused = feat_ir + feat_vis就够了;而加权融合值得单独写一个函数,课程设计报告里也建议把两种策略的实验结果各放一张图对比。
2.4 骨干网络的 PyTorch 实现与参数表
以下是 DenseFuse 风格骨干网络的 PyTorch 实现,结构上直接对应上面讨论的密集连接编码器和解码器:
import torch import torch.nn as nn class DenseBlock(nn.Module): def __init__(self, in_ch, growth=16, depth=3): super().__init__() self.growth = growth self.convs = nn.ModuleList() for i in range(depth): ch = in_ch + i * growth self.convs.append(nn.Sequential( nn.Conv2d(ch, growth, kernel_size=3, padding=1), nn.BatchNorm2d(growth), nn.ReLU(inplace=True) )) def forward(self, x): acc = [x] out = x for i, layer in enumerate(self.convs): concat = torch.cat(acc, dim=1) out = layer(concat) acc.append(out) return torch.cat(acc, dim=1) class FusionEncoder(nn.Module): def __init__(self, in_ch=1, mid_ch=32, growth=16, depth=3): super().__init__() self.conv_in = nn.Sequential( nn.Conv2d(in_ch, mid_ch, kernel_size=3, padding=1), nn.ReLU(inplace=True) ) self.dense = DenseBlock(mid_ch, growth, depth) def forward(self, x): return self.dense(self.conv_in(x)) class FusionDecoder(nn.Module): def __init__(self, in_ch=80, mid_ch=32, out_ch=1): super().__init__() self.conv1 = nn.Sequential( nn.Conv2d(in_ch, mid_ch, kernel_size=3, padding=1), nn.ReLU(inplace=True) ) self.conv2 = nn.Conv2d(mid_ch, out_ch, kernel_size=3, padding=1) def forward(self, x): return self.conv2(self.conv1(x))代码逻辑说明:DenseBlock里每次循环先把已有输出拼接成concat,再把concat送入当前卷积层,新输出追加进列表。第0层输入是 conv_in 的32通道,输出16通道;第1层输入是48通道;第2层输入是64通道。最终把32+16×3=80通道的特征一次性返回。
所有卷积的 padding 都设为1,因此整个编码过程不改变特征图的空间尺寸,解码器同样用 padding=1 保持尺寸不变,这让输入输出可以直接逐像素比较损失,也方便融合后叠加别的高级任务。growth参数影响特征通道增量,调大能增强细节恢复能力,但显存占用会明显上涨;课程设计的单卡环境建议保持16不变。
| 模块 | 输入通道 | 输出通道 | 卷积核 | padding | 说明 |
|---|---|---|---|---|---|
| conv_in | 1 | 32 | 3×3 | 1 | 灰度图到特征映射 |
| DenseBlock | 32 → 80 | 16/层 | 3×3 | 1 | 三层的稠密拼接 |
| decoder.conv1 | 80 | 32 | 3×3 | 1 | 降维和重建 |
| decoder.conv2 | 32 | 1 | 3×3 | 1 | 最终融合图输出 |
到这一步,模型骨架可以跑通 forward,但离真正出结果还差训练管道和损失函数两件事。
3. 在 Python 里搭起训练管道:数据集载入、训练循环与融合推理
3.1 数据组织:公开数据集与单模态重建训练策略
红外与可见光融合的公开数据集常用 TNO 和 RoadScene,TNO 场景覆盖面较广,RoadScene 的可见光图清晰度更高,课程设计优先选 TNO 就行。如果只是想演示效果,对图像数量要求不高,40到50对图足够做验证。数据集的存储结构一般按模态分目录放:
dataset/ ├── infrared/IR_001.png ├── infrared/IR_002.png ├── visible/VIS_001.png └── visible/VIS_002.png训练阶段不需要成对读图,原因前面说过:编码器、解码器只做单图重建。一个常见做法是把可见光目录中每一张图都当作独立训练样本,红外图留到推理步骤使用。这样处理的好处是天然回避了红外、可见光图像对齐不准的问题,也让数据规模直接扩大一倍以上。
3.2 PyTorch Dataset 与 DataLoader 实现
import os import cv2 import torch from torch.utils.data import Dataset class GrayscaleDataset(Dataset): """只读单模态灰度图,训练阶段不需要配对""" def __init__(self, image_dir, size=(256, 256)): self.paths = [ os.path.join(image_dir, f) for f in sorted(os.listdir(image_dir)) if f.lower().endswith(('.png', '.jpg')) ] self.size = size def __len__(self): return len(self.paths) def __getitem__(self, idx): img = cv2.imread(self.paths[idx], cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, self.size) t = torch.from_numpy(img.astype('float32')) / 255.0 return t.unsqueeze(0) # 返回 [1, H, W]逻辑说明:cv2.IMREAD_GRAYSCALE直接读成单通道,/255.0归一化到 0 到 1,unsqueeze(0)补上通道维。size统一缩放到 256×256,这个尺寸在消费级显卡上显存压力较小,也能保证红外目标轮廓不被过度压缩。如果实验室显卡只有 4G 显存,考虑改成 224 或 192。
3.3 训练循环主体代码与配置
接下来是训练主循环。这里把训练时的损失函数简化成 MSE 先跑通流程,第4章再替换成混合损失。
import torch.optim as optim from torch.utils.data import DataLoader device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') encoder = FusionEncoder().to(device) decoder = FusionDecoder(in_ch=80).to(device) optimizer = optim.Adam( list(encoder.parameters()) + list(decoder.parameters()), lr=1e-4 ) ds = GrayscaleDataset('dataset/visible', size=(256, 256)) loader = DataLoader(ds, batch_size=4, shuffle=True, num_workers=2) def mse_loss(recon, target): return torch.mean((recon - target) ** 2) encoder.train() decoder.train() for epoch in range(30): total_loss = 0.0 for batch in loader: x = batch.to(device) feat = encoder(x) recon = decoder(feat) loss = mse_loss(recon, x) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f'epoch {epoch + 1}: loss = {total_loss / len(loader):.6f}')这段代码做的事情很直接:输入一张可见光灰度图,编码器提特征,解码器还原图像,通过比较重建图和原图的均方误差来更新网络。训练完成后,encoder 学到的是如何把图像信息压缩进特征,decoder 学到的是如何从特征还原图像。二者合起来的特征空间就具备了跨模态融合的基础。
一个值得注意的细节:解码器的输入通道必须和编码器输出通道严格一致,这里编码器输出是80,所以FusionDecoder(in_ch=80)。如果中途改了 growth 参数,这里也要跟着改,很多新手在这一步容易把通道数写错导致维度不匹配。
| 配置项 | 设定值 |
|---|---|
| 输入尺寸 | 256×256 |
| 训练轮数 | 30 |
| 批大小 | 4 |
| 优化器 | Adam |
| 学习率 | 1e-4 |
3.4 融合推理与可视化代码
训练结束后,进入推理阶段。先补上 L1 加权融合函数,再写完整的推理流程:
def l1_weighted_fusion(feat_ir, feat_vis, eps=1e-8): # 逐通道绝对值求和,得到每个空间位置的活跃度 act_ir = torch.sum(torch.abs(feat_ir), dim=1, keepdim=True) act_vis = torch.sum(torch.abs(feat_vis), dim=1, keepdim=True) w_ir = act_ir / (act_ir + act_vis + eps) w_vis = act_vis / (act_ir + act_vis + eps) return w_ir * feat_ir + w_vis * feat_vis def run_fusion(inf_path, vis_path, encoder, decoder, mode='l1', size=(256, 256)): def _read(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, size) t = torch.from_numpy(img.astype('float32') / 255.0).unsqueeze(0).unsqueeze(0) return t.to(device) inf_t = _read(inf_path) vis_t = _read(vis_path) encoder.eval() decoder.eval() with torch.no_grad(): f_ir = encoder(inf_t) f_vis = encoder(vis_t) if mode == 'l1': f_fused = l1_weighted_fusion(f_ir, f_vis) else: f_fused = f_ir + f_vis # 加法融合作为对比 fused = decoder(f_fused).squeeze().cpu().numpy() return fused这个函数的输出是 256×256 的 numpy 数组,直接用 matplotlib 的imshow显示即可。建议把红外原图、可见光原图、融合图三张图放在同一行出图,这样视觉效果对比最强,也是课程设计报告里的标配展示方式。
到这里,一个可以跑通的完整流程已经成型:模型定义、数据加载、训练、推理都有了。下一步的关键是把损失函数换成感知质量更好的混合损失。
4. 损失函数与训练调参:用 SSIM 约束提升融合图像质量
4.1 为什么只用 MSE 训练会导致融合图像过度平滑
只用 MSE 训练的模型,重建图往往存在过度平滑的问题。MSE 对每个像素独立计算平方差,它惩罚的是整体数值偏差,但并不关心局部结构,比如边缘保持、对比度变化、亮度一致性。红外图像里的弱目标区域在红外模态里可能只有一个模糊边缘,MSE 训练时该区域权重与背景相同,重建出来就是一团糊。
SSIM 在局部窗口内对比亮度、对比度和结构三个分量,输出范围 0 到 1,越接近 1 表示两幅图结构越相似。在融合任务中,把 1 减去 SSIM 作为损失项,可以显著提升边缘锐度。pytorch_msssim 库提供了现成的实现,课程设计里直接pip install pytorch-msssim即可,不需要自己从头实现滑动窗口。
4.2 混合损失函数 PyTorch 实现与 alpha 参数效果
from pytorch_msssim import ssim def fusion_loss(recon, target, alpha=0.8): # 亮度保真项:MSE mse = torch.mean((recon - target) ** 2) # 结构保真项:1 - SSIM ssim_loss = 1.0 - ssim(recon, target, data_range=1.0, size_average=True) return alpha * mse + (1 - alpha) * ssim_lossalpha控制两项的权重比例。alpha越接近 1,结果越偏向像素级精度;越接近 0,越偏向结构完整度。课程设计调参时,可以固定 alpha=0.8 跑一组,alpha=0.5 跑一组,对比融合结果的差异后在报告里给出结论。
4.3 从学习率到 batch size 的推荐参数表
给出一组经多次验证可用的起点参数,方便直接抄:
| 参数 | 推荐值 | 调整方向 |
|---|---|---|
| 图像尺寸 | 256×256 | 显存不够时降为 192 或 224 |
| batch size | 4 | 显存小换 2,损失不稳定时适当增大 |
| 学习率 | 1e-4 | loss 震荡改为 5e-5 |
| epochs | 30~60 | 重建训练收敛快,一般 30 轮足够 |
| optimizer | Adam | 默认 betas=(0.9, 0.999) |
| alpha | 0.8 | 边缘模糊调低到 0.5 |
| growth | 16 | 想要更多细节但显存充足可加到 20 |
重点说两个最影响结果的参数。第一个是学习率,1e-4 是大多数公开实现的默认选择,如果 loss 曲线不停抖动,毫不犹豫降到 5e-5;第二个是 alpha,它在可视化效果里体现得最直观,alpha=0.8 平衡性最好,不用再额外做任何后处理就能达到上课展示的水准。
4.4 跨模态特征分布的对齐与推理阶段处理
单模态重建 loss 降到 0.001 并不代表融合图一定感知优秀,因为训练阶段看到的只有可见光图,融合阶段进入解码器的是两种模态混合特征,解码器对这个特征统计分布并不完全熟悉。最简单的缓解手段是推理时给红外特征和可见光特征做一次标准化,让两者统计分布对齐:
def normalize_feature(feat): mean = feat.mean(dim=[2, 3], keepdim=True) std = feat.std(dim=[2, 3], keepdim=True) + 1e-5 return (feat - mean) / std f_ir = normalize_feature(encoder(inf_t)) f_vis = normalize_feature(encoder(vis_t)) f_fused = l1_weighted_fusion(f_ir, f_vis)这件事属于课程设计里的加分项:在报告里写清楚这是跨模态特征分布对齐,然后给出标准化前后的融合效果对比图,比堆叠训练 epoch 次数更有说服力。
5. 课程设计交付:用三个指标量化融合效果并完成验证
5.1 PSNR、SSIM、SD 三个指标的适用场景
课程设计答辩环节,光靠三张图说看起来不错不够,最好配合定量指标。常用三个:PSNR 峰值信噪比、SSIM 结构相似度、SD 图像标准差。PSNR 衡量融合图与参考图之间的像素重建质量,SSIM 衡量结构保留程度,SD 则是无参考指标,反映融合图的对比度和信息丰富度。
5.2 一个可复用的评估脚本
在推理代码的基础上补一段评估代码,对测试集所有图像对计算三组平均值:
import numpy as np from pytorch_msssim import ssim as ssim_fn def calculate_metrics(fused, ref): # fused 与 ref 都是 0~1 的灰度 numpy 数组 mse = np.mean((fused - ref) ** 2) psnr = 20 * np.log10(1.0 / np.sqrt(mse + 1e-8)) ss = ssim_fn( torch.tensor(fused).unsqueeze(0).unsqueeze(0).float(), torch.tensor(ref).unsqueeze(0).unsqueeze(0).float(), data_range=1.0 ).item() sd = np.std(fused) return psnr, ss, sd # 遍历测试集图像对,累计平均,最后输出结果表calculate_metrics的输入是 0 到 1 之间的灰度数组,三个返回值分别对应 PSNR、SSIM、SD。遍历测试集时对每组图像对分别计算,最后取平均,就得到一组可以写进报告的数字。
5.3 实验对比表与一个演示技巧
| 指标 | 红外原图 | 可见光原图 | 融合图 |
|---|---|---|---|
| PSNR | — | — | 17.82 |
| SSIM | — | — | 0.71 |
| SD | 0.18 | 0.25 | 0.31 |
这种展示结构可以直接放进课程设计报告的结果章节,把数字替换成自己跑出的结果,答辩时对照三张图逐项解释,老师能立即看到融合图在 SD 上比两个源图都高,说明信息量确有提升。
指标计算完以后,如果还想让演示效果更突出,可以对融合结果做 CLAHE 局部直方图均衡化增强对比度,但注意指标必须基于增强前的原始融合图计算,否则前后就不一致了。
本文还有配套的精品资源,点击获取