前言

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

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

专栏链接: YOLO11改进专栏

介绍

image-20260814165941954

可见光-红外目标检测旨在利用不同模态之间的互补性,提升复杂环境下目标分类与定位的准确性。然而,现有多数方法更关注检测性能,却忽视了网络复杂度,限制了其在真实场景中的应用。为此,本文提出一种轻量级模态引导交叉注意力融合网络 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 逐步加入后检测性能提升,说明双向模态引导交互与门控融合是有效的。

核心代码

class HAFFormer(nn.Module):
    def __init__(self, dim):
        super(HAFFormer, self).__init__()
        bias = False
        num_heads = 8
        self.dim = dim

        self.mhca_rgb = CrossAttention_S(dim, num_heads, bias)
        self.mhca_ir = CrossAttention_S(dim, num_heads, bias)

        # Concat
        self.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)

    def forward(self, x):
        rgb_fea = x[0]
        ir_fea = x[1]

        # Cross Attention
        out_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 Fusion
        fea_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_ir

        return new_fea

YOLO11引入代码

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

import torch
import torch.nn as nn
from einops import rearrange

from ultralytics.nn.modules.conv import Conv

class Concat(nn.Module):
    # Concatenate a list of tensors along dimension
    def __init__(self, dimension=1):
        super(Concat, self).__init__()
        self.d = dimension

    def forward(self, x):
        # print(x.shape)
        return torch.cat(x, self.d)

class CrossAttention_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)

    def forward(self, x):
        fea_0 = x[0]  # 2024/11/1 added by wwc
        fea_1 = x[1]  # 2024/11/1 added by wwc
        b, 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)

        return out

class HAFFormer(nn.Module):
    def __init__(self, in_dim, out_dim):
        super(HAFFormer, self).__init__()
        bias = False
        num_heads = 8
        self.dim = out_dim

        self.mhca_rgb = CrossAttention_S(out_dim, num_heads, bias)
        self.mhca_ir = CrossAttention_S(out_dim, num_heads, bias)

        # Concat
        self.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) if in_dim[0] != out_dim else nn.Identity()
        self.conv1x1_2 = Conv(in_dim[1], out_dim, 1) if in_dim[1] != out_dim else nn.Identity()

    def forward(self, x):
        rgb_fea = self.conv1x1_1(x[0])
        ir_fea = self.conv1x1_2(x[1])

        # Cross Attention
        out_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 Fusion
        fea_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_ir

        return new_fea

注册

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

步骤1:

from ultralytics.nn.neck.HAFFormer import HAFFormer

步骤2

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

        elif m is HAFFormer:
            c1 = [ch[x] for x in f]
            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

# Parameters
nc: 80 # number of classes
scales: # 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 GFLOPs
  s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
  m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
  l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
  x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs

# YOLO11n backbone
backbone:
  # [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 head
head:
  - [-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)

实验

脚本

import warnings
warnings.filterwarnings('ignore')
from ultralytics import YOLO

if __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',
                )
    
 

结果

image-20260814171614655

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐