基于Transformer+YOLOv5的多光谱目标检测实践
发布时间:2026/9/1 1:17:06来源:尧图网络
简介本资源是一套面向深度学习工程师与计算机视觉初学者的多光谱目标检测实战项目聚焦于解决传统单光谱检测在遥感、安防、农业等场景中信息利用不足的问题。项目创新性融合Transformer的全局建模能力与YOLOv5的高效检测架构专为多光谱图像如红外可见光融合数据设计端到端检测流程兼顾精度与实时性。压缩包含115个文件主体为43个配置yaml文件定义模型结构与训练参数、30个核心py脚本涵盖数据预处理、多光谱特征融合、Transformer-YOLOv5联合训练及推理模块辅以shell部署脚本、Dockerfile容器化支持及效果演示gif与测试图像整体60.26MB结构清晰、模块解耦。已有139人下载学习提供从环境搭建、数据加载、模型训练到结果可视化的全流程可复现代码与详细教程特别包含多光谱通道对齐、跨模态特征交互等关键实现细节是深入理解前沿检测架构落地的优质实践材料。 去年年底我在做一个夜间巡检项目目标是在可见光几乎失效的场景下依然能稳定框出人和车辆。一开始我只用YOLOv5跑可见光晴天还行一到阴天、夜间或者逆光就直接拉胯。后来我把红外图像也接进来做了一套基于TransformerYOLOv5的多光谱目标检测方案效果一下子就不一样了。这套方案核心就两句话第一让模型同一时刻看到可见光和红外两路图像第二在YOLOv5的深层特征里插入Transformer注意力模块把两路特征里的关键信息拉出来用。这篇文章算是我从头到尾走完这个项目之后的工程笔记不是论文复现报告。我会把数据怎么准备、Transformer模块怎么嵌进YOLOv5、训练时踩过哪些坑、部署时要注意什么都按实际操作的顺序讲清楚。如果你也在做目标检测想在YOLO框架里引入注意力机制或者准备接触多光谱但不知道从哪儿下手这篇文章应该能帮你省掉不少试错的时间。1. 项目整体设计与思路拆解1.1 为什么非要多光谱不可先聊聊我为什么从单目可见光转向多光谱。可见光图像RGB的优点是纹理信息丰富、人眼看着直观但它最大的问题是严重依赖环境光照。夜间、雾天、逆光、阴影遮挡这些场景下目标与背景的对比度会断崖式下降模型很容易漏检。相比之下红外成像尤其是中长波红外依赖的是物体自身的热辐射不靠外部照明所以夜间、弱光、伪装遮挡场景下反而能“看得更清楚”。多光谱目标检测的本质就是把这两种互补的信息融合起来。我做的这个项目里输入是同一场景下配准好的可见光图和红外图输出依然是目标的类别和边界框。之所以叫“多光谱”是因为可见光0.4-0.7μm和红外8-14μm处在不同的电磁波谱段两路信息一起进模型比任何单一路都稳。这个思路在很多场景里都有实际价值夜间辅助驾驶行人、车辆检测、电力巡检高温部件定位、消防救援烟雾中搜人、无人机巡检热斑排查。我做的夜间巡检只是其中之一但技术路线是通用的。做多光谱最常见的一个误区是以为把两路图像拼在一起送进网络就行。早期我也这么试过效果并不理想。因为可见光和红外的特征分布差异很大简单拼接会让模型把大量参数花在“对齐特征”上反而忽略了真正的目标语义。所以这个项目里我做了两个关键设计一是双流输入让两路图像分别经过各自的Backbone提取特征二是在特征融合阶段引入Transformer注意力机制用自注意力去建模“可见光特征和红外特征之间哪些位置是相关的”。1.2 Transformer到底加在哪聊到Transformer很多人的第一反应是ViT、Swin Transformer那种整个Backbone都换成注意力结构的做法。但我这次没有这么干原因很简单工程落地追求的是精度和速度的平衡全Transformer结构在检测任务上推理速度偏慢训练数据不够时还容易过拟合。我的选择是保留YOLOv5作为整体检测框架只在合适的位置插入Transformer模块。这样既继承了YOLOv5成熟的训练流程、数据增强、锚框策略和部署生态又能利用Transformer的自注意力机制捕获全局上下文信息。这对多光谱融合尤其重要因为一个目标在可见光里可能很模糊但在红外里很清晰模型需要跨空间位置、跨模态找到对应线索自注意力机制天然适合干这件事。具体加在哪儿我比较推荐三个位置候选Backbone倒数第二层C3模块替换为C3STR特征图分辨率合适计算量可控能拿到全局感受野。Neck的PANet高层特征处对小目标检测有帮助但计算量会增加不少。双流融合点之后对两个模态融合后的特征做全局建模收益最直接。我最后选的是“Backbone最后一层C3替换成C3STR 融合后加一个Transformer Encoder”。这个组合在夜间行人、车辆检测上提升最明显mAP0.5比纯YOLOv5s提升了约6到8个点。再说回双流结构。YOLOv5原本只有一个Backbone我做多光谱时改成了两个Backbone分别吃可见光和红外输入。两个Backbone可以用同一个预训练权重初始化也可以各自用对应模态的预训练模型初始化。理论上两路最好使用相同的初始化这样训练初期比较稳定。2. 核心细节解析与实操要点2.1 多光谱数据的准备与对齐数据是整个项目里最花时间、也最容易翻车的环节。多光谱数据和普通单图数据最大的区别在于同一场景必须有两张成对的图而且像素级对齐。我先说数据集的选择。公开数据集里FLIR Thermal Dataset是车载场景的经典选择包含可见光和红外成对图像主要用于行人、车辆、自行车检测KAIST Multispectral Dataset也是行人检测常用的多光谱数据集如果是无人机视角VEDAI数据集可以用。我自己用的是团队采集的夜间巡检数据外加一部分FLIR做预训练。如果自己采集数据一定要注意两个传感器的配准。红外相机和可见光相机的位置、视场角、分辨率通常不一样直接拿两路原始图训练模型会学到错误的对应关系。解决方式是做离线的相机标定和图像配准我用的步骤如下分别标定两个相机的内参得到焦距、主点和畸变系数。计算两个相机之间的外参旋转矩阵和平移向量。以可见光图为基准把红外图像重投影到可见光图像坐标系下。配准之后两路图的尺寸需要保持一致我统一缩放到640×640。标注框只需要在一张图上标注我选择在可见光图上标注因为配准之后两图目标位置已经对齐标签可以直接复用。这里有一个经验不要迷信公开数据集里的标签一定要抽样检查配准质量。我遇到过FLIR某些样本红外图和可见光图有肉眼可见的偏移直接拿来训练会导致边界框位置不准。数据增强方面我建议对两路图像使用相同的随机变换种子保证几何变换翻转、缩放、旋转一致但光度变换亮度、对比度、噪声各自独立。这样模型能学到两路模态的独立特征而不是靠图像色彩关联硬凑。标注格式建议统一转成YOLO txt格式每行是class_id x_center y_center width height坐标是归一化到0-1的数值。我的数据目录结构长这样multispectral/ ├── images_rgb/ │ ├── train/ │ ├── val/ ├── images_ir/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── multispectral.yaml2.2 在YOLOv5中嵌入Transformer模块我选择的切入点是YOLOv5的C3模块。C3模块原本由几个Bottleneck卷积残差块组成我要做的事情是把C3内部的部分Bottleneck替换成Transformer Encoder层形成C3STR模块。原版YOLOv5官方代码仓库里其实有一个C3TR的实现思路就是C3模块里加一个TransformerLayer。我基于这个思路做了修改核心代码如下import torch import torch.nn as nn class TransformerLayer(nn.Module): # 单层Transformer Encoder def __init__(self, c, num_heads): super().__init__() self.q nn.Linear(c, c, biasFalse) self.k nn.Linear(c, c, biasFalse) self.v nn.Linear(c, c, biasFalse) self.ma nn.MultiheadAttention(embed_dimc, num_headsnum_heads) self.fc1 nn.Linear(c, c, biasFalse) self.fc2 nn.Linear(c, c, biasFalse) def forward(self, x): x self.ma(self.q(x), self.k(x), self.v(x))[0] x x self.fc2(self.fc1(x)) x return x class C3STR(nn.Module): # C3模块的Transformer变体 def __init__(self, c1, c2, n1, num_heads8): super().__init__() self.cv1 Conv(c1, c2, 1, 1) self.cv2 Conv(c1, c2, 1, 1) self.cv3 Conv(2 * c2, c2, 1, 1) self.m nn.Sequential(*(TransformerLayer(c2, num_heads) for _ in range(n))) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim1))这里有几个细节需要注意TransformerLayer的输入是序列数据YOLOv5的Backbone特征是[B, C, H, W]四维张量需要先展平成[B, H*W, C]再进入Transformer层输出后再reshape回四维。MultiheadAttention默认处理的是[Seq, Batch, Embed]顺序我这里传入时稍微转置了一下或者在forward里单独处理。注意力头数我建议设为8嵌入维度要和输入通道数保持一致。如果输入是512通道embed_dim就是512。位置编码我没有额外加因为目标检测的特征图本身就带有位置信息每个位置对应原图的局部区域再加上位置编码反而可能干扰平移等变性。C3STR插到哪个位置直接在YOLOv5的yaml配置文件里改就行。我把Backbone倒数第二层的C3_2替换成了C3STR具体是backbone: # [from, number, module, args] - [-1, 1, Conv, [64, 6, 2, 2]] # 0-P1/4 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/8 - [-1, 3, C3, [128]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/16 - [-1, 6, C3, [256]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/32 - [-1, 6, C3, [512]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/64 - [-1, 3, C3STR, [1024]] # 8 - [-1, 1, SPPF, [1024, 5]] # 9对于双流网络我用了一个简单的双流融合写法两个Backbone各自输出特征然后在同一特征尺度上做拼接再接一个1×1卷积把通道数压回去。这样改动最小。class DualBackbone(nn.Module): def __init__(self, backbone_rgb, backbone_ir, fuse_channels): super().__init__() self.backbone_rgb backbone_rgb self.backbone_ir backbone_ir self.fuse nn.Sequential( nn.Conv2d(fuse_channels * 2, fuse_channels, 1), nn.BatchNorm2d(fuse_channels), nn.SiLU() ) def forward(self, x_rgb, x_ir): f_rgb self.backbone_rgb(x_rgb) f_ir self.backbone_ir(x_ir) out torch.cat([f_rgb, f_ir], dim1) return self.fuse(out)2.3 关键超参数与Loss设计我用的基础配置是YOLOv5s加上C3STR和双流结构之后模型参数量比原版多了大约20%主要来自Transformer层和融合卷积。如果显存有限建议先用YOLOv5n或YOLOv5s起步不要一上来就YOLOv5l。训练超参数我记录一份供参考输入尺寸640×640Batch size8双流显存占用变高RTX 3060 12GB勉强够初始学习率0.001比YOLOv5默认的0.01低因为Transformer模块对学习率更敏感优化器SGDmomentum0.937weight_decay0.0005训练轮数150 epochwarmup前3个epoch做warmup从0.0001逐步升到0.001锚框用YOLOv5自带的autoanchor重新计算因为多光谱目标尺寸分布和单一数据集可能不同Loss方面我还用的是YOLOv5原始的CIoU Loss BCE分类Loss 置信度Loss没有额外改动。之前试过在Loss里单独加一个“多光谱一致性约束”强迫两路特征相似效果反而不好因为两路特征本来就不该完全相似可见光有的纹理红外没有强行约束会丢掉互补信息。关于训练轮数和早停150 epoch足够了我会保留best.pt验证集mAP最高和last.pt最后一轮。如果数据量比较少比如每类只有几百张建议用更强的数据增强mosaic、mixup并加上早停。3. 实操过程与核心环节实现3.1 环境搭建与项目结构环境依赖我建议直接用YOLOv5官方的requirements.txtPyTorch版本选择1.10以上都行。我实际用的组合是Python 3.9PyTorch 1.13.1 CUDA 11.7torchvision 0.14.1opencv-python 4.8.0numpy 1.24.3onnx 1.13.1这一套在Linux和Windows上都能跑主要区别是路径分隔符和CUDA驱动版本。如果在自己机器上装CUDA记得先确认显卡驱动支持的CUDA版本再用对应的PyTorch安装命令。项目源码结构我改完之后大概是这样的project/ ├── models/ │ ├── common.py # 新增C3STR、TransformerLayer、DualBackbone │ ├── yolo.py # 修改以支持双流输入 │ └── yolo_multispectral.yaml ├── data/ │ └── multispectral.yaml ├── utils/ ├── train_ms.py # 多光谱训练入口 ├── detect_ms.py # 多光谱推理入口 ├── requirements.txt └── runs/ └── train/exp/train_ms.py的核心改动是在数据加载部分同时读入RGB和IR两路图像修改Dataset类class MultispectralDataset(Dataset): def __init__(self, img_rgb_dir, img_ir_dir, label_dir, ...): self.img_rgb_dir img_rgb_dir self.img_ir_dir img_ir_dir self.label_dir label_dir # 确认两路图像文件名一一对应比如001.jpg和001_ir.jpg def __getitem__(self, idx): img_rgb load_image(os.path.join(self.img_rgb_dir, self.files[idx])) img_ir load_image(os.path.join(self.img_ir_dir, self.files[idx])) labels load_label(os.path.join(self.label_dir, self.files[idx])) # 对img_rgb和img_ir做相同的几何增强 return img_rgb, img_ir, labels这里最容易出错的是两路图像的文件名匹配。我的命名规则是可见光为frame_001.jpg红外为frame_001_ir.jpg在Dataset里做一个映射字典。如果文件名对不上训练时会报找不到图像排查起来非常头疼。3.2 模型训练与结果可视化数据就绪、代码改好之后训练命令很简单python train_ms.py \ --data data/multispectral.yaml \ --weights yolov5s.pt \ --batch-size 8 \ --epochs 150 \ --img 640 \ --device 0注意一点--weights yolov5s.pt是单模态YOLOv5的预训练权重。我的处理方式是把这个预训练权重同时加载到两个Backbone分支里相当于用同一个预训练模型做双流初始化然后再从融合层开始重新训练。这样做比从头训练收敛快很多大约前30个epoch就能看到mAP明显上升。训练过程中我主要看三个指标训练Loss、验证集mAP0.5、mAP0.5:0.95。其中最直白的是mAP0.5也就是IoU阈值0.5下的平均精度。我最后的结果多光谱Transformer方案在夜间测试集上mAP0.5是0.837而纯YOLOv5s跑可见光是0.761纯YOLOv5s跑红外是0.793。提升主要出现在小目标和遮挡目标上。训练过程还有一个观察加了C3STR之后训练Loss下降得比原版慢但最终收敛值更低。这是因为Transformer模块参数多需要更多迭代才能拟合但一旦拟合好对全局信息的利用是卷积结构比不了的。所以训练时不要因为前20个epoch mAP不高就急着调低学习率给它一点时间。可视化方面我习惯用Weights Biases或者TensorBoard记录曲线。YOLOv5自带--logdir参数可以输出TensorBoard日志训练完用tensorboard --logdir runs/train就能看到Loss和mAP曲线。3.3 模型推理与部署导出训练完成后我用detect_ms.py做推理python detect_ms.py \ --source test_rgb/ \ --ir_source test_ir/ \ --weights runs/train/exp/weights/best.pt \ --conf 0.4 \ --img 640推理逻辑是读取同一帧的可见光和红外图各自预处理后拼成一个batch经过双流网络输出检测结果。在RTX 3060上单帧640×640的推理耗时大约35ms也就是28FPS左右基本满足实时性要求。如果需要进一步部署到边缘设备比如Jetson系列或者RK3588建议先导出成ONNX再转成TensorRTpython export.py \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --batch 1 \ --include onnx engine这里有一个比较容易踩的坑自定义的C3STR模块里用了nn.MultiheadAttention导出ONNX时有些版本会报算子不支持。解决办法是手写一个简化版的多头注意力实现只保留nn.Linear和torch.matmul这样ONNX导出最顺利。我后来重构了一个SimpleMultiheadAttention替代原始的TransformerLayer里的nn.MultiheadAttention没有精度损失。4. 常见问题与排查技巧实录4.1 训练阶段典型问题速查我整理了一张问题排查表都是实际遇到过的情况。问题现象可能原因解决方案训练Loss震荡不稳学习率过大或warmup太短初始LR降到0.001warmup拉到5个epoch红外单模态能收敛双流不收敛两路图像未配准或数据增强不一致检查配准结果确保几何增强用相同随机种子mAP一直很低锚框设置不合理开启autoanchor让YOLOv5自动重新聚类锚框显存不足双流BackboneTransformer占用太高Batch size减半启用gradient accumulation检测框偏移半格图像尺寸/填充方式不一致RGB和IR的letterbox参数必须完全一致导出ONNX报错nn.MultiheadAttention算子不支持替换为手写的简化注意力实现推理速度慢Transformer层计算量大只保留一个C3STR或者把嵌入维度降为512最值得说的是配准问题。我第一次双流训练时mAP到了40个epoch还在0.5以下后来发现是红外图像和可见光图有大约4个像素的偏移。这个偏移在单张图上看不出来但训练时模型会发现“同一个目标在两个模态里的特征位置对不上”注意力机制学到的东西都是乱的。后来我把红外图重新配准mAP在20个epoch内就冲上了0.8。数据质量决定模型上限这句话在多光谱项目里体现得格外明显。还有一个容易被忽略的大坑是图像填充方式。YOLOv5的letterbox会在图像周围填充灰边如果可见光和红外的填充尺寸不一致检测框的坐标就会受影响。我的做法是在数据加载工具类里复用同一个letterbox变换对两路图使用完全相同的参数目标尺寸、填充颜色确保两路图的空间位置严格对齐。4.2 单模态基线先跑通再融合我给所有想做类似项目的朋友一个建议不要一上来就搭双流Transformer的完整结构先跑通单模态基线再一步步加东西。我个人推荐这个顺序第一步用原始YOLOv5s分别跑可见光和红外单模态训练确认数据没问题、结果可复现。第二步把Backbone改成双流结构但暂不加入Transformer只做特征拼接观察融合是否比单模态有提升。第三步在Backbone融入C3STR观察注意力机制带来的变化。第四步如果还不够再考虑在融合点加跨模态注意力。每一步都保留对应checkpoint方便回退和对比。这种递进式改动最大的好处是出现问题能快速定位是哪一部分引入的。我见过不少朋友一次性改完所有代码训练失败后连“到底是双流的问题还是Transformer的问题”都分不清排错成本极高。关于资源消耗双流Transformer的结构确实比原版YOLOv5s高不少。如果显存有限还有几个降级方案两个Backbone可以共享权重减少一半Backbone参数Transformer模块只加在其中一个模态的深层特征上或者融合层用1×1卷积替代注意力牺牲一点精度换速度。这些取舍没有什么标准答案完全取决于你的部署目标和算力预算。这个项目做完之后我的体会是Transformer和YOLOv5不是对立关系而是互补关系。YOLOv5负责高效、稳定地把检测框架跑起来Transformer负责把卷积网络看不到的全局关联补上。多光谱提供了更多物理维度的信息Transformer则让这些信息真正被有效利用起来。最后再分享一个我在项目里验证过的小技巧训练时把可见光和红外两路的损失曲线分开记录看哪一路更容易过拟合。如果只有红外路过拟合说明红外数据多样性不足可以单独对红外路做更强的光度增强如果两路都稳再考虑加大模型容量。这个调试思路在多模态项目里非常实用。资料里附带的项目源码和流程教程就是按我上面讲的结构组织的。你拿到之后建议先对照第一、二章把网络结构和数据处理看明白再跑第三章的训练脚本。代码本身能跑通但每个数据集都有自己的“脾气”参数调整是不可避免的。祝你在自己的数据上也能看到那个mAP跳涨的时刻。本文还有配套的精品资源点击获取
网站建设高端定制企业官网