FCOS:简洁的anchor-free目标检测器

论文题目:FCOS: Fully Convolutional One-Stage Object Detection

其亮点:

  • 基于FCN构建全卷积检测器,使得视觉任务(如语义分割)可以统一在FCN框架
  • anchor-free,proposal free,避免了训练阶段关于anchor或者proposal的iou计算.更重要的是,避免了一切与anchor有关的超参数
  • 简单的Backbone;neck;head检测算法框架

原始anchor-base的缺点:

  • 检测器对anchor的大小、纵横比、数量比较敏感;在RetinaNet,更改这些超参数会影响性能高达4% ap(coco基准).因此在使用基于anchor的检测器时要仔细调关于anchor的超参数
  • 由于anchor的比例和纵横比在初始时保持固定,检测器在处理形状变化较大的候选对象时能力不够
  • 为了实现较高的召回率,anchor-base的检测器将anchor密集地放置在图像特征中,导致训练过程加大了正负样本的不平衡,当然也显著增加训练过程的计算量(一般通过计算与GT之间的IOU来计算loss,anchor生成的proposal越多,计算量就越大)

本文方法:

逐像素回归预测

信息表示:

对于训练目标ground-truth bounding boxes,我们用其四元向量表示:B_{i}=\left(x_{0}^{(i)}, y_{0}^{(i)}, x_{1}^{(i)} y_{1}^{(i)}, c^{(i)}\right)

其中(x_{0}^{i},y_{0}^{i})代表了边框的左上角点坐标,(x_{1}^{i},y_{1}^{i})代表了边框的右上角坐标,c_{i}代表了其目标框的类别.

backbone CNN网络提取的特征图F_{i} \in \mathbb{R}^{H \times W \times C}属于第i层,其中缩放的步幅(stride)为s.
对于特征图中的每一对坐标点(x,y),我们可以与原始图像建立一一对应关系\left(\left\lfloor\frac{s}{2}\right\rfloor+ x s,\left\lfloor\frac{s}{2}\right\rfloor+ y s\right);

不同于anchor-base的检测器,fcos对每一个特征图上的坐标(x,y)都作为训练样本进行回归(也就是像素级别回归).

如同上面的对应关系,如果(x,y)落在任何一个ground-truth bounding box中,那么它是一个正训练样本,其标签是ground-truth的标签c^{*},如果不落在box中,则该样本则为负样本,c=0.

除此之外,fcos还对每一个像素进行回归预测一个四元组向量t^{*}=(l^{*}, t^{*}, r^{*}, b^{*}),分别代表了其四个边框到中心点的距离;

某个点落入边框B_{i}内,则回归的目标为:
\begin{array}{l} l^{*}=x-x_{0}^{(i)}, \quad t^{*}=y-y_{0}^{(i)} \\ r^{*}=x_{1}^{(i)}-x, \quad b^{*}=y_{1}^{(i)}-y \end{array}

image

作者认为fcos优于其他训练器的方式在于利用了尽可能多的前景样本来训练回归器,而不是通过计算iou来获取正样本.

网络输出

image

根据逐像素级别的回归目标,我们得到其训练目标为C-d向量代表了其分类结果,4-d向量t=(l,t,r,b)代表了其边框坐标回归信息.

fcos采取训练C个二分类器的方式进行类别判断,而不是直接训练一个多分类判别器.

Moreover, since the regression targets are always positive, we employ exp(x) to map any real number to (0,∞) on the top of the regression branch

在回归分支中像yolo一样采取exp函数对坐标进行比例map.

loss 函数

整体loss分为分类loss与边框信息loss:

\begin{aligned} L\left(\left\{\boldsymbol{p}_{x, y}\right\},\left\{\boldsymbol{t}_{x, y}\right\}\right) &=\frac{1}{N_{\text {pos }}} \sum_{x, y} L_{\text {cls }}\left(\boldsymbol{p}_{x, y}, c_{x, y}^{*}\right) \\ &+\frac{\lambda}{N} \sum \mathbb{1}_{\left\{c_{x, y}^{*}>0\right\}} L_{\text {reg }}\left(\boldsymbol{t}_{x, y}, \boldsymbol{t}_{x, y}^{*}\right) \end{aligned}

其中L_{cls}采取focal loss;

L_{reg}则是计算预测框与真实框的 iou loss.

N_{pos}为正样本的数量.

\lambda为平衡系数,用来平衡分类损失与坐标损失.

多级预测提高召回率

利用FPN多级特征图尺寸来提高召回率;

image

如结构图所示,作者使用了FPN的5个层次的特征图为别为{P_{3},P_{4},P_{5},P_{6},P_{7}},其stride分别为是:8,16,32,64和128.

那么对于回归目标(l_{*},t^{*},r^{*},b^{*}),其处于不同的特征图,最大最小值也有了一定的限制;

如果\max \left(l^{*}, t^{*}, r^{*}, b^{*}\right)>m_{i},或者\max \left(l^{*}, t^{*}, r^{*}, b^{*}\right)<m_{i-1},则其是一个负样本,不参与训练计算;

m_{2},m_{3},m_{4},m_{5},m_{6},m_{7}的取值分别为0,64,128,256,512;

center-ness 分支

作者发现,通过原始的办法,由于在远离中心点的位置产生了许多低质量的预测边框,导致检测器性能不高,所以作者提出了一个新的分支,center-ness,用来抑制这些低质量边框;

对于同一个坐标点的回归坐标信息(l^{*},t^{*},b^{*},r^{*}),通过以下公式计算其中心度:
\text { centerness }^{*}=\sqrt{\frac{\min \left(l^{*}, r^{*}\right)}{\max \left(l^{*}, r^{*}\right)} \times \frac{\min \left(t^{*}, b^{*}\right)}{\max \left(t^{*}, b^{*}\right)}}

远离中心点的边框,其max(l^{*},r^{* })会很大,导致center-ness分数很低.这样就可以有效地抑制这些低质量边框.

center-ness 乘以分类分数来得到最终score(用来计算预测框的排名).最后,这些低质量的边框可能通过最终的NMS过程来滤除掉.

代码阅读

本代码采取mmdetion 框架中的fcos代码部分;

其backbone跟fpn均采取普通设置(resnet+fpn多尺度预测),来窥探下fcos_heads.py

#/mmdet/models/anchor_heads/fcos_heads.py
@HEADS.register_module
class FCOSHead(nn.Module):
    """
    Fully Convolutional One-Stage Object Detection head from [1]_.

    The FCOS head does not use anchor boxes. Instead bounding boxes are
    predicted at each pixel and a centerness measure is used to supress
    low-quality predictions.

    References:
        .. [1] https://arxiv.org/abs/1904.01355

    Example:
        >>> self = FCOSHead(11, 7)
        >>> feats = [torch.rand(1, 7, s, s) for s in [4, 8, 16, 32, 64]]
        >>> cls_score, bbox_pred, centerness = self.forward(feats)
        >>> assert len(cls_score) == len(self.scales)
    """

    def __init__(self,
                 num_classes,
                 in_channels,
                 feat_channels=256,
                 stacked_convs=4,
                 #定义了其采样步长,回归边框
                 strides=(4, 8, 16, 32, 64),
                 regress_ranges=((-1, 64), (64, 128), (128, 256), (256, 512),
                                 (512, INF)),
                #三种loss分别为focalloss,iouloss 跟CrossEntropyLoss计算
                 loss_cls=dict(
                     type='FocalLoss',
                     use_sigmoid=True,
                     gamma=2.0,
                     alpha=0.25,
                     loss_weight=1.0),
                 loss_bbox=dict(type='IoULoss', loss_weight=1.0),
                 loss_centerness=dict(
                     type='CrossEntropyLoss',
                     use_sigmoid=True,
                     loss_weight=1.0),
                 conv_cfg=None,
                 norm_cfg=dict(type='GN', num_groups=32, requires_grad=True)):
        super(FCOSHead, self).__init__()

        self.num_classes = num_classes
        self.cls_out_channels = num_classes - 1
        self.in_channels = in_channels
        self.feat_channels = feat_channels
        self.stacked_convs = stacked_convs
        self.strides = strides
        self.regress_ranges = regress_ranges
        self.loss_cls = build_loss(loss_cls)
        self.loss_bbox = build_loss(loss_bbox)
        self.loss_centerness = build_loss(loss_centerness)
        self.conv_cfg = conv_cfg
        self.norm_cfg = norm_cfg
        self.fp16_enabled = False

        self._init_layers()
    def _init_layers(self):
        #每一个head都有两个分支:分类分支和回归坐标分支,经过四次采样.
        self.cls_convs = nn.ModuleList()
        self.reg_convs = nn.ModuleList()
        for i in range(self.stacked_convs):
            chn = self.in_channels if i == 0 else self.feat_channels
            self.cls_convs.append(
                ConvModule(
                    chn,
                    self.feat_channels,
                    3,
                    stride=1,
                    padding=1,
                    conv_cfg=self.conv_cfg,
                    norm_cfg=self.norm_cfg,
                    bias=self.norm_cfg is None))
            self.reg_convs.append(
                ConvModule(
                    chn,
                    self.feat_channels,
                    3,
                    stride=1,
                    padding=1,
                    conv_cfg=self.conv_cfg,
                    norm_cfg=self.norm_cfg,
                    bias=self.norm_cfg is None))
        self.fcos_cls = nn.Conv2d(
            self.feat_channels, self.cls_out_channels, 3, padding=1)
        self.fcos_reg = nn.Conv2d(self.feat_channels, 4, 3, padding=1)
        self.fcos_centerness = nn.Conv2d(self.feat_channels, 1, 3, padding=1)

        self.scales = nn.ModuleList([Scale(1.0) for _ in self.strides])

        #前馈计算方式,分类分支,回归分支,center-ness 分支分别计算结果
    def forward_single(self, x, scale):
        cls_feat = x
        reg_feat = x

        for cls_layer in self.cls_convs:
            cls_feat = cls_layer(cls_feat)
        cls_score = self.fcos_cls(cls_feat)
        centerness = self.fcos_centerness(cls_feat)

        for reg_layer in self.reg_convs:
            reg_feat = reg_layer(reg_feat)
        # scale the bbox_pred of different level
        # float to avoid overflow when enabling FP16
        bbox_pred = scale(self.fcos_reg(reg_feat)).float().exp()
        return cls_score, bbox_pred, centerness
        #center-ness 的定义
    def centerness_target(self, pos_bbox_targets):
        # only calculate pos centerness targets, otherwise there may be nan
        left_right = pos_bbox_targets[:, [0, 2]]
        top_bottom = pos_bbox_targets[:, [1, 3]]
        centerness_targets = (
            left_right.min(dim=-1)[0] / left_right.max(dim=-1)[0]) * (
                top_bottom.min(dim=-1)[0] / top_bottom.max(dim=-1)[0])
        return torch.sqrt(centerness_targets)

总结

  • FCN的方式进行像素级别中心点回归预测边框方式
  • 提出center-ness分支,有效解决了离中心点较远的低质量预测框问题
  • 建立统一的FCN视觉任务框架

reference

[1] Tian Z, Shen C, Chen H, et al. Fcos: Fully convolutional one-stage object detection[C]//Proceedings of the IEEE International Conference on Computer Vision. 2019: 9627-9636.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容