观文听傑

返回

上一篇已经得到了语义更一致的 P3...P5 特征金字塔(Feature Pyramid)。但“每层都能看见目标”并不等于“每层都应该学这个目标”。若一辆车同时在 P3、P4 和 P5 上被标为正样本,训练时三层会争抢责任;推理时又会吐出多个几乎重合的框。

本文只讲透这一个接口问题:以无锚框检测器 FCOS(Fully Convolutional One-Stage Object Detection)为例,看清一个原图目标如何按回归距离分给某个 FPN 层,各层预测如何还原到同一坐标系,以及非极大值抑制(Non-maximum Suppression, NMS)如何清理跨层重复框。

01 FPN 输出之后,还缺一份“责任表”#

假设原图是 [N,3,256,256],FPN 通道数都是 D=256D=256

层级步幅 sls_l特征形状空间位置数更适合的回归距离示例
P38[N,256,32,32]1024(0,64)(0,64)
P416[N,256,16,16]256(64,128)(64,128)
P532[N,256,8,8]64(128,+)(128,+\infty)

密集检测头(Dense Detection Head)会在每个空间位置输出类别、边界框和中心度。若有 KK 个类别,一层的数据流可写成:

P_l [N,D,H_l,W_l]

  ├─ 分类分支 ──► cls_l [N,K,H_l,W_l] ─► [N,H_l W_l,K]
  ├─ 回归分支 ──► box_l [N,4,H_l,W_l] ─► [N,H_l W_l,4]
  └─ 中心分支 ──► ctr_l [N,1,H_l,W_l] ─► [N,H_l W_l,1]
text

金字塔解决了“特征从哪里来”,但没有自动定义以下契约:

  1. 训练时,哪些位置是某个真值框(Ground-truth Box)的正样本;
  2. 一个目标应由哪个尺度层负责;
  3. 推理时,各层的局部预测怎样还原为原图像素坐标。

这三条若有一条在数据增强、训练和推理之间不一致,张量形状仍可完全正确,模型却会学到错位的标签。

02 先把网格位置送回原图坐标系#

对步幅为 sls_l 的特征层,一种常见约定是把网格 (i,j) 的预测点放在原图中:

x=(j+0.5)sl,y=(i+0.5)slx=(j+0.5)s_l,\qquad y=(i+0.5)s_l

因此 P3 的 (i=7,j=7) 对应 (x,y)=(60,60)(x,y)=(60,60),而不是 (7,7)(7,7)。不同检测实现可能使用不同的网格原点;关键不是强行选 0.5,而是标签生成、框解码和可视化共用同一约定。

设真值框为 B=(x0,y0,x1,y1)B=(x_0,y_0,x_1,y_1),且不含逆序或零面积框。点 (x,y)(x,y) 到四条边的距离是:

(l,t,r,b)=(xx0, yy0, x1x, y1y)(l,t,r,b)=(x-x_0,\ y-y_0,\ x_1-x,\ y_1-y)

它们都大于 0 时,该点严格位于框内。反过来,已知点和预测距离 l^,t^,r^,b^\hat l,\hat t,\hat r,\hat b,就能解码为:

B^=(xl^, yt^, x+r^, y+b^)\hat B=(x-\hat l,\ y-\hat t,\ x+\hat r,\ y+\hat b)

这里的边界框与预测点必须在同一度量下。若回归目标除以了步幅,推理解码前必须乘回同一步幅。

03 只要点在框内,为什么还不够?#

一个大框可能包含数百个 P3 位置。若它们全是正样本,靠近边缘的点必须预测非常不对称的长距离,而邻近物体的重叠区又容易出现归属歧义。

FCOS 引入中心采样(Center Sampling):候选点除了位于真值框内,还要靠近框中心 (cx,cy)(c_x,c_y)。若半径系数为 ρ\rho,常用条件是:

max(xcx,ycy)<ρsl\max(|x-c_x|,|y-c_y|)<\rho s_l

这是以 LL_\infty 距离定义的中心方形,不是欧氏圆。半径随层级步幅放大,使不同密度网格拥有近似的“几个单元”责任范围。

04 尺度范围怎样把目标分给某一层?#

对某个候选点和真值框,定义回归难度的尺度:

m=max(l,t,r,b)m=\max(l,t,r,b)

然后为每个层级设定责任区间 (al,bl)(a_l,b_l),只有 al<m<bla_l<m<b_l 时才允许匹配。它不是直接用目标面积或长边,而是看“这个位置需要回归多远”。

torchvision 0.28 当前 FCOS 源码中,默认每层的点由一个边长等于步幅的单锚框表示,匹配下界为 4sl4s_l,上界为 8sl8s_l;最浅层下界改为 0,最深层上界改为 ++\infty。因此用 P3/P4/P5 的简化金字塔时,就得到表中的 (0,64)(0,64)(64,128)(64,128)(128,+)(128,+\infty)

小回归距离                         大回归距离
0                 64                128                         +∞
├───── P3 负责 ────┤───── P4 负责 ────┤─────── P5 负责 ───────►
网格密,定位细                     中等尺度                  网格稀,感受野大
text

这些边界不是物理定律。更换输入分辨率、金字塔层数、数据集目标分布或匹配器后,都要重新检查正样本是否均衡,不能只复制一组阈值。

05 用一个框手算 P3 与 P4 的归属#

设真值框为:

B=(36,36,92,84),(cx,cy)=(64,60)B=(36,36,92,84),\qquad (c_x,c_y)=(64,60)

在 P3 上取预测点 q3=(60,60)q_3=(60,60),步幅 s3=8s_3=8,中心半径系数 ρ=1.5\rho=1.5。四边距离为:

(l,t,r,b)=(24,24,32,24),m=32(l,t,r,b)=(24,24,32,24),\qquad m=32
  • 四个距离都为正,点在框内;
  • max(6064,6060)=4<1.5×8=12\max(|60-64|,|60-60|)=4<1.5\times8=12,通过中心采样;
  • 0<32<640<32<64,落在 P3 责任区间。

因此 q3q_3 是该框的 P3 正样本。

再看 P4 上靠近中心的点 q4=(56,56)q_4=(56,56)s4=16s_4=16

(l,t,r,b)=(20,20,36,28),m=36(l,t,r,b)=(20,20,36,28),\qquad m=36

它在框内,且 max(8,4)=8<1.5×16=24\max(8,4)=8<1.5\times16=24,却不满足 P4 的 64<m<12864<m<128。这正是尺度分配的作用:P4 “看得到”这个框,但不对它负责。

若同一个点同时满足多个重叠真值框,torchvision FCOS 当前会匹配面积更小的那一个。这个冲突规则也是标签契约的一部分。

06 不调用检测器,先写出匹配本体#

下面的函数把所有层的点展平后一次匹配。points 形状为 [P,2]boxes[M,4]regress_ranges[P,2];中间距离张量是 [P,M,4]

这段代码刻意不计算分类损失:匹配先决定“谁是正样本”,损失再决定“错多少要罚多重”。把两者混成一个黑盒时,正样本全空往往只表现为损失“很稳定”。

07 训练时的完整数据流#

原图 + 原图框
      │  resize / crop / pad:图像和框必须同步变换

训练坐标系中的 image [N,3,H,W] 与 boxes [M,4]

      ├─► backbone + FPN ─► P3...P7 ─► cls / ltrb / centerness
      │                                      │
      └─► 生成各层 points ─► 中心采样 + 尺度匹配 ─┼─► losses

loss.backward() ◄─ head ◄─ FPN ◄─ backbone ◄───────────┘
text

对每个匹配点,常见的中心度目标为:

c=min(l,r)max(l,r)min(t,b)max(t,b)c^*=\sqrt{ \frac{\min(l,r)}{\max(l,r)} \frac{\min(t,b)}{\max(t,b)} }

在框中心,lrl\approx rtbt\approx bcc^* 接近 1;越贴近边缘则越小。torchvision FCOS 当前用分类损失、GIoU 框回归损失和中心度损失训练三条分支,并按前景点数归一化。

训练前至少要记录每层:num_pointsnum_positivepositive / gt、最大回归距离分布和三项损失。总损失下降时,P5 可能仍然从未得到正样本。

08 推理时为什么还会出现跨层重复框?#

尺度匹配是训练目标,不是推理时的硬门。学到的决策边界不会严格停在 64 或 128 像素,相邻层也可能对同一物体输出高分框。

常见后处理数据流是:

每个层级:
logits + centerness ─► score ─► 阈值过滤 ─► level top-k
ltrb + point       ─► 解码为训练图像坐标 ─► clip

所有层 boxes / scores / labels ─► concat ─► 分类别 NMS

                                      └─► 缩放回原始图像坐标
text

torchvision FCOS 当前将每个候选的分类概率 pp 与中心度 cc 合成为 pc\sqrt{pc},逐层做阈值和 topk_candidates 过滤,再拼接全部层级。这样先减少候选数,避免对数万个低分框直接做 NMS。

09 用两个框手算 NMS#

设 P3 和 P4 对同一辆车分别输出:

A=(36,36,92,84),sA=0.90A=(36,36,92,84),\quad s_A=0.90 B=(38,35,94,85),sB=0.82B=(38,35,94,85),\quad s_B=0.82

AA 面积为 56×48=268856\times48=2688BB 面积为 56×50=280056\times50=2800。交集宽高为 54544848,交集面积为 2592,所以交并比(Intersection over Union, IoU)为:

IoU(A,B)=25922688+28002592=259228960.895\operatorname{IoU}(A,B)= \frac{2592}{2688+2800-2592} =\frac{2592}{2896}\approx0.895

若 NMS 阈值是 0.6,先保留得分更高的 AA,再因 0.895>0.60.895>0.6 删除 BB。层级编号不应阻止这次比较:两个框虽来自不同 FPN 层,但类别相同、坐标系相同,就是跨层重复候选。

反之,若两个高重叠框分别预测为“自行车”与“摩托车”,分类别 NMS 不会互相抑制。是否需要跨类别竞争属于另一个任务决策。

10 用 torchvision 0.28 当前官方 API 做跨层去重#

torchvision.ops.batched_nms 接收 boxes[N,4]scores[N] 和用于分组的 idxs[N]。只有 idxs 相同的框会互相抑制;因此这里传类别编号,不要传 FPN 层级编号。

当前官方文档规定框为 (x1,y1,x2,y2),宽高必须为正;返回的 int64 索引按保留框得分降序排列。在得分完全相同时,CPU 与 GPU 对被保留框的选择不一定相同;评测代码不要依赖并列框的固定顺序。

11 完整 FCOS API 的输入与输出#

torchvision 0.28 当前的 fcos_resnet50_fpn 检测模块仍标记为 Beta,不保证向后兼容。无权重的最小调用如下:

该模型先在内部保持宽高比缩放图像,将短边尽量调到 min_size,同时不让长边超过 max_size。训练框会同步变换,推理结果再还原到每张输入图的原始尺寸。因此若在模型外自己写匹配或后处理,必须先说清代码位于内部变换之前还是之后。

12 一条可执行的调试路径#

  1. 单独画坐标网格。 在 resize/pad 后图像上画出 P3–P5 预测点,检查原点、x/yx/y 顺序、步幅和半像素偏移。
  2. 对一个框画匹配掩码。 用不同颜色显示 inside_boxinside_centerinside_scale,不要只画最终正样本。
  3. 逐层统计正样本。 检查每个真值框至少有一个匹配,也检查是否大量框卡在区间边界。
  4. 用真值距离做解码回环。 将生成的 (l,t,r,b) 立即解码,应在浮点误差内还原原框。
  5. 跟踪三个坐标系。 同时打印原图尺寸、模型变换后尺寸、padding 后 batch 尺寸,不要用一个 H,W 变量代指全部。
  6. 在 NMS 前保留来源层。 分层统计阈值前数量、top-k 后数量和被 NMS 删除数量,才能看到跨层重复来自哪里。
  7. 先过拟合一张图。 只留一个边界不在阈值附近的目标;若无法学到近乎完美的框,先修坐标和匹配,不要先调学习率。

13 最常见的“能训练,但契约错了”#

  • 用特征图坐标减原图框。 一边是 7,另一边是 92,距离值没有意义。
  • 数据增强只改图像,没改框。 水平翻转后训练仍可运行,却等于主动制造错标。
  • 用目标面积代替最大回归距离。 两者相关但不等价,尤其对极端长宽比目标。
  • 区间边界两边都用闭区间。 m=64m=64 可能同时分给 P3 和 P4;若改规则,必须明确哪一层包含边界。
  • 回归目标除以步幅,解码时没乘回。 小框会缩在预测点周围,且误差随层级变化。
  • 合并前没把各层框解码到同一图像坐标。 此时 IoU 与 NMS 结果都不可解释。
  • batched_nmsidxs 传了金字塔层级。 这会禁止跨层抑制,正好保留了要删除的重复框。
  • 只凭 NMS 后框数判断阈值。 框少可能是正确去重,也可能是密集物体被误删,必须与召回率和分类别案例一起看。

14 它会在哪些场景失败?#

  • 尺度分布严重偏斜。 遥感小目标可能几乎都落到最浅层,其他层缺乏监督;固定范围没有自动平衡能力。
  • 密集或遮挡场景。 同类物体高度重叠时,硬 NMS 可能把真实的第二个实例删除。
  • 长条形目标。 m=max(l,t,r,b)m=\max(l,t,r,b) 可能因一条长边把窄小目标分到过深层,损失横向细节。
  • 区间边界抖动。 目标稍微 resize 就可能从 P3 跳到 P4,使监督对尺度增强不连续。
  • 后处理成为延迟瓶颈。 阈值太低或逐层 top-k 太大会让 NMS 候选数暴涨,尤其在类别很多时。
  • 训练与上线前处理不一致。 上线端若用拉伸代替保宽高比 resize,尺度归属和框坐标会同时改变。

15 与相近方法的边界#

方法训练时如何定义候选尺度责任需要的去重/筛选
FCOS 固定范围网格点 + 中心采样(l,t,r,b) 最大值区间通常需要 NMS
Anchor-based 检测预设尺寸/比例的锚框通常按锚框与真值 IoU通常需要 NMS
MultiScale RoIAlign先有 proposal/RoI按 RoI 尺寸选池化层候选生成与最终阶段可各有 NMS
ATSS多层候选中选距中心近的样本用候选 IoU 统计自适应阈值通常需要 NMS
学习式匹配用分类与定位代价联合分配由当前模型质量动态决定取决于检测器设计

固定尺度范围的优点是规则清晰、向量化简单、调试可视化。它的局限也来自同一点:责任边界是手工的,不知道当前样本在哪一层实际预测得更好。

16 今天真正需要记住什么?#

  1. FPN 只产生多尺度特征;检测器还必须定义网格点、真值框、尺度范围与冲突处理的训练契约。
  2. FCOS 用点到四边的 (l,t,r,b) 回归框;框内、中心采样和尺度范围需同时满足。
  3. 尺度分配只是训练责任,不能保证推理时只有一层响应;所有层候选必须先解码到同一坐标系。
  4. 分类别 NMS 应比较同类别的跨层框;把层级号当成 batched_nms 分组会让重复框永远无法相遇。
  5. 最有价值的调试信号不是总损失,而是每层正样本、解码回环、NMS 前后候选数和原图/变换图/pad 图的坐标记录。

17 思考题与小练习#

  1. 真值框为 (16,24,144,88),分别对 P3 点 (76,60) 和 P4 点 (72,56) 计算 (l,t,r,b)mm。按本文范围判断它们是否通过尺度匹配,再检查 ρ=1.5\rho=1.5 的中心采样。
  2. assign_fcos_points 扩展为返回 [P] 的真值索引和中心度。构造两个重叠框,验证同时合法时面积更小者获得该点。
  3. 在三层候选中保留 source_level,分别将类别号和层级号传给 batched_nmsidxs。统计两种结果的重复框数,并解释密集目标上为何不能只用“框更少”作为优劣标准。

相关工作#

18 下一篇预告#

至此,图像主线已从卷积、下采样走到了多尺度特征与检测责任分配。下一篇将转向序列建模:固定窗口为什么记不住任意长的上下文,循环神经网络(Recurrent Neural Network, RNN)如何用隐状态在时间上传递信息。

同一个目标该交给哪层负责?FPN 的尺度分配与跨层去重
https://zwjcode.cn/blog/fpn-scale-assignment-cross-level-nms
作者
发布于 2026年9月4日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。