【目標檢測】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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章