FCN算是图像分割的开篇之作,在它之前,分割任务就是当做分类去做的。
最简单的分类就是通过一系列的卷积操作进行特征提取,最后加上几个全连接层,通过softmax得到分类的结果。
最初的分割方式,就是通过划窗之类的策略,提取到一个个的patch,将这个patch作为当前像素的上下文,输入到分类网络中,然后得到当前像素的类别,最终所有像素的分类结果就是我们的分割结果了。
这个做法显然有很多的问题,如
1、滑窗带来的计算量大、计算效率低(相邻的像素块基本上是重复的)。
2、patch相对于全图而言会比较小,只能提取局部的特征,使得最后的效果也没那么好。
3、这不是端到端的,需要有预处理,后处理的环节。
而FCN做的就是在分类模型的基础上,稍加变动,直接得到分割网络,因此是端到端的,新的网络称为全卷积网络(就是没有全连接层的网络)
就像概述中提到的,FCN做的改变其实不多,主要是两个部分,一个是将全连接层改成卷积操作,另一个就是将最后加了一个反卷积操作(上采样)用于还原回全分辨率的分割图,而全连接之前的特征提取部分,不做变动。
以VGG这个分类网络的改造为例,以下是VGG16的结构:
在我们得到7x7x512的特征图之后,VGG会将它展平成一维向量,然后做一次全连接得到4096维得到数据,然后做一次维度不变的全连接,最后再做一次全连接,得到目标类别的维数,简略版的实现如下:
self.fc1=nn.Linear(512*7*7, 4096)
self.fc2=nn.Linear(4096, 4096)
self.fc3=nn.Linear(4096, num_classes)
然后我们要把三层全连接改成卷积操作,并且保证参数相同,如下:
self.fc1=nn.Conv2D(512,4096,7,1,0)
self.fc2=nn.Conv2D(4096,4096,1,1,0)
self.fc3=nn.Conv2D(4096,self.num_classes,1,1,0)
相应的参数计算:
全连接
fc1:(512*7*7)*4096
fc2:4096*4096
fc3:4096*num_classes
卷积
fc1:7*7*512*4096
fc2:1*1*4096*4096
fc3:1*1*4096*num_classes
上述过程的话,我们完成了从全连接操作到卷积操作的变换,最终得到的输出是 1 ∗ 1 ∗ n u m − c l a s s e s 1*1*num-classes 1∗1∗num−classes,接着我们只需要使用上采样操作,如反卷积,将特诊图的尺寸变为输入尺寸 224 ∗ 224 224*224 224∗224即可。
新的问题出现了:很明显,从
1
∗
1
直接采样到
224
∗
224
1*1直接采样到224*224
1∗1直接采样到224∗224,得到的结果肯定很差。
也就是最终的输出尺寸需要变大一点,我们将第一个卷积操作的卷积核从7改到1(之前卷积核取7是为了实现和全连接层一样的效果),最终可以得到
7
∗
7
∗
n
u
m
−
c
l
a
s
s
e
s
7*7*num-classes
7∗7∗num−classes的输出,然后继续上述操作,得到一个更好一点的分割结果。
继续,下一步怎么提升分割效果呢?得到更大的输出尺寸吗?如果是这么操作,就需要改变特征提取网络了,这个不是希望的。因此,另一个思路出现就是skip-connection。简而言之就是在不断上采样的过程中使用已有信息弥补上采样带来的损失,具体点就是
7
∗
7
∗
n
u
m
−
c
l
a
s
s
e
s
7*7*num-classes
7∗7∗num−classes的输出,不直接上采样到全分辨率,而是上采样两倍,和特征提取网络对应尺度的输出融合,然后持续这个过程。
具体一点的过程如下:
FCN-8s的得到过程是没有skip-connection的,直接将
7
∗
7
∗
n
u
m
−
c
l
a
s
s
e
s
7*7*num-classes
7∗7∗num−classes的输出上采样
FCN-16s的得到过程
FCN-8s的得到过程
几个模型的效果:
import paddle
import paddle.nn as nn
paddle.set_device("cpu")
class FCN(nn.Layer):
def __init__(self, arch,num_classes=2):
super().__init__()
self.num_classes=num_classes
self.in_channels=3
self.conv3_64=self.make_layer(64,arch[0])
self.conv3_128=self.make_layer(128,arch[1])
self.conv3_256=self.make_layer(256,arch[2])
self.conv3_512a=self.make_layer(512,arch[3])
self.conv3_512b=self.make_layer(512,arch[4])
self.pool=nn.MaxPool2D(2)
# 以下是变动
self.fc1=nn.Sequential(
nn.Conv2D(512,4096,7,1,0),
nn.BatchNorm2D(4096),
nn.ReLU()
)
self.fc2=nn.Sequential(
nn.Conv2D(4096,4096,1,1,0),
nn.BatchNorm2D(4096),
nn.ReLU()
)
self.up=nn.UpsamplingBilinear2D(scale_factor=2)
self.fc3_conv=nn.Conv2D(4096,self.num_classes,1,1,0)
self.s32=nn.UpsamplingBilinear2D(scale_factor=32)
self.down4_conv=nn.Conv2D(512,self.num_classes,1,1,0)
self.s16=nn.UpsamplingBilinear2D(scale_factor=16)
self.down3_conv=nn.Conv2D(256,self.num_classes,1,1,0)
self.s8=nn.UpsamplingBilinear2D(scale_factor=8)
def make_layer(self,channels,nums):
layers=[]
for i in range(nums):
layers.append(nn.Conv2D(self.in_channels,channels,3,1,1))
layers.append(nn.BatchNorm2D(channels))
layers.append(nn.ReLU())
self.in_channels=channels
return nn.Sequential(*layers)
def forward(self,x):
# --------------
# 骨干网络
# --------------
# x:[n,3,224,224]
x=self.conv3_64(x) # x:[n,3,224,224]->[n,64,224,224]
down1=self.pool(x)
x=self.conv3_128(down1) # x:[n,64,224,224]->[n,128,112,112]
down2=self.pool(x)
x=self.conv3_256(down2) # x:[n,128,112,112]->[n,256,56,56]
down3=self.pool(x)
x=self.conv3_512a(down3) # x:[n,256,56,56]->[n,512,28,28]
down4=self.pool(x)
x=self.conv3_512b(down4) # x:[n,512,28,28]->[n,512,14,14]
down5=self.pool(x) # x:[n,512,14,14]->[n,512,7,7]
# --------------
# FCN-32s
# --------------
down5=self.fc1(down5)
down5=self.fc2(down5)
down5=self.fc3_conv(down5)
s32=self.s32(down5)
# --------------
# FCN-16s
# --------------
down4=self.down4_conv(down4)
down4=down4+self.up(down5)
s16=self.s16(down4)
# --------------
# FCN-8s
# --------------
down3=self.down3_conv(down3)
down3=down3+self.up(down4)
s8=self.s8(down3)
return s32,s16,s8
def main():
x=paddle.randn(shape=[1,3,224,224])
fcn=FCN([2, 2, 3, 3, 3])
s32,s16,s8=fcn(x)
print(s32.shape,s16.shape,s8.shape)
if __name__ == '__main__':
main()