Keras深度学习实战(18)——语义分割详解

Posted 盼小辉丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Keras深度学习实战(18)——语义分割详解相关的知识,希望对你有一定的参考价值。

Keras深度学习实战(18)——语义分割详解

0. 前言

《使用 U-Net 架构进行图像分割》一节中,我们学习图像分割的基本概念,并且构建了基于 U-Net 网络模型的图像分割模型,在图像中仅包含一个对象时实现图像分割的方法。在本节中,我们将更进一步学习更复杂的语义分割,以便我们能够区分图像中的多个对象。

1. 语义分割基本概念

语义分割 (Semantic Segmentation) 是为了便于图像分析而为图像中的每个像素分配标签的过程,可以将语义分割认为是一种为场景理解提供支持的高层任务,场景理解不仅是语义分割领域需要解决的重难点问题,更是计算机视觉领域的核心问题之一。
语义分割在包括自动驾驶、行人检测和医学影像分析等领域显示出良好的应用前景。语义分割技术主要包括两类:第一类是传统的语义分割算法,首先需要使用传统的图像处理技术提取图像特征,然后通过为每个像素分类来实现图像分割,此类方法的优点在于操作简单,计算难度较小,但其适用性较低,在有外界因素(例如光照、旋转等)的干扰下分割效果不佳;第二类是基于深度学习的语义分割算法,通过使用深度卷积神经网络提取图像特征,然后对每个像素进行分类,借助卷积神经网络强大的拟合和泛化能力,提取到的图像特征能够适应复杂的外部环境变化,能够实现很好的分割效果。
本节中,我们继续使用 U-Net 架构实现交通图像语义分割模型,区分图像中多中不同类别的对象。

2. 模型与数据集分析

2.1 模型训练流程

在本节中,我们将在道路图像数据集中执行语义分割,在构建模型进行训练之前,我们首先对模型训练流程进行简要介绍:

  • 使用与上节相同的数据集,该数据集中包含原始图像以及对应的带有多种类别对象所在位置的标签模板图像
    • 语义图像数据样本示例如下:

  • 将输出蒙版转换为多维数组,其中通道维度与对象所有可能的类别数相同
  • 在此数据集,由于有 12 个不同的对象,因此需要将输出图像转换成尺寸为 224 x 224 x 12 的图像:
    • 通道的值表示在该图像对应位置中是否存在与该通道相对应的对象,例如存在汽车对象,假设其对应通道索引为 0,则在通道 0 上汽车对应的位置上像素值为 1,其它位置像素值为 0,每个通道对应 12 个不同的对象之一,类似于标签的独热编码
  • 构建模型架构训练具有 12 个通道输出的模型,与构建的真实标记相对应
  • 最后,调整输出值,将其转换为可用于可视化的单通道图像(灰度图像):
    • 使用 np.argmax 提取 12 个通道中最大值所在的索引作为单通道图像对应位置的值,例如通道 2 中对应位置像素值为 1 表示该位置存在树木,则在单通道图像中对应位置像素值为 2,这一步骤是上述获取 12 通道标签输出的逆过程

2.2 模型输出

为了更加直观的了解上述模型输出,我们对比原始带有蒙版的标签图像与处理后的 12 通道图像。可以看到,原始图像为 0 的位置,在 12 通道图像中的第 0 个通道相应位置处的值为 1;原始图像为 3 的位置,在 12 通道图像中的第 3 个通道相应位置处的值为 1

原始图像: [[0 0 0 ... 5 5 5]
 [0 0 0 ... 5 5 5]
 [0 0 0 ... 5 5 5]
 ...
 [3 3 3 ... 3 3 3]
 [3 3 3 ... 3 3 3]
 [3 3 3 ... 3 3 3]]
第0通道: [[1. 1. 1. ... 0. 0. 0.]
 [1. 1. 1. ... 0. 0. 0.]
 [1. 1. 1. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
第3通道: [[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [1. 1. 1. ... 1. 1. 1.]
 [1. 1. 1. ... 1. 1. 1.]
 [1. 1. 1. ... 1. 1. 1.]]

接下来,我们实现上述语义分割模型,所用的数据集与在《使用 U-Net 架构进行图像分割》一节中使用的数据集相同。

3. 实现语义分割模型

3.1 加载数据集

(1) 导入相关库,并读取数据集:

from glob import glob
import os
import numpy as np
import matplotlib.pyplot as plt

dir_data = 'dataset1'
dir_seg = dir_data + '/annotations_prepped_train/'
dir_img = dir_data + '/images_prepped_train/'
all_img_paths = glob(os.path.join(dir_img, '*.png'))
all_mask_paths = glob(os.path.join(dir_seg, '*.png'))

(2) 将图像及其相应的标签存储到列表中,并查看加载的数据集:

import cv2
x = []
y = []
for i in range(len(all_img_paths)):
    img = cv2.imread(all_img_paths[i])
    img = cv2.resize(img, (224, 224))
    mask_path = dir_seg + all_img_paths[i].split('/')[-1]
    img_mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
    img_mask = cv2.resize(img_mask, (224, 224))
    x.append(img)
y.append(img_mask)
plt.subplot(221)
plt.imshow(x[0, :, :, ::-1])
plt.title('Original image')
plt.subplot(222)
plt.imshow(255-y[0], cmap='gray')
plt.title('Masked image')
plt.subplot(223)
plt.imshow(x[2, :, :, ::-1])
plt.subplot(224)
plt.imshow(255-y[2], cmap='gray')
plt.show()

(3) 定义函数 get_segmentation_arr,将单通道的灰度输出图像转换成 12 个通道(由于图像中有 12 个不同类别)。
首先,提取输出中存在的不同对象数:

n_classes = len(set(np.array(y).flatten()))

将蒙版图像转换为 12 通道的独热编码输出,其通道数与数据集中的对象数量相同,原始图像中存在对象的位置在相应通道的位置像素值为 1。可以看到,不同通道对应不同的对象类别:

def get_segmentation_arr(img):
    seg_labels = np.zeros((224, 224, n_classes))
    for c in range(n_classes):
        seg_labels[:, :, c] = (img == c).astype(int)
    return seg_labels

y2 = np.array(y2)
x = np.array(x) / 255
plt.subplot(241)
plt.imshow(x[4, :, :, ::-1])
plt.title('Original image')
for i in range(7):
    plt.subplot(2,4,i+2)
    plt.imshow(y2[4,:,:,i], cmap='gray')
    plt.title('Channle '.format(i))
# plt.imshow(255-y[2], cmap='gray')
plt.show()

3.2 模型构建与训练

(1) 构建模型,将图像输入预训练的 VGG16 模型提取图像特征:

from keras.applications.vgg16 import VGG16
from keras.layers import Input, Conv2D, UpSampling2D, BatchNormalization
from keras.applications.vgg16 import preprocess_input
from keras.layers import concatenate, Dropout
from keras.models import Model
from keras.optimizers import Adam

base_model = VGG16(input_shape=(224, 224, 3), include_top=False, weights='imagenet')
base_model.trainable = False

conv1 = Model(inputs=base_model.input,outputs=base_model.get_layer('block1_conv2').output).output
conv2 = Model(inputs=base_model.input,outputs=base_model.get_layer('block2_conv2').output).output
conv3 = Model(inputs=base_model.input,outputs=base_model.get_layer('block3_conv3').output).output
conv4 = Model(inputs=base_model.input,outputs=base_model.get_layer('block4_conv3').output).output
drop4 = Dropout(0.5)(conv4)
conv5 = Model(inputs=base_model.input,outputs=base_model.get_layer('block5_conv3').output).output
drop5 = Dropout(0.5)(conv5)

(2) 将卷积特征与上采样层相同尺寸的特征串联起来,使用在《使用 U-Net 架构进行图像分割》一节中介绍 U-Net 架构:

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)
conv6 = BatchNormalization()(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)
conv7 = BatchNormalization()(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)
conv8 = BatchNormalization()(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(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = BatchNormalization()(conv9)

conv10 = Conv2D(n_classes, 1, activation = 'sigmoid')(conv9)

model = Model(inputs=base_model.input, outputs=conv10)
model.summary()

(3) 冻结预训练的VGG16卷积层参数,然后编译并拟合模型:

for layer in model.layers[:18]:
    layer.trainable = False
    
adam=Adam(1e-3, decay = 1e-6)
model.compile(optimizer=adam, loss='binary_crossentropy', metrics = ['acc'])

history = model.fit(x, y2,
                    epochs=20,
                    batch_size=2,
                    validation_split=0.1)

模型训练过程中,在训练集和测试集上的损失值和准确率变化情况如下所示:

(4) 使用测试图像测试训练完成的语义分割模型性能:

y_pred = model.predict(x[-2:].reshape(2,224,224,3))
y_predi = np.argmax(y_pred, axis=3)
y_testi = np.argmax(y2[-2:].reshape(2,224,224,12), axis=3)

plt.subplot(231)
plt.imshow(x[-1, :, :, ::-1])
plt.title('Original image')
plt.subplot(232)
plt.imshow(255-y[-1], cmap='gray')
plt.title('Masked image')
plt.subplot(233)
plt.imshow(255-y_predi[-1], cmap='gray')
plt.title('Predicted masked image')
plt.subplot(234)
plt.imshow(x[-2, :, :, ::-1])
plt.subplot(235)
plt.imshow(255-y[-2], cmap='gray')
plt.subplot(236)
plt.imshow(255-y_predi[-2], cmap='gray')
plt.show()

模型预测的语义分割结果图像和实际的语义分割图像如下:

结合上图和模型训练过程准确率变化图,我们可以看出训练完成的语义分割模型能够以较高的准确率(约为 90% )识别图像中的语义结构。 ·

小结

语义分割是使计算机理解复杂场景过程中的重要一步,该任务是为了便于图像分析而为图像中的每个像素分配标签的过程。本节首先对语义分割的基本概念进行了简要介绍,阐述了语义分割领域主流的技术现状,并基于 U-Net 架构实现了一个交通图像语义分割模型,用于区分图像中的多种不同类别的对象。

系列链接

Keras深度学习实战(1)——神经网络基础与模型训练过程详解
Keras深度学习实战(2)——使用Keras构建神经网络
Keras深度学习实战(3)——神经网络性能优化技术
Keras深度学习实战(4)——深度学习中常用激活函数和损失函数详解
Keras深度学习实战(5)——批归一化详解
Keras深度学习实战(6)——深度学习过拟合问题及解决方法
Keras深度学习实战(7)——卷积神经网络详解与实现
Keras深度学习实战(8)——使用数据增强提高神经网络性能
Keras深度学习实战(9)——卷积神经网络的局限性
Keras深度学习实战(10)——迁移学习详解
Keras深度学习实战(11)——可视化神经网络中间层输出
Keras深度学习实战(12)——面部特征点检测
Keras深度学习实战(13)——目标检测基础详解
Keras深度学习实战(14)——从零开始实现R-CNN目标检测
Keras深度学习实战(15)——从零开始实现YOLO目标检测
Keras深度学习实战(16)——自编码器详解
Keras深度学习实战(17)——使用U-Net架构进行图像分割

以上是关于Keras深度学习实战(18)——语义分割详解的主要内容,如果未能解决你的问题,请参考以下文章

Keras深度学习实战(26)——文档向量详解

Keras深度学习实战(17)——使用U-Net架构进行图像分割

TensorFlow2深度学习实战(十三): 语义分割算法 SegNet 实战

Keras深度学习实战——深度学习中常用激活函数和损失函数详解

Keras深度学习实战(20)——神经风格迁移详解

Keras深度学习实战(23)——DCGAN详解与实现