简介:红外与可见光融合目标检测项目的Python源码包,适合计算机视觉研究者、算法工程师以及具备一定基础的深度学习爱好者。该项目通过将红外与可见光图像融合后送入目标检测网络,有效提升夜间、低光照、遮挡等复杂场景下的检测鲁棒性,完整覆盖多模态数据预处理、融合策略、模型训练与推理评估等环节。压缩包共133个文件,大小约18.1MB;其中70个py文件为算法核心实现,35个yaml文件用于定义模型结构及训练参数,另有Dockerfile、Shell脚本和Jupyter Notebook便于环境部署与交互式调试,附带测试图像、GIF演示和说明文档,可辅助效果验证与应用对照。目前已有555人学习或下载,代码目录结构清晰,并提供了CPU、ARM64等多平台部署配置,便于在不同硬件上快速复现;整份源码既能支撑学术实验,也可为工程化落地提供参考实现。
1. 红外可见光融合目标检测:单模态的瓶颈与融合的价值
白天跑得好好的可见光目标检测器,一入夜mAP掉一半,雨天雾天同样力不从心。这不是模型不够强,而是可见光传感器依赖环境光照,红外成像却能靠温差画出目标轮廓。把红外与可见光融合起来做检测,核心不是简单叠加两张图,而是设计一套让网络同时读取两种模态的训练与推理流程。
这类项目源码.zip里通常装着双模态数据加载器、融合模块、检测头和指标脚本,属于多模态目标检测的工程骨架。拿到手后,关键在于看懂融合发生在哪一层、参数在哪里调、数据怎么对齐。适合正在做夜间安防、自动驾驶感知或工业巡检的工程师,也适合想把单模态检测模型扩展成双模态输入的团队。
2. 融合策略选型与Python实现:像素级、特征级与决策级怎么选
融合发生在哪一层,直接决定你能复用多少现有代码,也决定后续换模型时的改动范围。
2.1 先定位融合层级,再决定改动量
像素级融合发生在输入阶段,把可见光和红外预处理后合成一张图,直接喂给单流检测器。优点是检测网络零改动,可以用现成的YOLO、Faster R-CNN;代价是对配准精度要求高,两种模态的信息在像素上混叠后可能出现伪影。特征级融合发生在骨干网络输出特征图之后,两条backbone各提各的特征,在中间层用拼接、注意力或Transformer模块汇合,是当前多模态目标检测的主流路线。决策级融合绕开网络改动,两套单模态检测器各自出框,再做框合并,实现最快但推理开销翻倍。
场景决定选择:固定机位安防监控配准一次就能长期复用,像素级足够;自动驾驶要求响应快、精度高,通常选特征级;手头已有两个训练好的模型又不想动训练管线,决策级是成本最低的过渡方案。融合位置越靠后,特征语义性越强但空间分辨率越低,所以小目标场景通常把融合点放在骨干网络中间层而不是最后一层;融合位置越靠前,空间细节越完整但模态间对齐误差的影响越直接。下面三条路线都给出最小可运行代码。
2.2 像素级融合最小原型:加权叠加与环境自适应
import cv2 import numpy as np def fuse_pixel(vis, ir, alpha=0.5): # 红外通常是单通道,先统一尺寸和通道数 if vis.shape[:2] != ir.shape[:2]: h, w = vis.shape[:2] ir = cv2.resize(ir, (w, h)) if len(ir.shape) == 2: ir = cv2.cvtColor(ir, cv2.COLOR_GRAY2BGR) # alpha 是可见光占比,1-alpha 是红外占比 fused = cv2.addWeighted(vis, alpha, ir, 1 - alpha, 0) return fused红外图通常是单通道灰度,而可见光是三通道BGR,代码里先统一尺寸和通道数再叠加。addWeighted的数学本质是dst = alpha * vis + (1 - alpha) * ir,alpha是可见光的占比权重。固定alpha会吃亏,白天环境光充足时alpha取0.6到0.7保留纹理细节,夜间红外热信号更有区分度时alpha降到0.3到0.4。更进一步的方案是环境自适应:先计算红外图灰度均值,均值低说明环境暗,就动态降低alpha,形成简单的光照感知开关。
2.3 特征级融合:双流backbone与1x1卷积汇合
特征级融合让网络自己学"该信谁"。可见光分支提供纹理和颜色,红外分支提供热轮廓和显著性,两者在特征图上互补。将两路特征在通道维拼接后送入融合层,让1x1卷积学习通道组合权重:
import torch import torch.nn as nn class DualStreamFusion(nn.Module): def __init__(self, backbone_vis, backbone_ir, fusion_ch=512): super().__init__() self.backbone_vis = backbone_vis self.backbone_ir = backbone_ir # 拼接后先压缩通道,再做归一化和激活 self.fusion = nn.Sequential( nn.Conv2d(fusion_ch * 2, fusion_ch, 1, bias=False), nn.BatchNorm2d(fusion_ch), nn.ReLU(inplace=True) ) def forward(self, vis, ir): f_vis = self.backbone_vis(vis) # [B, C, H, W] f_ir = self.backbone_ir(ir) # [B, C, H, W] f = torch.cat([f_vis, f_ir], dim=1) return self.fusion(f)fusion_ch必须与backbone实际输出通道一致,否则拼接后第一个卷积输入维度对不上,这是最常见的报错位置。1x1卷积在此不是压缩参数,而是学习两种模态特征通道间的线性组合,BN将组合后的分布拉回稳定范围。两个backbone的输入分辨率必须一致,红外传感器若只有640x512而可见光是1920x1080,统一到640x512更划算;统一到高分辨率只会拖慢速度,红外侧没有更多信息可补。想升级成注意力融合,只需替换fusion模块,检测头完全不用动。
2.4 决策级融合:两套检测器结果合并
决策级融合只处理两套模型的输出框。先将两路框拼在一起做加权NMS,IoU重叠时保留置信度高的框。但红外检测器输出分数普遍偏低,直接NMS会让红外框被可见光框吞掉,所以合并前要给红外框乘一个补偿系数。三种策略的取舍可以参考下表:
| 融合层级 | 网络改动 | 配准要求 | 推理开销 | 典型场景 |
|---|---|---|---|---|
| 像素级 | 无 | 高 | 低 | 固定监控、无人机航拍 |
| 特征级 | 中 | 中 | 中 | 自动驾驶、移动巡检 |
| 决策级 | 低 | 低 | 高 | 已有模型快速集成 |
决策级的容错性好,某一模态被遮挡或过曝时另一路仍能兜底,但两路检测器各自都要维护,且输出质量取决于各自单模态召回率。一个夜间频繁漏检的可见光模型,加一路红外不会自动变强,这一点容易被低估。
3. 红外与可见光图像对齐配准:用Python做好数据预处理
融合模型的效果一半取决于数据是否对齐。两种传感器的分辨率、视场角和安装位置天然不同,原图直接拼到一起训练,模型会把红外热斑和可见光人影当作同一物体在不同位置的响应,损失函数很难收敛。
3.1 空间配准:单应矩阵与透视变换
固定机位的设备通常走离线标定路线,用棋盘格或人工选点估计一个单应性矩阵H,之后所有红外帧都用同一H变换到可见光坐标系。自动特征匹配在红外图上不稳定,因为红外图像对比度低、角点稀少,ORB检测点经常不足,需要在提取前做增强:
import cv2 import numpy as np def estimate_homography(vis, ir): # 红外图做CLAHE增强后特征点更稳定 ir_enhanced = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(ir) orb = cv2.ORB_create(2000) k1, d1 = orb.detectAndCompute(vis, None) k2, d2 = orb.detectAndCompute(ir_enhanced, None) if d1 is None or d2 is None: return None bf = cv2.BFMatcher(cv2.NORM_HAMMING) matches = bf.knnMatch(d1, d2, k=2) good = [m for m, n in matches if m.distance < 0.75 * n.distance] if len(good) < 8: print("匹配点不足,改用手工标定") return None src = np.float32([k1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst = np.float32([k2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) H, _ = cv2.findHomography(src, dst, cv2.RANSAC, 4.0) return H这段以可见光坐标系为目标,把红外图warp过去,src取可见光特征点、dst取红外特征点。RANSAC阈值4.0表示内点重投影误差容忍4像素,阈值调大配准变粗但更稳,红外配对从4.0起步合理。H矩阵不要在单帧上测一次就写死,温度变化会让镜头产生轻微位移,要在多段视频里抽样验证。
提示:评估配准好坏别只看重投影误差,把融合结果喂给检测器对比mAP,配准参数应以检测精度为最终标准。
3.2 同步增强:双模态共用随机种子
训练时双模态的几何增强必须同步,翻转、缩放、旋转和透视扰动要用同一个随机状态生成参数。若各做各的变换,融合层学到的对应关系在增强之后就不存在了:
import numpy as np def sync_augment(vis, ir, boxes): h, w = vis.shape[:2] if np.random.rand() > 0.5: # 水平翻转,两图和标注框一起翻 vis = vis[:, ::-1].copy() ir = ir[:, ::-1].copy() for b in boxes: b[0] = w - b[0] # x_center 翻转 return vis, ir, boxes这段只展示了水平翻转,x_center坐标同步映射。随机裁剪和旋转同理,裁剪范围与旋转角度必须对两张图用同一组参数。色彩抖动只能加在可见光分支,红外分支可做的是灰度扰动:随机对比度、随机直方图均衡,模拟不同温度分布下的成像差异。白天阳光直射和夜间热成像的灰度分布差异很大,提前加入这类扰动能让融合层对灰度偏移更鲁棒。
3.3 配对数据不足时的两个替代路径
公开的可见光-红外配对数据集有限,常见做法是先让backbone在单模态大数据上训熟,再在少量配准pair上整体微调。更进一步可以用CycleGAN把可见光图迁成伪红外图扩充预训练语料,伪图和真实红外在纹理细节上差距明显,仅用来把融合层权重推到合理区间,最终用真红外微调。反向也成立,红外图转伪可见光同样有效。另一个思路是缩小任务口径,红外小目标检测有相对充足的公开数据集,把检测目标限定为行人、车辆等小尺寸类别,用滑窗把高分辨率红外图切patch后再做融合训练,能明显缓解小目标漏检。
3.4 标签格式与样本筛选
配对数据还要处理标签框坐标系。YOLO格式的标签是相对于单张图像宽高的归一化坐标,如果红外和可见光的宽高比不同,同一份标签不能直接共用。常见做法是把两路图像先统一到相同宽高比再生成标签,或者在读取时分别归一化。样本筛选同样要关注:完全没有目标的空背景帧占比过高时,物体会被背景淹没,把背景帧按比例下采样,能有效稳定训练过程。
4. 双流目标检测模型训练:检测头改造与融合参数调优
拿到这类项目源码包,建议先看目录结构再动手:data目录下是配对图像和标签,models里是融合模块与检测头定义,train.py和infer.py是入口。压缩包解压后的第一件事不是急着安装依赖,而是把train.py里写到别人机器上的路径常量改成本机绝对路径。接下来要考虑的是训练路线怎么走。
4.1 初始化策略决定能否收敛
双流backbone从零开始联合训练几乎必然不收敛或收敛极慢,两路特征分布初始差异太大,融合层梯度无法同时带动四条路径。常见做法有两种:一种是用ImageNet预训练权重初始化两路backbone,前几个epoch冻结它们只训练融合层,稳定后再解锁全部参数;另一种是两路共享同一份预训练权重,训练中按不同学习率微调。我更常用共享权重起步,这等价于告诉网络两路输入初始时是同一分布。多模态微调目标检测是目前落地最常用的路径,微调时两路backbone用0.1倍主干学习率,融合与检测头用全量学习率,既保住底层视觉知识,又让上层模块尽快适配新模态。
4.2 融合模型的必调参数速查表
下表的参数以双流YOLO系列为参考,固化到配置文件里方便追踪和复现:
| 参数 | 推荐初始值 | 调整依据 |
|---|---|---|
| 输入分辨率 | 640x640 | 小目标占比高时试1280 |
| batch size | 16(双卡各8) | 双流显存约为单流两倍 |
| 初始学习率 | SGD 0.01 / AdamW 0.0001 | 冻结backbone时可放大 |
| 融合层学习率系数 | 1.0 | 融合层梯度振荡时降到0.1 |
| 置信度阈值(训练) | 0.25 | 低阈值保留更多难例 |
| NMS IoU阈值 | 0.5 | 决策级融合时试降到0.4 |
前三个epoch做线性warmup,从0.1倍学习率升到目标值,可以把融合层前期的剧烈波动压住。训练中建议保留EMA模型的权重,EMA几乎不增加成本,但在红外小目标类别上通常比普通权重更稳。两个backbone共享初始化会经历先同后异的演变,监控它们权重的余弦相似度,如果在训练中后期相似度仍然接近1,说明模态差异没有被利用起来,应检查红外分支输入是否被当成普通灰度图处理。
4.3 双输入数据集类与通道对齐技巧
import cv2 import torch import numpy as np from torch.utils.data import Dataset class FusionDataset(Dataset): def __init__(self, vis_list, ir_list, label_list): self.vis_list = vis_list self.ir_list = ir_list self.label_list = label_list def __getitem__(self, idx): vis = cv2.imread(self.vis_list[idx]).astype(np.float32) / 255.0 ir = cv2.imread(self.ir_list[idx], cv2.IMREAD_GRAYSCALE).astype(np.float32) / 255.0 # 单通道复制成3通道,复用预训练权重 ir = np.stack([ir] * 3, axis=-1) vis = torch.from_numpy(vis).permute(2, 0, 1) ir = torch.from_numpy(ir).permute(2, 0, 1) label = np.loadtxt(self.label_list[idx], dtype=np.float32) if label.ndim == 1: label = label.reshape(1, -1) return vis, ir, label红外单通道复制成三通道,是为了直接复用ImageNet预训练backbone第一层的3通道卷积权重。通道数量对齐即可,像素值仍是灰度信息;如果你担心三份冗余,也可以把第一层权重按通道平均后复制三份,效果接近。label加载时注意处理空标注文件,np.loadtxt对空文件会抛错,建议先判断文件大小再读取。这里返回的是归一化后的float张量,配合PyTorch的标准Collate即可组batch。
4.4 损失函数、小目标检测头与训练监控信号
多模态融合不需要重新发明损失函数,分类和回归损失沿用单模态检测器即可。真正的差异在小目标能力上:红外场景的远距离目标在特征图上往往只有几个像素,YOLO默认的三层检测头不够敏感。常见做法是在neck中增加P2层检测头,也就是4倍下采样的输出分支,配合把该类别的loss权重从1.0提高到2.0来平衡正负样本。这个改动会增加计算量,但配合融合层注意力机制,小目标AP通常能提升一到两个百分点。
除了常规的loss和mAP曲线,还要观察融合模型内部两条分支的独立表现。做法是临时把融合输出替换成两条分支各自过检测头,单独计算loss下降趋势。如果融合模型的整体loss低于两条单路中最好的一方,说明融合学到了互补信息;如果融合模型曲线紧跟可见光单路,说明红外侧信息没有进入决定性路径,优先排查配准误差和红外分支的梯度是否消失。这两个分支的监控代码不必永久保留,训练排查期用一下即可。
5. 推理验证与踩坑清单:评估指标与交叉注意力融合
5.1 评估指标按场景分层
mAP@0.5是通用基线,但热成像场景漏检代价往往高于错检,要额外看recall。红外小目标领域常提到的SCRG、BSF等指标用于衡量背景抑制和信杂比增益,评价单帧预处理效果还行,端到端检测最直接的是按类别拆分的AP,尤其小目标类别的AP。数据集划分要按视频片段为单位而非按帧随机拆,否则同一物体在不同帧的强相似样本同时进入训练集和测试集,指标虚高没有参考价值。
5.2 五个高频踩坑点
融合检测跑起来之后,问题往往不在模型结构而在工程细节。以下五个问题按出现频率排序,都是实际项目中常见且能明显影响mAP的点,建议当成回归测试清单用。
- 配准漂移:标定的H矩阵在镜头受热变形后失效,户外早晚温差大时尤其明显,可周期选取匹配点多的帧重新估计。
- 红外过曝:热源接近饱和后灰度截断成白斑,融合结果充满噪声,进网络前做CLAHE比直接归一化更稳。
- 双流推理速度接近两倍:先把融合输出接共享检测头再考虑TensorRT半精度,不要只在batch维度拼接两张图,那不是融合。
- 场景偏移:白天训练的权重在夜间或雨天适用性下降,动态alpha能缓解,即根据红外灰度分位数调整融合权重。
- 增强不一致:常见的复制粘贴错误是两个分支没有共用随机源,按帧检查对齐关系,一张错位图就能污染整个训练集。
5.3 交叉注意力融合的升级与验证
在特征级融合跑通的基础上,把1x1卷积融合层换成轻量交叉注意力通常能再涨一点:
class CrossAttentionFusion(nn.Module): def __init__(self, dim): super().__init__() self.q = nn.Conv2d(dim, dim, 1) self.k = nn.Conv2d(dim, dim, 1) self.v = nn.Conv2d(dim, dim, 1) def forward(self, f_vis, f_ir): # 用可见光特征生成查询,红外特征生成键和值 attn = torch.sigmoid((self.q(f_vis) * self.k(f_ir)).sum(dim=1, keepdim=True)) return f_vis + attn * self.v(f_ir)attn是一个[B, 1, H, W]的空间权重图,sigmoid后每个像素位置落在0到1之间,表示该位置对红外信息的信任程度。可见光纹理清晰的位置权重自然变小,红外轮廓清晰的位置权重变大,这比固定权重拼接更贴合两张图灰度分布差异大的场景。代价是三个1x1卷积的参数量和约15%的推理耗时,算力紧张时可以把融合层的输出通道减半再接入,效果基本持平。验证时在TensorBoard里观察attn的统计分布,如果某个位置长期接近0,说明该区域没有学到互补信息,优先检查配准步骤与该通道的输入预处理。
本文还有配套的精品资源,点击获取