0%

分割篇:开山之作 FCN

CV 系列的论文和程序得一点点开坑了。目前准备的计划任务是:FCN,OHEM,Mask RCNN,YOLO,Focal loss,Seesaw loss。别问,问就是网上一点点查阅得到的,然后写写代码。这个系列完结后,大概会结合对抗样本 and 目标检测做一些东西。自己还是差的太远。

什么是语义分割

语义分割的直观解释可以见下图,计算照片中的每一个像素点的类别,进而得到哪些像素点属于同一类,把一些物体给分割出来:

FCN

CNN 能够对图片进行分类,可是怎么样才能识别图片中特定部分的物体,在这篇论文之前,还是一个未解难题。

  • 对于传统的分类网络,经过 CNN 不断卷积、池化的处理,最后进入全连接网络,预测当前图片的分类。但会丢失空间信息,无法预测每个像素的分类。
  • 对于目标检测的网络,也是经过 CNN 不断卷积、池化的处理,在最后的特征图上预测类别和位置。但识别出来的是目标框,并非物体的轮廓边界。

而 FCN 的创新之处在于,使用卷积操作替换了分类网络的全连接,使得输入输出保持在相同尺寸,这样就可以预测每个像素点的类别。加上使用了卷积,自然而然也就可以处理任意尺寸的图像。网络结构如下,用下面的卷积替换上面的全连接:

上采样

经过不断的卷积,图像的尺寸会减少而维度会增加。所以为了使得网络输出的图像尺寸和原图像一致,需要进行一些上采样,使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在与输入图等大小的特征图上对每个像素进行分类,逐像素地用 softmax 分类计算损失,相当于每个像素对应一个训练样本。这部分在论文的第三章有所描述。

而上采样采用的操作是转置卷积,如下图 2 所示,蓝色是输入,青色是输出,将图像的尺寸瞬间增加了一倍。而文中发现,这种形式的上采样是最有效的,且,可以通过叠加之前的输入(类此残差),获得更好的精度。此外文中特意表明了转置卷积也是卷积层,按照普通的卷积层进行训练即可。

融合操作

如上图所示,论文给出了 FCN 的三种版本。

  • 对于 FCN-32s,直接在最后一层进行 32 倍的上采样,原始空间信息倍大量丢失
  • 对于 FCN-16s,将 pool5 后的结果进行 2 倍上采样,与 pool4 的结果相加,得到结果 $F$,而后进行 16 倍上采样
  • 对于 FCN-8s,将 $F$ 与 pool3 后的结果相加,而后进行 8 倍上采样

论文中的结论是,FCN-8s 的效果要好一些,毕竟更多的利用了原始空间信息。网络结构图如下 3

程序

网上看到了份程序,逻辑写的还不错:

https://github.com/pochih/FCN-pytorch/blob/master/python/fcn.py

尺寸在我裁剪图片的时候进行了放缩,不要太在意。

解码

若要可视化展示结果,需要对网络输出的结果进行解码。如标注图片上的类别等。假设输入图像的尺寸是 [800, 800] 的,当前类别数量是 21,会得到 [bacthsize, num_classes, height, width] 的输出。假设当前 batchsize 是 1,那么就需要在 num_classes 张 [height, width] 大小的图片中选择出每个像素点的类别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
out = fcn(data)['out']
# 选择每个像素点的最大的类别
om = torch.argmax(out.squeeze(), dim=0).detach().cpu().numpy()
decode_segmap(om)

def decode_segmap(image, nc=21):
# 类别颜色,画图用
label_colors = np.array([
(0, 0, 0), # 0=background
# 1=aeroplane, 2=bicycle, 3=bird, 4=boat, 5=bottle
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
# 6=bus, 7=car, 8=cat, 9=chair, 10=cow
(0, 128, 128),
(128, 128, 128),
(64, 0, 0),
(192, 0, 0),
(64, 128, 0),
# 11=dining table, 12=dog, 13=horse, 14=motorbike, 15=person
(192, 128, 0),
(64, 0, 128),
(192, 0, 128),
(64, 128, 128),
(192, 128, 128),
# 16=potted plant, 17=sheep, 18=sofa, 19=train, 20=tv/monitor
(0, 64, 0),
(128, 64, 0),
(0, 192, 0),
(128, 192, 0),
(0, 64, 128)
])
r = np.zeros_like(image).astype(np.uint8)
g = np.zeros_like(image).astype(np.uint8)
b = np.zeros_like(image).astype(np.uint8)
for l in range(0, nc):
# 目标类的索引
idx = image == l
r[idx] = label_colors[l, 0]
g[idx] = label_colors[l, 1]
b[idx] = label_colors[l, 2]
rgb = np.stack([r, g, b], axis=2)
plt.imshow(rgb)
plt.savefig('result.png')

延伸

此外,U-Net 5 的网络结构也适合做分割,先记下来,也许某一天做语义分割的任务会用到。

reference

明人不说暗话,如果感觉这篇文章还不错,您的打赏是对我读书路上莫大的支持,当然一切全凭自愿。 实在不行,我,秦始皇,打钱。

欢迎订阅我的文章