语义分割


  • 前言
  • 一、什么是segnet模型
  • 二、segnet模型代码实现
  • 1.主干模型VGGnet
  • 2.segnet模型的Decoder部分
  • 代码测试



前言

语义分割也是图像领域一个重要的研究方向,而且目前应用范围越来越广,而且场景越来越丰富。下面从最简单的部分,来记录自己的学习过程。后续更新语义分割blog均使用斑马线的数据集进行测试。


一、什么是segnet模型

        Segnet模型是一个比较基础的语义分割模型,其结构比较简单,在说其结构之前,我们先讲一下convolutional Encoder-Decoder的结构。其主要结构与自编码(Autoencoder)类似,通过编码解码复原图片上每一个点所属的类别。
下图主要是说明利用卷积层编码与解码的过程。

all in one 语义分割 语义分割segnet_keras

 其主要结构如下图所示

all in one 语义分割 语义分割segnet_深度学习_02

        由结构可以看到,其利用Encoder中提取了多次特征的f4进行处理,利用Decoder进行多次上采样Upsampling2D。最后得到一个具有一定hw的filter数量为n_classes的图层。为什么filter要用n_classes呢,因为其代表的就是每个像素点所属的种类。

        用一句话概括就是 从主干模型中提取出卷积了多次,具有一定特征的层(典型的是hw经过了4次压缩后的层),然后利用UpSampling2D函数进行三次上采样,得到输出层(语句分割的结果)。

二、segnet代码实现

segnet代码主要分为两个部分

1.主干特征提取VGGnet

代码如下:

from tensorflow.keras.layers import *
IMAGE_ORDERING = 'channels_last'


def get_mobilenet_encoder(input_height=416, input_width=416, pretrained='imagenet'):
    img_input = Input(shape=(input_height, input_width, 3))
    # 将height=416 weight=416 大小的图片变为208 208 64
    # 将卷积使用same时,采用使用0填充,计算公式为w/s w为图片的大小,s为步长,所以卷积之后结果不变
    x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(img_input)
    x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
    x = MaxPool2D((2, 2), strides=(2, 2), name='block1_pool')(x)
    f1 = x

    # 208 208 64 ->104 104 128
    x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
    x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
    x = MaxPool2D((2, 2), strides=(2, 2), name='block2_pool1')(x)
    f2 = x

    # 第三次提取特征 128 128 128 -> 52 52 256
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
    x = MaxPool2D((2, 2), strides=(2, 2), name='block3_pool')(x)
    f3 = x
    # 第四次特征提取 64 64 256->28 28 512
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
    x = MaxPool2D((2, 2), strides=(2, 2), name='block4_pool')(x)
    f4 = x
    # 第五次特征提取
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
    x = MaxPool2D((2, 2), strides=(2, 2), name='block5_pool')(x)
    f5 = x
    return img_input, [f1, f2, f3, f4, f5]

2.segnet的Decoder解码部分

这一部分对应着上面segnet模型中的解码部分。
其关键就是把获得的特征重新映射到比较大的图中的每一个像素点,用于每一个像素点的分类。

代码如下:

from tensorflow.keras.models import *
from tensorflow.keras.layers import *

from net.convert import get_mobilenet_encoder

IMAGE_ORDERING = 'channels_last'

# n_up为提取特征的次数,因为索引从0开始,所以这里设置成3
def segnet_decoder(f, n_classes, n_up=3):
    o = f
    # 标记数据来自哪里
    # channels_last对应输入形状(batch, height, width, channels)
    # channels_first对应输入尺寸为(batch, channels, height, width)。
    # 默认为在Keras配置文件~ /.keras / keras.json中的image_data_format值。 如果你从未设置它,将使用 “channels_last”。
    o = (ZeroPadding2D((1, 1), data_format=IMAGE_ORDERING))(o)
    o = (Conv2D(512, (3, 3), padding='valid', data_format=IMAGE_ORDERING))(o)
    o = (BatchNormalization())(o)
    # 进行一次UpSampling2D,此时hw变为原来的1/8(注意:这里原来,是指输入图片的大小也就是416 416)
    # 52,52,512
    o = (UpSampling2D((2, 2), data_format=IMAGE_ORDERING))(o)
    o = (ZeroPadding2D((1, 1), data_format=IMAGE_ORDERING))(o)
    o = (Conv2D(256, (3, 3), padding='valid', data_format=IMAGE_ORDERING))(o)
    o = (BatchNormalization())(o)

    # 进行一次UpSampling2D,此时hw变为原来的1/4
    # 104,104,256
    for _ in range(n_up - 2):
        o = (UpSampling2D((2, 2), data_format=IMAGE_ORDERING))(o)
        o = (ZeroPadding2D((1, 1), data_format=IMAGE_ORDERING))(o)
        o = (Conv2D(128, (3, 3), padding='valid', data_format=IMAGE_ORDERING))(o)
        o = (BatchNormalization())(o)

    # 进行一次UpSampling2D,此时hw变为原来的1/2
    # 208,208,128
    o = (UpSampling2D((2, 2), data_format=IMAGE_ORDERING))(o)
    o = (ZeroPadding2D((1, 1), data_format=IMAGE_ORDERING))(o)
    o = (Conv2D(64, (3, 3), padding='valid', data_format=IMAGE_ORDERING))(o)
    o = (BatchNormalization())(o)

    # 此时输出为h_input/2,w_input/2,nclasses 注意:这个时候必须把通道数调整到和分类类别一致,即通道数即为类别数
    o = Conv2D(n_classes, (3, 3), padding='same', data_format=IMAGE_ORDERING)(o)
    return o


# n_classes类别 encoder编码也就是特征提取
def _segnet(n_classes, encoder, input_height=416, input_width=416, encoder_level=3):
    # encoder通过主干网络
    img_input, levels = encoder(input_height=input_height, input_width=input_width)

    # 获取hw压缩四次后的结果
    feat = levels[encoder_level]

    # 将特征传入segnet网络,通过segnet进行解码,feat提取的特征层,n_classes表示分类类别 n_up3表示第几个特征成
    o = segnet_decoder(feat, n_classes, n_up=3)
    # 将结果进行reshape
    o = Reshape((int(input_height / 2) * int(input_width / 2), -1))(o)
    o = Softmax()(o)
    model = Model(img_input, o)

    return model


# n_classes 代表类别,input_height input_width 图片大小
# encoder_level对应特征提取层,用到四层特征,序号从0开始所以这里是3
def mobilenet_segnet(n_classes, input_height=224, input_width=224, encoder_level=3):
    model = _segnet(n_classes, get_mobilenet_encoder, input_height=input_height, input_width=input_width,
                    encoder_level=encoder_level)
    model.model_name = "mobilenet_segnet"
    return model


if __name__ == '__main__':
    model = mobilenet_segnet(2, input_height=416, input_width=416)
    model.summary()

代码测试

直接运行segnet.py,然后会调用下面代码段

if __name__ == '__main__':
    model = mobilenet_segnet(2, input_height=416, input_width=416)
    model.summary()

正常运行后的结果如下:

all in one 语义分割 语义分割segnet_all in one 语义分割_03

到这里就完成了基于VGGnet模型的segnet的搭建。