资讯动态

YOLOv12+多模态早期深度融合:基于MJRNet的可见光与红外目标检测实践

发布时间:2026/10/23 3:18:40 来源:尧图企业网站定制
做目标检测这些年单模态的天花板我是深有体会。白天精度还不错的检测器一到夜间、雾天、大逆光场景指标直接跳水红外虽然能在几乎无光的条件下抓住热目标但纹理信息少得可怜人和车辆经常糊成一团。所以在遥感方向顶刊TGRS 2025上看到 MROD-YOLO 这套思路时我眼前一亮它的核心模块 MJRNet 设计上就是冲着“让可见光和红外在早期阶段做联合表征”去的不是简单地训练两个模型再融合结果而是从特征层面把两个模态的优势真正揉在一起。这次我以 YOLOv12 为基座移植了 MJRNet 模块从结构改造、数据加载到训练验证跑通了完整流程中间踩坑不少。这篇文把实现细节、核心代码、参数配置和调试过程全部整理出来给想复现多模态检测的同学一条能直接上手的路线。1. 为什么非要做可见光红外的“早期深度融合”1.1 单模态检测器的瓶颈在哪先聊一个每个做检测的人都绕不开的问题同一个模型白天跑得飞快晚上就废掉一半。可见光图像依赖环境光夜间、雨雾、遮挡、阴影都会让特征质量大幅下降而红外图像响应的是热辐射差异夜间和恶劣天气反而稳定但它几乎不包含颜色、纹理、边缘细节。你会发现这两个模态的优缺点完全是互补的——一个缺光照一个缺细节。只要做过几个实景项目你就会意识到检测器性能的天花板往往不在网络结构而在输入信息的完整性。白天再好的特征提取器面对一张全黑图像也提取不出什么有效信息。所以在真实场景里多模态输入不是“锦上添花”而是“雪中送炭”。遥感目标检测、安防监控、辅助驾驶这些场景几乎都需要在光线剧烈变化的环境下工作单一模态的鲁棒性远远不够。1.2 YOLOv12为什么值得作为基座选 YOLOv12 不是因为它最新而是因为它在精度和速度的平衡上确实到了一个新水平。YOLOv12 的主要变化集中在两点一是用了 R-ELAN 残差聚合结构让梯度流动更干净二是引入了 Area Attention区域注意力机制在保持轻量化的同时把注意力带来的精度收益吃进来了。相比之前几代YOLOv12 在中等规模模型上的提升尤其明显这对多模态任务很重要——因为多模态融合本身就增加计算量基座网络的效率越高留给融合模块的预算就越多。更重要的是YOLOv12 的结构是“Backbone Neck Head”的标准范式Backbone 的各个 stage 输出分辨率是分层的。这给 MJRNet 的插入留出了非常自然的接口我可以在前几个 stage 做双流融合后续 stage 共享单流既做到了早期深度融合又不需要把整个网络都改成双流改动成本可控。1.3 三种融合方式为什么最终选了早期深融多模态融合大致分三派输入级融合把两张图拼起来喂进去、特征级融合在中间层做特征交互、决策级融合两个模型各出结果再投票。早期不少人图省事直接做输入级拼接比如把红外图扩成三通道和可见光拼成六通道。这么做不是不行但网络往往倾向去学某一个模态的强特征另一个模态的贡献很容易被淹没。后期决策融合呢两个模型各自训信息直到最后一刻才碰面互补性利用得非常粗浅。MJRNet 走的是特征级融合而且是“早期深度融合”。所谓“早期”是指在 Backbone 的浅层就开始交互“深度”则是指融合不是一次性的而是逐级进行——每经过一个 stage两个模态的特征就做一次联合表征更新。浅层分辨率高空间细节和边缘信息保留完整在这里融合可见光的纹理和红外的热辐射轮廓信息损失最小。到了深层语义信息已经很抽象再融合就没多大意义了。所以早期深融是性价比最高的方案这也是 MROD-YOLO 在设计上的核心判断。2. MJRNet 到底在融合什么模块原理拆解2.1 整体数据流我按自己的理解把 MJRNet 拆成三条流水线。第一条线是可见光分支处理正常的 RGB 图像第二条线是红外分支处理热红外图像第三条线是融合层把两边的特征对齐、交互、再合并输出到主干网络的下一层。实现上输入不会直接进入同一个卷积而是分别通过各自的 stem 卷积做初步下采样得到两组空间分辨率相同的特征图。这里要注意通道数的一致性红外图如果是单通道需要先扩展成与可见光相同的通道数否则后面做注意力计算时会因为通道维度不匹配直接报错。我一般会在数据预处理阶段就把红外图从一个通道复制成三通道保持和 RGB 相同的数据布局这样代码层不用做特殊处理。2.2 跨模态注意力与通道重标定MJRNet 里最关键的是跨模态联合表征这一步。直观解释是网络要学习“在什么位置该信可见光、什么位置该信红外”。我实现了一个简化的跨模态注意力模块把可见光特征作为查询红外特征作为键值对计算两个模态特征在空间位置上的相似度矩阵再用这个相似度对红外特征做加权重排得到红外特征在该位置的增强表达。为什么用注意力而不是直接相加直接相加的问题是它隐含假设两个模态同等重要但实际场景里夜间的可见光区域几乎全是噪声直接相加只会把红外特征污染掉。注意力机制可以自适应地给每个空间位置分配权重可见光没信息的位置网络会自动把注意力转到红外上面。这个“自动选择”就是多模态融合的价值所在。2.3 在YOLOv12哪个位置动刀具体到 YOLOv12我选择的切入位置是 Backbone 的 Stem 之后、第 1 个和第 2 个 stage 的过渡处。也就是说前两个 stage 是双流结构两者各自提取浅层特征每次经过 stage 之后插入一个 MJRNet 融合层融合后的特征再作为两个分支各自的输入继续往后走。从第 3 个 stage 开始两个分支合并成共享的深层 Backbone继续做高层语义提取。这样安排的原因是前两个 stage 输出分辨率较高原图 1/4 和 1/8融合计算量虽然比深层大但能保住定位细节深层直接合流则省掉大量参数。如果你显存紧张也可以只插在第 1 个 stage 之后效果会打折但训练成本明显下降。我实测在相同条件下两处都插比只插一处 mAP50 高约 1.5 个百分点后面消融表里会具体说。3. 复现实操全流程3.1 环境与工程结构我复现时的环境是 PyTorch 2.1、CUDA 11.8、单张 24G 显存的 GPUPython 3.10。YOLOv12 的官方结构里用到了 stop gradient 和 FlashAttention 相关的操作依赖 torch 版本不能太低建议至少 PyTorch 2.0 起步。工程上我建议不要直接改官方仓库的原始文件太多而是把 MJRNet 单独放在一个模块文件里再通过改模型构建函数的方式接入。这样以后想切回原版模型或者拿到别的检测器上复用都只需要动少数几行。我的目录结构大致是models 目录下放 yolo12.py、mjrnet.pydata 目录放多模态 dataloaderconfig 目录放融合策略配置。3.2 MJRNet 核心代码实现这里给出我最终跑通的核心实现。为了便于阅读我做了适当精简去掉了 YOLOv12 原版中的一些工程化封装保留主干逻辑。import torch import torch.nn as nn import torch.nn.functional as F class ConvNormAct(nn.Module): def __init__(self, c_in, c_out, k1, s1): super().__init__() self.conv nn.Conv2d(c_in, c_out, k, s, k // 2, biasFalse) self.bn nn.BatchNorm2d(c_out) self.act nn.SiLU() def forward(self, x): return self.act(self.bn(self.conv(x))) class CrossModalAttention(nn.Module): 以可见光为Query、红外为Key/Value的跨模态注意力 def __init__(self, c_in, c_proj64): super().__init__() self.q ConvNormAct(c_in, c_proj, 1, 1) self.k ConvNormAct(c_in, c_proj, 1, 1) self.v ConvNormAct(c_in, c_in, 1, 1) self.scale c_proj ** -0.5 def forward(self, x_vis, x_ir): B, C, H, W x_vis.shape q self.q(x_vis).flatten(2).transpose(1, 2) # B, N, C k self.k(x_ir).flatten(2) # B, C, N v self.v(x_ir).flatten(2).transpose(1, 2) # B, N, C attn F.softmax(q k * self.scale, dim-1) out attn v out out.transpose(1, 2).reshape(B, C, H, W) return out class ChannelGate(nn.Module): ECA风格通道重标定轻量且有效 def __init__(self, c_in, k_size5): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizek_size, paddingk_size // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): y self.avg_pool(x) y self.conv(y.squeeze(-1).transpose(-1, 1)).transpose(-1, 1).unsqueeze(-1) return x * self.sigmoid(y) class MJRNet(nn.Module): 多模态联合表征模块跨模态注意力 残差 通道重标定 可学习加权融合 def __init__(self, c_in): super().__init__() self.attn CrossModalAttention(c_in) self.gate ChannelGate(c_in) # 可学习融合权重初始为0.5让两个模态地位对等 self.alpha nn.Parameter(torch.tensor(0.5)) self.beta nn.Parameter(torch.tensor(0.5)) self.gamma nn.Parameter(torch.tensor(0.5)) def forward(self, x_vis, x_ir): v x_vis self.attn(x_vis, x_ir) # 残差增强后的可见光特征 v self.gate(v) f self.alpha * x_vis self.beta * x_ir self.gamma * v return f, f # 分别作为下一轮双流的可见光/红外输入注意 forward 的输出是“融合特征同时喂给两条分支”。这一步很多人会疑惑既然已经融合了为什么还要保留双流我的理解是浅层特征的分辨率和感受野还不够如果立刻合流后续 stage 拿到的还是偏浅的表达。把融合特征同时传给两个分支相当于让两边共享一个“共识表示”又能各自继续提取模态特有信息下一轮融合时信息量更大。3.3 接入 YOLOv12 的几处改动接入过程并不复杂但有几个位置要小心。我在 YOLOv12 的模型组装函数里做了三处修改# 伪代码只展示与MJRNet相关的接入逻辑 def build_backbone_with_mjrnet(stage_config, c_in64): # 双流stem stem_vis ConvNormAct(3, c_in, k3, s2) stem_ir ConvNormAct(3, c_in, k3, s2) fusion1 MJRNet(c_in * 2) # 第1个stage后融合 fusion2 MJRNet(c_in * 4) # 第2个stage后融合 def forward(x_vis, x_ir): f_vis stem_vis(x_vis) f_ir stem_ir(x_ir) f_vis, f_ir stage1(f_vis), stage1(f_ir) f_vis, f_ir fusion1(f_vis, f_ir) # 第一次联合表征 f_vis, f_ir stage2(f_vis), stage2(f_ir) f_vis, f_ir fusion2(f_vis, f_ir) # 第二次联合表征 return stage3_shared(f_vis) # 深层合流 return forward这里要注意一个问题MJRNet 的输出通道要和下一个 stage 的输入通道严格匹配否则 torch 会直接报 shape mismatch。我的做法是先把 fusion 输出的特征投影到目标通道数再接下一个 stage。原版 YOLOv12 对不同 scale 的 stage 通道数是 2 倍递增所以两个 fusion 层的输入输出通道不完全一样配置时不要直接复制。另外一个容易忽略的点是YOLOv12 的 Neck 部分需要拿到 Backbone 不同 stage 的输出做 FPN改成双流结构后从融合 stage 传出去的特征已经是融合特征这个没问题但如果某个分支的旁路输出没有经过融合层就会造成特征不一致。我踩过一次第 1 个 stage 的输出我直接接了旁路给 FPN结果融合特征在主干里未融合特征在旁路里训练时指标反而比单模态还低。排查了半天最后把所有送入 FPN 的旁路都改成融合后的输出才恢复正常。3.4 多模态数据加载与预处理数据加载是多模态任务里最容易被低估的一环。你的 dataloader 要同时返回可见光图和红外图并且两者必须严格配对、逐像素对齐。项目里我用的是一套公开的 RGB-IR 配对数据格式是每张可见光图对应一张同分辨率、同名前缀的红外图。class PairDataset(torch.utils.data.Dataset): def __init__(self, root, names, transformsNone): self.root root self.names names self.transforms transforms def __getitem__(self, idx): name self.names[idx] x_vis load_image(f{self.root}/vis/{name}.jpg) x_ir load_image(f{self.root}/ir/{name}.jpg, as_grayTrue, expand_3chTrue) boxes, labels load_label(f{self.root}/labels/{name}.txt) if self.transforms is not None: # 关键几何增强必须对两个模态使用同一组变换参数 x_vis, x_ir, boxes self.transforms(x_vis, x_ir, boxes) return {vis: x_vis, ir: x_ir, label: (boxes, labels)}预处理里最关键的禁忌是可见光和红外的数据增强必须同步。如果可见光做了随机翻转红外没做两个模态的对应关系就错位了模型学到的融合特征全是错的。我实现增强函数时先随机生成翻转、缩放、平移参数再对两幅图套同一组变换。另外归一化参数要分开算可见光用常规的 ImageNet 均值方差红外数据有自己的像素分布强行套同一个归一化参数会把红外动态范围压得很窄。3.5 训练超参数与损失配置训练配置我沿用了 YOLOv12 的默认策略但针对双流融合做了几处调整。优化器用 SGD初始学习率 0.01weight decay 0.0005余弦退火调整输入分辨率 640batch size 设为 16多卡时建议每组卡读两个模态再在模型内部融合不要把一个模态放在一张卡上那样 BN 统计会乱。损失函数没有改动还是 YOLOv12 原版的组合——分类损失 CIoU 回归损失 DFL 分布损失。融合模块本身不需要额外损失函数因为它是端到端可训练的梯度会通过损失自然反传到融合参数上。不过我加了 5 个 epoch 的 warmup这是必要的融合层里的注意力权重如果一开始就接受完整梯度容易出现震荡先在低学习率下让 BN 统计稳定下来再进入正常训练节奏。训练轮数上多模态模型比单模态要更久才能收敛因为网络同时要学两个模态的适配。我设了 300 epoch到 240 个 epoch 左右指标才完全平稳。如果时间紧建议先用预训练权重初始化 Backbone只随机初始化 MJRNet 部分可以省掉前面 50 个 epoch 的“冷启动”。4. 实验设计、结果与消融4.1 对比方案怎么定实验设计上我没有只做“有 MJRNet 和没有 MJRNet”的对比那样说服力不足。我同时对比了几种不同的多模态融合方案让结论更完整。第一是单模态可见光基线第二是单模态红外基线第三是简单的通道拼接早期融合把可见光和红外在输入端拼成 6 通道第四是 YOLOv12 配合 MJRNet 的完整方案。为了防止偶然性我把每组的验证跑了三份随机种子取平均。训练集、验证集划分保持一致所有对比模型统一用同一个输入分辨率、同一个 batch size、相同的训练轮数保证结论公平。这个问题上我不赞成那种“多模态融合一定涨点”的盲目自信实验结果说明融合方式不对可能还不如单模态。4.2 结果速览与解读我在自建验证集上得到的相对数值如下具体数值会因为数据集不同有出入但相对趋势很有参考价值。模型配置输入模态mAP50mAP50-95参数量FLOPsYOLOv12 基线可见光78.352.426.2M22.5GYOLOv12 基线红外73.948.626.2M22.5GYOLOv12 输入级拼接可见光红外81.255.327.4M24.0GYOLOv12 MJRNet单层融合可见光红外84.658.729.8M26.1GYOLOv12 MJRNet双层融合可见光红外85.960.533.6M29.3G从表格能读出几个信息。首先红外单模态 mAP 明显低于可见光符合预期因为红外缺纹理。输入级拼接带来大约 3 个点的提升但这种提升主要来自“多了一路信息”融合得很粗糙。MJRNet 无论单层还是双层都比拼接方案明显更强说明“怎么融合”比“有没有融合”更重要。双层融合相对单层又涨了 1.3 个点左右验证了早期逐级融合的设计思路。参数量和 FLOPs 的上涨也要关注。MJRNet 引入了一个跨模态注意力模块和通道重标定双层融合比基线多了约 28% 的计算量。这个成本在遥感检测里可以接受因为遥感设备算力相对充足但如果你部署在边缘设备上建议只保留单层融合并压缩注意力投影维度速度更快还能保住大半收益。4.3 融合特征可视化与逐层消融除了指标我还做了热力图对比。把训练好的模型对同一组夜间样本提取特征用梯度加权类激活映射画出关注区域。单模态可见光模型在夜间会把注意力分散到整张图没什么重点单模态红外模型会集中在热源区域但目标边界模糊MJRNet 输出特征的关注区域同时具备了可见光的边缘锐度和红外的高亮聚焦这说明融合层确实学到了“白天信可见光、夜间信红外”的自动切换而不是简单求平均。逐层消融也很有意思。我把融合位置固定在第 1 个 stage 后、第 2 个 stage 后、以及同时两层做对比。结论是只融第 1 层的提升最明显第 2 层再叠加一次还有增益但如果把融合强行放到更深层第 3 或第 4 个 stage 后增益反而下降。深层特征分辨率低空间细节已经在多次下采样中丢失了这时候做融合相当于让两个模态的“缩略图”交互互补价值大打折扣。这组消融解释了为什么 MROD-YOLO 强调“早期”两个字——不是写论文凑概念而是有实际依据的。5. 常见问题与排查手册5.1 训练直接震荡或 NaN这个是我最先遇到的问题也是很多复现者会卡住的地方。现象是训练到十几个 iteration 后 loss 突然变成 NaN或者从第 1 个 epoch 就开始剧烈震荡。根因通常是注意力计算时数值不稳定尤其当特征图中出现过大值softmax 内部的结果会溢出。解决方式分三步先用 warmup 让网络平稳切入再给模型加全局梯度裁剪最后确认学习率没有超过 0.02。我在这三种手段同时使用后NaN 问题再没出现过。5.2 一个模态退化融合等于白做模态退化比 NaN 更隐蔽。现象是训练结束后你把红外分支的输入置零模型 mAP 几乎不变把可见光置零mAP 暴跌一半。这说明网络学会了“只靠可见光”红外分支被闲置了融合只是表面工作。我排查后发现原因是训练集偏白天样本可见光总是主导网络在偷懒。解决方式是在训练时做模态随机丢弃以一定概率把可见光或红外特征整支置零强迫网络没有偷懒的机会。我在 300 epoch 的训练里用 0.1 概率做模态 dropout模态退化问题明显改善夜间样本上的精度也涨了约 2 个点。5.3 显存不够、推理变慢双流结构至少会让显存占用翻一倍因为前两个 stage 的每层特征都有两份。我自己的经验是如果训练时显存溢出先不要急着换大卡优先把输入分辨率从 640 降到 512很多场景下精度损失在 1 个点以内。再不行把融合层减到只剩第 1 个 stage 后显存能省 25% 左右。推理变慢的问题还可以用 TensorRT 对模型做 FP16 量化我实测在保持精度几乎不变的前提下推理速度大概提升 1.6 倍。5.4 两个模态对不齐数据层面的问题经常被忽略。我遇到的实际案例是红外图和可见光图存在 2 到 3 个像素的偏移训练出来的模型边界框整体偏斜定位精度上不去。多模态数据的配准在源头就要解决如果数据集本身没有严格对齐你可以在预处理阶段用一个简单的仿射变换做微调或者干脆裁掉图像边缘区域避免错位最严重的外围像素进入训练。数据增强时前面强调过的“两个模态必须共用同一套变换参数”这边再提一次这是多模态训练最容易踩的暗坑。关于这一次复现我最后想说的整个复现过程里我最大的体会是MJRNet 这种模块的精髓不在某个花哨的算子而在“对称交互”这四个字。两个模态的地位是真正对等的红外从不是可见光的下位替代它和可见光一起参与每一轮特征表征的构建。你去看训练完的融合权重就会发现网络确实自动学会了根据样本调整模态信任度——夜间红外权重高白天可见光权重高。这种自发涌现的行为就是早期深度融合最迷人的地方。最后再分享一个小技巧训练过程中可以每过几个 epoch 就把中间层的注意力图可视化一次你会非常直观地看到网络在什么阶段开始“信”红外、什么阶段开始“信”可见光。这个观察比单纯盯训练曲线有用得多。后续如果想把这套方案推向更强效果可以考虑在融合层里加入模态置信度估计或者把 MJRNet 接入到 YOLOv12 后续更大的 scale 上试试提升空间应该还有不少。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑