在自动驾驶、医学图像、目标检测领域,语义分割发挥着巨大的作用。相比于yolo、ssd等目标检测算法,Unet可以实现对图像中每个像素点的分类,精度大大提升。
语义分割,简单来说就是给出一张图,分割出图像中所需物体的一个完整准确的轮廓,其实也就相当于现实中的“抠图”。但这里“抠图”的难度在于,不是由人来抠,而是让机器学会自动帮我们抠。并且要求“抠图”的像素点要很精确,这个是人眼达不到的。
原始数据集有两个文件: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
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个55卷积核,步数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个55卷积核,步数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个55卷积核,步数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个33卷积核,步数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个33卷积核,步数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个33卷积核,步数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激励层。
自己训练了200轮,大概花费了10h,最后accuracy达到0.987左右。
用不同颜色对不同类别的像素点进行表示:
调用训练好的模型进行语义分割,效果如下:
思考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整除,这样后面反卷积还原时就非常方便。而填充的那部分,最后再裁减掉即可。
数据集加工
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)
如果代码跑不通,或者想直接使用训练好的模型,可以去下载项目链接:
https://blog.csdn.net/Twilight737