文章目录
论文:FCOS: Fully Convolutional One-Stage Object Detection
代码:https://github.com/aim-uofa/AdelaiDet/tree/master/configs/FCOS-Detection
出处:ICCV2019
FCOS 贡献:
- 证明了目标检测也可以像语义分割那样,使用单阶段来实现
- 检测任务可以使用 proposal-free 和 anchor-free 来实现,可以很大程度降低超参数的设计和调试,使得目标检测任务更优美简单
- FCOS 达到了当前单阶段检测的 SOTA,并且也可以被用作两阶段网络中的 RPN 网络
- 使用 ResNeXt-32x8d-101-FPN,在 COCO 上得到了 42.1% AP
一、背景
当时主流的目标检测算法,如 Faster RCNN、SSD、YOLOv2/v3 等,都是基于 proposal 或 anchor 的方法,使用这些预定义的 anchor 是这些方法成功的主要原因。但这些方法也有一些缺陷:
- 检测效果严重依赖于预定义框的尺寸、宽高比、数量等等,如 RetinaNet 通过调节这些超参数,就在 COCO 上提高了 4 AP,所以 anchor-based 方法需要细致的调节这些超参数
- 由于目标大小和尺度跨度较大,所以尽管使用很丰富的参数,也有不能覆盖的情况
- anchor-based 方法为了提升效果,一般会使用很多的 anchor,但很多 anchor 其实是覆盖到负样本上了,有很严重的正负样本不平衡问题
- 过多的 anchor 会在训练的时候和真值计算 IoU 的时候增加很大的计算量
基于 FCN 的方法在语义分割、关键点检测、深度估计等领域都取得了较好的效果,同样作为密集预测任务,目标检测由于有 anchor 所以一直不能实现端到端的单阶段预测。
所以研究者就提出了问题:目标检测能通过逐点预测来实现吗?
在 FCOS 之前,也有一些 FCN-based 方法用于解决目标检测的问题,如 Dense-Box 和 UnitBox。这些结构在输出的特征图上,直接预测一个 4D 向量 + class 类别,如图 1 左侧所示,4D 向量表示从某个点到 bbox 的 4 个边的距离。
为了解决 bbox 大小不同的问题,DenseNet 将输入图像 resize 到相同大小,所以就需要使用金字塔的特征来进行目标检测。且这些方法难以解决单个点对应多个目标的问题,如图 1 右侧所示。更多的用于文字检测等目标无相交的情况,难以解决目标高度相交的情况。
二、方法
FCOS 方法是第一个使用逐个像素点来预测的目标检测方法,并且提出了 centerness 分支,来抑制 low-quality bbox 并提升检测效果。
FCOS 的主要过程:
- 首先,将图像输入 Backbone,然后将 8 倍、16 倍、32 倍下采样的特征图送入 FPN,得到 FPN 的 5 层输出特征(8/16/32/64/128 倍下采样)
- 接着,在 FPN 的每一层后面都使用分类和回归头(不同分辨率特征的 head 参数是共享的),对 FPN 特征图上的每个位置都进行得分和位置的预测,得分是属于类别的得分,位置是该特征点到 gt 框的左、上、右、下四条边的距离
- 然后,将FPN 的每层特征图中的点都当做初始 anchor point,并将 anchor point 映射回原图中,根据 anchor point 是否落入 gt 框内部(或 3x3 区域内)来判断是否为正 anchor,其他的特征点赋值 80,作为负样本
- 接着,对所有正 anchor 分配对应的 gt,并根据 gt 的真实面积大小将这些 anchor 分配到不同的 FPN 特征图上去 (每层特征图负责对应大小的目标,((-1, 64), (64, 128), (128, 256), (256, 512), (512, INF))),如果某个 anchor 属于多个 gt 框内部,则选择面积最小的作为对应的 anchor
2.1 全卷积单阶段目标检测器
1、训练样本构建
假设:
- backbone 的第 i i i 层的特征图为 F i ∈ R H × W × C F_i\in R^{H\times W\times C} Fi∈RH×W×C
- s s s 是在该层前所经历的 stride
- bbox 的真值可以表示为 B i {B_i} Bi, B i = ( x 0 i , y 0 i , x 1 i , y 1 i , c i ) ∈ R 4 × 1 , 2 , . . . , C B_i = (x_0^i, y_0^i, x_1^i, y_1^i, c^i) \in R^4 \times{1, 2,..., C} Bi=(x0i,y0i,x1i,y1i,ci)∈R4×1,2,...,C (分别表示左上和右下角点,和类别)
对于特征图 F i F_i Fi 中的任意位置 ( x , y ) (x, y) (x,y),可以将其推回到该层特征图的输入特征图的位置 ( ⌊ s 2 ⌋ + x s , ⌊ s 2 ⌋ + y s ) (\lfloor \frac{s}{2} \rfloor + xs, \lfloor \frac{s}{2} \rfloor + ys) (⌊2s⌋+xs,⌊2s⌋+ys)。
本文提出的检测器是直接回归每个位置上的目标的bbox,也就是将每个像素位置看做一个训练样本,而非将每个 anchor box 看做训练样本。(anchor-based 检测器是回归每个 anchor 和真实框的偏移)
如何判定正负样本:
- 如果某个像素位置落入真实的 bbox 内,而且类别和真实的类别相同,则定义为正样本
- 否则判定其为负样本,且类别为 0 (background class)
回归的目标:
- t ∗ = ( l ∗ , t ∗ , r ∗ , b ∗ ) t^* = (l^* , t^* , r^* , b^* ) t∗=(l∗,t∗,r∗,b∗) 是每个像素位置(sample)所要回归的目标,分别表示从 sample 的位置到 bbox 的四个边的距离,如图 1 左侧所示。当一个点同时落入多个框的时候,被认为是 “ambiguous sample”,也就是 “模棱两可” 的框,选择最小面积的框作为回归目标。
- 如果一个像素位置
(
x
,
y
)
(x, y)
(x,y) 和 bounding box
B
i
B_i
Bi 是相关的,则训练时的回归目标可以被格式化为如下形式,从下面的公式中也能看出,FCOS 能够使用尽可能多的 sample 来作为训练样本(作者认为这也是 FCOS 能超越 anchor-based 方法的一个主要原因)
2、网络输出
在 backbone 的后面,连接了 4 层卷积层,来进行分类和回归,并且由于回归的目标总数正的,所以,作者在回归分支的后面使用 e x p ( x ) exp(x) exp(x) 来将任何 ( 0 , ∞ ) (0, \infty) (0,∞) 的实数进行映射。
- 80D 的分类结果(以 COCO 为例)
- 4D 的回归结果 t = ( l , t , r , b ) t = (l, t, r, b) t=(l,t,r,b)
分类器:C 个二分类分类器
优势:输出参数少
- FCOS 比 anchor-based 方法的输出参数少 9x,因为 anchor-based 方法在每个位置都放置了约 9 个不同大小的 anchors
3、Loss 函数
- L c l s L_{cls} Lcls 是 focal loss
- l r e g l_{reg} lreg 是 IoU loss
- N p o s N_{pos} Npos 是正样本的数量
- λ \lambda λ 是 1(为了平衡权重)
- Σ \Sigma Σ 的作用范围是特征图上的所有像素点
- 1 c i ∗ 0 1_{{c_i^*\>0}} 1ci∗0 是一个调节因子,当 c i ∗ > 0 c_i^*>0 ci∗>0 时为 1,其他情况为 0
4、推理
FCOS 的推理是很直接的:
- 输入图像并经过 backbone 和 head
- 得到每个位置的特征图分类得分 p x , y p_{x,y} px,y 和回归得分 t x , y t_{x,y} tx,y
- 选择 p x , y > 0.05 p_{x,y}>0.05 px,y>0.05 的作为正样本,并得到预测框(公式1)
2.2 Multi-level Prediction with FPN for FCOS
下面介绍 FCOS 可能产生的两个问题,但可以通过多尺度特征 FPN 解决的:
1、特征图分辨率大幅降低(如 16x)可能产生的低 Recall
在 anchor-based 检测器中,由于降低分辨率导致的低 recall 可以通过使用低 IoU 阈值来得到补偿。
但对于 FCOS,可能第一眼会觉得低分辨率会造成其 recall 比 anchor-based 方法低的原因在于其不可能恢复在最终的特征图上没有对应sample的目标。
但在表 1 中,作者也验证了 FCOS 能达到和 RetinaNet 等方法甚至更好的 recall,而且通过使用 FPN,能进一步提高。
2、真值框的重合可能导致训练时候的样本模棱两可
在训练过程中,到底该位置应该回归哪个框呢?
这个模棱两可的问题可能降低 FCN based 检测器。
本文也验证了可以很好的解决该问题:
和 FPN 一样,本文作者也在不同分辨率的特征图上来预测不同尺度的目标,使用了 5 个 level 的特征图 P 3 , P 4 , P 5 , P 6 , P 7 {P_3, P_4, P_5, P_6, P_7} P3,P4,P5,P6,P7,其总 stride 分别为 8、16、32、64、128。
- P 3 , P 4 , P 5 P_3, P_4, P_5 P3,P4,P5 是通过 backbone C 3 , C 4 , C 5 C3, C_4, C_5 C3,C4,C5 经过 1x1 卷积来得到的(如图 2)
- P 6 P_6 P6 是在 P 5 P_5 P5 后使用 stride=2 的卷积得到的, P 7 P_7 P7 是在 P 6 P_6 P6 后使用 stride=2 的卷积得到的
Anchor-based 是怎么给不同分辨率的特征图分配 anchor 的呢?
Anchor-based 方法是给不同 level 的特征图分配不同大小的 anchor box
FCOS 是怎么分配 anchor 的呢?
FCOS 直接限制 bbox 回归的数值的范围
- 首先,在每个特征图的每个位置上计算回归目标值 t ∗ = ( l ∗ , t ∗ , r ∗ , b ∗ ) t^* = (l^* , t^* , r^* , b^* ) t∗=(l∗,t∗,r∗,b∗)
- 接着,如果某个位置的结果满足 m a x ( l ∗ , t ∗ , r ∗ , b ∗ ) > m i max(l^* , t^* , r^* , b^*)>m_i max(l∗,t∗,r∗,b∗)>mi 或 m a x ( l ∗ , t ∗ , r ∗ , b ∗ ) < m i − 1 max(l^* , t^* , r^* , b^*)<m_{i-1} max(l∗,t∗,r∗,b∗)<mi−1,则认定该位置为负样本,也不用回归 bbox。 m i m_i mi 是在 level i i i 的特征图需要回归的最远距离。 m 2 , m 3 , m 4 , m 5 , m 6 m_2, m_3, m_4, m_5, m_6 m2,m3,m4,m5,m6 分别设置为 0, 64, 128, 256, 512 和 ∞ \infty ∞。由于不同大小的目标被分配到了不同 level 的特征图上,而且有重叠的目标一般大小都不一样,可以被分配到不同尺度的特征图上去,所以,多尺度金字塔特征,能够很好的解决 FCOS 的前景模棱两可问题。
- 最后,作者对不同尺度特征共享 head,可以提高检测效果,并提升了效率。但是,由于不同 level 的特征图需要回归不同尺度的目标(如 P3 回归的尺度为 [0,64]),所以使用一个共享头是不太合理的。故此,作者没有使用标准的 e x p ( x ) exp(x) exp(x),而是使用了 e x p ( s i x ) exp(s_ix) exp(six), s i s_i si 也是一个可训练的参数,可以根据特征图的 level 自动调节尺度。
2.3 Center-ness for FCOS
虽然使用 FPN 可以弥补不同尺度目标的识别问题,但 FCOS 还是和 anchor-based 方法的效果有一定的差距。
主要原因在于,预测产生了很多距离目标中心点很远的低质量 bbox
如何解决距离中心点很远的低质量 bbox?
作者在原来的两个 branch 的基础上,添加了一个 centerness 分支,来预测每个位置的 “centerness”,即该位置和真实目标中心点的位置,如图 2 所示。
Centerness 是给所有判定为正样本的样本点来计算的,判定为负样本的样本点是不会被计算 Centerness 的,可以看做是对所有的正样本,按其距离 gt 中心点的距离来设置权重,距离越近,则质量越高,权重越大,越有可能被 NMS 排序的时候排到前面。
下面的公式展示了 centerness 真值的计算方法,loss 使用的是二值交叉熵损失函数。
- 为了减缓衰落,使用了根号
- centerness 的范围是 (0,1),使用二值交叉熵损失来训练(添加到公式 2 的 loss 函数)
- 在测试时,final score(用于 NMS 框排序)是 centerness × \times × classification score 得到的,所以可以降低远离中心点的位置(即 low-quality 位置)的权重,可以通过 NMS 过滤掉很大一部分 low-quality 位置,提升检测效果。
极端情况下:
- 如果某一个点在box边界,那么centerness就是0
- 如果刚好在box中心,这个值就是1。centerness的值在0-1之间
- 测试的时候,作者将centerness乘以类别score作为新的score,这样就降低了远离中心点的location的分数,在NMS阶段将会大概率过滤掉它们,NMS 的 IoU 过滤阈值为 0.5。
从 anchor-based 检测器的角度来看,anchor-based 方法使用两个 IoU 阈值( T l o w T_{low} Tlow 和 T h i g h T_{high} Thigh 来将 anchor 分为 negative、ignore、positive,centerness 可以被看做一个 soft threshold。
三、效果
使用 ResNeXt-32x8d-101-FPN,在 COCO 上得到了 42.1% AP
使用 centerness 的效果对比:
如图 7 所示:
- 使用 centerness 前,有很多 low-quality bbox 的类别得分很高,很难被 NMS 消除
- 使用 centerness 后,这些点都被推到了左上角,即降低类别得分置信度,更容易被消除
如图 8 所示,FCOS 可以很好的检测被遮挡、高重叠的各种不同大小的目标。
四、训练代码
在 mmdetection 中,可以打印 FCOS Head 的结构如下:
1、loss 函数:
<bound method FCOSHead.forward_single of FCOSHead(
(loss_cls): FocalLoss()
(loss_bbox): IoULoss()
(loss_centerness): CrossEntropyLoss(avg_non_ignore=False)
(conv_cls): Conv2d(256, 80, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv_reg): Conv2d(256, 4, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv_centerness): Conv2d(256, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(scales): ModuleList(
(0): Scale()
(1): Scale()
(2): Scale()
(3): Scale()
(4): Scale()
)
2、分类的卷积层
(cls_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(2): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(3): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
3、回归的卷积层
(reg_convs): ModuleList(
(0): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(1): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(2): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
(3): ConvModule(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(gn): GroupNorm(32, 256, eps=1e-05, affine=True)
(activate): ReLU(inplace=True)
)
)
)
init_cfg={'type': 'Normal', 'layer': 'Conv2d', 'std': 0.01, 'override': {'type': 'Normal', 'name': 'conv_cls', 'std': 0.01, 'bias_prob': 0.01}}>
(
输入 FCOS Head 的 FCN 共有 5 个尺度:
(Pdb) feats[0].shape
torch.Size([2, 256, 100, 152])
(Pdb) feats[1].shape
torch.Size([2, 256, 50, 76])
(Pdb) feats[2].shape
torch.Size([2, 256, 25, 38])
(Pdb) feats[3].shape
torch.Size([2, 256, 13, 19])
(Pdb) feats[4].shape
torch.Size([2, 256, 7, 10])
这 5 组特征相比原图的 strides 如下:
self.strides = [8, 16, 32, 64, 128]
下面以第一组特征(最大的一组)为例,来看看 centerness 的输入和输出,FPN 的 5 组特征都会经过下面的操作,在每个特征点位置上获得对应的 cls_score 和 box_pred:
# 下面这 2 组特征,都是从 FCOS 的 head 输出来的特征
# bbox_pred.shape = [2, 4, 100, 152], 每个位置输出 4 个距离,一般后面还会对 bbox_pred 使用 exp 操作来进行映射
# cls_score.shape = [2, 80, 100, 152],每个位置输出 80 个向量组成的类别特征
# 下面这两组特征,都是倒数第二层特征,一般使用 cls_feat 输入到 centerness conv 中去,在每个位置都输出一个 centerness 值
# cls_feat.shape = [2, 256, 100, 152]
# reg_feat.shape = [2, 256, 100, 152]
# self.conv_centerness = Conv2d(256, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
centerness = self.conv_centerness(cls_feat)
# centerness.shape = [2, 1, 100, 152]
5 组 FPN 特征得到的分类得到特征图大小分别为:
[2, 80, 100, 152]
[2, 80, 50, 76]
[2, 80, 25, 38])
[2, 80, 13, 19])
[2, 80, 7, 10])
5 层特征图分别负责的目标大小为:
((-1, 64), (64, 128), (128, 256), (256, 512), (512, INF))
Loss 的计算:
mmdet/models/builder.py # 整体结构
mmdet/models/dense_heads/base_dense_head.py # dense_head 的基本框架
mmdet/models/dense_heads/anchor_free_head.py # 继承 dense_head 并实现 anchor-free 的结构
mmdet/models/dense_heads/fcos_head.py # fcos_head 特有的内容会写在这里边
mmdet/core/anchor/point_generator.py # 生成 point 的位置,然后在 fcos_head 中再进行正负样本的判断
mmdet/models/losses/focal_loss.py # 计算分类的 focal_loss
1、先在 /mmdet/core/anchor/point_generator.py(80)grid_priors()
中生成 point,输入为 featmap_size,即为 [torch.Size([100, 152]), torch.Size([50, 76]), torch.Size([25, 38]), torch.Size([13, 19]), torch.Size([7, 10])]
,会给每层特征单独生成 point feature。
五层特征图对应的 point feature 大小分别如下,分别对应原图的 [4, 8, 16, 32, 64] 间隔的位置点:
(Pdb) all_level_points[0].shape
torch.Size([15200, 2])
(Pdb) all_level_points[1].shape
torch.Size([3800, 2])
(Pdb) all_level_points[2].shape
torch.Size([950, 2])
(Pdb) all_level_points[3].shape
torch.Size([247, 2])
(Pdb) all_level_points[4].shape
torch.Size([70, 2])
最小的一层特征图生成的 point 如下:
all_level_points[4]
tensor([[ 64., 64.],
[ 192., 64.],
[ 320., 64.],
[ 448., 64.],
[ 576., 64.],
[ 704., 64.],
[ 832., 64.],
[ 960., 64.],
[1088., 64.],
[1216., 64.],
[ 64., 192.],
[ 192., 192.],
[ 320., 192.],
[ 448., 192.],
[ 576., 192.],
[ 704., 192.],
[ 832., 192.],
[ 960., 192.],
[1088., 192.],
[1216., 192.],
[ 64., 320.],
[ 192., 320.],
[ 320., 320.],
[ 448., 320.],
[ 576., 320.],
[ 704., 320.],
[ 832., 320.],
[ 960., 320.],
[1088., 320.],
[1216., 320.],
[ 64., 448.],
[ 192., 448.],
[ 320., 448.],
[ 448., 448.],
[ 576., 448.],
[ 704., 448.],
[ 832., 448.],
[ 960., 448.],
[1088., 448.],
[1216., 448.],
[ 64., 576.],
[ 192., 576.],
[ 320., 576.],
[ 448., 576.],
[ 576., 576.],
[ 704., 576.],
[ 832., 576.],
[ 960., 576.],
[1088., 576.],
[1216., 576.],
[ 64., 704.],
[ 192., 704.],
[ 320., 704.],
[ 448., 704.],
[ 576., 704.],
[ 704., 704.],
[ 832., 704.],
[ 960., 704.],
[1088., 704.],
[1216., 704.],
[ 64., 832.],
[ 192., 832.],
[ 320., 832.],
[ 448., 832.],
[ 576., 832.],
[ 704., 832.],
[ 832., 832.],
[ 960., 832.],
[1088., 832.],
[1216., 832.]], device='cuda:0')
2、生成 anchor point 后,先将所有的 anchor 都映射到原图中去,对每个 anchor 都根据是否在 gt 框内部来区分正负样本,将负样本赋值为背景 80,得到每个样本为正或为负的结果。并且会根据计算得到的 gt 的面积来划分应该在哪一层被检测,如下所示:
# 0 表示正样本及类别
# 80 表示负样本
[ 0, 80, 80, 0, 0, 80, 80, 80, 80, 0, 0 ...]
3、计算分类 loss ,将预测类别、正负样本标记结果、正样本数量都送入 loss 函数,focal loss 的 γ = 2 \gamma=2 γ=2, α = 0.25 \alpha=0.25 α=0.25
F o c a l _ L o s s = { − α ( 1 − p ) γ l o g ( p ) , y = 1 − ( 1 − α ) p γ l o g ( 1 − p ) , y = 0 Focal\_Loss = \begin{cases} -\alpha (1-p)^\gamma log(p), & y=1 \\ -(1-\alpha) p^\gamma log(1-p), & y=0 \end{cases} Focal_Loss={−α(1−p)γlog(p),−(1−α)pγlog(1−p),y=1y=0
loss_cls = self.loss_cls(flatten_cls_scores, flatten_labels, avg_factor=num_pos)
得到 loss:
tensor(1.1565, device='cuda:0', grad_fn=<MulBackward0>)
4、计算回归 loss 和 centerness loss,首先将正样本处的 bbox 和 centerness 选择出来,然后再做 loss
(Pdb) loss_bbox
tensor(7.9373, device='cuda:0', grad_fn=<MulBackward0>)
(Pdb) loss_centerness
tensor(0.7539, device='cuda:0', grad_fn=<MulBackward0>)
5、回到 mmdet/models/detectors/base.py (251)
,得到最后的 loss 结构:
{'loss': tensor(9.8477, device='cuda:0', grad_fn=<AddBackward0>), 'log_vars': OrderedDict([('loss_cls', 1.1565027236938477), ('loss_bbox', 7.937312602996826), ('loss_centerness', 0.7539195418357849), ('loss', 9.847734451293945)]), 'num_samples': 2}
五、推理
- 首先,输入图像经过 backbone,并得到 FPN 的 5 层输出特征
- 然后,FPN 特征经过分类和回归头,得到每个位置上的分类和回归结果,将分类得分 > 0.05 的位置看做正样本,保留前 1000 个位置
- 接着,对正样本,根据回归得到的左、上、右、下的距离,反推出框的边界
- 最后,使用 NMS,将分类得分和 centerness 得分相乘(过滤掉距离远的低质量样本)作为 NMS 排序准则,NMS 的 IoU 过滤阈值为 0.5