原论文链接:SegNet
ResNet传送门:Resnet-cifar10
Inceptionv3传送门:inceptionv3
逐像素的semantic segmentation是目前比较活跃的一个研究热点。在深度网络出现之前,效果比较好的方法有随机数,boosting等。而目前比较热门的语义分割结构有:U-Net、SegNet、DeepLab、FCN、ENet、LinkNet等等。这篇文章主要介绍SegNet的结构和tensorflow实现以及对应的CamVid数据集的使用方法。
SetNet是由Vijay、Alex等人发表在IEEE上的一种deep convolutional encoder-decoder 结构的图像分割方法。该分割的核心训练部分包含一个encoder network,一个相对应的decoder network,最后是一个逐像素的分类层。其中encoder network使用的是VGG16中的前13层卷积网络结构,只是添加了少许改动。decoder network的作用在于将原图像经由encoder network计算出的feature maps从低分辨率映射到和原图尺寸一致的分辨率以便于做逐像素的分类处理。而SegNet的创新就在于其decoder net对低分辨率的feature map(s)做上采样(upsample)的一个设计。其方法是,在encoder的每一个max-pooling过程中保存其池化索引(最大值的index),在decoder层使用这些得到的索引来做非线性上采样。这些经过上采样的特征图是稀疏的,再对其做可训练的卷积操作产生密集feature maps,最后将其送入multi-class softmax分类器中进行分类。 原文中作者将其与FCN、Deeplab、DeconvNet做了一个比较,有兴趣的可以看看论文。
下图为SegNet的结构图,从图来看,我们可以将其做成end-to-end的结构。
Encoder network
其encoder部分使用了VGG16的前13层卷积结构。即conv1_1-conv1_2-pool-conv2_1-conv2_2-pool-conv3_1-conv3_2-conv3_3-pool-conv4_1-conv4_2-conv4_3-pool的结构。每个conv层包含convolution+Batch normalization+ReLU操作。pool层采用2X2的窗口,以及stride 2的步长。每次pool层相当于对图像做一个分辨率减少一半的降采样。并在每次maxpool的过程中,将feature maps中的每个池化窗口中的最大值的位置记录下来。简而言之,你可以认为这就是一个VGG net,不过多了一步记录maxpool 索引的操作。事实上,大部分分割结构都使用相同的encoder 结构,其主要变化在于decoder network。
Decoder network
在经过卷积池化得到输入图像的feature maps之后,使用记录下来的最大池化的索引来对其做上采样处理。如下图所示,左图为SegNet的上采样过程,右图为FCN的上采用过程。
这么做会产生稀疏的feature maps。再对执行卷积操作产生密集feature maps。值得注意的是,在与encoder中第一层对应的decoder层中(即decoder的最后一层卷积),与原图像为RGB的3通道不同,该层产生的是一个通道为K(类别数)的multi-channel feature maps,然后将其送入softmax分类器,做逐像素的分类处理。
CamVid Road Scenes Dataset
CamVid road scenes dataset 是一个小型数据集,其中包含有367训练样本,233涨测试样本,均为360X480分辨率的RGB图像。其标签中包含有11个类别。在对数据集进行训练之前,先对其做local contrast normalization 处理,其方法可以参考这篇博文:local response normalization。
训练过程
encoder和decoder的权值初始化均采用
Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification中介绍的方法。采用SGD方法进行训练,learing rate 为0.1 以及0.9的momentum。并对训练集做shuffle处理,使用softmax cross-entropy loss,loss 计算一个mini-batch中的所有像素的单个loss之和。并添加了 median frequency balancing方法优化训练过程, 该方法即是对每一个类别添加一个权值,对于物体较大的类别(车,路,天空)设置一个较小的权值,对于物体较小的类别,设置一个较大的权值。
结果及代码使用
先上结果图,第一张图为测试集中的最后一张图的分割图(别人训练好的),后两张为我自己迭代6000次(GTX1066,约1个小时),再用来测试的测试集的第一张图。
代码中实现的是论文中所提及的SegNet-base,即4个encoder,4个decoder。
def inference(images, labels, batch_size, phase_train):
# norm1
norm1 = tf.nn.lrn(images, depth_radius=5, bias=1.0, alpha=0.0001, beta=0.75, #local response normalization.
name='norm1')
# conv1
conv1 = conv_layer_with_bn(norm1, [7, 7, images.get_shape().as_list()[3], 64], phase_train, name="conv1")
# pool1
pool1, pool1_indices = tf.nn.max_pool_with_argmax(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1],
padding='SAME', name='pool1')
# conv2
conv2 = conv_layer_with_bn(pool1, [7, 7, 64, 64], phase_train, name="conv2")
# pool2
pool2, pool2_indices = tf.nn.max_pool_with_argmax(conv2, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool2')
# conv3
conv3 = conv_layer_with_bn(pool2, [7, 7, 64, 64], phase_train, name="conv3")
# pool3
pool3, pool3_indices = tf.nn.max_pool_with_argmax(conv3, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool3')
# conv4
conv4 = conv_layer_with_bn(pool3, [7, 7, 64, 64], phase_train, name="conv4")
# pool4
pool4, pool4_indices = tf.nn.max_pool_with_argmax(conv4, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool4')
""" End of encoder """
""" start upsample """
# upsample4
# Need to change when using different dataset out_w, out_h
# upsample4 = upsample_with_pool_indices(pool4, pool4_indices, pool4.get_shape(), out_w=45, out_h=60, scale=2, name='upsample4')
upsample4 = deconv_layer(pool4, [2, 2, 64, 64], [batch_size, 45, 60, 64], 2, "up4")
# decode 4
conv_decode4 = conv_layer_with_bn(upsample4, [7, 7, 64, 64], phase_train, False, name="conv_decode4")
# upsample 3
# upsample3 = upsample_with_pool_indices(conv_decode4, pool3_indices, conv_decode4.get_shape(), scale=2, name='upsample3')
upsample3= deconv_layer(conv_decode4, [2, 2, 64, 64], [batch_size, 90, 120, 64], 2, "up3")
# decode 3
conv_decode3 = conv_layer_with_bn(upsample3, [7, 7, 64, 64], phase_train, False, name="conv_decode3")
# upsample2
# upsample2 = upsample_with_pool_indices(conv_decode3, pool2_indices, conv_decode3.get_shape(), scale=2, name='upsample2')
upsample2= deconv_layer(conv_decode3, [2, 2, 64, 64], [batch_size, 180, 240, 64], 2, "up2")
# decode 2
conv_decode2 = conv_layer_with_bn(upsample2, [7, 7, 64, 64], phase_train, False, name="conv_decode2")
# upsample1
# upsample1 = upsample_with_pool_indices(conv_decode2, pool1_indices, conv_decode2.get_shape(), scale=2, name='upsample1')
upsample1= deconv_layer(conv_decode2, [2, 2, 64, 64], [batch_size, 360, 480, 64], 2, "up1")
# decode4
conv_decode1 = conv_layer_with_bn(upsample1, [7, 7, 64, 64], phase_train, False, name="conv_decode1")
""" end of Decode """
""" Start Classify """
# output predicted class number (6)
with tf.variable_scope('conv_classifier') as scope:
kernel = _variable_with_weight_decay('weights',
shape=[1, 1, 64, NUM_CLASSES],
initializer=msra_initializer(1, 64),
wd=0.0005)
conv = tf.nn.conv2d(conv_decode1, kernel, [1, 1, 1, 1], padding='SAME')
biases = _variable_on_cpu('biases', [NUM_CLASSES], tf.constant_initializer(0.0))
conv_classifier = tf.nn.bias_add(conv, biases, name=scope.name)
logit = conv_classifier
loss = cal_loss(conv_classifier, labels)
return loss, logit
这是SegNet-base的网络结构,其为自定义函数,详情可以自己阅读代码。
该代码转载自github,使用前现在main.py文件中修改运行参数,如image_dir,test_dir,_val_dir,以及log_dir和max_steps。然后开始训练,训练完后可以使用tensorboard查看训练记录。再之后可以修改tesing或finetune的参数(该参数为载入训练好的模型,可以是你的log_dir,也可以将Log_dir里的模型复制到指定文件目录下),进行测试或微调。
下载地址:
代码及数据集下载