Mask R-CNN

Mask R-CNN 是 Faster R-CNN 的扩展版,通过新增 mask 掩码分支可以实现实例分割任务。不仅如此,实际上 mask rcnn 是一个简单、优雅、扩展性极强的框架结构,其最大特点是任务扩展性强,这是很多 one-stage 目标检测算法所无法比拟的,通过新增不同分支就可以实现不同的扩展任务,例如还可以将 mask 分支替换为关键点分支即可实现多人姿态估计。其主要创新点可以归纳如下:

  1. 基于 FPN + Faster R-CNN 结构,通过新增 mask 或者关键点分支,可以实现实例分割或者多人姿态估计

  2. 为解决特征图与原始图像上的 RoI 不对准的问题,提出了 ROI Align 模块

1 算法分析

1.1 ROI Align 模块

为了理解 ROI Align ,你需要先阅读 Faster R-CNN 中提到的 roipool 操作,假设输出大小是 2x2,其可视化如下:

roipool 存在两次取整操作:

  • 将候选框边界转化为整数点坐标值。

  • 将整数化后的边界区域平均分割成 k x k 个单元(bin),对每一个单元的边界进行整数化。

经过上述两次整数化,此时的候选框已经和最开始回归出来的位置有一定的偏差,对整数化后切割的 roi 特征图就已经不是我们想要的了,这个偏差会影响检测或者分割的准确度。在论文里作者把它总结为“不匹配问题”(mis-alignment)。

举个简单例子说明,假设输入图片大小是 800x800,在原图的 x1y1x2y2 为(10,10,675,675)坐标处有 gt bbox。图片经过 resnet 骨架网络,stride=16,映射到特征图上 x1y1x2y2 是(0.625,0.625,42.18,42.18),由于无法取整,故其会进行第一次坐标量化(假设是四舍五入)变成(1,1,42,42),可以发现现在 x1 坐标就偏移了 0.375,对于到原图是偏差 6 个像素。接下来需要把框内的特征池化为 7x7 大小,因此将上述 roi 平均分割成 7x7 个矩形区域,每个矩形区域的边长为 5.85,其含有小数,此时 roipool 会进行第二次量化,再次把它整数化到 5,此时就出现了 0.85 个偏差了。可以简单粗略计算经过两次量化,就已经出现了 0.85+0.375=1.225,对应到原图上是 19.6 个像素误差。这个偏差对于小物体来说不容小觑,对于需要精细 mask 预测任务来说那其实就是灾难了。

为了解决这个问题,ROI Align 方法取消两次整数化操作,保留了小数,每个小数位置都采用双线性插值方法获得坐标为浮点数的特征图上数值。其可视化如下所示:

左边是 roipool,右边是 roialign。其具体操作可视化如下:

假设黑色大框是要切割的 bbox,并且打算统一成 2x2 输出,则是先把黑色大 bbox 均匀切割为 4 个小 bbox,然后在每个小 bbox 内部均匀采 4 个点(相当于每个小 bbox 内部再次均匀切割为 2x2 共 4 个小块,取每个小块的中心点即可),首先对每个采样点利用双线性插值函数得到该浮点值处的值(插值的 4 个整数点是上下左右最近的 4 个点),然后对 4 个采样点采样值取 max 操作得到该小 bbox 的最终值。采样个数 4 是超参,实验发现设置为 4 最合适,速度和精度都是最合适的。

1.2 mask 分支

mask rcnn 算法可以构建在 faster rcnn 或者 fpn 上,现在主流是采用 fpn,故本文分析是基于 faster rcnn+fpn 的主流结构进行。其结构如下:

可以看出 mask 分支是和 bbox 分支并列,只不过考虑到 mask 分支需要更加精细特征图,故其 roialign 后尺寸不再是 7x7,而是 14x14,然后经过 4 个 3x3 卷积和反卷积上采样模块,变成 28x28 的特征图,最后经过 1x1 卷积得到 nun_class 个通道输出。

其核心配置如下:

mask_roi_extractor=dict(
    type='SingleRoIExtractor',
    roi_layer=dict(type='RoIAlign', output_size=14, sampling_ratio=0),
    out_channels=256,
    featmap_strides=[4, 8, 16, 32]),
mask_head=dict(
    type='FCNMaskHead',
    num_convs=4,
    in_channels=256,
    conv_out_channels=256,
    num_classes=80,
    loss_mask=dict(
        type='CrossEntropyLoss', use_mask=True, loss_weight=1.0))))

通过配置可以发现其包括两个 roialign 提取器,分别作用于 bbox 分支和 mask 分支。和 bbox 分支运行流程类似,mask 分支运行流程为:

  1. 利用 RPN 提出的 n 个 roi 区域,结合 FPN 中的映射规则,采用 roialign 模块在 FPN 输出的 5 个特征图上进行切割操作,变成(batch,n,256,14,14)输出

  2. 对(batchxn,256,14,14)的特征图输入到全卷积的 mask head 中,输出(batchxn,num_class,14,14)输出

注意由于 mask 分支仅仅需要计算正样本 roi,故其输入的 n 个 roi 区域仅仅包括正负样本定义阶段所确定的正样本而已。 为了方便大家整体把握算法流程,下面介绍 mask rcnn 的 rcnn 部分整体核心训练流程:

# 遍历每张图片,对rpn提取的proposal_list和对应gt bbox进行正负样本定义和正负样本采样
# 此时就可以确定每个proposal_list的正负和忽略样本属性了
for i in range(num_imgs):
    assign_result = self.bbox_assigner.assign(
        proposal_list[i], gt_bboxes[i], gt_bboxes_ignore[i],
        gt_labels[i])
    sampling_result = self.bbox_sampler.sample(
        assign_result,
        proposal_list[i],
        gt_bboxes[i],
        gt_labels[i],
        feats=[lvl_feat[i][None] for lvl_feat in x])
# bbox分支
# 将所有rois映射到对应特征图上,并且通过roialign操作统一输出
rois = bbox2roi([res.bboxes for res in sampling_results])
bbox_feats = self.bbox_roi_extractor(
    x[:self.bbox_roi_extractor.num_inputs], rois)
# 最后输入到分类和bbox回归分支进行预测
cls_score, bbox_pred = self.bbox_head(bbox_feats)

# mask分支
# 仅仅将正样本roi映射到对应特征图上,并且通过roialign操作统一输出
pos_rois = bbox2roi([res.pos_bboxes for res in sampling_results])
mask_feats = self.mask_roi_extractor(
    x[:self.mask_roi_extractor.num_inputs], rois)
# 最后输入到mask分支进行预测
mask_pred = self.mask_head(mask_feats)

由于多了个 mask 分支,故 rcnn 部分的 loss 变成了: L=Lcls+Lbox+LmaskL=L_{cls}+L_{box}+L_{mask}

注意 mask 分支的输出通道数 num_class,并不是 num_class+1,故其 Loss 其实是 bce loss 即对每个类别独立地预测一个二值 mask,没有引入类间竞争,每个二值 mask 的类别提取依靠网络 ROI 分类分支给出的分类预测结果。

上图是 FCIS 预测结果,下图是 mask rcnn 预测结果。

1.3 关键点分支

将 mask 分支替换为关键点分支即可实现多人关键点检测(当然 mask 分支想保留也可以)。做法和 mask 分支非常类似,假设每个人检测 17 个关键点,那么该分支输出 shape 是(batchxn,17,56,56),其 target 设置为在 56x56 的特征图上如果某个位置有关键点,那么该位置为 1,其余位置全部为 0,优化 loss 是 ce loss,不是 bce loss,因为这本身就存在类间竞争,不可能某个位置存在两个关键点。之所以设置为 56x56,而不是 28x28 是因为实验发现高分辨输出特征图对关键点检测精度影响很大。

2 推理流程

以实例分割为例分析,流程如下:

  1. 对于单张图片输入到 resnet 和 fpn 中,输出 5 个不同大小的特征图

  2. 对 5 个输出图都经过 rpn head,分别预测前后景和 bbox 坐标

  3. 采用 rpn 测试配置对预测结果进行解码,并且经过 nms 得到 proposal_list

  4. 利用 fpn 映射规则,确定 proposal_list 中每个 proposal 应该切割哪个特征图层

  5. 对每个 proposal 在指定特征图层进行 roialign 切割操作,变成统一大小

  6. 组成 batch 输入到 rcnn bbox head 进行类别分类和 proposal refine 回归

  7. 对预测结果再次进行 nms 操作得到优化后的最终 bbox_list

  8. 对上述得到的 bbox_list(可以认为是新的 proposal_list),利用 fpn 映射规则,确定 bbox_list 中每个 bbox 应该切割哪个特征图层

  9. 对每个 bbox 在指定特征图层进行 roialign 切割操作,变成统一大小

  10. 组成 batch 输入到 rcnn mask head 进行 mask 预测,得到每个 bbox 的掩码

需要特别注意的是在测试阶段,mask 分支的输入不是 RPN 预测输出的 proposal_list,而是经过 bbox 分支 refine 后得到的 bbox_list,这样才能保证 bbox 和 mask 完全一致。

3 总结

mask rcnn 是一个非常通用的 two-stage 框架,通过简单的扩展即可实现不同的任务。虽然提出时间比较早,但是依然是目前的主流算法。

Last updated