前言
最近在学习有关语义分割的知识,关于语义分割可能有的同学不清楚是什么含义,这里简单解释一下。
语义分割属于计算机视觉(Computer Vision,CV)中的一类任务,下面这张图展示了CV中的几大任务:
按照难以程度来看,由易到难依次为图像分类、目标检测、语义分割、实例分割,其中实例分割是语义分割的更进一步,具有挑战性,因为它需要正确检测图像中的目标,还要分出每个实例,而语义分割只要分割出像素类别即可,不需要知道图像中有多少个实例。
这么说并不意味着语义分割就很简单,实际上语义分割也具有挑战性。
常见语义分割模型
FCN系列:FCN-32、FCN-16、FCN-8。其中的32,16,8表示步长,可以理解为特征提取后特征图相对于原图的缩放比例,步长越大,提取的特征就越抽象,丢失的细节也就越多,在上采样时精度就越低。
U-net:U-net是2015年提出的编码器-解码器结构的语义分割模型,主要应用于医学图像,也可以用于一般图像,但是效果就比较一般,这个后面会详细介绍。
Deeplab系列:Deeplab v1、Deeplab v2、Deeplab v3、Deeplab v3+。这几种模型是谷歌开发并依次完善的,其中Deeplab v3+代表了当年语义分割的最高水平。
编码器/解码器(encoder/decoder)结构
下图为U-NET的结构图
U-net之所以被称为U-net,从这张图就可以一眼看出来,模型结构就类似于U形,包含了两个部分:特征提取部分与上采样部分,这种结构也被称为编码器-解码器结构。
与FCN系列不同的是,U-net采用了完全不同的特征融合方式:拼接。U-net采用将特征在channel维度拼接在一起,形成更厚的特征。而FCN融合时使用的对应点相加,并不形成更厚的特征。
U-net模型搭建
使用gkeras可以很容易地搭建U-net。如下:
from keras.models import *
from keras.layers import *
from keras.optimizers import *
IMG_SIZE = 512
def unet(pretrained_weights=None, input_size=(IMG_SIZE, IMG_SIZE, 3),num_class=2):
inputs = Input(input_size)
conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(inputs)
conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool1)
conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool2)
conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv3)
pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool3)
conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv4)
drop4 = Dropout(0.5)(conv4)
pool4 = MaxPooling2D(pool_size=(2, 2))(drop4)
conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool4)
conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv5)
drop5 = Dropout(0.5)(conv5)
up6 = Conv2D(512, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
UpSampling2D(size=(2, 2))(drop5))
merge6 = concatenate([drop4, up6], axis=3)
conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge6)
conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv6)
up7 = Conv2D(256, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
UpSampling2D(size=(2, 2))(conv6))
merge7 = concatenate([conv3, up7], axis=3)
conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge7)
conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv7)
up8 = Conv2D(128, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
UpSampling2D(size=(2, 2))(conv7))
merge8 = concatenate([conv2, up8], axis=3)
conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge8)
conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv8)
up9 = Conv2D(64, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
UpSampling2D(size=(2, 2))(conv8))
merge9 = concatenate([conv1, up9], axis=3)
conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge9)
conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv9)
conv9 = Conv2D(num_class, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv9)
if num_class == 2:
conv10 = Conv2D(3, 1, activation='sigmoid')(conv9)
loss_function = 'binary_crossentropy'
else:
conv10 = Conv2D(num_class, 1, activation='softmax')(conv9)
loss_function = 'categorical_crossentropy'
model = Model(input=inputs, output=conv10)
model.compile(optimizer=Adam(lr=1e-4), loss=loss_function, metrics=["accuracy"])
model.summary()
if (pretrained_weights):
model.load_weights(pretrained_weights)
return model
U-net人像分割
我尝试使用U-net做人像分割,人像数据集地址,其中包含了训练与测试集。
完整的代码在我的github上。
以下是原图与掩膜图的演示(貌似比较肥猪流…):
训练过程
训练过程就不详细说了,其中有很多超参数,大家可以自己尝试一下。
测试结果
测试结果,有效果,但不太理想,放几张图感受一下
后记
U-net毕竟是针对医学图像开发的,医学图像的同一场景变化不大,所以只需要较少的训练数据就可以达到较好的效果,且U-net特征提取不宜太深,否则会丢失很多细节,不便于上采样,所以U-net用于自然图像分割,效果一般,这是我的一些个人理解。
下面打算将Deeplab v3+这个模型学习一下,然后用于对人像分割,据说效果很好…我这就来试试!