语义分割的里程碑式模型FCN于2015年出现,一举将语义分割的准确率提高近20%个点,MIOU也有极大改善,FCN的表现远超传统模型。FCN叫做全卷积网络,顾名思义网络的各个层都是卷积层,即不再使用全连接层。这种方式使FCN很好地保存了特征的空间信息。
在传统的卷积网络中,一层一层的卷积核池化使特征维度不断降低,而语义分割最后是要得到和原图同尺寸的分割图。FCN的做法是使用上采样提高分辨率。而对于前面卷积层不断丢失的信息,FCN的做法是使用跳接结构,以此来保留特征。
关于FCN的具体介绍,请移步这里。本文主要是FCN的代码实现
博主同样实现了keras搭建unet网络,与本文使用的是相同的方法,具体请移步这里
基于猫狗数据的代码实现:
数据准备:
本文用的是猫狗数据集,images里面存放的是对应的图片(3通道),annotation存放的是语义分割的标签(1通道)。
数据的读取、转换和打乱操作:
def read_jpg(path):
img=tf.io.read_file(path)
img=tf.image.decode_jpeg(img,channels=3)
return img
def read_png(path):
img=tf.io.read_file(path)
img=tf.image.decode_png(img,channels=1)
return img
#现在编写归一化的函数
def normal_img(input_images,input_anno):
input_images=tf.cast(input_images,tf.float32)
input_images=input_images/127.5-1
input_anno-=1
return input_images,input_anno
#加载函数
def load_images(input_images_path,input_anno_path):
input_image=read_jpg(input_images_path)
input_anno=read_png(input_anno_path)
input_image=tf.image.resize(input_image,(256,256))
input_anno=tf.image.resize(input_anno,(256,256))
return normal_img(input_image,input_anno)
# 读取图像和目标图像
images=glob.glob(r"images\*.jpg")
anno=glob.glob(r"annotations\trimaps\*.png")
#现在对读取进来的数据进行制作batch
np.random.seed(1)
index=np.random.permutation(len(images)) # 随机打乱7390个数
images = np.array(images)[index]
anno = np.array(anno)[index]
#创建dataset
dataset=tf.data.Dataset.from_tensor_slices((images,anno))
# 测试数据量和训练数据量,20%测试。
test_count=int(len(images) * 0.2)
train_count=len(images) - test_count
# 取出训练数据和测试数据
data_train=dataset.skip(test_count) # 跳过前test的数据
data_test=dataset.take(test_count) # 取前test的数据
data_train=data_train.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE)
data_test=data_test.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE)
#现在开始batch的制作,不制作batch会使维度由四维将为3维
BATCH_SIZE = 8
data_train=data_train.shuffle(100).batch(BATCH_SIZE)
data_test=data_test.batch(BATCH_SIZE)
经过了batch,我们的数据就是4维的了,可以送进模型了。
fcn模型:
本文使用了vgg16作为基础模型,也可以选择resnet等其他模型。fcn因为中间的图片维度非常小,经过一层层上采样再还原成原尺寸,会丢失许多信息,所以采用了跳级操作,具体看实现方法:
def fcn():
conv_base = tf.keras.applications.VGG16(weights='imagenet',
input_shape=(256, 256, 3),
include_top=False)
conv_base.summary()
# 现在我们就可以拿到block5_conv3的输出,进行跳级连接了,连接之后再上采样
sub_model = tf.keras.models.Model(inputs=conv_base.input,
outputs=conv_base.get_layer('block5_conv3').output)
sub_model.summary()
# 现在创建多输出模型,三个output
layer_names = [
'block5_conv3',
'block4_conv3',
'block3_conv3',
'block5_pool'
]
# 得到这几个曾输出的列表,为了方便就直接使用列表推倒式了
layers_output = [conv_base.get_layer(layer_name).output for layer_name in layer_names]
# 创建一个多输出模型,这样一张图片经过这个网络之后,就会有多个输出值了
multiout_model = tf.keras.models.Model(inputs=conv_base.input,
outputs=layers_output)
multiout_model.summary()
multiout_model.trainable = False
inputs = tf.keras.layers.Input(shape=(256, 256, 3))
# 这个多输出模型会输出多个值,因此前面用多个参数来接受即可。
out_block5_conv3, out_block4_conv3, out_block3_conv3, out = multiout_model(inputs)
# 现在将最后一层输出的结果进行上采样,然后分别和中间层多输出的结果进行相加,实现跳级连接
# 这里表示有512个卷积核,filter的大小是3*3
x1 = tf.keras.layers.Conv2DTranspose(512, 3,
strides=2,
padding='same',
activation='relu')(out)
# 上采样之后再加上一层卷积来提取特征
x1 = tf.keras.layers.Conv2D(512, 3, padding='same',
activation='relu')(x1)
# 与多输出结果的倒数第二层进行相加,shape不变
x2 = tf.add(x1, out_block5_conv3)
# x2进行上采样
x2 = tf.keras.layers.Conv2DTranspose(512, 3,
strides=2,
padding='same',
activation='relu')(x2)
# 直接拿到x3,不使用
x3 = tf.add(x2, out_block4_conv3)
# x3进行上采样
x3 = tf.keras.layers.Conv2DTranspose(256, 3,
strides=2,
padding='same',
activation='relu')(x3)
# 增加卷积提取特征
x3 = tf.keras.layers.Conv2D(256, 3, padding='same', activation='relu')(x3)
x4 = tf.add(x3, out_block3_conv3)
# x4还需要再次进行上采样,得到和原图一样大小的图片,再进行分类
x5 = tf.keras.layers.Conv2DTranspose(128, 3,
strides=2,
padding='same',
activation='relu')(x4)
# 继续进行卷积提取特征
x5 = tf.keras.layers.Conv2D(128, 3, padding='same', activation='relu')(x5)
# 最后一步,图像还原
preditcion = tf.keras.layers.Conv2DTranspose(3, 3,
strides=2,
padding='same',
activation='softmax')(x5)
model = tf.keras.models.Model(
inputs=inputs,
outputs=preditcion
)
model.summary() # 因为有跳级结构,因此这个函数不能够进行很好的表示
return model
看一下模型输出:
在代码中我将输入调整成了256。当然,你也可以根据自己的爱好随意调整它。
原始的vgg模型
整理后的FCN:
训练
将数据送进模型,编译,训练,保存模型。
model = fcn()
# 编译
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['acc']#这个参数应该是用来打印正确率用的,现在终于理解啦啊
)
# 可视化
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="E:\jupyter\log2", histogram_freq=1)
# 训练
model.fit(data_train,
epochs=1,
batch_size = 8,
validation_data=data_test,
callbacks = [tensorboard_callback])
# 保存模型
model.save('FCN_model.h5')
#加载保存的模型
new_model=tf.keras.models.load_model('FCN_model.h5')
#查看模型的架构:
new_model.summary()
训练结果
tensorboard:
不好意思,只训练了一个epoch。
全部代码:
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
import os
import glob
from tensorflow.compat.v1 import ConfigProto
from tensorflow.compat.v1 import InteractiveSession
# 调整显存使用情况,避免显存占满
config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)
def read_jpg(path):
img=tf.io.read_file(path)
img=tf.image.decode_jpeg(img,channels=3)
return img
def read_png(path):
img=tf.io.read_file(path)
img=tf.image.decode_png(img,channels=1)
return img
# 归一化的函数
def normal_img(input_images,input_anno):
input_images=tf.cast(input_images,tf.float32)
input_images=input_images/127.5-1
input_anno-=1
return input_images,input_anno
# 加载函数
def load_images(input_images_path,input_anno_path):
input_image=read_jpg(input_images_path)
input_anno=read_png(input_anno_path)
input_image=tf.image.resize(input_image,(256,256))
input_anno=tf.image.resize(input_anno,(256,256))
return normal_img(input_image,input_anno)
def fcn():
conv_base = tf.keras.applications.VGG16(weights='imagenet',
input_shape=(256, 256, 3),
include_top=False)
conv_base.summary()
# 现在我们就可以拿到block5_conv3的输出,进行跳级连接了,连接之后再上采样
sub_model = tf.keras.models.Model(inputs=conv_base.input,
outputs=conv_base.get_layer('block5_conv3').output)
sub_model.summary()
# 现在创建多输出模型,三个output
layer_names = [
'block5_conv3',
'block4_conv3',
'block3_conv3',
'block5_pool'
]
# 得到这几个曾输出的列表,为了方便就直接使用列表推倒式了
layers_output = [conv_base.get_layer(layer_name).output for layer_name in layer_names]
# 创建一个多输出模型,这样一张图片经过这个网络之后,就会有多个输出值了
multiout_model = tf.keras.models.Model(inputs=conv_base.input,
outputs=layers_output)
multiout_model.summary()
multiout_model.trainable = False
inputs = tf.keras.layers.Input(shape=(256, 256, 3))
# 这个多输出模型会输出多个值,因此前面用多个参数来接受即可。
out_block5_conv3, out_block4_conv3, out_block3_conv3, out = multiout_model(inputs)
# 现在将最后一层输出的结果进行上采样,然后分别和中间层多输出的结果进行相加,实现跳级连接
# 这里表示有512个卷积核,filter的大小是3*3
x1 = tf.keras.layers.Conv2DTranspose(512, 3,
strides=2,
padding='same',
activation='relu')(out)
# 上采样之后再加上一层卷积来提取特征
x1 = tf.keras.layers.Conv2D(512, 3, padding='same',
activation='relu')(x1)
# 与多输出结果的倒数第二层进行相加,shape不变
x2 = tf.add(x1, out_block5_conv3)
# x2进行上采样
x2 = tf.keras.layers.Conv2DTranspose(512, 3,
strides=2,
padding='same',
activation='relu')(x2)
# 直接拿到x3,不使用
x3 = tf.add(x2, out_block4_conv3)
# x3进行上采样
x3 = tf.keras.layers.Conv2DTranspose(256, 3,
strides=2,
padding='same',
activation='relu')(x3)
# 增加卷积提取特征
x3 = tf.keras.layers.Conv2D(256, 3, padding='same', activation='relu')(x3)
x4 = tf.add(x3, out_block3_conv3)
# x4还需要再次进行上采样,得到和原图一样大小的图片,再进行分类
x5 = tf.keras.layers.Conv2DTranspose(128, 3,
strides=2,
padding='same',
activation='relu')(x4)
# 继续进行卷积提取特征
x5 = tf.keras.layers.Conv2D(128, 3, padding='same', activation='relu')(x5)
# 最后一步,图像还原
preditcion = tf.keras.layers.Conv2DTranspose(3, 3,
strides=2,
padding='same',
activation='softmax')(x5)
model = tf.keras.models.Model(
inputs=inputs,
outputs=preditcion
)
model.summary() # 因为有跳级结构,因此这个函数不能够进行很好的表示
return model
# 读取图像和目标图像
images=glob.glob(r"images\*.jpg")
anno=glob.glob(r"annotations\trimaps\*.png")
#现在对读取进来的数据进行制作batch
np.random.seed(1)
index=np.random.permutation(len(images)) # 随机打乱7390个数
images = np.array(images)[index]
anno = np.array(anno)[index]
#创建dataset
dataset=tf.data.Dataset.from_tensor_slices((images,anno))
# 测试数据量和训练数据量,20%测试。
test_count=int(len(images) * 0.2)
train_count=len(images) - test_count
# 取出训练数据和测试数据
data_train=dataset.skip(test_count) # 跳过前test的数据
data_test=dataset.take(test_count) # 取前test的数据
data_train=data_train.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE)
data_test=data_test.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE)
#现在开始batch的制作,不制作batch会使维度由四维将为3维
BATCH_SIZE = 8
data_train=data_train.shuffle(100).batch(BATCH_SIZE)
data_test=data_test.batch(BATCH_SIZE)
model = fcn()
# 编译
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['acc']#这个参数应该是用来打印正确率用的,现在终于理解啦啊
)
# 可视化
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="E:\jupyter\log2", histogram_freq=1)
# 训练
model.fit(data_train,
epochs=1,
batch_size = 8,
validation_data=data_test,
callbacks = [tensorboard_callback])
# 保存模型
model.save('FCN_model.h5')
#加载保存的模型
new_model=tf.keras.models.load_model('FCN_model.h5')
#查看模型的架构:
new_model.summary()