Keras-Unet-语义分割

Keras-U-net-语义分割

在自动驾驶、医学图像、目标检测领域,语义分割发挥着巨大的作用。相比于yolo、ssd等目标检测算法,Unet可以实现对图像中每个像素点的分类,精度大大提升。

在这里插入图片描述

语义分割,简单来说就是给出一张图,分割出图像中所需物体的一个完整准确的轮廓,其实也就相当于现实中的“抠图”。但这里“抠图”的难度在于,不是由人来抠,而是让机器学会自动帮我们抠。并且要求“抠图”的像素点要很精确,这个是人眼达不到的。



1、原始数据集

原始数据集有两个文件:combined.npy、segmented.npy。

combined.npy是一个(5000, 64, 84)的数组,表示5000个样本,每个样本是一张(64, 84)的灰度图片。图片中分别含有0-9的数字,包括背景颜色共11个类别。

segmented.npy是一个(5000, 64, 84)的数组,表示5000个样本的类别标记信息。每个(64, 84)的数组中含有数字0-10,其中数字0-9表示0-9对应的像素点,数字10表示背景信息对应的像素点。

对原始数据集进行加工,制作训练数据集train_x, train_y,测试集test_x, test_y。

将combined.npy中的数组,除以255归一化,前4900个样本作为train_x,后100个样本作为test_x。

将segmented.npy中的数组,加工成(5000, 64, 84, 11)形式。每个样本(64, 84)的类别标记图映射成(64, 84, 11)的类别标记长方体。长方体的每一个面都由0、1组成,1表示该像素位置含有对应的这个类别。前4900个样本作为train_y,后100个样本作为test_y。

train_x : 4900, 64, 84
train_y : 4900, 64, 84, 11
test_x : 100, 64, 84
test_y : 100, 64, 84, 11

2、U-net网络细节

U-net网络结构如下:
在这里插入图片描述

U-net网络主要分为三大块。前1/3是图中左边部分的特征提取,feature map不断卷积池化,尺寸不断减小。中间1/3是图中下面部分的卷积过渡,最小尺寸的feature map进行一些卷积变换。后1/3是图中右边部分的上采样,feature map不断反卷积,特征融合,尺寸不断扩大。由于此网络整体结构类似于大写的英文字母U,故得名U-net。

由于本例中输入图像的尺寸是(64, 84),我们对原论文U-net网络稍作调整。

初始加工网络层:
第1层:输入层,Input(64, 84, 1)。
第2层:padding层,将(64, 84, 1)尺寸填充成(64, 96, 1)。

第一轮卷积池化,尺寸放缩为1/2:
第3层:32个55卷积核,步数1,padding=‘same’。
第4层:LeakyRelu激励。
第5层:32个5
5卷积核,步数1,padding=‘same’。
第6层:最大池化,pool_size=3,strides=2。
第7层:LeakyRelu激励。
第8层:标准化归一层,BatchNormalization。

第二轮卷积池化,尺寸放缩为1/4:
第9层:64个55卷积核,步数1,padding=‘same’。
第10层:LeakyRelu激励。
第11层:64个5
5卷积核,步数1,padding=‘same’。
第12层:最大池化,pool_size=3,strides=2。
第13层:LeakyRelu激励。
第14层:标准化归一层,BatchNormalization。

第三轮卷积池化,尺寸放缩为1/8:
第15层:128个55卷积核,步数1,padding=‘same’。
第16层:LeakyRelu激励。
第17层:128个5
5卷积核,步数1,padding=‘same’。
第18层:最大池化,pool_size=3,strides=2。
第19层:LeakyRelu激励。
第20层:标准化归一层,BatchNormalization。

第四轮卷积池化,尺寸放缩为1/16:
第21层:128个33卷积核,步数1,padding=‘same’。
第22层:LeakyRelu激励。
第23层:128个3
3卷积核,步数1,padding=‘same’。
第24层:最大池化,pool_size=3,strides=2。
第25层:LeakyRelu激励。
第26层:标准化归一层,BatchNormalization。

第五轮卷积池化,尺寸放缩为1/32:
第27层:128个33卷积核,步数1,padding=‘same’。
第28层:LeakyRelu激励。
第29层:128个3
3卷积核,步数1,padding=‘same’。
第30层:最大池化,pool_size=3,strides=2。
第31层:LeakyRelu激励。
第32层:标准化归一层,BatchNormalization。

中间卷积过渡:
第33层:128个33卷积核,步数1,padding=‘same’。
第34层:LeakyRelu激励。
第35层:标准化归一层,BatchNormalization。
第36层:128个3
3卷积核,步数1,padding=‘same’。
第37层:LeakyRelu激励。
第38层:标准化归一层,BatchNormalization。

第一轮反卷积,特征融合,尺寸放大为1/16:
第39层:128个5*5反卷积核,步数2,padding=‘same’。
第40层:与第26层融合,concatenate。
第41层:LeakyRelu激励。
第42层:标准化归一层,BatchNormalization。

第二轮反卷积,特征融合,尺寸放大为1/8:
第43层:128个5*5反卷积核,步数2,padding=‘same’。
第44层:与第20层融合,concatenate。
第45层:LeakyRelu激励。
第46层:标准化归一层,BatchNormalization。

第三轮反卷积,特征融合,尺寸放大为1/4:
第47层:64个5*5反卷积核,步数2,padding=‘same’。
第48层:与第14层融合,concatenate。
第49层:LeakyRelu激励。
第50层:标准化归一层,BatchNormalization。

第四轮反卷积,特征融合,尺寸放大为1/2:
第51层:64个5*5反卷积核,步数2,padding=‘same’。
第52层:与第8层融合,concatenate。
第53层:LeakyRelu激励。
第54层:标准化归一层,BatchNormalization。

第五轮反卷积,改变通道数,尺寸放大为1:
第55层:N_CLASSES=11个5*5反卷积核,步数2,padding=‘same’。
第56层:LeakyRelu激励。
第57层:标准化归一层,BatchNormalization。

最后裁减加工网络层:
第58层:N_CLASSES=11个5*5反卷积核,步数1,padding=‘same’。
第59层:裁减层,Cropping2D,将(64, 96, 1)尺寸裁减成(64, 84, 1)。
第60层:outputs = softmax激励层。

3、模型求解结果

自己训练了200轮,大概花费了10h,最后accuracy达到0.987左右。
在这里插入图片描述用不同颜色对不同类别的像素点进行表示:
在这里插入图片描述

调用训练好的模型进行语义分割,效果如下:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
结果还是比较让人满意的。

4、对模型的一些深入思考

思考1:对于彩色rgb图像,训练时到底要不要转化为灰度图,还是直接利用3通道的卷积核?对于0-255像素矩阵,到底要不要除以255转化到0-1之间?

一般还是不用rgb图,改用灰度图像。梯度信息对于物体识别来说很重要,而大多数rgb提供的信息很少,所以反而用灰度图像处理效果更好。我们识别物体最关键的因素是梯度,很多特征提取方法,SIFT、HOG,本质都是梯度的统计信息,而计算梯度自然就用到灰度图像了。而颜色本身,非常容易受到光照等因素的影响,而且同类的物体颜色有很多变化,所以颜色本身难以提供关键信息。

一般用CNN做图像处理时,推荐将0-255的像素值转化为0.0-1.0范围内的实数。

思考2:如何理解1*1卷积可以增减通道数?

如果(1, 1)卷积核的输入只是一个平面,那么(1, 1)的卷积核没什么意义。但如果卷积的输入是一个长方体,(1, 1)的卷积核,就是对每个像素点,在不同的channel上进行线性加权,相当于特征信息进行融合,且保留了图片的原有平面结果,调控depth,从而完成升降维的过程。
在这里插入图片描述

既然是对多个feature map进行线性加权,为什么要特意引入卷积这个概念?

(1, 1)的卷积就是多个feature channels线性叠加,只不过这个组合系数恰好可以看成是一个(1, 1)的卷积。这种表示的好处是,完全可以回到模型中其他常见keras框架下,不用定义新的层,直接借助之前的Conv2d函数就可以实现了。

思考3:如何理解前向传播中反卷积计算过程?
在这里插入图片描述在这里插入图片描述在这里插入图片描述
现在我们有向量Y的具体数值,矩阵C的具体数值,该如何反解计算出向量X的值呢?

由线性代数方程解的相关性质可知,矩阵C的秩小于未知变量的个数,向量X具有无穷多解。但借助矩阵论中广义逆的知识可知,我取pinv(C)*Y作为向量X的解,是所有无穷多解中性质较好的一个解,我们就把这个作为算出的X值。

由于正交矩阵的性质,最后可以得到:
在这里插入图片描述

思考4:全卷积网络FCN的亮点是什么?它和U-net有什么区别?

FCN网络主要的亮点在于:
(1)全卷积化。全连接层都变成卷积层,适应任意尺寸输入。
(2)上采样。上采样可以让图像变成更高分辨率,最后输出结果不再映射成数字或向量,而映射成为具有空间结构的矩阵。
(3)跳跃结构(Skip Layer)。如果只利用最后一层的特征图进行上采样,由于特征图太小,我们会损失很多细节。作者提出跳跃结构,将最后一层的特征图(有更富的全局信息)和更浅层的特征图(有更多的局部细节)进行融合。

U-net与FCN的区别在于:

U-net采用了完全不同的特征融合方式,将feature map拼接在一起,形成更厚的特征。而FCN融合时将对应feature map相加,并不形成更厚的特征。

思考4:为什么在代码中,需要对input层先padding填充,再cropping裁减?

我们输入图像的尺寸为(64, 84),84这个数字非常不好,经过不断的池化操作,不断的1/2放缩,会出现奇数,后面反卷积还原起来就非常麻烦。

而如果填充成(64, 84)尺寸,96可以一直用2整除,这样后面反卷积还原时就非常方便。而填充的那部分,最后再裁减掉即可。

5、具体代码

数据集加工

import numpy as np
import cv2


def img_to_cuboid(annotation, p):
    size = annotation.shape
    cuboid = np.zeros((size[0], size[1], size[2], p))

    for i in range(size[0]):
        for j in range(p):
            annotation_img = annotation[i]
            slice_img = np.zeros((size[1], size[2]))
            slice_img[annotation_img == j] = 1
            cuboid[i, :, :, j] = slice_img

    return cuboid


def read_img():
    p = 11
    path1 = '/home/archer/CODE/PF/data/combined.npy'
    path2 = '/home/archer/CODE/PF/data/segmented.npy'
    image = np.load(path1)/255            # (5000, 64, 84)
    annotation = np.load(path2)           # (5000, 64, 84)
    cuboid = img_to_cuboid(annotation, p)

    # cv2.namedWindow("Image")
    # cv2.imshow("Image", image[0])
    # cv2.waitKey(0)

    train_x = image[0:4900, :, :]
    test_x = image[4900:5000, :, :]
    train_y = cuboid[0:4900, :, :, :]
    test_y = cuboid[4900:5000, :, :, :]

    return train_x,  train_y, test_x, test_y


U-net网络搭建

import numpy as np
import keras
import matplotlib.pyplot as plt
from keras.models import load_model
import cv2


def create_network():
    inputs = keras.layers.Input((64, 84, 1))
    pad = keras.layers.ZeroPadding2D(((0, 0), (0, 96 - 84)))(inputs)

    # First extract feature map  1/2
    conv1 = keras.layers.Conv2D(32, kernel_size=5, strides=1, padding='same')(pad)
    lk1 = keras.layers.LeakyReLU()(conv1)
    conv2 = keras.layers.Conv2D(32, kernel_size=5, strides=1, padding='same')(lk1)
    pool1 = keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(conv2)
    lk2 = keras.layers.LeakyReLU()(pool1)
    bn1 = keras.layers.BatchNormalization()(lk2)

    # Second extract feature map  1/4
    conv3 = keras.layers.Conv2D(64, kernel_size=5, strides=1, padding='same')(bn1)
    lk3 = keras.layers.LeakyReLU()(conv3)
    conv4 = keras.layers.Conv2D(64, kernel_size=5, strides=1, padding='same')(lk3)
    pool2 = keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(conv4)
    lk4 = keras.layers.LeakyReLU()(pool2)
    bn2 = keras.layers.BatchNormalization()(lk4)

    # Third extract feature map  1/8
    conv5 = keras.layers.Conv2D(128, kernel_size=5, strides=1, padding='same')(bn2)
    lk5 = keras.layers.LeakyReLU()(conv5)
    conv6 = keras.layers.Conv2D(128, kernel_size=5, strides=1, padding='same')(lk5)
    pool3 = keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(conv6)
    lk6 = keras.layers.LeakyReLU()(pool3)
    bn3 = keras.layers.BatchNormalization()(lk6)

    # Fourth extract feature map  1/16
    conv7 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(bn3)
    lk7 = keras.layers.LeakyReLU()(conv7)
    conv8 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(lk7)
    pool4 = keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(conv8)
    lk8 = keras.layers.LeakyReLU()(pool4)
    bn4 = keras.layers.BatchNormalization()(lk8)

    # Fifth extract feature map  1/32
    conv9 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(bn4)
    lk9 = keras.layers.LeakyReLU()(conv9)
    conv10 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(lk9)
    pool5 = keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(conv10)
    lk10 = keras.layers.LeakyReLU()(pool5)
    bn5 = keras.layers.BatchNormalization()(lk10)

    # Intermediate transition
    conv11 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(bn5)
    lk11 = keras.layers.LeakyReLU()(conv11)
    bn6 = keras.layers.BatchNormalization()(lk11)

    conv12 = keras.layers.Conv2D(128, kernel_size=3, strides=1, padding='same')(bn6)
    lk12 = keras.layers.LeakyReLU()(conv12)
    bn7 = keras.layers.BatchNormalization()(lk12)

    # First Deconvolution and expansion    1/16
    d_conv1 = keras.layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding='same')(bn7)
    merge1 = keras.layers.concatenate([bn4, d_conv1])
    lk13 = keras.layers.LeakyReLU()(merge1)
    bn8 = keras.layers.BatchNormalization()(lk13)

    # Second Deconvolution and expansion    1/8
    d_conv2 = keras.layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding='same')(bn8)
    merge2 = keras.layers.concatenate([bn3, d_conv2])
    lk14 = keras.layers.LeakyReLU()(merge2)
    bn9 = keras.layers.BatchNormalization()(lk14)

    # Third Deconvolution and expansion    1/4
    d_conv3 = keras.layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding='same')(bn9)
    merge3 = keras.layers.concatenate([bn2, d_conv3])
    lk15 = keras.layers.LeakyReLU()(merge3)
    bn10 = keras.layers.BatchNormalization()(lk15)

    # Fourth Deconvolution and expansion    1/2
    d_conv4 = keras.layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding='same')(bn10)
    merge4 = keras.layers.concatenate([bn1, d_conv4])
    lk16 = keras.layers.LeakyReLU()(merge4)
    bn11 = keras.layers.BatchNormalization()(lk16)

    # Fifth Deconvolution and expansion    1/1
    d_conv4 = keras.layers.Conv2DTranspose(11, kernel_size=5, strides=2, padding='same')(bn11)
    lk17 = keras.layers.LeakyReLU()(d_conv4)
    bn12 = keras.layers.BatchNormalization()(lk17)

    # Final process and Crop
    d_conv5 = keras.layers.Conv2DTranspose(11, kernel_size=5, strides=1, padding='same')(bn12)
    crop = keras.layers.Cropping2D(((0, 0), (0, 96 - 84)))(d_conv5)
    outputs = keras.layers.Activation('softmax')(crop)

    model = keras.models.Model(inputs=inputs, outputs=outputs)
    model.summary()

    return model


# batch generator: reduce the consumption of computer memory
def generator(train_x, train_y, batch_size):

    while 1:
        row = np.random.randint(0, len(train_x), size=batch_size)
        x = train_x[row]
        y = train_y[row]
        yield x, y


# create model and train and save
def train_network(train_x, train_y, test_x, test_y, epoch, batch_size):
    train_x = train_x[:, :, :, np.newaxis]
    test_x = test_x[:, :, :, np.newaxis]

    model = create_network()
    model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy'])

    model.fit_generator(generator(train_x, train_y, batch_size), epochs=epoch,
                        steps_per_epoch=len(train_x) // batch_size)

    score = model.evaluate(test_x, test_y, verbose=0)
    print('first_model test accuracy:', score[1])

    model.save('first_model.h5')


# Load the partially trained model and continue training and save
def load_network_then_train(train_x, train_y, test_x, test_y, epoch, batch_size, input_name, output_name):
    train_x = train_x[:, :, :, np.newaxis]
    test_x = test_x[:, :, :, np.newaxis]

    model = load_model(input_name)
    history = model.fit_generator(generator(train_x, train_y, batch_size),
                                  epochs=epoch, steps_per_epoch=len(train_x) // batch_size)

    score = model.evaluate(test_x, test_y, verbose=0)
    print(output_name, 'test accuracy:', score[1])

    model.save(output_name)
    show_plot(history)


# plot the loss and the accuracy
def show_plot(history):
    # list all data in history
    print(history.history.keys())

    plt.plot(history.history['loss'])
    plt.title('model loss')
    plt.ylabel('loss')
    plt.xlabel('epoch')
    plt.savefig('loss1.jpg')
    plt.show()

    plt.plot(history.history['accuracy'])
    plt.title('model accuracy')
    plt.ylabel('accuracy')
    plt.xlabel('epoch')
    plt.savefig('accuracy1.jpg')
    plt.show()


# show the real_img and the  network_training img
def plot_result(test_x, input_name, index):

    model = load_model(input_name)
    test_x = test_x[:, :, :, np.newaxis]
    net_result = model.predict(test_x)

    real_img = test_x[index]
    cv2.namedWindow("Real_Image")
    cv2.imshow("Real_Image", real_img)
    cv2.waitKey(0)
    cv2.imwrite('/home/archer/CODE/PF/real6.png', real_img * 255)

    net_cuboid = net_result[index]
    size = net_cuboid.shape
    mask = np.zeros((size[0], size[1]))
    net_img = np.zeros((size[0], size[1], 3))

    for i in range((size[0])):
        for j in range((size[1])):
            index = np.argmax(net_cuboid[i, j, :])
            mask[i, j] = int(index)

    print('Number in this picture contain :')
    print(np.unique(mask))

    # 0 - purplish red
    # 1 - orange
    # 2 - green
    # 3 - pink
    # 4 - white
    # 5 - gray
    # 6 - yellow
    # 7 - violet
    # 8 - dark blue
    # 9 - black
    # 10 -light blue

    colour = np.array([[255, 0, 255], [0, 0, 255], [0, 255, 0], [255, 192, 203],
                       [225, 255, 255], [155, 155, 155], [0, 255, 255], [120, 0, 128],
                       [255, 0, 0], [0, 0, 0], [255, 255, 0]])

    for i in range(size[0]):
        for j in range(size[1]):
            for k in range(size[2]):
                if mask[i, j] == k:
                    net_img[i, j, :] = colour[k]/255

    cv2.namedWindow("Net_Image1")
    cv2.imshow("Net_Image1", net_img)
    cv2.waitKey(0)
    cv2.imwrite('/home/archer/CODE/PF/network6.png', net_img*255)


主函数调用

import getdata as gt
import network as nt


if __name__ == "__main__":
    train_x, train_y, test_x, test_y = gt.read_img()
    nt.train_network(train_x, train_y, test_x, test_y, epoch=2, batch_size=16)
    nt.load_network_then_train(train_x, train_y, test_x, test_y, epoch=2, batch_size=16,
                               input_name='first_model.h5', output_name='second_model.h5')
    nt.plot_result(test_x, input_name='second_model.h5', index=0)


6、项目链接

如果代码跑不通,或者想直接使用训练好的模型,可以去下载项目链接:
https://blog.csdn.net/Twilight737

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值