【目标检测】FCOS:Fully Convolutional One-Stage Object Detection【附pytorch实现】

Abstract

我们提出了一种完全卷积的一阶段目标检测器(FCOS),以按像素预测的方式来解决对象检测,类似于语义分割。几乎所有最新的物体检测器(例如RetinaNet,SSD,YOLOv3和Faster R-CNN)都依赖于预定义的锚框。相反,我们提出的目标检测器FCOS不含锚点和锚框。通过消除预定义的锚框,FCOS完全避免了与锚框相关的复杂计算,例如在训练过程中计算重叠。更重要的是,我们还避免了所有与锚框相关的超参数,这些超参数通常对最终检测性能非常敏感。借助唯一的后处理非最大抑制(NMS),带有ResNeXt-64x4d-101的FCOS达到44.7%在AP中进行单模型和单规模测试,其优点是简单得多,超越了以前的单级检测器。我们首次展示了一种更简单,更灵活的检测框架,可提高检测精度。我们希望所提出的FCOS框架可以作为许多其他实例级任务的简单而强大的替代方案。

1. Introduction

目标检测是计算机视觉中一项基本但具有挑战性的任务,它要求算法为图像中每个感兴趣的实例预测带有类别标签的边界框。当前所有主流检测器,例如Faster R-CNN [24],SSD [18]和YOLOv2,v3 [23]都依赖于一组预定义的锚框,长期以来人们一直认为使用锚框是目标检测中的。尽管取得了巨大的成功,但值得注意的是基于锚的检测器仍存在一些缺点:1)如图[15,24]所示,检测性能对锚盒的大小,纵横比和数量敏感。例如,在RetinaNet [15]中,改变这些超参数会影响COCO基准[16]上AP的性能高达4%。结果,这些超参数需要在基于锚点的检测器中仔细调整。 2)即使精心设计,由于锚盒的比例和纵横比保持固定,检测器在处理形状变化较大的候选对象时(尤其是小型对象)仍然遇到困难。预定义的锚框还妨碍了检测器的泛化能力,因为它们需要针对具有不同对象尺寸或纵横比的新检测任务进行重新设计。 3)为了实现较高的召回率,需要使用基于锚的检测器将锚框密集地放置在输入图像上(例如,特征金字塔网络(FPN)中有超过18万个锚框[14])短边是800)。在训练过程中,大多数这些锚框被标记为阴性样本。负样本数量过多会加剧训练中正样本与负样本之间的不平衡。 4)锚定框还涉及复杂的计算,例如计算IoU分数。

最近,全卷积网络(FCN)[20]在诸如语义分割,深度估计,关键点检测[3]和计数[2]等密集的预测任务中取得了巨大的成功。 作为高级视觉任务之一,目标检测可能是唯一偏离使用全卷积每像素预测的框架,这主要是由于使用了锚框。 我们自然会问一个问题:例如,我们能否用每像素预测方式(类似于FCN进行语义分割)解决对象检测? 因此,那些基本的视觉任务可以(几乎)统一在一个框架中。 我们证明答案是肯定的。 而且,我们首次证明,基于FCN的检测器比基于锚的检测器更简单,其性能甚至更高。

在文献中,一些工作尝试利用基于FCN的框架进行对象检测,例如DenseBox [12]。具体来说,这些基于FCN的框架可直接预测4D向量以及特征图水平上每个空间位置的类别类别。如图1(左)所示,4D向量描述了从边界框的四个侧面到该位置的相对偏移。这些框架与FCN语义分割相似,不同之处在于每个位置都需要回归4D连续向量。但是,要处理具有不同大小的边界框,DenseBox [12]会将训练图像裁剪并调整为固定大小。因此,DenseBox必须对图像金字塔进行检测,这与FCN一次计算所有卷积的哲学背道而驰。此外,更重要的是,这些方法主要用于特殊领域的异物检测,例如场景文本检测[33,10]或面部检测[32,12],因为我们认为这些方法在应用于通用对象时效果不佳高度重叠的边界框进行检测。如图1(右)所示,高度重叠的边界框导致难以理解的模糊性:不清楚对于重叠区域中的像素,哪个边界框回归。
在这里插入图片描述
在续篇中,我们仔细研究了这个问题,并表明使用FPN可以消除这种歧义。 结果,我们的方法已经可以获得与传统的基于锚的检测器相当的检测精度。此外,我们观察到我们的方法可能会在远离目标物体中心的位置产生许多低质量的预测边界框 。 为了抑制这些低质量的检测,我们引入了一种新颖的“centerness”分支(仅一层),以预测像素与其相应边界框中心的偏差,如等式中所定义。 然后将此分数用于降低检测到的低质量的边界框权重,并将检测结果合并到NMS中。 简单而有效的中心度分支使基于FCN的探测器在完全相同的训练和测试设置下胜过基于锚的探测器。

2. Related Work

Anchor-based Detectors
基于锚框的检测器继承了传统滑动窗口和基于提议的检测器(如Fast R-CNN [6])的思想。 在基于锚的检测器中,可以将锚框视为预定义的滑动窗口,将其分类为正片或负片,并进行额外的偏移回归以完善对边界框位置的预测。 因此,可以将这些检测器中的锚框视为训练样本。 与之前的快速RCNN检测器(可重复为每个滑动窗口/建议计算图像特征)不同,锚点框可利用CNN的特征图来避免重复计算特征,从而极大地加快了检测过程。 锚框的设计已由Faster R-CNN在其RPN [24],SSD [18]和YOLOv2 [22]中普及,并已成为现代检测器中的惯例。

但是,如上所述,锚框会导致过多的超参数,通常需要仔细调整才能获得良好的性能。除了上面描述anchor shape的超参数以外,基于锚的检测器还需要其他超参数。 参数将每个锚定框标记为正样本,被忽略样本或负样本。 在以前的工作中,他们经常在锚框和ground truth之间使用交集相交(IOU)来确定锚框的标签(例如,如果其IOU在0.5-1之间,则为正锚)。 超参数已对最终精度产生了很大影响,因此需要启发式调整。 同时,这些超参数特定于检测任务,从而使检测任务偏离了用于其他密集预测任务(如语义分段)的全卷积网络体系结构。

Anchor-free Detectors
最受欢迎的无锚检测器可能是YOLOv1 [21]。 YOLOv1不使用锚定框,而是预测对象中心附近的点处的边界框。 仅使用靠近中心的点,因为它们被认为能够产生更高质量的检测。 但是,由于仅使用中心附近的点来预测边界框,因此YOLOv1的召回率较低,如YOLOv2所述[22]。 结果,YOLOv2 [22]也使用了锚框。 与YOLOv1相比,FCOS利用地面真值边界框中的所有点来预测边界框,并且所提出的“centerness”分支抑制了检测到的低质量边界框。 结果,如我们的实验所示,FCOS能够与基于锚的探测器提供类似的召回率。

CornerNet [13]是最近提出的单级无锚检测器,它检测边界框的一对角并将其分组以形成最终检测到的边界框。 CornerNet需要更复杂的后处理才能将属于同一实例的角对进行分组。 为了分组的目的,学习了额外的距离度量。

诸如[32]之类的另一类无锚检测器基于DenseBox [12]。 由于难以处理重叠的边界框并且召回率相对较低,该系列检测器被认为不适合用于一般物体检测。 在这项工作中,我们表明,使用多级FPN预测可以大大缓解这两个问题。 此外,我们还展示了与我们提出的centerness分支一起,比基于锚的同类检测器更简单而且性能更好。

3. Our Approach

在本节中,我们首先以每个像素的预测方式重新构造目标检测器。 接下来,我们说明如何利用多级预测来提高召回率并解决边界框重叠导致的歧义。 最后,我们提出了“centerness”分支,该分支有助于抑制检测到的低质量边界框并大幅度提高整体性能。
在这里插入图片描述

3.1. Fully Convolutional OneStage Object Detector

对于特征图Fi上的每个位置(x, y),我们可以将其映射回输入图像为(s/2+xs,s/2+ys),它与(x,y)的感受野接近**。与基于锚的检测器不同,后者将输入图像上的位置视为(多个)锚框的中心,并使用这些锚框作为参考来回归目标边界框,我们直接在该位置回归目标边界框**。 换句话说,我们的检测器直接将位置视为训练样本,而不是基于锚的检测器中的锚框,这与用于语义分割的FCN相同[20]。

具体来说,如果位置(x,y)落入任何一个真实的目标框中,且该位置的类别标签c是真实框的类别标签,则将其视为正样本。 否则,它是一个负样本c = 0(背景类)。 除了分类标签外,我们还有一个4D实向量t =(l,t,r,b)作为位置的回归目标。 l,t,r和b是从位置到边界框的四个边的距离,如图1所示(左)。 如果某个位置落入多个边界框,则将其视为模棱两可的样本。 我们只需选择面积最小的边界框作为回归目标。 在下一部分中,我们将显示通过多级预测,可以显着减少歧义样本的数量,因此它们几乎不会影响检测性能。 形式上,如果location(x,y)与边界框Bi相关联,则该位置的训练回归目标可以公式化为:
在这里插入图片描述
值得注意的是,FCOS可以利用尽可能多的前景样本来训练回归器。 它与基于锚的检测器不同,后者仅将锚框和目标框具有足够IOU才视为阳性样本。 我们认为,这可能是FCOS优于其基于锚的检测器原因之一。

Network Outputs
与训练目标相对应,我们网络的最后一层预测了分类标签的80D向量p和4D向量t =(l,t,r,b)边界框坐标。 根据[15],我们训练C个二分类器,而不是训练多分类器。 类似于[15],我们在骨干网的特征图之后分别添加了四个卷积层,用于分类和回归分支。 此外,由于回归目标始终为正,因此我们使用exp(x)将任何实数映射到回归分支顶部的(0,1)。 值得注意的是,FCOS的网络输出变量比流行的基于锚的检测器[15、24]少9倍,每个位置有9个锚框。

Loss Function
在这里插入图片描述在这里插入图片描述

3.2. Multilevel Prediction with FPN for FCOS

在这里,我们展示了如何使用FPN [14]通过多级预测解决FCOS的两个可能问题。 1)CNN中最终特征图的大步幅(例如16步)可能会导致相对较低的最佳可能召回率(BPR)1)对于基于锚的检测器,可以通过降低正锚盒所需的IOU分数在一定程度上补偿由于步幅较大而导致的较低召回率。对于FCOS,乍一看,可能会认为BPR可能比基于锚的检测器低得多,因为不可能召回由于步幅较大而最终特征图上没有位置编码的对象。在这里,我们根据经验表明,即使步幅较大,基于FCN的FCOS仍然能够产生良好的BPR,甚至可以比官方实现的Detectron [7]中基于锚的探测器RetinaNet [15]的BPR更好。 (请参阅表1)。因此,BPR实际上不是FCOS的问题。此外,借助多级FPN预测[14],可以进一步改进BPR,以匹配基于锚点的RetinaNet可以实现的最佳BPR。 2)目标框中的重叠会引起难以理解的歧义,即重叠中的某个位置应该回归哪个边界框?这种歧义导致基于FCN的检测器性能下降。在这项工作中,我们表明可以通过多级预测极大地解决歧义,并且与基于锚的检测器相比,基于FCN的检测器可以获得同等甚至更好的性能。

类似FPN [14],我们在不同级别的特征图上检测到不同大小的对象。 具体来说,我们利用五级特征图定义为{P3; P4; P5; P6; P7}。 P3,P4和P5由主干CNN的特征图C3,C4和C5生成,后跟[14]中具有自上而下连接的1 x 1卷积层,如图2所示。 通过在P5和P6上分别应用步幅为2的一个卷积层来实现。 结果,特征等级P3,P4,P5,P6和P7分别具有步幅8、16、32、64和128。

与基于锚的检测器不同,将具有不同大小的锚框分配给不同的特征级别,我们直接限制每个级别的边界框回归的范围。更具体地说,我们首先在所有特征级别上为每个位置计算回归目标l,t,r和b。接下来,如果位置满足max(l; t; r; b)> mi或max(l,t; r; b)<mi-1,它被设置为负样本,因此不再需要回归边界框。mi是特征i级别需要回归的最大距离。在这项工作中,将m2,m3,m4,m5,m6和m7分别设置为0、64、128、256、512和无穷大,因为将具有不同大小的对象分配给不同的特征级别,并且大多数重叠发生在具有明显的大小差异情况下。如果即使使用了多级预测,位置仍被分配给多个真实框的时候,我们只需选择面积最小的框作为目标即可。如我们的实验所示,多级预测可以大大减轻上述歧义,并将基于FCN的检测器提高到与基于锚的检测器相同的水平。

最后,根据[14,15],我们在不同特征级别之间共享头部,不仅提高了检测器参数的效率,而且提高了检测性能。但是,我们观察到需要不同的特征级别来回归不同的大小范围(例如 ,尺寸范围对于P3为[0-64],对于P4为[64-128]),因此对于不同的特征级别使用相同的头是不合理的。 结果,我们没有使用标准exp(x),而是使用带有可训练标量si的exp(six)自动调整特征级Pi的指数函数的基数,从而略微提高了检测性能。

3.3. Centerness for FCOS

在FCOS中使用多级预测后,FCOS与基于锚的检测器之间仍然存在性能差距,我们观察到这是由于远离对象中心的位置产生了许多低质量的预测边界框 。

我们提出了一种简单而有效的策略来抑制这些低质量的检测到的边界框,而无需引入任何超参数。 具体来说,我们添加一个与分类分支(如图2所示)平行的单层分支,以预测位置2的“centerness”。 中心度描述了从位置到该位置所负责的对象中心的标准化距离,如图7所示。给定位置的回归目标l,t,r和b,则 中心目标定义为
在这里插入图片描述
我们在此处使用sqrt来减慢中心性的衰减。 中心度的范围是0到1,因此用二进制交叉熵(BCE)损失函数训练。 损耗被添加到损耗函数方程式中(2)。 测试时,通过将预测的中心度乘以相应的分类分数来计算最终分数(用于对检测到的边界框进行排名)。 因此,中心度可以降低远离对象中心的边界框的分数。 结果,这些低质量的边界框很有可能被最终的非最大抑制(NMS)过程滤除,从而显着提高了检测性能。
在这里插入图片描述

4. Experiments

Training Details.
Unless specified, ResNet-50 [8] is used as our backbone networks and the same hyper-parameters with RetinaNet [15] are used. Specifically, our network is trained with stochastic gradient descent (SGD) for 90K iterations with the initial learning rate being 0.01 and a minibatch of 16 images. The learning rate is reduced by a factor of 10 at iteration 60K and 80K, respectively. Weight decay and momentum are set as 0.0001 and 0.9, respectively. We initialize our backbone networks with the weights pretrained on ImageNet [4]. For the newly added layers, we initialize them as in [15]. Unless specified, the input images are resized to have their shorter side being 800 and their longer side less or equal to 1333.

Inference Details.
We firstly forward the input image through the network and obtain the predicted bounding boxes with a predicted class. Unless specified, the following post-processing is exactly the same with RetinaNet [15] and we directly make use of the same post-processing hyperparameters of RetinaNet. We use the same sizes of input images as in training. We hypothesize that the performance of our detector may be improved further if we carefully tune the hyper-parameters.
在这里插入图片描述
在这里插入图片描述

6. Conclusion

我们已经提出了无锚和无候选框的一阶段目标检测器FCOS。 如实验所示,FCOS可以与包括RetinaNet,YOLO和SSD在内的流行的基于锚的单级检测器相媲美,但设计复杂度要低得多。 FCOS完全避免了与锚框相关的所有计算和超参数,并以每像素预测的方式解决了对象检测,这与其他密集的预测任务(如语义分割)类似。 FCOS还实现了一级检测器的最先进性能。 我们还表明,FCOS可用作两级检测器Faster R-CNN中的RPN,并且在很大程度上优于其RPN。 鉴于其有效性和效率,我们希望FCOS可以作为当前主流基于锚的探测器的强大而简单的替代方案。 我们还相信,FCOS可以扩展为解决许多其他实例级识别任务。
在这里插入图片描述

7. Pytorch实现

import torch
import torch.nn as nn
import torchvision

def Conv3x3ReLU(in_channels,out_channels):
    return nn.Sequential(
        nn.Conv2d(in_channels=in_channels,out_channels=out_channels,kernel_size=3,stride=1,padding=1),
        nn.ReLU6(inplace=True)
    )

def locLayer(in_channels,out_channels):
    return nn.Sequential(
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1),
        )

def conf_centernessLayer(in_channels,out_channels):
    return nn.Sequential(
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1),
    )

class FCOS(nn.Module):
    def __init__(self, num_classes=21):
        super(FCOS, self).__init__()
        self.num_classes = num_classes
        resnet = torchvision.models.resnet50()
        layers = list(resnet.children())

        self.layer1 = nn.Sequential(*layers[:5])
        self.layer2 = nn.Sequential(*layers[5])
        self.layer3 = nn.Sequential(*layers[6])
        self.layer4 = nn.Sequential(*layers[7])

        self.lateral5 = nn.Conv2d(in_channels=2048, out_channels=256, kernel_size=1)
        self.lateral4 = nn.Conv2d(in_channels=1024, out_channels=256, kernel_size=1)
        self.lateral3 = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1)

        self.upsample4 = nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=4, stride=2, padding=1)
        self.upsample3 = nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=4, stride=2, padding=1)

        self.downsample6 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=2, padding=1)
        self.downsample5 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=2, padding=1)

        self.loc_layer3 = locLayer(in_channels=256,out_channels=4)
        self.conf_centerness_layer3 = conf_centernessLayer(in_channels=256,out_channels=self.num_classes+1)

        self.loc_layer4 = locLayer(in_channels=256, out_channels=4)
        self.conf_centerness_layer4 = conf_centernessLayer(in_channels=256, out_channels=self.num_classes + 1)

        self.loc_layer5 = locLayer(in_channels=256, out_channels=4)
        self.conf_centerness_layer5 = conf_centernessLayer(in_channels=256, out_channels=self.num_classes + 1)

        self.loc_layer6 = locLayer(in_channels=256, out_channels=4)
        self.conf_centerness_layer6 = conf_centernessLayer(in_channels=256, out_channels=self.num_classes + 1)

        self.loc_layer7 = locLayer(in_channels=256, out_channels=4)
        self.conf_centerness_layer7 = conf_centernessLayer(in_channels=256, out_channels=self.num_classes + 1)

        self.init_params()

    def init_params(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        x = self.layer1(x)
        c3 =x = self.layer2(x)
        c4 =x = self.layer3(x)
        c5 = x = self.layer4(x)

        p5 = self.lateral5(c5)
        p4 = self.upsample4(p5) + self.lateral4(c4)
        p3 = self.upsample3(p4) + self.lateral3(c3)

        p6 = self.downsample5(p5)
        p7 = self.downsample6(p6)

        loc3 = self.loc_layer3(p3)
        conf_centerness3 = self.conf_centerness_layer3(p3)
        conf3, centerness3 = conf_centerness3.split([self.num_classes, 1], dim=1)

        loc4 = self.loc_layer4(p4)
        conf_centerness4 = self.conf_centerness_layer4(p4)
        conf4, centerness4 = conf_centerness4.split([self.num_classes, 1], dim=1)

        loc5 = self.loc_layer5(p5)
        conf_centerness5 = self.conf_centerness_layer5(p5)
        conf5, centerness5 = conf_centerness5.split([self.num_classes, 1], dim=1)

        loc6 = self.loc_layer6(p6)
        conf_centerness6 = self.conf_centerness_layer6(p6)
        conf6, centerness6 = conf_centerness6.split([self.num_classes, 1], dim=1)

        loc7 = self.loc_layer7(p7)
        conf_centerness7 = self.conf_centerness_layer7(p7)
        conf7, centerness7 = conf_centerness7.split([self.num_classes, 1], dim=1)

        locs = torch.cat([loc3.permute(0, 2, 3, 1).contiguous().view(loc3.size(0), -1),
                    loc4.permute(0, 2, 3, 1).contiguous().view(loc4.size(0), -1),
                    loc5.permute(0, 2, 3, 1).contiguous().view(loc5.size(0), -1),
                    loc6.permute(0, 2, 3, 1).contiguous().view(loc6.size(0), -1),
                    loc7.permute(0, 2, 3, 1).contiguous().view(loc7.size(0), -1)],dim=1)

        confs = torch.cat([conf3.permute(0, 2, 3, 1).contiguous().view(conf3.size(0), -1),
                           conf4.permute(0, 2, 3, 1).contiguous().view(conf4.size(0), -1),
                           conf5.permute(0, 2, 3, 1).contiguous().view(conf5.size(0), -1),
                           conf6.permute(0, 2, 3, 1).contiguous().view(conf6.size(0), -1),
                           conf7.permute(0, 2, 3, 1).contiguous().view(conf7.size(0), -1),], dim=1)

        centernesses = torch.cat([centerness3.permute(0, 2, 3, 1).contiguous().view(centerness3.size(0), -1),
                           centerness4.permute(0, 2, 3, 1).contiguous().view(centerness4.size(0), -1),
                           centerness5.permute(0, 2, 3, 1).contiguous().view(centerness5.size(0), -1),
                           centerness6.permute(0, 2, 3, 1).contiguous().view(centerness6.size(0), -1),
                           centerness7.permute(0, 2, 3, 1).contiguous().view(centerness7.size(0), -1), ], dim=1)

        out = (locs, confs, centernesses)
        return out

if __name__ == '__main__':
    model = FCOS()
    print(model)

    input = torch.randn(1, 3, 800, 1024)
    out = model(input)
    print(out[0].shape)
    print(out[1].shape)
    print(out[2].shape)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值