论文内容提要
- 一种简单、高效的自底向上的全景分割方法
- 基于DeeplabV3+的语义分割和基于中心点回归的实例分割的结合
- 特征解码部分采用双ASPP(空洞卷积池化金字塔)的双解码
- 实现了目前很好的结果,能够以近乎实时的速度(16帧)处理分辨率为1025×2049的图片。
整体抽象结构图:
背景介绍
全景分割的目的:
对每个像素点给予不同的label + instance ID(如果对于stuff,比如地面,背景,不需要instance ID)
目前2种流行思路:Top-down & Bottom-up
- Top-down方式:目前多为在Mask-RCNN的基础上(当然也可以是其他实例分割的网络)接一个语义分割的头部,但是这种方式会造成语义分割和实例分割之间的冲突(因为一个像素点可能被实例分割成一类,但语义分割又变成了另一类),为了解决这个问题,就需要通过某种设定的方式来融合语义分割的score + 实例分割的score。之前的解决办法:置信度分数或者类别匹配关系。
由于top-down的方式会有一个很长的pipeline(RPN + RCNN/Mask),外加一个semantic,因此通常会比较慢 - bottom-up方式:通常从语义分割预测开始,然后进行分组操作以生成实例掩码。 以这样的顺序处理全景分割可允许简单快速的方案(例如多数投票)合并语义和实例分割结果。
bottom-up系列和top-down相比更快,但往往性能更低一些。
结构
Panoptic-DeepLab由四个组件组成:
(1)用于语义分割和实例分割的编码器主干Backbone(ImageNet-pretrained),
(2)解耦的ASPP模块,用于提取multi-scale的context
(3)特定于每个任务的解耦解码器模块
(4)特定于任务的预测头
细节
(1)在backbone最后一个block加入了空洞卷积以获取更为稠密的特征图。
(2)解码器块采用Deeplabv3+的结构,不同在于向解码器引入了1/8分辨率的附加low-level feature(如上图编解码器中红色箭头那里,代表skip connection),并且在每个上采样后,采用单个卷积核为5×5 的可分离卷积。
(3)语义分割支路采用了The weighted bootstrapped cross entropy loss,也就是加权自引导交叉熵损失。
这个损失函数是在《DeeperLab: Single-Shot Image Parser》这篇论文里提出的。
(4)实例分割,对于每个前景像素(即,其类别为“Thing”的像素),进一步预测到其相应质心的偏移量。 Ground Truth由2D高斯编码,标准偏差为8个像素。 采用均方误差(MSE)损失来最大程度地减少预测的Hotmap和2D高斯编码的Ground Truth热图之间的距离。 我们将L1损失用于偏移预测,该预测仅在属于对象实例的像素处激活。
总结就是通过实例的质心 + 实例对应的每个像素点对于质心的偏移量来表征一个实例。
对于质心的回归用L2,偏移量的回归用L1。
后处理
后处理主要包括2部分:
(1)一个是将实例的质心和偏移量组合起来,形成若干实例(原图每个目标(thing)都会带有实例id)
- 实例表示形式:目标质心 Cn : (in, jn)
首先执行基于关键点的非最大抑制(NMS):在最终输出的Feature Map上进行NMS(实际上是Max Pooling,保留Pool前后未改变的坐标,作为实例的中心)。
最后,使用阈值过滤掉具有低置信度的预测,并且仅保留置信度得分最高的前k个位置。
kernelsize = 7 thres = 0.1 top-k = 200
- 实例分组
利用实例中心点回归获取每个像素的实例id。根据offset map,需要找到每个offset对应的中心点。给定坐标点( i , j ) ,它的Offset是O ( i , j ),本文将( i , j ) 对应的实例label定义为O ( i , j ) 与( i , j )所指向的坐标点最近的中心点:
(2)另一个是将语义分割的结果和实例分割结果高效地归并在一起。
- 这里通过offset + center得到的实例,是没有类别的。而类别的分数,是从语义分割结果中取的。
文中说的是多数表决来获取实例分割的类别:通过对应的预测语义标签的多数表决来推断预测实例掩码的语义标签。
代码训练
进行训练测试之前,我们先来详细看一下网络的结构是怎么样的。这里以Resnet101为例。
backbone
首先是一个卷积块:conv1初始化输入为64通道。
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
然后是layer1,包含3个Bottleneck块:
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
再接layer2,类似与layer1:
(layer2): Sequential(
(0): Bottleneck(
(conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
同样,后面再接layer3(共23个Bottleneck块),layer4(3个Bottleneck块)。不过奇怪的是,论文中提到在编码器的最后一个block(layer4里面)中,应该是有空洞卷积的。但是在其提供的代码得到的网络结构中,却未发现有这个dilation参数???。同时在配置文件中,也是false
BACKBONE:
NAME: “resnet101”
DILATION: (False, False, False)
PRETRAINED: True
说明其实在backbone中,并没有使用空洞卷积,只是在后面的ASPP模块用了。
(layer4): Sequential(
(0): Bottleneck(
(conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(2048, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
对于解码器部分,首先是ASPP块。这里的dialation设为了[1,3,6,9],论文写的[1,6,12,18]
(aspp): ASPP(
(convs): ModuleList(
(0): Sequential(
(0): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): ASPPConv(
(0): Conv2d(2048, 256, kernel_size=(3, 3), stride=(1, 1), padding=(3, 3), dilation=(3, 3), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU()
)
(2): ASPPConv(
(0): Conv2d(2048, 256, kernel_size=(3, 3), stride=(1, 1), padding=(6, 6), dilation=(6, 6), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU()
)
(3): ASPPConv(
(0): Conv2d(2048, 256, kernel_size=(3, 3), stride=(1, 1), padding=(9, 9), dilation=(9, 9), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU()
)
(4): ASPPPooling(
(aspp_pooling): Sequential(
(0): AdaptiveAvgPool2d(output_size=1)
(1): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(2): ReLU()
)
)
)
后面的就同论文的类似了。
损失函数:
(semantic_loss): DeepLabCE(
(criterion): CrossEntropyLoss()
)
(center_loss): MSELoss()
(offset_loss): L1Loss()
)
训练
数据集准备
数据集采用cityscopes,可直接去其官网下载。注意:原始的cityscopes是不包含全景分割的标注的,需要通过开源api从原本标签生成。期望的data结构如下所示:
cityscapes/
-----gtFine/
----------train/
--------------aachen/
----------------color.png, instanceIds.png, labelIds.png, polygons.json,
----------------labelTrainIds.png
…
----------val/
----------test/
# below are generated Cityscapes panoptic annotation
---------cityscapes_panoptic_train.json
---------cityscapes_panoptic_train/
---------cityscapes_panoptic_val.json
---------cityscapes_panoptic_val/
---------cityscapes_panoptic_test.json
---------cityscapes_panoptic_test/
----- leftImg8bit/
--------train/
--------val/
--------test
首先:
pip install git+https://github.com/mcordts/cityscapesScripts.git
从git上git clone生成全景数据label的标签的api。
然后cd到cityscopeScripts,生成标签
CITYSCAPES_DATASET=/path/to/abovementioned/cityscapes python cityscapesscripts/preparation/createPanopticImgs.py
/path/to/abovementioned/
就是上面存数据的路径。到这里就准备好数据啦。
使用Detectron2训练
作者已经将其项目加入到detectron2的项目中,故可以通过detectron2更方便的加载数据和训练。
首先要安装detectron2:
git clone https://github.com/facebookresearch/detectron2.git
python -m pip install -e detectron2
然后将上述数据路径导入到detectron2的数据路径。
export DETECTRON2_DATASETS=/path/to/datasets
然后就可以开始训练啦。首先cd到 detectron2 / projects / ,会发现包含 Panoptic-DeepLab的项目文件。直接运行train_net.py即可开始训练。这里由于我只有两块gpu,所以将num-gpus 设为2。
cd / path / to / detectron2 / projects / Panoptic-DeepLab
python train_net.py --config-file configs / Cityscapes-PanopticSegmentation / panoptic_deeplab_R_52_os16_mg124_poly_90k_bs32_crop_512_1024.yaml --num-gpus 2
注意:如果自己的gpu不够,需要修改上述作者提供的模型yaml文件中的batchsize:ims_per_batch。这里我设置为6的时候已经将两块1080ti拉满了。。同时由于只是测试,将迭代次数从90000也降低为20000。
效果
迭代了20000次,耗时11个小时。
测试图片结果:
总结
- 为什么要这么设计?
个人感觉:
整体网络为典型encoder-decoder结构,加深了网络深度以及提取特征与特定任务分离。
backbone:提取粗糙特征。
ASPP模块:提取、融合多尺度信息。
decoder部分:
1、采用Skip connection,将backbone模块中的各级low level feature与经过上采样的high level feature 融合,得到更为精细的特征。
2、采用深度可分离卷积(depthwise deparable conv),减少参数,加速。
sematic head/instance head:最后的预测分类层
- 实例分割部分原理是怎么样的?
实例分割head包含两个方面,一个是对于thing是的描述:center of mass也就是质心的预测。另外一个是对于偏移量的回归。将每一个像素点通过预测的偏移量平移,与thing的预测的质心做差,那么同哪个things的预测质心最近,那么就给这个像素点打上哪个things的id。
- 另外感觉过长的双通道设计有点冗余,按作者的测试来看,仅仅提升了0.7%个PQ点。但是个人感觉这部分占了更多的计算量和时间(没测试过…),不值得。
- 全景分割也太要GPU了吧!