做双模态目标检测的这几个月我最大的感受是很多人对“输入级融合”有偏见觉得它就是简单拼一下通道没技术含量。实际跑下来恰恰相反输入级融合在工程量、性价比和论文可解释性上是被低估的方案。这篇文章我就把 RGB-IR 双模态检测的输入级融合掰开揉碎讲清楚从通道拼接的基线写法到带门控的可学习融合模块再到实验设计和调试中容易踩的坑一次说透。做毕设或者准备论文改进的同学可以直接照抄思路和代码。1. 先划清界限输入级融合到底“融合”了些什么1.1 “输入级”不是模糊地带它是一条清晰的流水线位置RGB-IR 双模态检测核心问题只有一个两个模态的信息在什么时候、以什么方式合并。这个“什么时候”就是融合的层级划分标准。输入级融合指的是在骨干网络开始提取特征之前就把可见光和红外图像合并成一张新的输入图合并的对象是原始像素而不是经过卷积抽象后的特征图。后续的检测主干完全可以复用一个纯 RGB 模型甚至直接加载公开的预训练权重。很多人会把输入级融合和特征级融合搞混尤其是当融合代码里出现了一两个卷积层时。有一个简单判断标准如果融合模块的输出仍然是一张“看起来像图像”的张量并且后续接的仍然是从头开始的骨干网络那它就是输入级融合如果两个模态各自走了几层卷积中间某个 feature map 才汇合那就是特征级融合的早期版本别再往输入级上靠了。论文里定义清楚这个边界答辩或评审时才不会被问住。1.2 为什么我不建议一上来就上双流结构我见过不少同学做多模态方案默认就是双流结构——RGB 一支、IR 一支各跑一套特征提取网络中间再各种交互。这种方案确实上限更高但问题也很现实训练难度陡增、显存占用翻倍、训练时间变长而且双流的交互模块如果设计得不好结果可能还不如单模态 baseline 高。输入级融合最大的优势是“没有架构破坏性”。融合发生在网络入口之前检测主干、损失函数、后处理逻辑完全不用动你可以把精力集中在数据预处理和融合头的设计上。从工程角度讲它能让你在最短时间内得到一个可用的双模态结果先确认“多模态信息确实有增益”再决定要不要进一步上更重的结构。毕设和论文初期这个策略能省下大量时间。1.3 三类输入级融合做法的能力对比我按实现复杂度从低到高把输入级融合常见做法分成了三档融合方式实现方式新增参数量适用场景四通道直通RGB 3通道 IR 1通道直接拼接几乎为零最稳的 baseline快速验证模态增益静态融合固定权重加权平均或用传统图像融合算法零硬件条件受限、无法训练的场景可学习融合小型卷积网络或门控机制自适应合并少几十K几M论文创新点、追求更高性能这里我最建议走第一条和第三条组合的路线先用四通道直通跑通全流程拿到一个保底结果再把直通改成可学习门控融合作为论文里的方法模块。两条路线的代码差异很小但实验的逻辑链条非常完整评审挑不出毛病。2. 四通道直通最稳的入口改进与代码实现2.1 数据加载端先把两个模态拼到一起四通道直通的原理不复杂RGB 图是 H×W×3IR 图是 H×W×1沿着通道维度拼成 H×W×4 输入网络。但真写代码时很多人的工程认知会在这里翻车——训练代码里的数据加载、增强、归一化都是按单模态写的需要全部改成“双模态同步处理”。一个比较省心的做法是在 Dataset 层就把拼接完成后续训练循环完全无感知。我自己习惯的写法是这样的import torch import random from PIL import Image from torchvision import transforms class RGBIRDataset(torch.utils.data.Dataset): def __init__(self, rgb_paths, ir_paths, labels, modetrain): super().__init__() self.rgb_paths rgb_paths self.ir_paths ir_paths self.labels labels self.mode mode self.rgb_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # IR 部分建议用自己统计的 mean / std不要直接套 RGB 的 self.ir_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.0], std[1.0]), ]) def __getitem__(self, idx): rgb Image.open(self.rgb_paths[idx]).convert(RGB) ir Image.open(self.ir_paths[idx]).convert(L) # 单通道灰度 if self.mode train: rgb, ir self.sync_augment(rgb, ir) rgb self.rgb_transform(rgb) # [3, H, W] ir self.ir_transform(ir) # [1, H, W] x torch.cat([rgb, ir], dim0) # [4, H, W] # 如果检测器需要归一化的 bbox 等标注这里一并返回 label self.labels[idx] return x, label def sync_augment(self, rgb, ir): # 关键两个模态必须使用完全相同的几何变换 # 这里只做示例实际可按需求加翻转、缩放、平移 if random.random() 0.5: rgb rgb.transpose(Image.FLIP_LEFT_RIGHT) ir ir.transpose(Image.FLIP_LEFT_RIGHT) return rgb, ir2.2 网络入口从3通道改成4通道预训练权重这样迁数据端拼出 4 通道后检测器第一层卷积的输入通道数必须跟着改。大多数检测框架的骨干网络第一层都是一个卷积找到它并把in_channels改成 4 即可。但更关键的问题是预训练权重怎么办直接丢给模型会报形状不匹配因为原权重是按 3 通道训练的。踩了几次坑后我形成了一套比较稳定的初始化策略前 3 个通道直接继承原 RGB 预训练权重新增的 IR 通道用 RGB 三通道权重的均值来初始化然后把整体权重乘以 3/4 做缩放。这样做能保证初始激活值和纯 RGB 模型保持同一个量级网络早期不会因为新通道产生震荡。def load_pretrained_with_4ch(model, state_dict, stem_namemodel.0): 把预训练权重迁移到 4 通道输入模型上 # 处理第一层卷积权重 old_weight state_dict[f{stem_name}.conv.weight].data # [out, 3, k, k] new_in_ch 4 out_ch, _, kh, kw old_weight.shape new_weight torch.zeros(out_ch, new_in_ch, kh, kw) # 前 3 通道继承 RGB 权重 new_weight[:, :3] old_weight # 第 4 通道用 RGB 三通道均值初始化 new_weight[:, 3] old_weight.mean(dim1) # 按通道比例缩放保持初始激活尺度 new_weight * 3.0 / 4.0 state_dict[f{stem_name}.conv.weight] new_weight # 其他层的权重正常加载 model.load_state_dict(state_dict, strictFalse) return model2.3 IR预处理决定成败别把16bit数据直接塞进去红外数据容易踩一个大坑相机的原始输出往往是 16bit 甚至更高位深直接转成 8bit 会损失大量动态范围信息但直接塞进网络又会让训练很不稳定。我建议在数据加载链路里先把 IR 图转成 32 位浮点做一次 min-max 归一化到 [0,1]再按自己统计的 mean/std 做标准化。有个细节值得注意现在很多公开 RGB-IR 数据集里的 IR 图已经被厂商保存成伪彩色图。这种图虽然看起来是 3 通道彩色但里面存的是调色板映射后的显示结果不是真实的红外物理量。如果你拿伪彩色 IR 去和 RGB 拼接模型的性能上限会很低因为它学到的“红外特征”其实是厂商的调色风格。能用原始灰度图就尽量用灰度图别图省事。3. 可学习融合模块让模型自己决定 RGB 和 IR 各信几分3.1 融合头设计一轻量卷积做输入级“翻译”四通道直通虽然简单但它本质上只是把两个模态的信息“并列”放在一起并没有显式建模它们的互补关系。想让效果再上一个台阶可以加一个轻量级的融合头让网络学出一个从双模态到三通道输入的映射。这个融合头可以放在检测器入口之前输出 3 通道这样检测器主干结构完全不用改预训练权重可以直接加载。最简单的融合头是两个卷积层第一阶段把 RGB 和 IR 分开做浅层编码第二阶段把两者拼接后融合输出。你可能觉得这跟上文提到的特征级融合很像但只要输出还是 H×W×3 的“图”后续接的还是完整骨干网络它就是输入级融合。import torch.nn as nn class ConvFusionHead(nn.Module): 把 RGB(3通道) IR(1通道) 融合成 3通道输入 def __init__(self, rgb_ch3, ir_ch1, mid_ch32): super().__init__() self.rgb_branch nn.Conv2d(rgb_ch, mid_ch, 3, padding1, biasFalse) self.ir_branch nn.Conv2d(ir_ch, mid_ch, 3, padding1, biasFalse) self.act nn.ReLU(inplaceTrue) self.fuse nn.Sequential( nn.Conv2d(mid_ch * 2, mid_ch, 1), nn.BatchNorm2d(mid_ch), nn.ReLU(inplaceTrue), nn.Conv2d(mid_ch, rgb_ch, 3, padding1), nn.BatchNorm2d(rgb_ch), ) # 最后一层输出 3 通道接回 RGB 检测器 def forward(self, rgb, ir): fr self.act(self.rgb_branch(rgb)) fi self.act(self.ir_branch(ir)) out self.fuse(torch.cat([fr, fi], dim1)) return out3.2 融合头设计二自适应门控更符合物理直觉轻量卷积融合头虽然有效但它是“黑盒”映射没办法告诉你网络到底靠谁的贡献得出的结果。如果想让方案的可解释性更好更适合写进论文可以在此基础上引入自适应门控机制让网络为每个空间位置生成两组权重分别表示 RGB 和 IR 的信任程度再做加权求和。门控的物理含义非常直观白天光照充足模型应该给 RGB 更高权重夜间或者弱光场景IR 的权重自动增大。这种“按像素位置动态调节模态贡献”的做法在论文里非常好讲故事也比固定权重融合更灵活。class AdaptiveGateFusion(nn.Module): 自适应门控输入级融合输出3通道供检测器使用 def __init__(self, rgb_ch3, ir_ch1, mid_ch32): super().__init__() self.rgb_proj nn.Conv2d(rgb_ch, mid_ch, 3, padding1, biasFalse) self.ir_proj nn.Conv2d(ir_ch, mid_ch, 3, padding1, biasFalse) self.act nn.ReLU(inplaceTrue) # 生成每个像素位置的门控权重 self.gate_net nn.Sequential( nn.Conv2d(mid_ch * 2, mid_ch, 1), nn.ReLU(inplaceTrue), nn.Conv2d(mid_ch, 2, 1), ) # 融合后再过一个小卷积输出 3 通道 self.out_conv nn.Sequential( nn.Conv2d(mid_ch, rgb_ch, 3, padding1), nn.BatchNorm2d(rgb_ch), ) def forward(self, rgb, ir): fr self.act(self.rgb_proj(rgb)) # [B, mid, H, W] fi self.act(self.ir_proj(ir)) # [B, mid, H, W] gates self.gate_net(torch.cat([fr, fi], dim1)) # [B, 2, H, W] wr, wi torch.softmax(gates, dim1) # 软门控和为 1 fused wr * fr wi * fi # [B, mid, H, W] out self.out_conv(fused) # [B, 3, H, W] return out这里门控用的是软注意力不是硬选择。每个位置 RGB 和 IR 的权重加起来等于 1梯度能顺畅地回传到两个分支不会出现某个模态的支路因为被硬开关卡住而学不到东西的情况。3.3 模态缺失怎么办给训练过程加一点“容错”真实场景里红外相机偶尔会掉线或者某块区域因为传感器故障变成噪点。如果你辛辛苦苦训好的输入级融合模块遇到这种情况会直接崩掉。解决思路很简单在训练阶段人为制造“模态缺失”样本让融合模块学会单模态输入时也能输出合理结果。具体做法是以一定概率我常用 10%把其中一路输入置为该模态的训练集均值。用均值而不是全零可以让后续 BN 层的统计量不至于被异常值带偏。加入这个训练策略后即使部署时某个模态彻底失效检测结果也不会完全不可用只是置信度下降一些。这个点写进论文的鲁棒性分析里非常加分。def random_modality_drop(rgb, ir, rgb_mean0.0, ir_mean0.0, drop_prob0.1): if random.random() drop_prob: if random.random() 0.5: ir torch.full_like(ir, ir_mean) # 红外缺失用均值填充 else: rgb torch.full_like(rgb, rgb_mean) # 可见光缺失 return rgb, ir4. 实验设计别糊弄这几张对比表和消融表一定要有4.1 对比基线怎么列评审才不会问“为什么没有某某方法”使用输入级融合最怕一个问题你在论文里说自己的方法好但评审追问“比特征级融合好在哪里”你却答不出来。所以实验部分一个可复现的策略是把从简单到复杂的方法都拉出来对比一遍。我通常至少会列单模态 RGB、单模态 IR、四通道拼接、静态加权融合、可学习轻量融合头、自适应门控融合再加一个特征级融合作为对照。方法mAP50mAP50:95参数量变化推理FPSRGB-only68.241.5-142IR-only65.739.1-1434通道拼接72.545.20141静态加权融合71.944.80140轻量卷积融合头73.646.00.08M138自适应门控融合74.847.60.2M134特征级融合对照75.348.12.5M118上面这组数字是我在自建双模态数据集上跑出的趋势代表仅供相对比较参考。不同数据集、不同检测器绝对数值会有差异但各方法之间的相对高低基本是稳定的。这张表能同时证明三件事双模态比单模态强输入级融合只损失极小的速度就换来明显涨点输入级融合比特征级融合的性价比高得多。4.2 消融实验拆三条线逐步证明每个设计都有用消融实验是论文的重头戏。我的建议是至少拆三条线第一IR 通道的初始化方式第二融合模块本身拆掉门控、拆掉输出残差后的变化第三是否有模态缺失训练的影响。这三条线对应了工程上最容易解释的改进点。实验设置mAP504通道拼接 随机初始化IR通道69.84通道拼接 均值初始化IR通道72.1轻量卷积融合头73.6自适应门控融合74.2门控 模态缺失训练74.8门控 模态缺失 输出残差75.1写论文时不要只写一张大表就完事。每删掉一个模块最好配一小段定性解释说明它到底贡献了什么。比如随机初始化 IR 通道那组掉点明显是因为新通道的随机梯度干扰了预训练权重的稳定性均值初始化缓解了这个过程。这种解释能让评审觉得你对方法本身有深刻理解。4.3 可视化是论文肉眼可见的加分项数值指标只是论文的一部分可视化更能直观体现输入级融合的作用。如果你用的是门控融合门控权重图本身就是很好的可视化素材白天图像中 RGB 权重高、夜间图像中 IR 权重高或者目标所在区域 IR 权重明显上升这一幕直接截图放在论文里比任何文字都有说服力。另外建议在弱光、强光、遮挡这三类容易出“故事”的场景中各挑两组样本展示检测框和置信度的变化。输入级融合改进的往往是漏检和小目标召回对比图上框的数量和目标类别置信度会有明显差异把这些差异圈出来讲清楚即可。5. 调试中跑出来的几个坑每一个都值得记进笔记5.1 双模态配准误差比想象中更容易毁掉实验RGB 和 IR 相机通常存在一定的视差如果两个模态的像素没有严格对齐输入级融合时同一个物体在 RGB 和 IR 图里的位置可能差出好几个像素。虽然人眼看不出来但网络对空间一致性极其敏感配准误差会造成目标的边缘“重影”检测框偏大小目标的特征被互相抵消。如果手里的数据没有配准我建议在训练前做离线配准哪怕只是基于标定结果的仿射变换也能大幅改善效果。没有标定条件时可以加一个强数据增强训练时随机把其中一个模态平移 1~3 个像素让网络学会容忍小范围的空间错位。5.2 BN统计漂移与预处理不一致的连锁反应输入级融合模块里如果接了 BN 层训练和推理时的预处理必须保持完全一致否则 BN 的 running mean 和 running variance 会失配。我在一次实验中调整了 IR 数据的归一化方式但没有同步更新测试代码结果训练时 mAP 一直在涨验证时却越训越差。排查了半天才发现测试链路里 IR 的 mean/std 还是老版本。多卡训练时融合模块里的 BN 尽量改成同步 BN否则小 batch 下统计量不稳定。5.3 双模态数据增强的同步问题数据增强是最容易出现隐性 bug 的地方。很多人训练 RGB-IR 模型时沿用单模态的增强流水线RGB 和 IR 各自做了不同的随机裁剪或翻转两个模态的空间对应关系被打乱模型相当于在“对不齐”的输入上训练效果自然很差。我在 Dataset 里专门写了sync_augment强制两个模态共享同一套几何变换参数。颜色抖动、高斯噪声这类对几何无关的增强可以分开加反正它们不影响空间位置一致性。5.4 训练 IO 与显存的小优化双模态训练意味着每个 step 都要多读一张 IR 图IO 开销相比单模态会明显上升。一个比较实用的优化是把 RGB 和 IR 打包成同一个样本里的一对路径在预处理时并行读取不要让 CPU 串行等待。如果显存有富余也可以把每张 IR 图在进入网络前先被融合头“翻译”成 3 通道后再走检测主干相当于把双模态结构藏起来后续推理逻辑完全保持单模态习惯。5.5 涨点到底该归功于融合还是模态数量这是个很容易被忽视的问题。有时候输入级融合效果提升并不是因为融合模块设计得多厉害而是因为模型终于看到了 IR 模态多了一份可见光没有的温差信息。为了在写论文时把这个矛盾讲清楚我会额外做一个“单模态上限分析”把 RGB 和 IR 分别单独训练出各自最好的基线再把融合结果和这两个基线的上限比。如果融合结果显著优于任何一个单模态的上限才说明融合本身创造了价值。这一步放到论文里通常会让结论更加扎实。6. 收尾前再给几句实在话你可能已经发现了输入级融合的整个技术栈并不复杂但如果每一步都做到位它足够撑起一个完整的毕设工作或者一篇论文的核心改进点。我个人做算法的经验是不要一上来就追求复杂的双流交互结构先用四通道直通跑通流程再用可学习门控融合做方法创新最后用配准、预处理、模态缺失这些工程细节去拉满鲁棒性。逻辑链条清晰能讲故事也能出活。这套东西在我自己的实验里是实打实跑出过稳定涨点的你拿去用的时候记得把 IR 数据预处理和网络入口的通道修改这两处先检查好这两处最容易出问题。