【论文复现】AAAI 2026 AerialMind 中的 SACR 模块:航拍小目标增强,即插即用!附赠YOLO26改进
📅 2026/8/1 14:18:03
👁️ 阅读次数
📝 编程学习
一、为什么需要 SACR?
无人机航拍场景下的目标检测一直有个老大难问题——小目标看不见。
原因很简单:
目标在画面中占比极小,可能只有几十个像素
飞行高度变化导致目标尺度剧烈变化
背景极其复杂(屋顶、路面、树木、车辆混在一起)
现有的 Deformable DETR 架构虽然绕过了传统 FPN,但它有一个致命短板:最高分辨率的特征图感受野太有限了。也就是说,模型"看"到小目标的时候,周围的上下文信息不够,很容易被背景噪声干扰,把屋顶当车、把树影当人。
为了解决这个问题,论文作者提出了SACR(Scale‑Adaptive Contextual Refinement,尺度自适应上下文精炼) 模块,放在 Encoder 和 Decoder 之间。
它的设计目标很明确:
不降低空间分辨率(小目标不能糊掉)
捕获多尺度上下文信息
抑制背景噪声
结构足够轻量,可以即插即用到各种网络中
二、SACR 做了什么?
SACR 本质上就两步,非常简单:
第一步:多尺度空洞卷积
在最高分辨率的特征图上,同时用多个不同"扩张率"的 3×3 卷积去提取特征。论文用的是 6、12、18 这三个扩张率,再加上一个 1×1 卷积,一共四条支路,最后把所有结果拼在一起。
这样做的好处是:不用降低图像分辨率,就能看到不同大小的"周围区域",相当于给小目标补上了上下文信息。
第二步:自适应通道重校准
拿到多尺度特征之后,对每一个通道做一个"重要性打分"。具体做法是:
对每个通道做全局平均池化,得到通道级别的统计值
通过一个一维卷积学习通道之间的关系(卷积核大小会根据通道数自动调整)
用 Sigmoid 激活得到每个通道的权重
把权重乘回到原来的特征上
这一步的作用是:让跟小目标相关的通道信号更强,把背景噪声对应的通道压下去。
两步串在一起,就是一个完整的 SACR 模块。
三、模块结构图
四、完整代码以及适配yolo26的代码
import math import torch import torch.nn as nn from ultralytics.nn.modules import C2f, C3 from ultralytics.nn.modules.block import PSABlock #论文: https://arxiv.org/abs/2511.21053v2 __all__=["SACR","C2PSA_SACR","C3k2_SACR"] def autopad(k,p=None,d=1): if d>1: k=d*(k-1)+1 if isinstance(k,int) else [d*(x-1)+1 for x in k] if p is None: p=k//2 if isinstance(k,int) else [x//2 for x in k] return p class Conv(nn.Module): default_act=nn.SiLU() def __init__(self,c1,c2,k=1,s=1,p=None,g=1,d=1,act=True): super().__init__() self.conv=nn.Conv2d(c1,c2,k,s,autopad(k,p,d),groups=g,dilation=d,bias=False) self.bn=nn.BatchNorm2d(c2) self.act=self.default_act if act is True else act if isinstance(act,nn.Module) else nn.Identity() def forward(self,x): return self.act(self.bn(self.conv(x))) class ECA(nn.Module): def __init__(self,c): super().__init__() k=int(abs((math.log2(c)+1)/2)); k=k if k%2 else k+1 self.pool=nn.AdaptiveAvgPool2d(1) self.conv=nn.Conv1d(1,1,k,padding=(k-1)//2,bias=False) self.sig=nn.Sigmoid() def forward(self,x): y=self.pool(x).squeeze(-1).transpose(-1,-2) y=self.conv(y).transpose(-1,-2).unsqueeze(-1) return x*self.sig(y) class SACR(nn.Module): def __init__(self,c): super().__init__() m=max(c//4,1) self.b0=Conv(c,m,1) self.b1=Conv(c,m,3,d=6) self.b2=Conv(c,m,3,d=12) self.b3=Conv(c,m,3,d=18) self.fuse=Conv(m*4,c,1) self.eca=ECA(c) def forward(self,x): y=torch.cat([self.b0(x),self.b1(x),self.b2(x),self.b3(x)],1) y=self.eca(self.fuse(y)) return x+y class Bottleneck_SACR(nn.Module): def __init__(self,c1,c2,shortcut=True,g=1,k=(3,3),e=0.5): super().__init__() c_=int(c2*e) self.cv1=Conv(c1,c_,k[0]); self.cv2=Conv(c_,c2,k[1],g=g) self.add=shortcut and c1==c2 self.att=SACR(c2) def forward(self,x): y=self.att(self.cv2(self.cv1(x))) return x+y if self.add else y class C3k_SACR(C3): def __init__(self,c1,c2,n=1,shortcut=True,g=1,e=0.5,k=3): super().__init__(c1,c2,n,shortcut,g,e) c_=int(c2*e) self.m=nn.Sequential(*(Bottleneck_SACR(c_,c_,shortcut,g,((3,3),(3,3)),1.0) for _ in range(n))) class PSABlock_SACR(nn.Module): def __init__(self,c,shortcut=True): super().__init__() self.att=SACR(c) self.ffn=nn.Sequential(Conv(c,c*2,1),Conv(c*2,c,1,act=False)) self.add=shortcut def forward(self,x): x=x+self.att(x) if self.add else self.att(x) x=x+self.ffn(x) if self.add else self.ffn(x) return x class C2PSA_SACR(nn.Module): def __init__(self,c1,c2,n=1,e=0.5): super().__init__(); assert c1==c2 self.c=int(c1*e) self.cv1=Conv(c1,2*self.c,1); self.cv2=Conv(2*self.c,c1,1) self.m=nn.Sequential(*(PSABlock_SACR(self.c) for _ in range(n))) def forward(self,x): a,b=self.cv1(x).split((self.c,self.c),1); b=self.m(b) return self.cv2(torch.cat((a,b),1)) class C3k2_SACR(C2f): def __init__(self,c1,c2,n=1,c3k=False,e=0.5,attn=False,g=1,shortcut=True): super().__init__(c1,c2,n,shortcut,g,e) self.m=nn.ModuleList((nn.Sequential(Bottleneck_SACR(self.c,self.c,shortcut,g),PSABlock(self.c,attn_ratio=0.5,num_heads=max(self.c//64,1))) if attn else (C3k_SACR(self.c,self.c,2,shortcut,g) if c3k else Bottleneck_SACR(self.c,self.c,shortcut,g))) for _ in range(n)) if __name__=="__main__": x=torch.randn(1,64,80,80) m=SACR(64) y=m(x) print(x.shape,y.shape)五、yolo26yaml文件
1.yolo26_C2PSA_SACR
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license # Ultralytics YOLO26 object detection model with P3/8 - P5/32 outputs # Model docs: https://docs.ultralytics.com/models/yolo26 # Task docs: https://docs.ultralytics.com/tasks/detect # Parameters nc: 80 # number of classes end2end: True # whether to use end-to-end mode reg_max: 1 # DFL bins scales: # model compound scaling constants, i.e. 'model=YOLO26n.yaml' will call YOLO26.yaml with scale 'n' # [depth, width, max_channels] n: [0.50, 0.25, 1024] # summary: 260 layers, 2,572,280 parameters, 2,572,280 gradients, 6.1 GFLOPs s: [0.50, 0.50, 1024] # summary: 260 layers, 10,009,784 parameters, 10,009,784 gradients, 22.8 GFLOPs m: [0.50, 1.00, 512] # summary: 280 layers, 21,896,248 parameters, 21,896,248 gradients, 75.4 GFLOPs l: [1.00, 1.00, 512] # summary: 392 layers, 26,299,704 parameters, 26,299,704 gradients, 93.8 GFLOPs x: [1.00, 1.50, 512] # summary: 392 layers, 58,993,368 parameters, 58,993,368 gradients, 209.5 GFLOPs #来自:YOLO目标检测 改进 # YOLO26n 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, 3, True]] # 9 - [-1, 2, C2PSA_SACR, [1024]] # 10 # YOLO26n head head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 2, C3k2, [512, True]] # 13 - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 2, C3k2, [256, True]] # 16 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 13], 1, Concat, [1]] # cat head P4 - [-1, 2, C3k2, [512, True]] # 19 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 10], 1, Concat, [1]] # cat head P5 - [-1, 1, C3k2, [1024, True, 0.5, True]] # 22 (P5/32-large) - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)2.YOLO26_C3K2_yaml
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license # Ultralytics YOLO26 object detection model with P3/8 - P5/32 outputs # Model docs: https://docs.ultralytics.com/models/yolo26 # Task docs: https://docs.ultralytics.com/tasks/detect # Parameters nc: 80 # number of classes end2end: True # whether to use end-to-end mode reg_max: 1 # DFL bins scales: # model compound scaling constants, i.e. 'model=yolo26n.yaml' will call yolo26.yaml with scale 'n' # [depth, width, max_channels] n: [0.50, 0.25, 1024] # summary: 260 layers, 2,572,280 parameters, 2,572,280 gradients, 6.1 GFLOPs s: [0.50, 0.50, 1024] # summary: 260 layers, 10,009,784 parameters, 10,009,784 gradients, 22.8 GFLOPs m: [0.50, 1.00, 512] # summary: 280 layers, 21,896,248 parameters, 21,896,248 gradients, 75.4 GFLOPs l: [1.00, 1.00, 512] # summary: 392 layers, 26,299,704 parameters, 26,299,704 gradients, 93.8 GFLOPs x: [1.00, 1.50, 512] # summary: 392 layers, 58,993,368 parameters, 58,993,368 gradients, 209.5 GFLOPs #来自:YOLO目标检测 改进 # YOLO26n 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_SACR, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 2, C3k2_SACR, [1024, True]] - [-1, 1, SPPF, [1024, 5, 3, True]] # 9 - [-1, 2, C2PSA, [1024]] # 10 # YOLO26n head head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 2, C3k2_SACR, [512, True]] # 13 - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 2, C3k2, [256, True]] # 16 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 13], 1, Concat, [1]] # cat head P4 - [-1, 2, C3k2_SACR, [512, True]] # 19 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 10], 1, Concat, [1]] # cat head P5 - [-1, 1, C3k2_SACR, [1024, True, 0.5, True]] # 22 (P5/32-large) - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)
编程学习
技术分享
实战经验