百度飞桨图像分割七日打卡营学习笔记
(来源:飞桨图像分割教程,课程链接https://aistudio.baidu.com/aistudio/course/introduce/1767)
1.语义分割概念:
图像分割是计算机视觉中除了分类和检测外的另一项基本任务,它意味着要将图片根据内容分割成不同的块。相比图像分类和检测,分割是一项更精细的工作,因为需要对每个像素点分类,如下图的街景分割,由于对每个像素点都分类,物体的轮廓是精准勾勒的,而不是像检测那样给出边界框。图像分割可以分为两类:语义分割(Semantic Segmentation)和实例分割(Instance Segmentation)。区别如下图所示:语义分割只是简单地对图像中各个像素点分类,但是实例分割更进一步,需要区分开不同物体,这更加困难,从一定意义上来说,实例分割更像是语义分割加检测。这里我们主要关注语义分割。
2.语义分割经典模型:
2.1 FCN
对于一般的分类CNN网络,如VGG和Resnet,都会在网络的最后加入一些全连接层,经过softmax后就可以获得类别概率信息。但是这个概率信息是1维的,即只能标识整个图片的类别,不能标识每个像素点的类别,所以这种全连接方法不适用于图像分割。而FCN提出可以把后面几个全连接都换成卷积,这样就可以获得一张2维的feature map,后接softmax获得每个像素点的分类信息,从而解决了分割问题,如下图,
对于特征融合的不同情况,作者在原文种给出3种网络结果对比,明显可以看出效果:FCN-32s < FCN-16s < FCN-8s,即使用多层feature融合有利于提高分割准确性。
- 对于FCN-32s,直接对pool5 feature进行32倍上采样获得32x upsampled feature,再对32x upsampled feature每个点做softmax prediction获得32x upsampled feature prediction(即分割图)。
- 对于FCN-16s,首先对pool5 feature进行2倍上采样获得2x upsampled feature,再把pool4 feature和2x upsampled feature逐点相加,然后对相加的feature进行16倍上采样,并softmax prediction,获得16x upsampled feature prediction。
- 对于FCN-8s,首先进行pool4+2x upsampled feature逐点相加,然后又进行pool3+2x upsampled逐点相加,即进行更多次特征融合。具体过程与16s类似,不再赘述。
2.1.1 上采样
上采样有3种常见的方法:双线性插值(bilinear),反卷积(Transposed Convolution),反池化(Un-pooling)
1.双线性插值:是实践过程常用的上采样手段,计算简单,而且得到效果也不差。
2.反卷积:准确的说,应该叫做转置卷积(Transpose Convolution)
卷积操作(矩阵演示):
转置卷积(矩阵演示):
3.反池化:Un-Pooling,不太常用,比较老的方法
2.1.2 FCN实践过程中的巧妙之处:
def __init__(self,num_classes=59):
super(FCN8s,self).__init__()
backbone = VGG16BN(pretrained=False)
self.layer1 = backbone.layer1
self.layer1[0].conv._padding = [100,100]
self.pool1 = Pool2D(pool_size=2,pool_stride=2,ceil_mode=True)
self.layer2 = backbone.layer2
self.pool2 = Pool2D(pool_size=2,pool_stride=2,ceil_mode=True)
self.layer3 = backbone.layer3
self.pool3 = Pool2D(pool_size=2,pool_stride=2,ceil_mode=True)
self.layer4 = backbone.layer4
self.pool4 = Pool2D(pool_size=2,pool_stride=2,ceil_mode=True)
self.layer5 = backbone.layer5
self.pool5 = Pool2D(pool_size=2,pool_stride=2,ceil_mode=True)
self.fc6 = Conv2D(512,4096,7,act='relu')
self.fc7 = Conv2D(4096,4096,1,act='relu')
self.drop6 = Dropout()
self.drop7 = Dropout()
#1x1conv
self.score = Conv2D(4096,num_classes,1)
self.score_pool3 = Conv2D(256,num_classes,1)
self.score_pool4 = Conv2D(512,num_classes,1)
#up-smapling
self.up_output = Conv2DTranspose(num_channels=num_classes,
num_filters=num_classes,
filter_size=4,
stride=2,
bias_attr=False)
self.up_pool4 = Conv2DTranspose(num_channels=num_classes,
num_filters=num_classes,
filter_size=4,
stride=2,
bias_attr=False)
self.up_final = Conv2DTranspose(num_channels=num_classes,
num_filters=num_classes,
filter_size=16,
stride=8,
bias_attr=False)
这里的FCN8s的编码backbone是VGG16,继续添加两个全连接层FC6,FC7(本质为conv),然后使用转置卷积进行上采样得到最后的mask。
卷积操作的输出尺寸计算,Houtput=(Hinput-K+2P)/S+1
根据卷积操作,那么转置卷积的输出尺寸计算,Houtput=(Hinput-1)XS+K-2P
思考:
1.为何在Conv1时加入padding = [100,100]?
一般来说,示例输入图像(HXW)尺寸为224x224->(conv1+pool1)->112x112->(conv2+pool2)->56x56->(conv3+pool3)->28x28->(conv4+pool4)->14x14->(conv5+pool5)->7x7 ->FC6(conv7x7)->1x1->FC7(conv1x1)->1x1->…
当图像尺寸小于224x224时,上面尺寸变化还会如此顺利吗?极端的考虑,把输入图像设为1x1,那么:
1x1->(conv1+pool1)->1x1->(conv2+pool2)->1x1->(conv3+pool3)->1x1->(conv4+pool4)->1x1->(conv5+pool5)->1x1 ->FC6(conv7x7)->Error->FC7(conv1x1)->…在经过FC6时就会报错。
所以,为了让网络可以接受任意大小的输入,就可以在conv1时加入100的padding,这样上面的过程就变为:
1x1->(conv1(padding100))->199x199->(pool1)->100x100->(conv2+pool2)->50x50->(conv3+pool3)->25x25->(conv4+pool4)->13x13->(conv5+pool5)->7x7 ->FC6(conv7x7)->1x1->FC7(conv1x1)->1x1->…
def forward(self,inputs):
x = self.layer1(inputs)
x = self.pool1(x)
x = self.layer2(x)
x = self.pool2(x)
x = self.layer3(x)
x = self.pool3(x)
pool3 = x
x = self.layer4(x)
x = self.pool4(x)
pool4 = x
x = self.layer5(pool4)
x = self.pool5(x)
x = self.fc6(x)
x = self.drop6(x)
x = self.fc7(x)
x = self.drop7(x)
x = self.score(x)
x = self.up_output(x)
up_output = x #1/16
x = self.score_pool4(pool4)
#crop操作
x = x[:,:, 5:5+up_output.shape[2],5:5+up_output.shape[3]]
up_pool4 = x
x = up_pool4 + up_output
x = self.up_pool4(x)
up_pool4 = x
x = self.score_pool3(pool3)
#crop操作
x = x[:,:, 9:9+up_pool4.shape[2],9:9+up_pool4.shape[3]]
up_pool3 = x #1/8
x = up_pool3 + up_pool4
x = self.up_final(x)
#crop操作
x = x[:,:, 31:31+inputs.shape[2],31:31+inputs.shape[3]]
return x
2.为什么要crop(5,9,31)?
优点:
任意尺寸输入、效率高、结合浅层信息
缺点:
分割结果不够精细、没有考虑上下文信息(“看看旁边有啥,左顾右盼”)
2.U-Net
(2015年MICCAI)
U-Net是原作者参加ISBI Challenge提出的一种分割网络,能够适应很小的训练集(大约30张图)。U-Net与FCN都是很小的分割网络,既没有使用空洞卷积,也没有后接CRF,结构简单。整个U-Net网络结构如上图,类似于一个大大的U字母:首先进行Conv+Pooling下采样,将特征抽象,获取丰富的高级语义信息,有助于分割目标物体的类别判断;然后Deconv反卷积进行上采样,将图像恢复到模板大小,但在直接恢复会导致分割出来的结果粗糙,所以crop之前的低层feature map,进行融合,获得更为精细的分割结果;然后再次上采样。重复这个过程,直到获得输出388x388x2的feature map,最后经过softmax获得output segment map。总体来说与FCN思路非常类似。
与FCN逐点相加不同,U-Net采用将特征在channel维度拼接在一起,形成更“厚”的特征。所以:
语义分割网络在特征融合时也有2种办法:
- FCN式的逐点相加,对应caffe的EltwiseLayer层,对应tensorflow的tf.add()
- U-Net式的channel维度拼接融合,对应caffe的ConcatLayer层,对应tensorflow的tf.concat()
相比其他大型网络,FCN/U-Net还是蛮简单的,就不多废话了。
总结一下,CNN图像语义分割也就基本上是这个套路:
- 下采样+上采样:Convlution + Deconvlution/Resize
- 多尺度特征融合:特征逐点相加/特征channel维度拼接
- 获得像素级别的segement map:对每一个像素点进行判断类别
即使是更复杂的DeepLab v3+依然也是这个基本套路
3.PSPNet(Pyramid Scene Parsing Network)
FCN缺点:没有考虑上下文信息
1.什么是上下文信息?
2.PSP网络如何获取上下文?
增大感受野(Receptive Field)。
感受野(RF)= 用于产生特征的输入图像中的区域大小
只针对于局部操作:例如conv,pooling
3.如何增大感受野?
不同scale的“feature pyramid”
4.PSP模块及特征图变化
5.Adaptive Pooling概念:
floor():向下取整
6.PSP完整结构
4.DeepLab系列
1.空洞卷积:
空洞卷积后的尺寸变化:
D=1,没有空洞
作用及优点:扩大感受野,计算量增加少
(dilated ResNet)
2.DeepLab 系列网络
如果觉得PSPNet,DeepLab等大模型在单卡上难以训练,可以考虑将后面的分割部分加到轻量级MobileNet V1等系列上去,训练可能会更快。
1)DeepLab V1
2)DeepLab V2
3)DeepLab V3
Backbone:
![[]](https://img-blog.csdnimg.cn/20201025095937608.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hhb2FoMzE2,size_16,color_FFFFFF,t_70#pic_center)
后续有相关内容会继续补充,欢迎各位有想法的小伙伴们积极指导和讨论。感谢观看~