YOLOv11改进-Neck | HAFFormer双向模态引导交叉注意力融合模块,互补挖掘VIS/IR信息、自适应门控融合,助力可见光-红外目标检测轻量涨点 | Pattern Recognitio

前言

本文介绍了面向可见光-红外目标检测的轻量级跨模态融合模块 HAFFormer,源于 LCAFNet 中的双向模态引导交叉注意力与门控融合思想。该模块分别利用可见光和红外特征生成交叉注意力增强表示,在保留原始模态信息的同时充分挖掘纹理细节、热目标响应与边缘结构等互补信息,并通过门控权重自适应分配两种模态贡献。我们将 HAFFormer 成功集成进 YOLO11 的 Neck 特征融合阶段,替代原有拼接融合模块,实现更鲁棒、更轻量的多模态特征交互与检测表达。

文章目录: YOLO11改进大全:卷积层、轻量化、注意力机制、损失函数、Backbone、SPPF、Neck、检测头全方位优化汇总

专栏链接: YOLO11改进专栏

介绍

可见光-红外目标检测旨在利用不同模态之间的互补性,提升复杂环境下目标分类与定位的准确性。然而,现有多数方法更关注检测性能,却忽视了网络复杂度,限制了其在真实场景中的应用。为此,本文提出一种轻量级模态引导交叉注意力融合网络 LCAFNet,用于可见光-红外目标检测。该网络由 visible-guided cross-attention block(VG-CAB)、infrared-guided cross-attention block(IG-CAB)和 gated fusion block(GFB)组成。VG-CAB 和 IG-CAB 利用一个模态的注意力权重去引导另一个模态的信息聚合,从两个不同视角实现跨模态交互与特征融合,生成同时包含可见光和红外信息的互补特征,从而获得更全面、更鲁棒的多模态表征。在 VG-CAB 与 IG-CAB 生成增强互补特征的基础上,GFB 通过门控策略实现自适应融合。此外,本文还引入双分支骨干网络提取的浅层 VIS 和 IR 特征,用于挖掘更多空间与边缘信息,进一步提升检测模型的定位和分类能力。大量实验表明,LCAFNet 在五个常用公开数据集上相比优秀模型获得更好的检测性能和更低的网络复杂度;例如在 DroneVehicle 数据集上,它比当前先进模型提升 1.6% mAP50,同时参数量仅为其八分之一。

文章链接

论文地址:论文地址

代码地址:代码地址

基本原理

1. 解决的关键问题

HAFFormer 用双向模态引导交叉注意力提取可见光与红外之间的互补信息,再通过门控机制完成自适应融合。它解决的关键问题是可见光-红外目标检测中的“模态差异”和“轻量化融合”矛盾。

可见光图像通常包含更丰富的纹理、边缘和颜色线索,在光照正常时有利于精细定位;红外图像对热辐射敏感,在低照度、夜间、烟雾或遮挡场景下更可靠。但两者也有明显差异:VIS 容易受光照、阴影、天气影响,IR 纹理较弱、分辨率和细节不足,还可能存在热干扰。如果简单相加或拼接,两种模态中的噪声和偏差可能一起进入检测头,导致漏检、误检或定位不准。

传统 Transformer 式跨模态交互可以建模长程关系,但参数量和计算量往往偏高,不适合轻量部署。论文因此设计 LCAFNet,用轻量投影器、双向 cross-attention 和门控融合,在较低复杂度下完成 VIS/IR 信息互补。代码中的 HAFFormer 正好把这一过程浓缩成一个模块:先分别做 RGB 引导和 IR 引导的交叉注意力,再用卷积门控决定两个增强分支的融合比例。

2. 整体架构

论文整体检测框架由双分支 backbone、四个 LCAFNet 融合网络和 YOLO 检测头组成。双分支 backbone 分别输入 VIS/RGB 图像和 IR 图像,提取多尺度特征F_vis^iF_ir^i。不同于一些只使用中深层特征的多模态检测方法,论文还强调使用第二阶段浅层特征,因为浅层 VIS/IR 特征包含更多空间、纹理与边缘信息,有助于小目标定位和类别判别。

在论文表述中,LCAFNet 由VG-CABIG-CABGFB组成。 ,HAFFormer的两个CrossAttention_S分支可以理解为双向模态引导交叉注意力:mhca_rgb([rgb_fea, ir_fea])用一侧模态引导另一侧特征聚合,随后与rgb_fea残差相加;mhca_ir([ir_fea, rgb_fea])则进行反向引导,并与ir_fea残差相加。这样得到两个增强后的模态特征out_fea_rgbout_fea_ir

随后,HAFFormer 将两个增强特征在通道维拼接,通过1×1 Conv + GELU做通道压缩与非线性变换,再用3×3 depthwise conv + sigmoid生成门控权重w。最终输出为w * out_fea_rgb + (1 - w) * out_fea_ir。这与论文 GFB 的思想一致:不是固定比例融合,而是根据输入内容动态分配 VIS 和 IR 的贡献。

3. 技术原理

HAFFormer 的第一层逻辑是双向交叉注意力。标准跨注意力通常让一个模态产生 Query,另一个模态产生 Key/Value,用二者相似性建立跨模态关系。但论文指出,不同模态存在语义冲突、噪声模式和空间特征差异,直接让不同模态的 Q/K 混合计算可能产生不稳定注意力图。LCAFNet 的解决方式是“模态引导”:VG-CAB 使用 VIS 模态内部关系生成注意力权重,去引导 IR 信息聚合;IG-CAB 则使用 IR 模态内部关系生成注意力权重,去引导 VIS 信息聚合。这样既保留各自模态的结构一致性,又能从另一模态中提取互补内容。

代码里的mhca_rgbmhca_ir正好体现了这个双向结构。out_fea_rgb = mhca_rgb([rgb_fea, ir_fea]) + rgb_fea表示 RGB 分支获得跨模态增强后仍保留原始 RGB 表征;out_fea_ir = mhca_ir([ir_fea, rgb_fea]) + ir_fea表示 IR 分支同样获得另一方向的互补增强。残差连接很重要,因为它避免跨模态交互覆盖原始模态的可靠信息,使模块更稳定。

第二层逻辑是轻量化。论文中的投影器使用1×1 Conv + 3×3 DWConv,以较低参数量完成通道映射与局部空间建模;你贴出的 HAFFormer 中也使用1×1 Conv降维和 depthwise convolution 生成融合权重。1×1 Conv负责混合通道并把2*dim压回dimDWConv则以低成本感知局部空间上下文,避免使用昂贵的大型融合网络。

第三层逻辑是门控融合。拼接后的特征fea_cat经过卷积得到fea_conv,再通过 depthwise convolution 和 sigmoid 得到w,其值域在 0 到 1 之间。最终new_fea = w * out_fea_rgb + (1 - w) * out_fea_ir,表示模型可以在每个位置和通道上动态选择更依赖 RGB 还是 IR。当场景光照较好、纹理边界明显时,门控可能更偏向 RGB;当低照度、遮挡或夜间场景下红外响应更稳定时,门控可以增强 IR 的贡献。论文实验表明,LCAFNet 在 DroneVehicle、FLIR、M3FD、MFAD、LLVIP 等数据集上取得更好的精度-复杂度平衡;消融实验也显示,VG-CAB、IG-CAB 和 GFB 逐步加入后检测性能提升,说明双向模态引导交互与门控融合是有效的。

核心代码

classHAFFormer(nn.Module):def__init__(self,dim):super(HAFFormer,self).__init__()bias=Falsenum_heads=8self.dim=dim self.mhca_rgb=CrossAttention_S(dim,num_heads,bias)self.mhca_ir=CrossAttention_S(dim,num_heads,bias)# Concatself.concat=Concat(dimension=1)self.conv=nn.Sequential(nn.Conv2d(2*dim,dim,kernel_size=1,stride=1,padding=0,bias=bias),nn.GELU())self.dwconv=nn.Conv2d(dim,dim,kernel_size=3,stride=1,padding=1,groups=dim,bias=bias)defforward(self,x):rgb_fea=x[0]ir_fea=x[1]# Cross Attentionout_fea=self.mhca_rgb([rgb_fea,ir_fea])out_fea_rgb=out_fea+rgb_fea out_fea=self.mhca_ir([ir_fea,rgb_fea])out_fea_ir=out_fea+ir_fea# Gated Fusionfea_cat=self.concat([out_fea_rgb,out_fea_ir])fea_conv=self.conv(fea_cat)w=self.dwconv(fea_conv).sigmoid()new_fea=w*out_fea_rgb+(1-w)*out_fea_irreturnnew_fea

YOLO11引入代码

在根目录下的ultralytics/nn/目录,新建一个neck目录,然后新建一个以HAFFormer为文件名的py文件, 把代码拷贝进去。

importtorchimporttorch.nnasnnfromeinopsimportrearrangefromultralytics.nn.modules.convimportConvclassConcat(nn.Module):# Concatenate a list of tensors along dimensiondef__init__(self,dimension=1):super(Concat,self).__init__()self.d=dimensiondefforward(self,x):# print(x.shape)returntorch.cat(x,self.d)classCrossAttention_S(nn.Module):def__init__(self,dim,num_heads,bias):super(CrossAttention_S,self).__init__()self.num_heads=num_heads self.temperature=nn.Parameter(torch.ones(num_heads,1,1))self.v=nn.Conv2d(dim,dim,kernel_size=1,bias=bias)self.v_dwconv=nn.Conv2d(dim,dim,kernel_size=3,stride=1,padding=1,groups=dim,bias=bias)self.qk=nn.Conv2d(dim,dim*2,kernel_size=1,bias=bias)self.qk_dwconv=nn.Conv2d(dim*2,dim*2,kernel_size=3,stride=1,padding=1,groups=dim*2,bias=bias)self.project_out=nn.Conv2d(dim,dim,kernel_size=1,bias=bias)defforward(self,x):fea_0=x[0]# 2024/11/1 added by wwcfea_1=x[1]# 2024/11/1 added by wwcb,c,h,w=fea_0.shape qk=self.qk_dwconv(self.qk(fea_0))q,k=qk.chunk(2,dim=1)v=self.v_dwconv(self.v(fea_1))q=rearrange(q,'b (head c) h w -> b head c (h w)',head=self.num_heads)k=rearrange(k,'b (head c) h w -> b head c (h w)',head=self.num_heads)v=rearrange(v,'b (head c) h w -> b head c (h w)',head=self.num_heads)q=torch.nn.functional.normalize(q,dim=-1)k=torch.nn.functional.normalize(k,dim=-1)attn=(q @ k.transpose(-2,-1))*self.temperature attn=attn.softmax(dim=-1)out=(attn @ v)out=rearrange(out,'b head c (h w) -> b (head c) h w',head=self.num_heads,h=h,w=w)out=self.project_out(out)returnoutclassHAFFormer(nn.Module):def__init__(self,in_dim,out_dim):super(HAFFormer,self).__init__()bias=Falsenum_heads=8self.dim=out_dim self.mhca_rgb=CrossAttention_S(out_dim,num_heads,bias)self.mhca_ir=CrossAttention_S(out_dim,num_heads,bias)# Concatself.concat=Concat(dimension=1)self.conv=nn.Sequential(nn.Conv2d(2*out_dim,out_dim,kernel_size=1,stride=1,padding=0,bias=bias),nn.GELU())self.dwconv=nn.Conv2d(out_dim,out_dim,kernel_size=3,stride=1,padding=1,groups=out_dim,bias=bias)self.conv1x1_1=Conv(in_dim[0],out_dim,1)ifin_dim[0]!=out_dimelsenn.Identity()self.conv1x1_2=Conv(in_dim[1],out_dim,1)ifin_dim[1]!=out_dimelsenn.Identity()defforward(self,x):rgb_fea=self.conv1x1_1(x[0])ir_fea=self.conv1x1_2(x[1])# Cross Attentionout_fea=self.mhca_rgb([rgb_fea,ir_fea])out_fea_rgb=out_fea+rgb_fea out_fea=self.mhca_ir([ir_fea,rgb_fea])out_fea_ir=out_fea+ir_fea# Gated Fusionfea_cat=self.concat([out_fea_rgb,out_fea_ir])fea_conv=self.conv(fea_cat)w=self.dwconv(fea_conv).sigmoid()new_fea=w*out_fea_rgb+(1-w)*out_fea_irreturnnew_fea

注册

ultralytics/nn/tasks.py中进行如下操作:

步骤1:

fromultralytics.nn.neck.HAFFormerimportHAFFormer

步骤2

修改def parse_model(d, ch, verbose=True):

elifmisHAFFormer:c1=[ch[x]forxinf]c2=make_divisible(min(args[0],max_channels)*width,8)args=[c1,c2,*args[1:]]

配置yolo11-HAFFormer.yaml

# Ultralytics YOLO 🚀, AGPL-3.0 license# YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect# Parametersnc:80# number of classesscales:# model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'# [depth, width, max_channels]n:[0.50,0.25,1024]# summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPss:[0.50,0.50,1024]# summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPsm:[0.50,1.00,512]# summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPsl:[1.00,1.00,512]# summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPsx:[1.00,1.50,512]# summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs# YOLO11n backbonebackbone:# [from, repeats, module, args]-[-1,1,Conv,[64,3,2]]# 0-P1/2-[-1,1,Conv,[128,3,2]]# 1-P2/4-[-1,2,C3k2,[256,False,0.25]]-[-1,1,Conv,[256,3,2]]# 3-P3/8-[-1,2,C3k2,[512,False,0.25]]-[-1,1,Conv,[512,3,2]]# 5-P4/16-[-1,2,C3k2,[512,True]]-[-1,1,Conv,[1024,3,2]]# 7-P5/32-[-1,2,C3k2,[1024,True]]-[-1,1,SPPF,[1024,5]]# 9-[-1,2,C2PSA,[1024]]# 10# YOLO11n headhead:-[-1,1,nn.Upsample,[None,2,"nearest"]]-[[-1,6],1,HAFFormer,[512]]# cat backbone P4-[-1,2,C3k2,[512,False]]# 13-[-1,1,nn.Upsample,[None,2,"nearest"]]-[[-1,4],1,HAFFormer,[256]]# cat backbone P3-[-1,2,C3k2,[256,False]]# 16 (P3/8-small)-[-1,1,Conv,[256,3,2]]-[[-1,13],1,HAFFormer,[512]]# cat head P4-[-1,2,C3k2,[512,False]]# 19 (P4/16-medium)-[-1,1,Conv,[512,3,2]]-[[-1,10],1,HAFFormer,[512]]# cat head P5-[-1,2,C3k2,[1024,True]]# 22 (P5/32-large)-[[16,19,22],1,Detect,[nc]]# Detect(P3, P4, P5)

实验

脚本

importwarnings warnings.filterwarnings('ignore')fromultralyticsimportYOLOif__name__=='__main__':# 修改为自己的配置文件地址model=YOLO('./ultralytics/cfg/models/11/yolo11-HAFFormer.yaml')# 修改为自己的数据集地址model.train(data='./ultralytics/cfg/datasets/coco8.yaml',cache=False,imgsz=640,epochs=10,single_cls=False,# 是否是单类别检测batch=8,close_mosaic=10,workers=0,optimizer='SGD',amp=True,project='runs/train',name='HAFFormer',)

结果