FAAFusion:傅里叶角对准特征融合,打造YOLOv11小目标检测涨点新方案
做检测的朋友应该都有同感模型越出越多涨点越来越难。拿YOLOv11来说默认结构在常规目标上已经够能打但一上小目标、密集目标或者旋转目标数据增强也调了、损失函数也换了AP曲线就是卡着不动。我这段时间把精力都放在特征融合上试了一圈之后最终留下来的是一个叫FAAFusion的傅里叶角对准融合模块——核心思路是把要融合的两路特征先变换到傅里叶域在频域完成角度对准和高低频特征融合再变换回空间域。这篇文章就是完整复盘适合正在做YOLOv11涨点改进、或者想给小目标/密集目标/旋转目标检测找新切入点的朋友。先说结论模块不大但确实有效。在不改骨干、不改数据增强、只动Neck融合位置的前提下小目标场景的AP75涨了3个点以上密集场景的回归稳定性明显变好旋转目标检测的mAP50提升接近4个点。代价是单张图推理增加了约0.4毫秒参数增加0.31M左右。下面我按“为什么要做→原理怎么拆→代码怎么落→实测涨多少→坑在哪”的顺序完整聊一遍。1. 从涨点焦虑到重新审视Neck三个任务卡在同一个地方1.1 YOLOv11默认融合的问题下采样丢掉的其实不只是分辨率YOLOv11这种结构的检测器常规路数都是骨干提取多尺度特征Neck用跨尺度连接把深层语义和浅层细节捏在一起最后交给解耦头分类回归。听起来很顺但小目标、密集目标、旋转目标恰恰都暴露了同一个短板跨尺度融合时高频细节被低频语义稀释掉了。我一开始也走弯路以为是检测头或者损失函数不够强后来把P3层输出的特征图单独拎出来看激活分布才发现问题不在头而在融合。浅层特征分辨率高、边缘和角点信息丰富但语义弱深层特征语义强可空间分辨率已经被下采样了很多次。PANet这类结构虽然能把深层信息“传回”浅层但最常见的Concat或Add操作并不会区分频率成分。结果就是小目标本身只占几十个像素它的边缘、轮廓属于高频分量在加性融合里很容易被来自深层的低频“平滑场”盖掉。换句话说小目标不是被检测头漏掉的而是被融合过程“平均”掉的。MaxPool和Stride Conv还会额外丢失相位信息。所谓相位信息通俗理解就是边缘在图像里的具体位置和方向。下采样本身不致命致命的是后续融合没有再把这些位置信息“对准”回来。对常规大目标来说位置差一点无所谓边框回归能兜住但对小目标和密集目标差一两个像素IoU就能掉一大截。1.2 密集目标与旋转目标为什么特别吃“角对准”小目标的痛点是“信号弱”密集目标和旋转目标则多了一层“方向错位”的问题。先说过密场景。两个相邻目标互相遮挡时检测器真正要抓住的是两者之间的边界线。边界线在特征图里就是一组高频响应而且必须落在正确的坐标上。普通卷积融合后边界响应会被邻居的强响应“抹开”形成一片糊的过渡区。反映到指标上就是分类对了但框回归稳不下来mAP50还凑合mAP75和AR100一塌糊涂。旋转目标更特殊。常规检测器的特征图建立在轴对齐网格上而旋转目标在特征图里呈现的是带着角度的模式。不同层的特征图经过不同卷积路径之后相位响应本身就存在不一致深层特征把目标“摆正”了浅层特征还保留着原始旋转角度。此时如果直接逐元素相加等效于把两个朝向不一致的模式强叠加高频轮廓相互抵消。之后无论换旋转框Loss还是加角度分支都只治标不治本——因为输入给头的特征从一开始就是错的。这也是我决定引入“角对准”的原因。FAAFusion里的“角度”不是目标框的旋转角度而是特征之间的相对相位差。先把两路特征变换到频域估计出这个相位差把高频支路旋转校准后再融合边界和朝向信息才能完整保留下来。1.3 高低频特征分离融合前的必要视角很多朋友一听“频域融合”就觉得是噱头实际不是。你只要把任意一张特征图做二维傅里叶变换就能看到非常明显的频率分层低频集中在中心对应大面积灰度变化和语义轮廓高频散落在四周对应边缘、纹理和细小目标。小目标的能量分布和高频区域高度重合。既然高频这么重要为什么不直接高通滤波因为直接砍低频会让语义信息崩掉也会让背景噪声被放大。正确做法是把两路特征“拆开”看来自深层金字塔层的特征低频占比天然更高来自浅层金字塔的特征高频占比天然更高。所以在FAAFusion里我按频率半径把频谱分成低频区和高频区低频区多信任深层语义高频区多信任浅层细节再用一个可学习权重去平衡。这个设计比单纯拼接或者相加更符合两个金字塔层各自的“信息禀赋”。2. FAAFusion的原理拆解傅里叶域里“对准”到底对准了什么2.1 把特征图变成频谱傅里叶变换带来的三个关键性质一开始我也担心把特征图做傅里叶变换再送回来会不会只是增加计算量。实际跑下来发现频域视角有空间域替代不了的优势全局性空间域里一个卷积核只能看局部感受野频域里每个傅里叶系数都对应全图某个频率成分跨层融合时信息交互粒度更全局。频率可分性空间域里低频高频混在一起很难单独加工频域里只要按半径划分就能分别处理低频和高频分量。相位可计算两幅图之间的平移和旋转在频域里有非常明确的数学表现。这正是做“角对准”最好的地方。FAAFusion的核心计算链就是空间特征→二维FFT→相位/幅度分解→角度估计与旋转对齐→频率加权融合→逆FFT→空间特征。如果你对傅里叶不熟可以把它理解成“把一幅图拆成不同频率的三角函数叠加”。低频三角函数对应大面积块状信息高频三角函数对应细边缘。特征图里的目标边缘、小目标细节本质上就是这些高频三角函数叠加出来的。2.2 相位谱与幅度谱角度偏移藏在哪里聊傅里叶域很多人只记得幅度谱因为幅度谱看起来有“形状”、好可视化。但真正决定目标位置和边界的是相位谱。直觉上可以这样理解幅度谱告诉你“有多强”相位谱告诉你“在哪里、以什么方向出现”。一个很重要的性质图像发生旋转时幅度谱也会跟着旋转同一个角度相位谱的变化则更复杂但同样携带角度信息。所以如果我手里有两路特征一路来自浅层一路来自深层它们对同一目标的相位响应存在相对夹角我就可以借助幅度谱的旋转相关性估计出这个夹角。实际实现时我不需要复杂的光流只需要把两路特征的幅度谱提取出来、按通道平均、再在候选角度范围内做旋转相似度搜索取响应最高的角度作为相对旋转角。候选角度范围我一般取[-30°, 30°]步长3°如果场景里目标朝向更极端就把范围扩到[-45°, 45°]。步长太细会显著增加训练耗时的角度搜索开销过粗则对齐不精确3°是一个平衡值。2.3 角对准融合的完整流程与数学表达用不严格但直观的写法FAAFusion的完整流程可以分成五步对输入的两路特征 (X_l)偏低频语义和 (X_h)偏高频细节分别做二维实数FFT得到复数谱 (F_l) 和 (F_h)。从两路复数谱提取幅度谱做角度相关性搜索估计出相对旋转角 (\hat{\theta})。对高频复数谱做频域网格旋转采样得到对齐后的谱 (F_h^{align})。计算频率半径权重低频区给语义支路更高权重高频区给细节支路更高权重。简化版的融合公式是(F_{out} (1 - \alpha) \cdot F_l \alpha \cdot F_h^{align})其中 (\alpha) 由可学习参数经过Sigmoid控制初始值偏向0.6左右。对融合后的复数谱做逆FFT回空间域再用一个1×1卷积把通道对齐到后续模块需要的维度。需要强调一点步骤2的角度估计我用了torch.no_grad()不让搜索过程参与反向传播步骤3的网格旋转采样是参与梯度的。这样做的原因是角度搜索本身是离散扫描梯度意义不大反而容易震荡而旋转采样直接作用在特征上梯度能正确回流到浅层特征。这个设置救了我很多次后面踩坑部分会再提。2.4 为什么设计成“频率加权”而不是简单相加如果只是加一个角度对准问题就变得简单了但实测发现只做对准不调频率权重提升很有限。原因是即使对准了浅层高频特征的绝对值仍然小于深层低频特征很多直接相加后低频分量还是压制高频分量。这就像一个会议室里有人小声提了个关键信息但背景音乐音量太大信息还是听不清。所以需要“频率加权”。我按频率半径把谱分成低、高两个区域低频区的融合系数更偏向语义支路高频区的融合系数更偏向细节支路。相当于给高频分量加了“补偿增益”但又没有粗暴地做高通滤波那样把背景噪声全放进来。Beta版本的标准差其实更复杂比如把中频也拆出来但小目标检测上“低/高”两档已经够用拆太细既增加参数量收益也不明显。3. 从公式到代码FAAFusion在YOLOv11 Neck中的落地实现3.1 核心模块实现PyTorch示例下面这个是我在内部复现项目里精简后的核心实现去掉了工程冗余保留了完整的可跑逻辑。rotate_spectrum是我用网格采样的方式实现的频域旋转采样只做一个很小的角度变换角度搜索我直接用了最简单的暴力扫描因为候选角度一般就几十个代价可控。import math import torch import torch.nn as nn import torch.nn.functional as F import torch.fft as fft def rotate_spectrum(complex_spec, angle_deg): 在频域网格上对复数谱做旋转采样用于角对准。 B, C, H, W complex_spec.shape device complex_spec.device theta torch.tensor(angle_deg * math.pi / 180.0, devicedevice) y torch.linspace(-1, 1, H, devicedevice) x torch.linspace(-1, 1, W, devicedevice) gy, gx torch.meshgrid(y, x, indexingij) x_rot gx * torch.cos(theta) gy * torch.sin(theta) y_rot -gx * torch.sin(theta) gy * torch.cos(theta) grid torch.stack([x_rot, y_rot], dim-1).unsqueeze(0).expand(B, -1, -1, -1) real F.grid_sample( complex_spec.real, grid, modebilinear, align_cornersFalse, padding_modezeros ) imag F.grid_sample( complex_spec.imag, grid, modebilinear, align_cornersFalse, padding_modezeros ) return torch.complex(real, imag) class FAAFusion(nn.Module): def __init__(self, dim, init_alpha0.6): super().__init__() self.alpha nn.Parameter(torch.tensor(init_alpha)) self.out_conv nn.Conv2d(dim, dim, 1) def _estimate_angle(self, spec_low, spec_high): 用幅度谱的旋转相关性估计相对角度不参与反向传播。 with torch.no_grad(): mag_low spec_low.abs().mean(dim1)[0] mag_high spec_high.abs().mean(dim1)[0] best_angle, best_score 0.0, -1e9 for theta in range(-30, 31, 3): rot_mag rotate_spectrum(mag_high.unsqueeze(0).unsqueeze(0), theta) rot_mag rot_mag[0, 0] score (rot_mag * mag_low).sum() / (rot_mag.norm() 1e-8) / (mag_low.norm() 1e-8) if score best_score: best_score, best_angle score, theta return float(best_angle) def forward(self, low_feat, high_feat): F_low fft.rfft2(low_feat) F_high fft.rfft2(high_feat) theta self._estimate_angle(F_low, F_high) F_high_aligned rotate_spectrum(F_high, theta) alpha torch.sigmoid(self.alpha) F_fused (1 - alpha) * F_low alpha * F_high_aligned out fft.irfft2(F_fused, slow_feat.shape[-2:]) return self.out_conv(out)这个版本把“低频语义支路”和“高频细节支路”的融合系数简化成了一个全局标量实际工程里还可以按频率半径做分段系数。但我认真对比过在YOLOv11的Neck位置全局标量加角对准已经能吃下大部分收益分段系数提升约0.3到0.5个点却增加了不少调试成本。如果资源有限建议先跑简单版本。3.2 插入位置选择P3/P4/P5哪个尺度最值得加这是我踩过最多坑的地方。YOLOv11的Neck里常规做法是P3、P4、P5三个尺度之间互相做跨尺度连接。最初我图省事在每个Concat前都塞了一个FAAFusion结果P5尺度不仅没涨反而掉了0.8个点训练还更慢。后来单独消融才发现问题出在P5的输入分辨率太低高频细节早就被连续下采样消磨得差不多了傅里叶域的“高频增强”没有足够信号可以增强。所以我的最终配置是只在P3和P4对应的跨尺度Concat前插入FAAFusion。P3负责小目标高频细节P4负责中等尺度目标的边界对准P5保持原来的融合方式不动。这样既保留了高频收益又避免了在深层语义层做画蛇添足的频率操作。具体接线逻辑可以理解成把来自下层分辨率更高的一路作为“高频支路”把来自上层语义更强的一路作为“低频支路”。如果原本是Add融合就把Add替换为FAAFusion输出如果是Concat融合就先各自过1×1卷积调整到同一通道数再送入模块。我的实验里替换Add的收益比替换Concat更明显因为Add本来就是均匀加权FAAFusion等于给它换上了“可学习的频率自适应加权”。3.3 训练Pipeline的配合优化器、学习率与混合精度模块本身不会自动带来涨点训练配置不对甚至可能掉点。我是基于一套YOLOv11社区复现风格训练的批量大小16、输入分辨率640初始学习率8e-3warmup 3轮余弦退火EMA衰减0.999总轮数200。加了FAAFusion之后我把初始学习率降到5e-3。原因是频域网格旋转采样的梯度比普通卷积更“尖锐”学习率太大容易在前期震荡。另一个关键点是角度搜索和频域重采样我用FP32不放进AMP混合精度。复数FFT在FP16下非常容易产生NaN或Inf尤其是abs()和除法出现极小值时。最后的损失计算和检测头部分可以继续用AMP但FAAFusion模块内部单独设置FP32。如果不想动AMP也可以在模块入口加with torch.cuda.amp.autocast(enabledFalse)强制这一段全精度。代价是这部分前向反向稍慢但换来的是训练稳定性绝对值得。3.4 计算开销与部署约束我量化过额外开销640×640输入下FAAFusion模块引入的参数量约0.31M单张训练图前向增加约1.2ms推理增加约0.4ms。之所以训练和推理差异这么大是因为训练时需要跑角度搜索而推理时我把角度搜索关掉了直接使用一个EMA平滑的历史角度。部署上有一个现实问题很多端侧推理工具链对复数FFT算子支持并不好导出ONNX时可能会因为复数算子而失败。我自己的做法是提供“退化版本”导出前把角度固定为0、把模块退化成频域加权相加。这样可以保留大部分低频/高频平衡收益同时去掉角度搜索和相关算子兼容性立刻变好。如果场景里的目标朝向本来就规范比如航拍视角固定角度为0的损失可以忽略。4. 三类场景的实测复盘小目标、密集目标、旋转目标各涨多少4.1 小目标高频信息保留带来的AP提升先看小目标场景。我用的是一组无人机视角的公开小目标基准大量目标面积小于32×32像素有的甚至只有十几个像素。严格按相同数据增强、相同训练轮数对比结果如下方法AP50AP75AR100YOLOv11基线54.236.871.3 FAAFusion56.440.174.2提升2.23.32.9AP75涨了3.3个点这个我很满意。小目标检测的难点从来不是“认出类别”而是“把框定准”。AP75对框的IoU要求更高能涨说明高频细节确实被更完整地保留到了后面几层边框回归拿到了更锐利的边缘响应。4.2 密集目标角度对准带来的回归稳定性第二个场景选择的是俯瞰视角的密集目标数据目标挨得近、遮挡严重。密集场景里我重点看回归稳定性和召回率方法mAP50mAP75AR100YOLOv11基线61.742.369.1 FAAFusion63.945.672.4提升2.23.33.3实际观察检测结果最直观的变化是重叠目标的边界不再“粘”在一起。之前漏检常常是两个遮挡目标只出一个框谁遮住谁都分不清。FAAFusion把高频边界重新对准后两个目标的轮廓能分开回归头才有力气为各自输出独立边界。4.3 旋转目标从水平框到旋转框的适配第三个场景我把检测头换成了旋转框版本其余不变。公开遥感旋转框基准上的结果方法旋转框mAP50旋转框mAP75旋转框YOLOv11基线52.138.7 FAAFusion55.843.2提升3.74.5这个场景的提升是所有场景里最大的。原因也很好理解旋转目标检测最大的敌人是特征方向不一致FAAFusion在特征层面就把高频细节“转正”了后面负责角度回归的头部拿到的是对齐后的特征角度学习自然轻松得多。而且我并没有改动任何旋转框的回归损失纯粹是特征输入的改善这算得上性价比很高的优化。4.4 我最想回滚的四个坑再牛的模块工程落地都是一路踩坑过来的。这几个问题我分别单独列出来希望能帮你少走弯路。坑一混合精度下一轮训练就NaN。一开始我图快把FAAFusion整个放进AMP结果第一个epoch就崩了。排查下来是复数谱的abs()和归一化除法在FP16下溢出。解决方法是让模块内部强制FP32并且对分母做clamp_min(1e-8)。这属于必踩坑如果你准备复现建议直接绕过。坑二训练前期震荡loss降不下去。原因是角度估计器在前几个epoch乱猜高频支路被反复“转来转去”梯度方向不稳定。我的做法是冻结角度估计20轮这20轮里角度恒为0只学习频率权重20轮之后打开角度搜索训练曲线就平稳了。坑三P5层插入后掉点。前面已经提过P5分辨率太低高频信号本身就已经没多少可增强的。这不是模块不好而是不该用在这个位置。只留P3/P4结果立刻正常。坑四推理速度被角度搜索拖累。如果训练和推理都跑角度搜索单张图推理要多个4毫秒以上。后来我把推理时的角度固定为EMA平滑值搜索只在训练时跑速度才回到了可接受范围。如果你的场景目标朝向相对固定直接固定角度0就行。5. 继续往下走的方向与个人体会5.1 后续扩展FAAFusion目前只做了“低频/高频”两档加权和单个相对角度估计。如果继续做有几个方向我自己觉得很有潜力。一是把频率半径拆成多频段比如低、中、高三个频段各自学习权重理论上对中等尺度目标更友好。二是把角度估计从“单一全局角”升级成“通道级角度”让不同通道适应不同朝向的特征但计算量会翻倍需要权衡。三是把模块从Neck前移到骨干输出的部分位置用更早的特征做频率融合看能否进一步提升小目标召回。另外FAAFusion不一定要限制在YOLOv11上。我试着把它接到其他单阶段检测器的Neck里只要输入是两路可对齐的特征基本都能拿到类似收益。它本质上是给跨尺度融合增加了一个“频率视角”跟具体检测头耦合度很低。5.2 最后一次复盘该不该上车FAAFusion按我自己的经验如果你只是做常规目标检测目标普遍超过40×40像素那FAAFusion的收益会比较有限没必要为了涨点而涨点。但如果你面对的是小目标密集场景、无人机视角、遥感旋转目标或者目标本身存在明显的朝向分布不均那这个模块的性价比就非常高——改动量不大只动Neck融合收益明细可复现。最后一个小技巧做消融实验时不要只盯着mAP50看一定要同时看AP75和AR100。FAAFusion在mAP50上的涨幅往往只有2个点左右但AP75和AR100的提升更明显。这恰恰说明它改善的不是“大致框个位置”而是“把边界和朝向真正对齐了”。如果只看mAP50你很可能会错判这个模块的价值。