小目标检测这个方向,我这两年真是又爱又恨。爱的是它场景足够多,无人机巡检、卫星遥感、自动驾驶、工业质检到处都用得上;恨的是它真的难搞,明明一个大目标轻轻松松能到90%的mAP,一到小目标上直接腰斩。常规的检测器在小目标上表现不佳,这已经是公开的难题了。最近我一直在跟SET这条技术路线,也就是标题里提到的Spectral Enhancement for Tiny Object Detection,CVPR 2025的工作。先说结论:这个方向让我眼前一亮,因为它不是继续在空间域里堆模块,而是换了个维度,把图像从频率域的角度重新审视了一遍。这篇东西我不会写成论文翻译,而是按我自己做工程、复现、踩坑的真实经验来讲清楚:小目标检测到底难在哪,SET为什么要用频谱增强来解,以及如果你想在自家数据集上复现它,有哪些参数和步骤值得重点关注。
1. 小目标检测,为什么“小”就成了致命问题
1.1 三个公认的“死穴”
我得先跟你掰扯清楚,为什么目标一变小,检测器就集体拉胯。很多人以为只是分辨率低、看不清,其实不完全是。小目标检测的核心痛点,业内基本已经归成三类。
第一是下采样丢信息。一张输入图经过backbone,通常是5次下采样,特征图缩到原来的1/32。一个10×10像素的小目标,经过第一层卷积后可能还剩5×5,到第二层就剩2~3个像素了。等它走到检测头,它的语义信息已经被周围背景“冲淡”得差不多了。这就是为什么小目标检测必须在浅层feature map上多做文章,但浅层又缺乏语义信息,这就是两难。
第二是正样本匹配困难。检测器训练的时候要给每个目标匹配锚框或query,小目标本身像素少,跟锚框的IoU天然偏低。常规的IoU阈值一卡,大量小目标直接成了负样本,根本没机会参与梯度更新。后来大家改用ATSS、OTA这些分配策略,改善了一些,但核心矛盾还在:小目标的监督信号太稀疏了。
第三是样本量本身不够。在一个航拍图像里,大目标可能几百个像素甚至上千个像素,小目标就那十几二十个像素。一个batch里小目标的数量远远少于大目标,模型训练时天然倾向于“学会”大目标,小目标变成了尾部分布。这也是为什么小目标数据集的标注成本极高——你放大图片都未必看得清那些目标。
以前解决这些问题的主流思路,无非是FPN/PANet这种多尺度融合,把浅层细节和深层语义拉在一起;或者是做数据增强,把图像切块放大,硬生生把小目标“变大”。再有就是在损失函数上想办法,比如NWD、EIoU这类对小目标更友好的度量。这些方法都有用,但都还在空间域里打转。
1.2 一个被忽略的视角:像素少,不代表频率信息少
我自己做了这么久检测,最直观的感受是:小目标在空间域里确实“信息贫瘠”,但它的信息其实没有完全消失,而是换了一种形式存在。这句话怎么理解?我们看一张图,本质上是看它的空间像素分布;但如果做一次傅里叶变换,把图像变换到频域,你会看到完全不同的景象。
大目标的低频成分突出,小目标则大量分布在中高频段。换句话说,小目标虽然像素少,但在频域里有对应的“能量成分”在,只是这部分能量常常被周围的纹理、噪声、背景细节盖过去了,所以你在空间域看不到、模型也学不到。SET的核心动机就在这儿:能不能在频域里把属于小目标的那部分能量检索出来,做一个增强,再送回检测器里去?
这就有点像收音机调频。空间域里看到的是所有声音混在一起的嘈杂广播,你听不清那个微弱频道里的内容;但你在频域里做一个滤波,把那个频道的信号放大,其他频段压低,你再听就清楚了。SET做的事情,本质上就是在图像里做这么一次“调频”。
2. SET的设计思路:把增强放在频域做,而不是继续堆分辨率
2.1 为什么不用超分辨率或者更大分辨率输入
你可能会问:既然小目标看不清,那我把输入分辨率拉高不就行了?比如把图像从640拉到1280甚至1920,小目标像素变多了,不就好检测了?这个思路不能说错,但工程上有两个硬伤。
第一是计算量爆炸。分辨率翻四倍,backbone的计算量基本上是平方级上涨。在一个实时性要求的项目上,这种方案根本落地不了。第二是收益边际递减。拉到一定分辨率之后,小目标像素确实变多了,但背景噪声也变大了,模型照样分不清目标和纹理。说白了,空间域里堆分辨率只是把“看不看得清”的问题推向极限,并没有真正解决信噪比的问题。
超分辨率也有问题。超分重建的目标是“看起来更像真实图像”,它会把小目标重建得看着更精细,但这些精细纹理未必是检测器需要的判别性特征。超分对比损失往往是基于像素相似度优化的,对检测任务来说不一定正向。我自己试过在检测前加一个超分头,结果是mAP没有明显提升,推理时间倒是真真切切翻倍了。
SET走的路线不一样。它不追求把图像变“清晰”,而是追求把“目标与背景的可分性”提上来。这个思路我觉得才是检测任务真正需要的。**它把增强的重点从空间分辨率转向频率选择性。**你可以把它理解为:与其给图像整体加一档清晰度,不如直接放大你关心的那几段频率。
2.2 频谱增强模块的两种可能形态
根据标题推断,SET应该是一个可插拔模块,不是一套全新的检测器。这种设计在CVPR这类会议上很常见,因为它足够flexible,能接入YOLO系列、RT-DETR、甚至Faster R-CNN这类经典结构里。模块的放置位置和形态,我判断可能有两种设计路线。
第一种是输入增强型。在图像进入检测器之前,先用FFT把图变换到频域,在频域里对幅值谱做增强和抑制,再逆变换回空间域,生成一张增强后的图。这张图和原图可以通过通道拼接或者注意力融合的方式一起喂进检测器。这个方案的好处是逻辑简单、可解释性强,坏处是在推理阶段增加了一整个FFT和IFFT的流程,实时性会有影响。
第二种是特征增强型。把频谱增强模块嵌入到backbone的某个stage之后,直接对feature map做频域变换。特征图的分辨率比原图小得多,FFT的开销可以被压得很低。同时,特征图本身经过多层卷积,语义更强,在频域里做增强能更精准地命中“小目标的特征频率”。这也是我个人认为更接近SET原始设计的路线,因为论文标题用的是“Enhancement”而非“Transform”,暗示这是一个嵌入式的增强模块。
其实这两种形态也可以混合:浅层特征用频域增强保细节,深层特征用频域增强提语义。具体怎么搭,就得看消融实验了。
2.3 与FPN、损失函数改进的关系:不是替代,是互补
很多人看到新方法就以为要推翻以前的做法。SET不是来替代FPN的,它跟现有的多尺度融合、标签分配策略是互补关系。FPN解决的是“跨层特征融合”,把浅层位置信息和深层语义信息叠在一起;NWD、EIoU解决的是“度量方式”,让损失函数对小目标更敏感。而SET解决的是“输入/特征表示”,它让送入检测器的特征本身就有更强的判别性。
这三个层面一点都不冲突,甚至可以叠加使用。我举个实际场景:假设你做一个航拍车辆检测项目,用了FPN做多尺度融合,用了ATSS做标签分配,效果到了瓶颈期,AP_small一直卡在15%上下。这时候你引入SET模块,对特征做频谱增强,哪怕只是把原有的部分特征重新校准了一下,很可能就能突破瓶颈。因为FPN没有能力在频域里区分目标和纹理,但SET可以。
这一点也是我推荐尝试这个方案的原因——它不是重新造轮子,而是在现有轮子上加了一个精细雕花的环节。从工程改造角度说,引入一个小模块比重新训练一整套模型要稳妥得多。
3. 核心实现细节:从FFT到频带选择的关键参数
3.1 变换这一步:FFT、DCT怎么选
频谱增强的第一步必然是图像或特征从空间域变换到频域。这里最常见的选择有两个:FFT(快速傅里叶变换)和DCT(离散余弦变换)。
FFT把信号表达为复数形式,包含幅度谱和相位谱。幅度谱告诉你各频率成分的强度,相位谱告诉你在空间里的位置信息。做频域增强的时候,几乎都是对幅度谱进行操作,相位谱一般保持不变。原因很朴素:相位谱一旦乱动,图像结构会产生严重畸变,出来的图肉眼看着是一团乱麻。这一点在复现的时候要格外注意,别把这个顺序搞反了。
DCT的好处是输出全是实数,计算量比FFT小,而且能量集中度更高,JPEG压缩用的就是DCT。不过DCT没有相位信息,如果要做那种保留空间结构的增强,FFT反而更自然。
以我个人的复现经验来说,如果SET模块放在特征层面,FFT是更合理的选择。因为特征图本身已经没有“人眼可理解”的视觉语义了,保留相位结构对后续卷积反而有用。而如果只是做输入图像的增强,DCT的低计算量优势又不明显,所以还是FFT居多。
3.2 频带划分:这是整篇文章的灵魂
频谱增强不是“把高频全放大”这么简单。事实上,高频部分包含了大量纹理和噪声,一股脑放大等于把噪声也放大了,检测器反而会被带偏。本质上,这是一场信噪比的博弈。因此,更合理的做法是把频谱按频段分成若干band,然后对每个band学习一个可调节的增益系数。
常见的频带划分方式有两种。一种是等分,就是把频谱从低频到高频均匀切成N段,N通常是8、16、32,然后每个频段单独处理。另一种是等能量划分,让每个频段包含大致相同的能量,这种方式在训练初期更稳定,因为每个频段的统计分布不会差太多。
我个人经验是:频带数量别一上来就定死,先试着拆成8个band跑一版,然后再试16个band对比一下。band数量太少,增强粒度太粗糙,小目标的特定频率可能被淹没在某个宽泛的频带里;band数量太多,每个band的统计量不稳定,训练容易震荡。
还有一种做法是每个通道独立做频段注意力。比如一个256通道的feature map,每个通道都预测一组频段增益系数,这会带来参数量上涨,但对小目标提升往往更明显。究竟是用共享权重还是通道独立权重,这需要看实验效果和环境对参数量的容忍度。
3.3 增益系数的预测机制与融合方式
频谱增强的增益系数(也就是每个band放大多少、抑制多少)有两种主流预测方式。
第一种是固定滤波器。比如手写一个高通或带通滤波器,把低频成分压一压、中高频提一提。好处是零参数量、稳定,坏处是太死板,不同数据集、不同目标尺度需要不同的频响特性,一个固定滤波器很难覆盖所有场景。
第二种是可学习滤波器。通过一个小型网络(比如几层卷积加全局池化)从输入特征预测一组频段增益系数,然后作用到幅度谱上。这个方案在我看来更接近SET“Enhancement”的本意——它是有针对性的、自适应的增强,而不是一刀切。
增强后的频域特征怎么跟原特征融合,也是需要实际对比的。最简单的做法是直接把增强后的特征加到原特征上,也就是残差连接。我见过做得复杂一些的,会把增强后的幅度谱和原有相位谱重组,再逆变换到空间域,然后跟原特征做通道拼接,最后过一个1×1卷积压缩通道数。这两种方案各有优劣,加性融合稳,不看输入,基本不会出现剧烈波动;通道拼接灵活,模型可以自己选择如何组合信息,但通道数翻倍对显存和参数量都不太友好。
我自己倾向于在一个刚起步的项目里先用加性融合+可学习滤波这个组合。它改动最小,效果也相对可控。等基线跑稳了,再往通道拼接方案上试,这样能明确看出来改进到底来自哪一步。
4. 实验与复现:怎么评估SET真有效果
4.1 数据集选择与评价指标的“坑”
小目标检测有一个专用的小目标数据集AI-TOD,里面的目标平均尺寸不到16像素,极端小目标甚至不到8像素。还有一个常用的是VisDrone,无人机视角,目标尺度分布很广,小目标占比很高。论文里如果用了这两个数据集做验证,说服力就比在COCO上调几个点要强得多。
评价指标上要注意一个陷阱:只看标准mAP是不够的,因为标准mAP在一定程度上被大量中大型目标拉高了,小目标的劣化可能被稀释。必须额外关注AP_small,或者数据集自带的特定指标。比如在AI-TOD里,攻击目标密集、目标极小时,常规的IoU阈值虽然还是一个衡量标准,但小目标的IoU天然失真,用基于距离的度量指标(比如NWD)会更合理。
所以你在复现一篇论文时,负责任的第一步,是把你自己的检测结果按目标尺寸分组计算指标,单独列出一张AP_small的变化表。如果SET有效,最明显的变化必然是AP_small的上升,标准AP的变化倒未必显著。
4.2 消融实验到底要看哪几个维度
消融实验不是走形式。在SET这个例子里,我认为至少要看清楚以下四件事。
第一,模块放在哪里收益最大。输入侧增强 vs 第一个stage后 vs 最后一个stage后,效果差异可能非常大。因为不同层的feature map频域能量分布不一样,小目标的判别性频率不会出现在所有层里。放在浅层,可能高频细节多;放在深层,语义频率明确但空间细节不够。这一步能帮你确定实际部署时的模块位置。
第二,增强策略是固定滤波还是可学习滤波。如果固定滤波和可学习滤波效果差不多,说明这组数据集上的目标频率分布比较稳定,没有复杂的场景变化和尺度变化,那你优先选固定方案,省训练时间。如果可学习滤波效果明显更好,说明数据集的频率分布随场景变化比较大,自适应是必要的。
第三,增强后融合位置作用在特征图还是检测头。作用于特征图是常规做法,但如果是特征增强,是作用于所有通道还是只作用于特定大通道组,也会影响最终效果。
第四,频带数量与性能的关系。要画一条“频带数量 vs AP_small”的曲线,理想情况应该是一个钟形曲线,先升后降。如果频带数量一多就掉点,说明模块对频带划分比较敏感,训练稳定性需要进一步优化。这一步决定了实际使用时的参数选择,很有工程价值。
4.3 一个可参考的训练设置(伪代码)
复现这类工作的时候,其实核心训练流程变化不大,关键是在哪里插入频域模块。我给自己留了一套伪代码模板,你直接拿去改改就能跑:
# 以YOLO-like检测器为例,插入SET模块 def forward(self, x, targets=None): # x: [B, C, H, W] # Step1: 空间域主分支 features = self.backbone(x) # Step2: 频谱增强分支(以第二个stage特征为例) feat = features['stage2'] # [B, C, H/8, W/8] # FFT变换 fft_feat = torch.fft.rfft2(feat, dim=(2, 3), norm='ortho') amp = torch.abs(fft_feat) phase = torch.angle(fft_feat) # 频带能量统计与增益预测(可学习) band_stats = self.band_energy_extractor(amp) # [B, C, num_bands] band_gain = self.band_gain_predictor(band_stats) # [B, C, num_bands] # 应用增益到幅度谱 amp_enhanced = amp * self.apply_gain(band_gain) # 反变换回空间域 fft_feat_enhanced = amp_enhanced * torch.exp(1j * phase) feat_enhanced = torch.fft.irfft2(fft_feat_enhanced, s=feat.shape[-2:], dim=(2, 3), norm='ortho') # 与原始特征做残差融合 feat_fused = feat + self.fusion_conv(feat_enhanced) # 继续后续neck和head features['stage2'] = feat_fused return self.neck(features), targets这个流程不用修改检测器的训练管线,全程只需要调整forward逻辑。我建议第一次跑的时候保持所有原始训练参数不变,只加模块,先用短epoch验证效果走向。如果AP_small明显上涨,再开始调模块内部超参;如果AP_small纹丝不动甚至下降,先别着急怪模块,检查你的频谱变换实现和融合位置,这两个地方最容易出问题。
5. 工程落地的坑与排查心得
5.1 频域增强反而掉点?先查这三处
我见过不少人在复现这类频域方法时,第一版效果不升反降。我的经验是先按顺序排查三个地方。
第一,检查相位谱处理。如果你在增强过程中动到了相位谱,或者进行IFFT时相位和幅度没有对齐,那基本必炸。相位一旦偏离,输出的空间域图像直接糊成一团,检测器再强也没用。正确做法是只改幅度谱,让相位谱原封不动地参与重构。
第二,检查频带划分是否覆盖了目标频率。有些数据集的背景极其复杂,比如林区航拍图里全是树冠纹理,这些纹理的频率和小目标频率高度重叠。如果你高频增益系数开得太大,目标特征是增强了,背景纹理也增强了,而且背景面积大得多,检测器学到的多半是“纹理=目标”。你需要压一压对应频段,或者让增益预测网络看到全局统计之后自动调低该频段的系数。
第三,确认模块位置确实在特征图承载小目标信息的层级。我曾经以为把模块加在深层stage效果会更好,结果是AP_small掉了1.2个点。后来把模块挪到浅层stage才确认了小目标的频率信息在更早期的特征图里。这个“位置敏感性”比想象中更明显。
5.2 训练震荡:频带增益不要一上来就放大
另一个常见的坑是训练震荡。原因是增益系数的初始化太激进,一开始就把某些频段放大好几倍,导致Loss在初期剧烈波动,甚至直接NaN。解决的办法有两个。
第一,把增益的初始值设为1附近,或者把增益网络设计成残差形式,让它的初始输出接近0。这样模块在训练初始阶段对网络的影响几乎为0,网络先学会一个正常的检测器,再逐步让频域分支起作用,训练稳定性会好很多。我在实际项目里踩过这个坑,加了残差初始化之后,训练曲线平稳得就像换了个模型。
第二,如果你确实需要用较大增益来扩大小目标信号,那至少把学习率调低一些,或者给增益分支单独设置不同的学习率。这个在不改整体训练策略的前提下,能有效缓解早期震荡。
5.3 推理变慢:FFT不是免费的
频域变换确实有计算开销,但没到无法接受的程度。实测下来,在特征图尺度做FFT和IFFT,对整体的推理耗时影响通常在个位数的毫秒级。但如果把模块放在输入图像侧,对高分辨率输入做频域变换,开销会明显增大,可能让FPS下降10%以上。
所以工程化的建议是:尽量在特征层面做,别在图像层面做。另外,如果你的检测器用的是TensorRT部署,注意FFT这块在TensorRT里支持得不一定完美,可能需要写成自定义plugin。我建议在部署前单独测一下这个算子在不同batch size下的耗时,别等整个模型都导完了才发现瓶颈。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查优先级 |
|---|---|---|
| AP_small不升反降 | 模块放置位置不对,目标频率与背景纹理混叠 | 高 |
| 训练Loss直接NaN | 增益系数初始化过大 | 高 |
| 训练震荡但收敛后正常 | 增强强度没有合理衰减,或学习率过高 | 中 |
| 推理耗时明显增加 | 模块放在图像输入侧而非特征侧 | 中 |
| 增强视觉效果怪异但AP不降 | 相位谱被修改或幅度谱增益过猛 | 低 |
| 小目标漏检集中在特定场景 | 该场景背景频率分布特殊,固定滤波不适用 | 低 |
我个人复现这类工作最大的体会是:频域视角并不是什么花活,它只是给了检测器一个多一种角度观察数据的机会。SET的思路我会建议大家认真跟进一下,不一定要完整复现,但把频谱增强这个小模块接到自己现在的检测器里跑一个消融,成本很低,却可能有意外的收益。另外想说一个细节,做这类实验一定要把AP_small作为核心指标来盯,不要被总mAP骗了。如果你在数据增强、标签分配这些空间域手段上已经优化得很透,不如试试从频域侧找找增量。顺手把FFT变换的数值稳定性验证一下,确保前后向传播都正确,你会少踩很多坑。