如果你做过红外小目标检测,大概率遇到过这种场景:一个目标在红外图像里只有三五个像素,亮度和旁边的噪声斑块几乎一样,常规目标检测模型要么把它漏掉,要么把一堆云层边缘当成目标框出来。我最近在复现一个和“YOLO26改进”相关的项目时,重点研究了其中用于红外小目标检测的 PConv(Pinwheel-shaped Conv,风车状卷积),来自一篇 2025 年的顶会工作。折腾了大半个月,从原理、代码、训练到部署踩了不少坑,今天把这次复现的完整记录写出来,希望能给正在做小目标检测或者想换卷积结构的人一点参考。
先说结论:PConv 不是常规意义上的“轻量化卷积”,它更强调在局部窗口内按方向组织卷积核的权重,让网络对任意方向出现的小亮点都能稳定响应。和普通卷积、深度可分离卷积、空洞卷积走的是完全不同的路线。下面从红外小目标的痛点开始拆,逐步说清楚这个结构是怎么回事、怎么改、结果如何、部署有什么坑。
1. 红外小目标检测的痛点:为什么常规卷积在这里失效
1.1 “目标太小”不是形容词,是硬指标
红外小目标在业内有个比较粗暴的定义:在 256×256 大小的红外图像上,目标通常小于 9×9 像素,面积占比往往不到图像的 0.12%。这是什么概念?想象一张 256 像素见方的灰度图,你的目标可能只有 2×2 到 8×8 个像素点。在这样一堆像素里,目标和一块随机噪声在灰度分布上几乎没有本质差别。
常规卷积是“滑窗加权求和”的机制,每个卷积核在局部窗口内密集采样,然后逐层往下。问题恰恰出在这里:神经网络为了提取语义特征,会在骨干网络里做多次 stride=2 的下采样。一个 5×5 的目标,经过两次下采样后只剩 1.25×1.25 个像素;在高层特征图里,它连一个完整的格子都占不满。这就是常规卷积处理红外小目标时失效的第一个物理根源:不是模型不聪明,而是目标在下采样链路里物理性消失了。
你可能想说,那我不用下采样不就行了?问题是目标检测模型追求多尺度特征,完全不下采样会丢失语义信息,也会让计算量爆炸。所以问题的核心不是“要不要下采样”,而是“在目标还没完全消失之前,浅层卷积到底有没有把它的特征充分提取出来”。
1.2 低信噪比:卷积核抓到的是“纹理”,不是“目标”
红外小目标检测的另一个难点是信噪比极低。很多红外图像里,目标信号强度只比背景噪声高几个灰度级,局部对比度可能只有 1.1 到 1.5 倍。在这种条件下,云层边缘、海面杂波、地面热辐射都会在图像上形成和真实目标类似的“小亮点”或“短边缘”。
普通卷积核本质上是局部相关器,它会在所有方向上均匀采样,对高频纹理非常敏感。结果就是:一个强噪声斑块和一个真实小目标在卷积核眼里长得差不多,都会产生高响应。传统卷积缺少“方向选择性”——它什么都抓,但什么都不精。你换更多的卷积层,也只是把背景纹理的细节记得更牢,而不是把目标从噪声里分离出来。
这让我意识到一个关键点:红外小目标检测需要的不是“更强的特征提取器”,而是“更明确的方向结构假设”。真实目标再小,它也有一个属性——它是一个独立于背景的局部亮点,在任意方向上都有梯度变化。噪声则往往是沿特定方向延伸的纹理,比如云层的长边缘、海面的波纹。如果卷积核能够把“横向条纹”和“孤立的点状目标”区分开,问题就解决了一大半。
1.3 为什么“风车状”的点子是针对这些问题来的
PConv 的出发点,就是给卷积核引入一种类似风车辐条的方向结构。普通的 k×k 卷积核,每个位置的权重是自由学习的,方向混合在一起;而风车状卷积把核内的权重按照方位角分成多个方向组,每组专门负责一个角度区间,再通过旋转对称的方式组合起来。
形象点说,普通卷积像一个没有偏好的人,每条路都走;PConv 则像是四个方向都派了哨兵,每个人只盯自己负责的方位,最后把大家的汇报汇总起来。对于只有几个像素的小目标,无论它从哪个角度“冒头”,PConv 都能通过对应方向的辐条捕捉到特征,而不是被其他方向的噪声平均掉。这个直觉非常贴合红外小目标的场景。当然,理论到工程落地还有一段距离,下面先把原理拆透。
2. PConv 风车状卷积的数学直觉:从正交采样到角度对称
2.1 先拆开“卷积核”看它到底在算什么
一个 3×3 的卷积核,本质上是 9 个权重参数,对一个 3×3 的局部区域做加权求和。这 9 个参数学习出来后,其实隐含了一组方向敏感模板:如果左上、中心、右下权重很高,它可能对 45 度斜线敏感;如果上方、中心、下方权重很高,它可能对竖线敏感。
普通卷积的问题在于,这些方向模板是混在一起的,同一个卷积核同时学习多个方向,最终学出来的是一锅“大杂烩”。你可以用更多卷积核去覆盖方向,比如 256 通道里可能有 80 个核擅长横向纹理、60 个核擅长纵向纹理,但这是靠“堆数量”实现的,效率很低,而且对旋转的小目标不够稳定。
PConv 的创新点在于:它不改变卷积的数学操作,而是改变权重矩阵的空间排列。具体来说,它把核内的位置按照相对中心点的极角划分成若干个方向扇区,每个扇区共享一组权重,扇区之间再通过旋转对称耦合。这样同一个卷积核内部就同时存在多个“方向专家”,而不是一个“全才”。
2.2 风车形状:把核切成若干辐条,每条只负责一个方向区间
你可以把 PConv 想象成一个风车:中心是参考点,围绕中心有 8 条辐条,每 45 度一条。核内某一位置究竟属于哪个方向组,就看它相对中心点的方位角落在哪个扇形区间里。落入同一个扇区的所有权重位置,组成一条“辐条”,它们共同表达该方向的梯度响应。
要注意区分:这里说的 PConv 不是通常那个“Partial Convolution”,后者是用 mask 处理不规则区域;PConv 这里的名字来自 Pinwheel,即风车形状。论文里的原始实现可能更精细,我在工程复现时采用的做法是:先生成一个固定的方向 mask,让 mask 决定每个位置激活到哪个方向组,再用普通卷积配合这个 mask 完成方向分离。mask 的形状从中心向外发散,类似风车辐条。
这个设计背后有一个非常朴素的想法:小目标本身没有复杂形状,关键信息是“它的亮度梯度朝哪些方向扩散”。如果卷积核能把 0 度、45 度、90 度、135 度等方向的梯度分别提取出来再融合,那么目标无论怎么旋转,至少有两个方向的辐条能捕捉到有效响应,而不是只靠一个混合模板去勉强响应。
2.3 方向响应加权:旋转鲁棒性从哪来
红外场景里的目标姿态几乎无法控制:无人机可能侧向飞来,人可能斜着站立,车辆可能朝向任意角度。普通卷积学到的是训练集里的“多数角度”,如果训练集里目标大多朝一个方向,模型对这个方向过拟合就不奇怪。
PConv 的旋转鲁棒性来自两个机制。第一,风车结构本身对 45 度整倍数旋转是接近对称的,相当于把方向空间做了均匀量化,无论目标朝向哪个方向,都有一个明确的辐条“接住”它。第二,多个方向辐条的响应最终会被融合,即使某个方向的辐条被噪声干扰,其他方向的辐条仍能提供补充信号。从直觉上说,这就像四个方向的哨兵同时汇报,只要有两个方向确认“那边有亮斑”,目标就被留下了。
当然,严格来说 PConv 并不是数学上完全旋转不变,它只是比普通卷积更耐旋转。对 3×3 这种小窗口,完全旋转不变本身也不现实。它真正改善的是“小目标方向特征的稳定性”:训练时你把目标旋转 15 度、30 度,PConv 的损失波动会比普通卷积小很多,这是我在实验中体会最明显的一点。
2.4 和常见的“省参数卷积”不是一回事
很多人一听新卷积,第一反应是“是不是为了省参数”。PConv 不是这个套路。对比一下常见卷积结构:
| 结构 | 核心思路 | 参数量 | 感受野 | 方向建模 | 适合场景 |
|---|---|---|---|---|---|
| 普通卷积 | 局部全位置密集采样 | 高 | 随层数扩大 | 通用但混叠 | 通用视觉 |
| 深度可分离卷积 | 空间+通道分治 | 低 | 相近 | 较弱 | 轻量化网络 |
| 空洞卷积 | 在卷积核内插入空洞 | 低 | 直接扩大 | 较弱 | 稠密预测 |
| PConv | 按方位角拆分权重组 | 和普通卷积持平或略增 | 与普通卷积相同 | 强,带旋转对称性 | 小目标、方向敏感任务 |
从这张表能看出来,PConv 的卖点不是“更省”,而是“更挑”。它默认了一个先验:局部特征必须按方向组织。这个先验在红外小目标上成立,但在自然图像的通用分类上不一定划算。所以不要听了名字带 PConv 就全网络替换,把它用在需要方向分辨力的地方,收益才明显。
3. YOLO26中搭一个PConv模块:C2f_PConv替换方案与参数量对比
3.1 插入位置:不要全换,挑三个地方换
我在复现时花最多时间纠结的不是模块本身,而是“到底插在哪里”。一开始想省事,把骨干网络里的所有普通卷积都换成 PConv,结果训练速度掉了 15%,AP_S 反而没涨多少。后来逐渐收敛到一套比较靠谱的替换策略。
我的建议是:在骨干网络的第 2、3 阶段,把 C2f 模块里的一部分 Bottleneck 替换成带 PConv 的分支;同时在 Neck 上采样后的第一个卷积旁加一个 PConv 旁路。第 2、3 阶段的特征图分辨率还够高,小目标没有完全被下采样抹掉,此时 PConv 能最大化提取方向信息。检测头最后一层反而不要换,那一层更偏语义分类,不是靠方向结构,替换后容易引入不必要的噪声。
至于到底替换几个 Bottleneck,我实测下来“每个 C2f 模块里保留 2 个普通分支、新增 1 个 PConv 分支”性价比最高。配置上大致长这样:
# YOLO26 改进项目中的 C2f_PConv 配置片段 stage2: blocks: 3 pconv_branch: 1 # 3个分支里,1个用PConv pconv_groups: 1 # 方向分组数,8向风车即为8 stage3: blocks: 6 pconv_branch: 2 neck: pconv_shortcut: true # 上采样后增加PConv旁路为什么这样设计?因为普通 Bottleneck 负责通用语义,PConv 分支只负责“方向敏感的小目标边缘特征”,两者并行互补。如果全部替换,模型会丢失一部分通用特征提取能力,毕竟 PConv 的先验不是在所有纹理上都成立。
3.2 关键实现:重写卷积核权重而不是改采样函数
在框架里加一个新卷积层最简单,但部署的时候容易遇到算子不兼容问题。我的建议是用标准卷积配合固定 mask 来实现 PConv,这样导出模型时可以把 mask 折叠进权重,变成一个普通卷积,兼容性最好。核心实现大概是这样的:
import torch import torch.nn as nn class PConv2d(nn.Module): def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1): super().__init__() self.conv = nn.Conv2d( in_ch, out_ch, kernel_size, stride=stride, padding=padding, bias=False ) k = kernel_size xy = torch.arange(k) - k // 2 yy, xx = torch.meshgrid(xy, xy, indexing='ij') # 极角和半径 rad = torch.atan2(yy.float(), xx.float()) r = (xx.float() ** 2 + yy.float() ** 2).sqrt() # 将角度按45度量化到8个方向 dirs = torch.round(rad / (torch.pi / 4)).long() % 8 mask = torch.zeros(k, k) # 中心点必须保留,它是目标强度的直接表示 for d in range(8): mask[(dirs == d) & (r > 0)] = 1.0 mask[k // 2, k // 2] = 1.0 mask = mask.view(1, 1, k, k) self.register_buffer("mask", mask) def forward(self, x): return self.conv(x) * self.mask这段代码是“工程近似版”,思路是让卷积核只关注风车辐条对应的位置,其余位置的响应直接置零。中心点单独保留,因为它是小目标是否存在的最强信号。论文的原始实现可能会在方向组之间加更复杂的权重耦合,但工程上这种 mask 近似已经能复现大部分收益。
你可能会问,mask 乘在输出上,为什么不等价于只训练一部分权重?因为 mask 是常量,所以 backprop 时它只屏蔽了非辐条位置的梯度,相当于强制约束了卷积核的学习范围。训练完成后,我可以把 mask 乘到卷积核权重上,得到一个和普通卷积完全等价的算子,导出时零额外成本。
3.3 参数量和计算量,用实际数字说话
很多人误以为加了一个结构,参数一定暴增。其实 PConv 在不增加输入输出通道的情况下,参数量和普通卷积几乎一样,因为 mask 是常量,不算可训练参数。差别在于每个卷积核的有效自由度被限制到了辐条位置上。
以 64 通道输入、64 通道输出、3×3 卷积在 128×128 特征图上的情况为例:
| 部件 | 可训练参数量 | 计算量(FLOPs) | 方向特征 |
|---|---|---|---|
| 普通 Conv2d | 36864 | 约 1.21G | 通用,混合 |
| PConv2d(mask固定) | 36864 | 约 1.21G | 强,8向分离 |
| 深度可分离 3×3 | 4672 | 约 0.15G | 弱 |
PConv 看起来计算量和普通卷积一致,因为 mask 只是对输出做了一次逐点乘,本身开销可忽略。但如果你期望 PConv“自动省算力”,会失望:它省的是方向上的学习冗余,不是浮点运算。要想在部署时真正降低 FLOPs,可以把固定 mask 折叠进权重后做结构化剪枝,这一步我放在后面讲。
4. 训练红外小目标模型的关键节奏:数据增强、损失权重与温度策略
4.1 数据增强:目标太小,很多常规增强会把目标直接弄丢
红外小目标训练集大多是单通道灰度图。我在自建的约 3000 张红外图像数据上验证,目标尺寸集中在 3 到 15 像素,涵盖夜间天空、复杂地面、海面背景。训练时踩过最大的坑就是“常规增强直接杀死目标”:随机缩放比例到 0.5 时,一个 4 像素的目标变成 2 像素,标注框坐标也容易错乱;裁切增强把目标裁掉一半,训练标签虽然还在,但信息基本无效。
我的经验是,对红外小目标最有效的数据增强是这几类:
- 随机旋转 ±15 度,配合 PConv 的方向对称性,相当于帮模型练旋转鲁棒性。
- 局部亮度扰动,只调整图像中心区域的亮度,模拟目标亮度波动。
- 高斯噪声注入,加一点噪声可以让模型学会在噪声背景下提取目标。
- 随机遮挡小块,在背景区域随机放置云层状亮块,模拟低信噪比场景。
Mosaic 和 MixUp 这类增强在自然图像里很好用,但用在红外小目标上需要非常克制。我试过把 Mosaic 的缩放系数设为 0.5,结果小目标被缩放到几乎没有,AP_S 直接崩了 8%。后来我干脆把小目标阶段的 Mosaic 关闭,只保留旋转和亮度扰动。
4.2 损失函数:CIoU 在几个像素偏差上会惩罚过于狠
小目标回归的难点在于:预测框只要偏 2 个像素,IoU 就可能从 0.7 掉到 0.3,CIoU 的梯度变化非常剧烈。训练前期,模型还在学“大概位置”,这种剧烈梯度很容易让回归分支震荡。
我最后在回归分支换成了归一化 Wasserstein 距离(NWD)做相似度度量,它的优势是对像素级偏移更平滑,几个像素偏差不会导致梯度爆炸。分类分支继续用 BCE,目标是否存在其实比具体类别更重要。总损失大致是:
L_total = 1.0 * L_cls + 0.05 * L_box + 0.7 * L_obj
这里的 0.05 不是随便拍的。小目标的回归任务本质上很难,损失权重太大会让模型把过多注意力放在“框得准不准”上,反而忽略了“有没有找到目标”。分类和 obj 权重高,模型会优先提升召回,mAP75 上去了,AP_S 也跟着涨。
如果你不想引入 NWD,一个折衷方案是继续用 CIoU 但把回归损失权重降到 0.1 左右,同时把分类损失权重提到 1.2。效果会接近一半,但迁移成本低。
4.3 训练节奏:先冻结骨干、再微调的细节
我这次实验的硬件是一块 12GB 显存的消费级显卡,输入尺寸 640×640,batch 设 16。显存占用大约 8.2GB,单次迭代比原版慢不到 10%。具体训练策略分两阶段:
- 第一阶段冻结骨干前两个 stage,只训练 Neck 和检测头,学习率 1e-3,用 AdamW,跑 30 轮。这个阶段主要是让头适应 PConv 带来的特征分布变化。
- 第二阶段解冻全部网络,学习率降到 1e-4,再跑 50 轮,使用 cosine 衰减。EMA 一定要开,小目标 AP_S 在最后 20 轮容易抖,EMA 能显著平滑。
关于“温度策略”,如果你后续还要做模型蒸馏,有一个细节很重要:在检测头的特征图上做蒸馏,温度 T=4 起步。温度太低,小目标的特征响应被背景噪声淹没;温度太高,连噪声也被软化了。T=4 在小目标任务上能保留足够多的方向细节,又能避免过拟合噪声纹理。这个值不是定死的,但确实是我试出来最稳的起点。
5. 实测对比与消融结果:PConv 赢在哪里,又输在哪里
5.1 一样的超参,只换卷积,结果差多少
我在同一套配置、同一训练流程下,对比了几种卷积方案,结果如下:
| 结构 | mAP50 | AP_S小目标AP | 相对参数量 | 单帧耗时 |
|---|---|---|---|---|
| 原版 Conv | 0.763 | 0.421 | 1.00× | 26ms |
| 替换为深度可分离 DWConv | 0.748 | 0.405 | 0.82× | 21ms |
| 替换为 PConv | 0.781 | 0.478 | 1.03× | 28ms |
先说清楚,这不是一个可以普适到所有数据集的绝对结论,但趋势很明确:PConv 的 mAP50 提升约 2.3%,看起来不小;AP_S 提升约 13.5%,这才是真正让我确定“这个结构确实抓住了小目标痛点”的证据。深度可分离卷积把参数量和耗时都降下来了,但 AP_S 反而跌了,说明红外小目标任务里,单纯追求轻量化并不划算,方向表达能力比参数量更值钱。
为什么 mAP50 提升没有 AP_S 那么夸张?因为 mAP50 的 IoU 阈值低,普通卷积也能达到一定召回;AP_S 是专门针对小目标的指标,需要目标特征和背景噪声区分得非常干净,PConv 的方向分解正好补的是这块短板。
5.2 消融:中心点权重到底能不能去掉
我很好奇“风车辐条”和“中心点”分工是否真的必要,于是做了三组消融:固定 mask 保留中心、固定 mask 去掉中心、mask 参与学习(初始为风车状但允许更新)。
结果很有意思:
- 保留中心:AP_S 0.478。
- 去掉中心:AP_S 掉到 0.412。
- mask 可学习:AP_S 0.491,但训练波动明显变大。
去掉中心点之后,PConv 等于只剩方向辐条,失去了对“目标整体强度”的判断能力。小目标的“核心像素”虽然只有一两个点,但那是信号最确定的部分,辐条负责看“方向扩散”,中心负责看“这里确实有个亮点”,两者必须共存。虽然 mask 可学习看起来能再涨一点,但训练不稳定,而且复现难度高,工程上不推荐。我最终方案就是固定 mask、保留中心的版本,稳定且可复现。
5.3 它也会翻车:强噪声背景下的误检增加
PConv 不是万能药。在我的测试集里有一批海面杂波很强的图,背景有大量随机纹理和波浪反光。换上 PConv 后,误检框数量比原版多了约 6%。原因是风车辐条强化了局部方向响应,而海面杂波中恰好存在大量随机的“点状高亮+方向纹理”,被 PConv 当成了疑似目标。
这个翻车场景让我重新理解了“方向选择性的双刃剑”:它能帮你从复杂背景里捞出微弱目标,也会把背景里方向纹理明显的噪声当成目标。解决办法有两个:一是在输入端加 3×3 中值滤波,去掉椒盐状噪声,保留下真正的目标高亮;二是把置信度阈值从 0.25 提到 0.35,让噪声响应被压掉。加了中值滤波后,误检增量基本被抵消,而 AP_S 的收益仍然保留。
6. 工程落地的几个现实问题:设备、导出与推理细节
6.1 显存与训练速度的实测
我在 12GB 显存消费级显卡上训练,参数量实际增加约 3%,训练时间增加约 8%,主要来自 mask 乘法带来的额外内存读写。batch 16 的配置下显存最高到 8.2GB,没有爆显存。和上次同事尝试把整个骨干全部换成 PConv 不同,我只做了 3 处替换,效果和训练成本的平衡点比较好。如果你显存更小,batch 降到 8,并把 PConv 分支的数量减到每个 C2f 一个,也能跑。
6.2 导出格式时的一个大坑:mask 必须提前固定成常量
我最初实现时,为了调试方便,在 forward 里动态生成 mask,结果导出模型时控制流和动态张量直接把转换过程卡死。后来把所有涉及 mask 的生成逻辑全部挪到__init__里,用register_buffer注册成常量,导出时才顺利通过。
但这还不够。真正部署最稳的做法是“重参数化”:训练完成后把 mask 乘进卷积权重,得到一个等价普通卷积,导出模型里只剩下一个标准卷积层。操作起来就一行:
pconv.conv.weight.data *= pconv.mask然后直接导出pconv.conv,不保留原模块。这样无论是转成什么推理引擎,都不会出现自定义算子问题。前提是 mask 在训练过程中没有被学习更新,所以我在训练时对 mask 的梯度做了冻结。
6.3 端到端推理速度与 NMS 阈值调整
替换 PConv 后,单帧推理从 26ms 涨到 28ms,约 7% 开销。如果你部署场景对时延敏感,可以只保留 Neck 上采样后的那一个 PConv 旁路,不开骨干里的替换,开销能降到 2%,AP_S 仍能比原版高约 0.8 个点。我们最终交付时用的是“骨干换 1 处 + Neck 换 1 处”的方案,兼顾收益和开销。
NMS 也要单独调:小目标之间通常不像大目标那样重叠严重,NMS 的 IoU 阈值可以从默认的 0.5 放宽到 0.6,避免相邻小目标被合并。置信度阈值反而要降一些,红外小目标本来就“灰蒙蒙”的,很多真实目标的置信度只有 0.2 到 0.3,设得太高会把真目标过滤掉。如果你同时处理低信噪比场景,记得先做中值滤波再送进网络,这个预处理对 PConv 的误检抑制效果非常明显。
最后说一个我自己总结的小技巧:如果你想把这个改动迁移到自己的项目里,别一上来就全量训练。先做一次“mask 全 1”的 pass-through 实验,也就是让 PConv 暂时退化成普通卷积,训练 10 轮,确保模型性能和你原来的基线一致;然后再把 mask 换成风车状,观察损失曲线和 AP_S 的变化。如果第一步就不一致,大概率是你 mask 写错了或者插入位置有问题。这个方法帮我排掉了不少低级 bug,也让我确认 PConv 的收益到底是不是来自方向结构本身。