简介:基于YOLOv5与Transformer的多光谱目标检测系统代码包,是一套高分项目方案,面向目标检测研究者、计算机视觉开发者和需要复现前沿算法的学习者。项目实现跨模态融合变换器(CFT),利用自注意力同时捕获可见光与热红外模态内的长距离依赖,并在特征提取阶段融合全局上下文,解决传统卷积网络难以处理多传感器信息互补的问题,可直接用于论文复现、算法对比或工程化改造。资源共114个文件,压缩包约39.75MB,以Python源码、YAML配置、编译缓存、Shell脚本和Docker部署文件为主,辅以说明文档与示例动图、图片,目录结构清晰,便于按模块阅读。已有997人学习下载,适合在多光谱目标检测方向快速搭建环境并验证效果的开发者使用。内容包含完整代码工程与部署支持,借助容器化配置和脚本可省去繁琐搭建步骤,演示素材可直观呈现检测结果,便于继续开展跨模态融合实验。
1. 多光谱目标检测:YOLOv5 打底、Transformer 补位的系统方案
做目标检测这几年,单模态模型的天花板很容易撞见:可见光图像一到夜间、雾天、目标被遮挡或伪装时,检测置信度掉得厉害。多光谱目标检测系统就是为了补上这一刀——把可见光与红外等多波段信息叠加起来,让模型在光照不足时仍能抓住目标。基于 YOLOv5 + Transformer 做这条技术路线,是我目前见过工程代价最低、精度收益却最明显的组合:YOLOv5 负责成熟的 anchor 检测框架和训练生态,Transformer 负责跨波段特征的交互与增强,两者分工明确,调起来也比纯 Transformer 检测器更可控。这篇文章面向正在调研多光谱检测方案、或准备把现有 YOLOv5 项目扩展成多光谱输入的工程师,我会把数据预处理、网络改造、训练参数和踩过的坑一次讲透。
2. 多光谱数据预处理:配准、归一化与融合策略三选一
多光谱检测的第一步从来不是改网络,而是把数据搞对。我见过不少人在单模态数据集上跑得飞起,一换到多光谱就掉点,最后查下来八成是数据预处理出了问题。
2.1 拿到双/多波段原始数据后,先做像素级配准
常见做法是,多光谱系统通过分光棱镜或独立传感器同时采集同一场景的可见光与红外图像,但两个传感器之间存在物理视差,导致画面中同一目标在两张图上的像素位置不完全重合。这种配准偏差对检测是致命的——目标在可见光图中心、在红外图上偏移几个像素,融合后会出现重影,小目标直接漏检。
我一般先用 OpenCV 的 ECC 或基于特征点的配准方法做几何对齐。对于固定安装的监控摄像头,仿射变换足够;对于无人机或手持设备,需要投影变换。
import cv2 import numpy as np def align_images(visible_path, infrared_path, warp_mode=cv2.MOTION_EUCLIDEAN): vis = cv2.imread(visible_path, cv2.IMREAD_GRAYSCALE) inf = cv2.imread(infrared_path, cv2.IMREAD_GRAYSCALE) # 统一尺寸,避免分辨率不一致导致配准失效 inf_resized = cv2.resize(inf, (vis.shape[1], vis.shape[0])) # ECC 迭代优化,求红外图到可见光图的变换矩阵 warp_matrix = np.eye(2, 3, dtype=np.float32) criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 5000, 1e-6) try: (cc, warp_matrix) = cv2.findTransformECC( vis, inf_resized, warp_matrix, warp_mode, criteria, None, 5 ) except cv2.error as e: print(f"ECC 配准失败,回退到直方图均衡后重试: {e}") vis_eq = cv2.equalizeHist(vis) inf_eq = cv2.equalizeHist(inf_resized) (cc, warp_matrix) = cv2.findTransformECC( vis_eq, inf_eq, warp_matrix, warp_mode, criteria, None, 5 ) aligned_inf = cv2.warpAffine(inf_resized, warp_matrix, (vis.shape[1], vis.shape[0]), flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP) return vis, aligned_inf这段代码里特别要注意warp_mode的选择:固定场景用MOTION_EUCLIDEAN(旋转+平移)就够,不要一上来就MOTION_HOMOGRAPHY,自由度太高容易收敛到错误的局部最优。findTransformECC的最后一个参数是高斯金字塔层数,设 5 能让多尺度搜索更稳。如果 ECC 报错,常见原因是两张图的亮度分布差异太大(红外图和可见光的灰度统计完全不同),这时先对两图分别做直方图均衡再配准,成功率会高很多。
配准完成后一定要做一步人工抽检:把两图以半透明方式叠加,看道路边缘、建筑轮廓是否重合。配准质量直接决定后续融合和训练的上限,这一步偷懒后面全在还债。
2.2 分波段归一化:红外和可见光不能共用一份统计量
很多从单模态迁移过来的同学会把归一化参数直接沿用 ImageNet 的 mean/std,在多光谱上这会让红外通道的数值被压缩到一个极窄的区间。红外图像的像素分布和可见光完全不是一回事:可见光三通道在 0-255 上接近均匀分布,而红外图像常常是背景暗、目标亮的高对比结构,共用归一化参数等于把红外信号的大部分动态范围扔掉了。
正确做法是分别统计每个波段的 mean 和 std,或者至少按模态分组统计。我在工程里一般直接算全数据集的逐通道均值方差,而不是用 ImageNet 预训练值。
import numpy as np from glob import glob vis_paths = sorted(glob("dataset/train/visible/*.png")) inf_paths = sorted(glob("dataset/train/infrared/*.png")) # 逐通道累计像素和与平方和,避免一次性载入全部图像 sums = np.zeros(4, dtype=np.float64) sqsums = np.zeros(4, dtype=np.float64) count = 0 for vp, ip in zip(vis_paths, inf_paths): vis = cv2.imread(vp).astype(np.float64) / 255.0 # BGR order inf = cv2.imread(ip, cv2.IMREAD_GRAYSCALE).astype(np.float64) / 255.0 img = np.dstack([vis, inf]) # [H, W, 4],前三个 B/G/R,第四个 IR sums += img.sum(axis=(0, 1)) sqsums += (img ** 2).sum(axis=(0, 1)) count += img.shape[0] * img.shape[1] mean = sums / count std = np.sqrt(sqsums / count - mean ** 2) print("per-channel mean:", mean) print("per-channel std:", std) # 输出后写入一个 npy 文件,训练和推理共用同一份 np.save("multispectral_mean_std.npy", np.stack([mean, std]))这段代码的输出会很明显:红外通道的 mean 通常显著低于可见光通道,std 却可能更高。把这些参数存成 npy 后,在 YOLOv5 的datasets.py里替换归一化逻辑就很方便。我这里把可见光的 BGR 与红外单通道堆叠成 4 通道,模型第一个卷积层的输入也要对应改成 4 通道,这部分在下一章详细说。
2.3 Early Fusion / Mid Fusion / Late Fusion 三选一
融合策略的选择直接决定你的网络结构怎么改。从业界和我的实践经验来看,三种方式各有适用场景:
Early Fusion(输入级融合)是把若干波段直接堆叠成多通道输入。优点是实现最简单,YOLOv5 只需改第一个卷积层的输入通道数;缺点是如果配准有误差,错误信息会一路传导到深层,且早期卷积要同时处理两套分布差异极大的特征,学习难度高。适合配准精度高、波段数少(2-4 个)的场景。
Mid Fusion(特征级融合)是让可见光和红外分别经过若干卷积层提取中层特征后再合并。优点是两个模态的特征先各自抽象,语义对齐后再融合,抗配准误差能力强;缺点是网络结构要动得比较大,backbone 需要做成双流结构,显存和训练时间几乎翻倍。适合配准精度一般、目标又小的场景。
Late Fusion(决策级融合)是各自跑一个检测器再做结果融合。优点是完全不需要改单模态模型,两套检测器可以独立迭代;缺点是两个模型的漏检无法互补,而且多光谱在浅层特征上的信息互补优势完全用不上,效果通常垫底。
我自己的经验排序是:配准做得好选 Early Fusion,省事且效果好;配准做不到像素级选 Mid Fusion 双流结构。这套系统的标题同时提到 YOLOv5 + Transformer,自然采取 Early Fusion + 在 neck 里用 Transformer 做特征交互——既保留了 YOLOv5 的 backbone 结构,又给跨波段特征融合留出了足够强的交互层。
3. 网络结构改造:YOLOv5 backbone 不变,在 neck 里注入 Transformer
多光谱数据准备到位后,下一步是把 YOLOv5 的网络结构改造成能同时吃 4 通道输入、并用 Transformer 做特征增强的方案。
3.1 先定位插入点:backbone / neck / head 三处改法差异
YOLOv5 的检测器分成 backbone(CSPDarknet 提取特征)、neck(PANet 多尺度融合)、head(检测头输出类别与框)三段。Transformer 可以插在任何一个位置,但效果和代价差异很大。
插在 backbone 里,常见做法是直接用 Swin Transformer 替换整个 CSPDarknet。优点是特征提取能力上限高,Swin Transformer 的窗口注意力在 COCO 上的表现确实优于 CSPDarknet;缺点是替换后预训练权重不能复用(YOLOv5 官方的yolov5s.pt是基于 CSPDarknet 训练的),得从头训起,而多光谱数据集通常没有 COCO 那么大,很容易训不收敛或严重过拟合。
插在 head 里,即把检测头换成基于 Transformer 的 DETR 风格解码端。理论上有全局建模能力,但 DETR 的小目标检测能力一直被诟病,而多光谱检测常见的行人、车辆、无人机等恰恰是小目标居多,所以我不会在这个位置动刀。
插在 neck 里是我最推荐的位置。PANet 本身做的就是多尺度特征融合,把主干输出的 C3、C4、C5 特征层送到 Transformer 编码器里做一次跨尺度、跨波段的全局交互,再回填给 PANet 做多尺度融合。这样做有三个实际好处:backbone 可以继续用官方预训练权重、训练收敛快;Transformer 装在最需要建模长距离依赖的特征融合阶段,正好和本研究需求吻合;改动面小,不需要重写整个推理流程。
3.2 把 YOLOv5 官方 C3 模块改造成 C3TR:给特征融合层加自注意力
YOLOv5 官方仓库里有一个实验性模块叫 C3TR,本质是把 C3 模块的 Bottleneck 替换成一个 TransformerEncoder 层。我一般会基于它做适配多光谱的改良,下面给出一个可直接用的实现。
# models/common.py 中新增 C3TR 模块 import copy import torch import torch.nn as nn from torch.nn import TransformerEncoder, TransformerEncoderLayer class TransformerLayer(nn.Module): """单层 Transformer Encoder,d_model 为特征通道数,nhead 为注意力头数""" def __init__(self, c1, c2, num_heads=8, num_layers=1): super().__init__() self.ma = nn.MultiheadAttention(c1, num_heads, dropout=0.1, batch_first=True) self.fc1 = nn.Linear(c1, c2) self.fc2 = nn.Linear(c2, c1) self.norm1 = nn.LayerNorm(c1) self.norm2 = nn.LayerNorm(c1) self.dropout = nn.Dropout(0.1) def forward(self, x): # x: [B, C, H, W] -> [B, H*W, C],注意力要求序列维度在前 b, c, h, w = x.shape x = x.flatten(2).permute(0, 2, 1) attn_out, _ = self.ma(x, x, x) x = self.norm1(x + self.dropout(attn_out)) x = self.norm2(x + self.dropout(self.fc2(self.dropout(torch.relu(self.fc1(x)))))) # 恢复为 [B, C, H, W] x = x.permute(0, 2, 1).reshape(b, c, h, w) return x class C3TR(C3): """在 C3 基础上把最后一个 Bottleneck 替换为 TransformerLayer""" def __init__(self, c1, c2, n=1, num_heads=8, num_layers=1): super().__init__(c1, c2, n) self.m = nn.Sequential( copy.deepcopy(TransformerLayer(c2, c2, num_heads, num_layers)) )这里有几个参数要重点说明:num_heads默认 8,但实际使用中如果特征层分辨率较高(比如输入 640x640 时 C3 层的特征图是 80x80),序列长度就是 6400,8 个头的注意力矩阵是 6400x6400 量级,显存消耗非常大。我实际调参时一般把 80x80 特征层的 C3TR 设为 4 个头甚至 2 个头,或者用window_size限制注意力范围,否则很容易爆显存。num_layers建议从 1 层开始叠加,不要一上来就 4 层,多光谱数据集不如 ImageNet 丰富,深层 Transformer 在这种数据规模下容易过拟合。
3.3 替换 backbone 为 Swin Transformer 的边界与成本
Swin Transformer 是视觉 Transformer 里少有的、在老检测框架里落地效果不错的主干网络,但用在多光谱检测上要算清楚成本。
先说收益:Swin Transformer 的层级式设计(patch merging 逐步降低分辨率)和窗口注意力(window attention)在 ImageNet 分类上超过了同等参数量的 CNN。如果换成 Swin-Tiny 作为 YOLOv5 的 backbone,理论上 C3、C4、C5 三层的感受野建模能力会比 CSPDarknet 更强,尤其对红外波段里那些对比度低、轮廓模糊的目标,注意力机制能更早聚焦到目标区域。
但成本有三笔。第一笔是预训练权重全部作废,Swin-Tiny 的 ImageNet 权重结构上和 CSPDarknet 完全不同,无法直接加载进 YOLOv5,只能从零开始训练,多光谱训练集通常几千张到几万张,完全不够让一个 2800 万参数的 Transformer backbone 收敛。第二笔是输入分辨率受限,Swin Transformer 的 patch 尺寸固定为 4x4,下采样倍率是固定的 4/8/16/32,YOLOv5 默认的 8/16/32 倍数正好匹配,但如果你想用更大的输入图(比如 1280 用于小目标检测),Swin 的窗口划分会引入额外的 padding 开销。第三笔是工程改造成本,要把 Swin 的 forward 输出对齐到 YOLOv5 的 Detect 层,需要改 C3、SPPF 等多个配套模块。
我的建议是:如果为了发布论文、需要对比 SOTA,可以做 Swin backbone,但要准备好大规模预训练或充足的蒸馏方案。如果是为了落地项目,C3TR 插在 neck 里是性价比最高的方案。
3.4 通道数对齐与第一个卷积层的原始输出
改完结构后有一个最容易被忽略的细节:YOLOv5 第一个卷积层的输入通道数写死为 3(RGB)。用 4 通道多光谱输入时,必须把models/yolov5s.yaml里第一层的ch: 3改成ch: 4。如果使用官方预训练权重yolov5s.pt继续训练,权重文件里第一个卷积层是 3x32 的卷积核,改为 4 通道后形状不匹配,加载会报错。
我的处理方式是:加载预训练权重时,对第一个卷积层做通道裁剪复制。这样可见光三通道可以复用 ImageNet 预训练学到的卷积核,红外通道则用三通道权重的均值初始化。
# 加载预训练权重时处理首层通道数不匹配 ckpt = torch.load("yolov5s.pt", map_location="cpu") state_dict = ckpt["model"].float().state_dict() old_weight = state_dict["model.0.conv.weight"] # [32, 3, 6, 6] new_weight = old_weight.mean(dim=1, keepdim=True).repeat(1, 4, 1, 1) # [32, 4, 6, 6] state_dict["model.0.conv.weight"] = new_weight # 首层 bias 直接复用,无需改动 torch.save({"model": state_dict}, "yolov5s_4ch_init.pt")这里的model.0.conv.weight是 YOLOv5 模型序列中的第一个卷积层,old_weight.mean(dim=1)把三个通道的卷积核做平均,再复制四份,等价于让红外通道复用可见光通道的平均特征。第一次 forward 时红外通道的激活值可能偏大或偏小,但经过 BN 层后会被拉回正常范围,训练大概几百步就能自适应。
4. 训练多光谱模型:超参数调整、anchors 聚类与分阶段解冻
网络改好、数据备齐,接下来进入训练环节。很多人直接把 YOLOv5 默认的超参数套上去跑,结果发现 mAP 涨不上去,或者 loss 震荡厉害。多光谱训练有自己的调参逻辑。
4.1 训练数据格式与标签对齐:不能直接套用单模态标注
多光谱数据集的标签文件格式与 YOLOv5 单模态数据集相同,还是 txt 格式,每行一个目标:class_id x_center y_center width height,坐标归一化到 [0, 1]。但有个关键点:标签坐标必须基于配准后的图像,而不是原始的可见光或红外图。如果你在可见光图上标注,而红外图未配准,融合后标签和目标实际位置偏差会在几个像素到几十像素之间,对小目标来说是致命的。
我的标注流程是:先跑一遍 2.1 的配准脚本,生成对齐后的可见光 + 红外配对图,然后在可见光图上标注,直接把标签同时用于两个通道。如果你有现成的单模态检测模型,也可以用它的预测结果生成伪标签,但要人工检查一遍,多光谱下目标在可见光中不可见时(比如夜间行人被树木遮挡),单模态模型根本预测不出来,伪标签会漏掉这些目标。
数据集目录结构我习惯这样组织:
dataset/ train/ images/ # 4通道融合图,存储为 .npy 或 4通道 PNG labels/ # txt 标签 val/ images/ labels/多通道图不建议存成 PNG,因为 PNG 最多支持 4 通道且压缩有损,我更推荐直接保存为 .npy 文件,YOLOv5 的 dataloader 需要自定义读取逻辑。也可以把 4 通道拆成可见光三通道 PNG + 红外单通道 PNG,训练时分别读取再拼接,这样更容易复用 YOLOv5 现成的图像加载流程。
4.2 anchors 重新聚类与输入分辨率设定
YOLOv5 默认的 anchors 是基于 COCO 数据集聚类出来的,COCO 的目标尺度分布偏向中等大小物体。多光谱检测的目标类型差异很大——行人、车辆可能比较大,但无人机、远处的人体目标非常小。直接沿用默认 anchors 会造成小目标检测率偏低,因为 anchor 的尺寸与真实框的 IoU 匹配率低,导致大量目标无法分配到正样本。我一般训练前用 YOLOv5 仓库的utils/autoanchor.py脚本对训练集重新聚类:
python utils/autoanchor.py --data dataset.yaml --img 640 --k 9这个命令会计算训练集所有真实框的宽高分布,用 k-means 聚类出 9 个新 anchor。执行完脚本会输出更新建议和新的 anchors 数值,你把它替换到models/yolov5s.yaml对应位置即可。如果聚类结果显示目标的宽高比极度不均(比如细长的人体目标),说明你的数据集和 COCO 差异很大,重新聚类基本是必须做的。
输入分辨率方面,多光谱图像我一般建议保持与单模态一致或略调低。原因很简单:Transformer 模块的显存占用与特征图分辨率强相关,输入从 640 提升到 1280,neck 里 80x80 特征层的序列长度变成了 160x160(25600 个 token),注意力计算量变为原来的 16 倍,显存直接爆掉。如果就是要检测小目标,可以用瓦片化推理(把大图切成 640 重叠块分别推理后在原图上合并 NMS 结果),而不是盲目拉高输入分辨率。
4.3 分阶段冻结训练与损失权重微调
多光谱训练最怕的是让整个网络从头学,结果过拟合。我通常采用三阶段策略,比一次端到端训练稳定得多。
第一阶段只训练首层卷积和 neck 里的 Transformer 模块,其余层全部冻结。这一步是让模型先学会把多光谱特征融合起来,backbone 保持 ImageNet 特征不动。学习率设在 1e-3 偏大一点,跑 10 个 epoch 左右,观察训练 loss 能否稳定下降。
第二阶段解冻 backbone 的高层(C4、C5 特征层),只冻结底层(C1、C2、C3)。因为底层特征通常是通用纹理和边缘,跨数据集泛化能力强,不需要大改;高层的语义特征需要针对多光谱目标做领域适配。学习率降到 1e-4,跑 50 个 epoch。
第三阶段全部解冻,学习率再降到 1e-5,全量微调 20 个 epoch。这样做的原因是多光谱数据规模有限,直接全量训练容易在小数据集上过拟合;分阶段训练相当于给模型一个从易到难的学习课程。
损失权重方面,CIoU loss、置信度 loss、分类 loss 三者默认权重是 0.05 : 1.0 : 0.5。多光谱场景下我一般把分类 loss 权重从 0.5 提到 0.7,因为红外通道对语义辨别的贡献更强(轮廓信息丰富而颜色基本缺失),需要强化分类任务的梯度回传。这只是我经验值,不同数据集差异较大,可以画几组 loss 曲线对比后定。
5. 避坑与排查:多光谱检测最容易翻车的 5 个问题
训练和部署过程中有不少怪问题,看起来像模型不行,实际上都是系统性工程细节出错了。以下是踩过最多遍的坑,每条都按现象到解决的方式给你说明白。
5.1 融合图像出现重影导致小目标漏检
现象:训练集上 loss 能正常下降,但验证集上小目标的 recall 明显偏低,可视化检测结果时发现目标框位置和实际目标有偏移。
原因:配准脚本的 ECC 算法在低纹理场景下收敛不到位,或者评价指标只看了整体 mAP 没留意小目标的 AP。重影对小目标影响最大,因为目标只占几个像素,融合后两个模态的目标位置不对齐,相当于把一个目标糊成了两个模糊投影,检测头无法形成强响应。
解决:在训练前对每张训练图做配准质量抽检,计算两图的归一化互信息(NMI),低于阈值的样本要么重新配准,要么直接丢弃。我写过一个批处理脚本,对配准后的每对图像计算 NMI 并输出分布直方图,低于 0.3 的样本基本都存在明显重影。另外,配准时不要用原始分辨率直接跑 ECC,把图像缩小到 320 再进行 ECC 配准,得到的变换矩阵用于原图,成功率更高,因为小尺度下 ECC 更容易收敛到全局最优。
5.2 BN 统计量震荡导致训练不收敛
现象:训练开始的几百步内,训练 loss 反复跳变,没有稳定下降趋势,甚至出现 loss 突然变成 NaN。
原因:可见光和红外两个波段的数据分布差异太大,而 BN 层在早期 batch 内统计出的均值和方差极不稳定。尤其红外通道的像素分布经常是双峰的(冷背景 + 热目标),BN 强行把它归一化成单峰高斯,梯度方向就会来回抖动。
解决:第一个方案是在数据预处理阶段做分波段归一化,用 2.2 节算好的 mean/std 提前把像素拉到接近零均值单位方差,减少 BN 的负担。第二个方案是把第一个卷积层后的 BN 冻结 5 个 epoch,让它先把底层特征跑稳定。第三个方案更彻底:把 BN 换成 GroupNorm,但 YOLOv5 全模型的 BN 替换工程量较大,只在 neck 的 C3TR 模块内使用 LayerNorm(代码里已经用了),这是 Transformer 类模块标配,不需要额外处理。
5.3 Transformer 模块显存占用过高,batch size 上不去
现象:加入 C3TR 后,原本能训 32 张图的 batch size 只能降到 4,训练速度慢到无法接受。
原因:Transformer 的自注意力计算复杂度是序列长度的平方。以 640x640 输入为例,C3 层产生的 80x80 特征图展平后是 6400 个 token,每个 token 要与全部 6400 个 token 计算注意力,单层单头的内存占用就在几十 MB 量级,多头叠加后非常可观。
解决:我一般从三个方向同时下手。一是减少头数,8 头改成 4 头,效果损失很小,显存几乎减半。二是限制注意力范围,把全局自注意力改成局部窗口注意力(类似 Swin 的思路),窗口设为 8x8 或 16x16,相当于把序列分成多个小块分别做注意力,这是显存开销下降最明显的措施。三是使用梯度累积替代大 batch:
python train.py --batch-size 16 --accumulate 4即每个 batch 实际只放 16 张图,梯度累积 4 个 step 后再更新一次权重,等效 batch size 为 64。这个命令在 YOLOv5 的 train.py 里会被正确解析为 nbs=64 的策略。显存占用直接是原来的 1/4,训练效果与真实 64 batch 几乎一致。
5.4 数据集过少导致严重过拟合
现象:训练 loss 和验证 loss 迅速分化,训练集 mAP 接近 1.0,验证集 mAP 只有 0.3-0.4。
原因:多光谱数据集的获取成本远高于单模态,通常只有几千张甚至几百张。而 C3TR 模块引入了额外参数,backbone 加上 Transformer 的参数量总量不小,小数据集根本喂不饱。
解决:除了常规的 mosaic、翻转增强,我强烈推荐一个针对多光谱的增强策略:随机丢弃模态。训练时以 50% 的概率只输入可见光三通道,以 25% 的概率只输入红外单通道,另外 25% 输入完整的 4 通道。这样做有两个好处:模型被迫在两个单模态上也能保持检测能力,不会过度依赖某个波段;等效于把数据量扩展了 3 倍,过拟合压力小很多。实现上只需在 dataloader 中按概率把某些通道置零,如果置零后目标在可见光或红外下完全不可见,模型就学会去另一个模态找信息,这正是多光谱互补性的意义。
5.5 锚框不匹配多光谱目标的真实尺度分布
现象:训练过程中正样本匹配率很低(可以看 loss 曲线,如果 obj loss 迟迟降不下去,大概率是正样本太少),recall 明显低于 precision。
原因:4.2 节的 anchors 聚类在操作后被很多人跳过,或者聚类时采用了错误的 metric。YOLOv5 的 autoanchor 使用 IoU 作为距离度量,但如果你的数据集里同时存在特别小的目标和特别大的目标,k-means 聚类会把 anchor 集中在中等尺度,极端尺度目标的正样本分配不足。
解决:重新聚类后要按目标面积分布检查 cluster 中心。有些实践者会把聚类数从 9 调整为 12(每个尺度 4 个 anchor),小目标上的 recall 会改善不少。聚类时可以为小目标增加权重,把面积小于 32x32 像素的真实框在聚类 loss 里乘以 2 的系数,这能让小目标的 anchor 分配更充分。另外,如果聚类得到的 anchor 长宽比过于极端(比如 1:10),需要检查标注框是否过于贴近目标边缘,而不是急着调 anchor 数量。
6. 从跑通到可信:验证融合有效性与部署提速
模型训练完成后,不能只看 mAP 数值就认为大功告成。我这个标题的系统要真正上线,至少还要验证三件事:融合是否真的有效、置信度阈值是否合理、推理速度能否满足业务。
6.1 Grad-CAM 热力图验证模型是否用到红外信息
一个常见质疑是:加了多光谱输入,模型会不会还是只靠可见光信息做检测,红外通道成了摆设?我用 Grad-CAM 对检测头的分类分支做可视化,能直观看到模型激活区域。YOLOv5 的 Grad-CAM 实现并不复杂,关键是在 forward 中拿到目标特征层(一般取最后一个 C3 层的输出),对该特征图做全局平均池化得到类别权重,再反传到输入空间。观察热力图时重点看测温目标(行人、车辆)区域是否同时在可见光和红外通道上有激活响应。如果红外通道的梯度始终接近零,说明参数初始化有问题或者网络没有学到跨模态互补,需要回到 3.4 节检查首层卷积权重初始化方式。
6.2 消融实验与置信度阈值选取
跑四组对比实验验证每个设计的必要性:纯 YOLOv5 RGB 输入、YOLOv5 4 通道输入(不加 Transformer)、YOLOv5 + C3TR 可见光输入、YOLOv5 + C3TR 多光谱输入。如果第三组相比第一组 mAP 提升不明显,说明 C3TR 在你的数据上没起什么作用;如果第四组相比第三组提升了,说明多光谱输入确实带来了信息增量。二值化统计后,你会发现模型对目标类别的区分能力在不同波段是不同的,这就是互补性的证据。
置信度阈值的选择不能只看 PR 曲线,要结合业务成本。如果目标漏检的代价高(比如安防中的入侵检测),阈值设在 0.15-0.2 更合理,宁可多几个误报;如果是资源受限的移动端部署,阈值设在 0.4-0.5 以减少后续跟踪模块的压力。我一般会导出所有阈值的 PR 曲线和 F1 曲线,选 F1 最高点作为默认值,再用业务数据做一次终验。
6.3 TensorRT 部署与 FP16 量化
多光谱检测系统如果要跑在边缘设备上,速度是关键卡点。我通常会把训练好的模型导出并转换成 TensorRT 引擎。FP16 量化对本系统的注意力模块影响比纯 CNN 更大,注意力中的 softmax 和 LayerNorm 在 FP16 下的精度损失可能导致小目标漏检,所以部署前一定要用验证集对比 FP16 和 FP32 的 mAP 差值和单帧延迟。如果 mAP 下降超过 1.5%,就退回 FP32,或者只在 C3TR 模块保留 FP32 计算(TensorRT 支持按层指定精度),其他卷积层继续用 FP16,这种混合精度配置通常能兼顾速度和精度。
跑通一个多光谱目标检测系统,我最大的教训是数据永远比模型重要:配准做不好后续所有技巧都失效,波段归一化做不好 C3TR 再强也收敛不了。这套 YOLOv5 + Transformer 的方案我现在已经沉淀成了内部模板,新接的项目只要换数据集和配准脚本就能快速迁移,省掉了很多从零探索的时间。希望这一篇的细节和经验能帮你的多光谱检测项目少走几步弯路。
本文还有配套的精品资源,点击获取