一、图像分割定义
- 这三个任务对图像的理解逐步深入。假设给定一张输入图像,
- 图像分类旨在判断该图像所属类别。
- 目标检测是在图像分类的基础上,进一步判断图像中的目标具体在图像的什么位置,通常是以外包矩形(bounding box)的形式表示。
- 图像分割是目标检测更进阶的任务,目标检测只需要框出每个目标的包围盒,语义分割需要进一步判断图像中哪些像素属于哪个目标。但是,语义分割不区分属于相同类别的不同实例。- 如上图所示,当图像中有多个cube时,语义分割会将所有立方体整体的所有像素预测为“cube”这个类别。与此不同的是,实例分割 需要区分出哪些像素属于第一个cube、哪些像素属于第二个cube……。
1. 任务类型
这里是基于传统CV的方式分割,后面会介绍神经网络分割
- 语义分割就是把图像中每个像素赋予一个类别标签,如下图我们将图像中的像素分类为人,羊,狗,草地即可。
- 实例分割,相对于语义分割来讲,不仅要区分不同类别的像素,还需要需要对同一类别的不同个体进行区分。如下图所示,不仅需要进行类别的划分,还要将各个个体划分出来:羊1,羊2,羊3,羊4,羊5等。
2. 常用开源数据集
图像分割常用的数据集是PASCAL VOC,城市风光Cityscapes数据集,coco数据集等。
3. 评价指标
3.1 像素精度
3.2 平均像素精度
3.3 平均交并比
二、语义分割 - FCN
FCN(Fully Convolutional Networks) 用于图像语义分割,自从该网络提出后,就成为语义分割的基本框架,后续算法基本都是在该网络框架中改进而来。 对于一般的分类CNN网络,如VGG和Resnet,都会在网络的最后加入一些全连接层,经过softmax后就可以获得类别概率信息。 简而言之,FCN和CNN的区别就是:CNN卷积层之后连接的是全连接层;FCN卷积层之后仍连接卷积层,输出的是与输入大小相同的特征图。
1. 网络结构
1.1 全链接部分
1.2 上采样部分
上采样部分将最终得到的特征图上采样得到原图像大小的语义分割结果。
在这里采用的上采样方法是反卷积(Deconvolution),也叫做转置卷积(Transposed Convolution):
反卷积是一种特殊的正向卷积 通俗的讲,就是输入补0+卷积。先按照一定的比例通过补0来扩大输入图像的尺寸,再进行正向卷积即可。 如下图所示:输入图像尺寸为3x3,卷积核kernel为3x3,步长strides=2,填充padding=1
2. 跳层链接
如果只利用反卷积对最后一层的特征图进行上采样的到原图大小的分割,由于最后一层的特征图太小,会损失很多细节。因而提出增加Skips结构将最后一层的预测(有更富的全局信息)和更浅层(有更多的局部细节)的预测结合起来。
3. 总结
- 优点
- 端到端的,可以接受任意大小的输入图像尺寸,比较高效。
- 局限性
- 得到的结果还是不够精细。进行8倍上采样虽然比32倍的效果好了很多,但是上采样的结果还是比较模糊的,对图像中的细节不敏感。
- 而且在对各个像素进行分类时,没有考虑像素与像素之间的关系。
三、UNet
- 整个网络由编码部分(左) 和 解码部分(右)组成,类似于一个大大的U字母,具体介绍如下:
- 1、编码部分是典型的卷积网络架构:
- 架构中含有着一种重复结构,每次重复中都有2个 3 x 3卷积层、非线性ReLU层和一个 2 x 2 max pooling层(stride为2)。(图中的蓝箭头、红箭头,没画ReLu)
- 每一次下采样后我们都把特征通道的数量加倍
- 2、解码部分也使用了类似的模式:
- 每一步都首先使用反卷积(up-convolution),每次使用反卷积都将特征通道数量减半,特征图大小加倍。(图中绿箭头)
- 反卷积过后,将反卷积的结果与编码部分中对应步骤的特征图拼接起来。(白/蓝块)
- 编码部分中的特征图尺寸稍大,将其修剪过后进行拼接。(左边深蓝虚线)
- 对拼接后的map再进行2次3 x 3的卷积。(右侧蓝箭头)
- 最后一层的卷积核大小为1 x 1,将64通道的特征图转化为特定类别数量(分类数量)的结果。(图中青色箭头)
- 1、编码部分是典型的卷积网络架构:
四、UNet 案例
1. 任务及数据集简介
使Oxford-IIIT Pet Dataset宠物图像分割数据集,包含37种宠物类别,其中有12种猫的类别和25种狗的类别,每个类别大约有200张图片,所有图像都具有品种,头部ROI和像素级分割的标注,如下图所示:
2. 数据集获取
# 在进行模型构建之前,我们将读取数据集,导入相应的工具包:
import os
from IPython.display import Image, display
from tensorflow.keras.preprocessing.image import load_img
import PIL
from PIL import ImageOps
# 路径及相关参数设置
# 图片位置
input_dir = "segdata/images/"
# 标注信息位置
target_dir = "segdata/annotations/trimaps/"
# 图像大小设置及类别信息
img_size = (160, 160)
batch_size = 32
num_classes = 4
# 图像的路径
input_img_paths = sorted(
[
os.path.join(input_dir, fname)
for fname in os.listdir(input_dir)
if fname.endswith(".jpg")
]
)
# 目标值路径
target_img_paths = sorted(
[
os.path.join(target_dir, fname)
for fname in os.listdir(target_dir)
if fname.endswith(".png") and not fname.startswith(".")
]
)
# 显示一个图像
display(Image(filename=input_img_paths[10]))
# 显示标注图像
img = PIL.ImageOps.autocontrast(load_img(target_img_paths[10]))
display(img)
构建数据集生成器:
from tensorflow import keras
import numpy as np
from tensorflow.keras.preprocessing.image import load_img
# 数据集获取:
class OxfordPets(keras.utils.Sequence):
# 在__init__方法中指定batch_size,img_size,input_img_paths,target_img_paths
def __init__(self, batch_size, img_size, input_img_paths, target_img_paths):
self.batch_size = batch_size # 批量大小
self.img_size = img_size # 图像大小
self.input_img_paths = input_img_paths # 输入图像路径
self.target_img_paths = target_img_paths # 标注图像路径
def __len__(self):
# 计算迭代次数
return len(self.target_img_paths) // self.batch_size
def __getitem__(self, idx):
"""
获取每一个batch数据
"""
i = idx * self.batch_size
# 获取输入的图像数据
batch_input_img_paths = self.input_img_paths[i: i + self.batch_size]
# 获取标签数据
batch_target_img_paths = self.target_img_paths[i: i + self.batch_size]
# 构建特征值数据:获取图像数据中每个像素的数据存储在x中
x = np.zeros((batch_size,) + self.img_size + (3,), dtype="float32")
for j, path in enumerate(batch_input_img_paths):
img = load_img(path, target_size=self.img_size)
x[j] = img
# 构建目标值数据:获取标注图像中每个像素中的数据存在y中
y = np.zeros((batch_size,) + self.img_size + (1,), dtype="uint8")
for j, path in enumerate(batch_target_img_paths):
img = load_img(path, target_size=self.img_size,
color_mode="grayscale")
y[j] = np.expand_dims(img, 2)
return x, y
3. 模型构建
导入相关工具包
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.layers import Input, Conv2D, Conv2DTranspose
from tensorflow.keras.layers import MaxPooling2D, Cropping2D, Concatenate
from tensorflow.keras.layers import Lambda, Activation, BatchNormalization, Dropout
from tensorflow.keras.models import Model
3.1 编码部分
# 输入:输入张量,卷积核个数
def downsampling_block(input_tensor, filters):
# 输入层
x = Conv2D(filters, kernel_size=(3, 3),padding='same')(input_tensor)
# BN层
x = BatchNormalization()(x)
# 激活函数
x = Activation('relu')(x)
# 卷积层
x = Conv2D(filters, kernel_size=(3, 3),padding="same")(x)
# BN层
x = BatchNormalization()(x)
# 激活层
x = Activation('relu')(x)
# 返回的是池化后的值和激活未池化的值,激活后未池化的值用于解码部分特征级联
return MaxPooling2D(pool_size=(2, 2))(x), x
3.2 解码部分
# 输入:输入张量,特征融合的张量,卷积核个数
def upsampling_block(input_tensor, skip_tensor, filters):
# 反卷积
x = Conv2DTranspose(filters, kernel_size=(2,2), strides=(2,2),padding="same")(input_tensor)
# 获取当前特征图的尺寸
_, x_height, x_width, _ = x.shape
# 获取要融合的特征图的尺寸
_, s_height, s_width, _ = skip_tensor.shape
# 获取特征图的大小差异
h_crop = s_height - x_height
w_crop = s_width - x_width
# 若特征图大小相同不进行裁剪
if h_crop == 0 and w_crop == 0:
y = skip_tensor
#若特征图大小不同,使级联时像素大小一致
else:
# 获取特征图裁剪后的特征图的大小
cropping = ((h_crop//2, h_crop - h_crop//2), (w_crop//2, w_crop - w_crop//2))
# 特征图裁剪
y = Cropping2D(cropping=cropping)(skip_tensor)
# 特征融合
x = Concatenate()([x, y])
# 卷积
x = Conv2D(filters, kernel_size=(3,3),padding="same")(x)
# BN层
x = BatchNormalization()(x)
# 激活层
x = Activation('relu')(x)
# 卷积层
x = Conv2D(filters, kernel_size=(3,3),padding="same")(x)
# BN层
x = BatchNormalization()(x)
# 激活层
x = Activation('relu')(x)
return x
3.3 模型构建
# 使用3个深度构建unet网络
def unet(imagesize, classes, features=64, depth=3):
# 定义输入数据
inputs = keras.Input(shape=img_size + (3,))
x = inputs
# 用来存放进行特征融合的特征图
skips = []
# 构建编码部分
for i in range(depth):
x, x0 = downsampling_block(x, features)
skips.append(x0)
# 下采样过程中,深度增加,特征翻倍,即每次使用翻倍数目的滤波器
features *= 2
# 卷积
x = Conv2D(filters=features, kernel_size=(3, 3),padding="same")(x)
# BN层
x = BatchNormalization()(x)
# 激活
x = Activation('relu')(x)
# 卷积
x = Conv2D(filters=features, kernel_size=(3, 3),padding="same")(x)
# BN层
x = BatchNormalization()(x)
# 激活
x = Activation('relu')(x)
# 解码过程
for i in reversed(range(depth)):
# 深度增加,特征图通道减半
features //= 2
# 上采样
x = upsampling_block(x, skips[i], features)
# 卷积
x = Conv2D(filters=classes, kernel_size=(1, 1),padding="same")(x)
# 激活
outputs = Activation('softmax')(x)
# 模型定义
model = keras.Model(inputs, outputs)
return model
model = unet(img_size, 4)
model.summary()
# 模型可视化
keras.utils.plot_model(model)
4. 模型训练
4.1 数据集划分
import random
# 将数据集划分为训练集和验证集,其中验证集的数量设为1000
val_samples = 1000
# 将数据集打乱(图像与标注信息的随机数种子是一样的,才能保证数据的正确性)
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_img_paths)
# 获取训练集数据路径
train_input_img_paths = input_img_paths[:-val_samples]
train_target_img_paths = target_img_paths[:-val_samples]
# 获取验证集数据路径
val_input_img_paths = input_img_paths[-val_samples:]
val_target_img_paths = target_img_paths[-val_samples:]
4.2 数据获取
# 获取训练集
train_gen = OxfordPets(
batch_size, img_size, train_input_img_paths, train_target_img_paths
)
# 模型验证集
val_gen = OxfordPets(batch_size, img_size, val_input_img_paths, val_target_img_paths)
4.3 模型编译
# 模型编译
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
4.4 模型训练
# 模型训练,epoch设为5
epochs = 15
model.fit(train_gen, epochs=epochs, validation_data=val_gen)
5. 模型预测
# 获取验证集数据,并进行预测
val_gen = OxfordPets(batch_size, img_size, val_input_img_paths, val_target_img_paths)
val_preds = model.predict(val_gen)
# 图像显示
def display_mask(i):
# 获取到第i个样本的预测结果
mask = np.argmax(val_preds[i], axis=-1)
# 维度调整
mask = np.expand_dims(mask, axis=-1)
# 转换为图像,并进行显示
img = PIL.ImageOps.autocontrast(keras.preprocessing.image.array_to_img(mask))
display(img)
五、实例分割 - Mask RCNN
1. Mask RCNN 流程
- 整体的流程是:
- 输入要处理的图片。
- 将图片送入到CNN特征提取网络得到特征图。
- 然后对特征图的每一个像素位置设定固定个数的ROI(对应于在FasterRCNN中的Anchor),然后将ROI区域送入RPN网络进行二分类(前景和背景)以及坐标回归,以获得精炼后的ROI区域(对应于FasterRCNN中的候选区域)。
- 对上个步骤中获得的ROI区域执行ROIAlign操作(是对ROIPooling的改进),即先将原图和feature map的pixel对应起来,然后将feature map和固定大小的feature对应起来。
- 最后对这些ROI区域进行多类别分类,候选框回归和引入FCN生成Mask,完成实例分割任务。
整个过程中与FasterRCNN中不同的是ROIAlign和分割分支,其他都是相同的;
2. ROIAlign
2.1 原理介绍
- 它的流程是:
- 输入图片的大小为800x800,其中狗这个目标框的大小为665x665,经过VGG16网络之后获得的特征图尺寸为800/32x800/32=25x25,其中32代表VGG16中的5次下采样(步长为2)操作。那么,对于狗这个目标,我们将其对应到特征图上得到的结果是665/32x665/32=20.78x20.78=20x20,因为坐标要保留整数所以这里引入了第一个量化误差即舍弃了目标框在特征图上对应长宽的浮点数部分。
- 接下来需要将这个20x20的ROI区域映射为7x7的ROI特征图,根据ROI Pooling的计算方式,其结果就是20/7x20/7=2.86x2.86,同样执行取整操作操作后ROI特征区域的尺寸为2x2,这里引入了第二次量化误差。
- 从上面的分析可以看出,这两次量化误差会导致原始图像中的像素和特征图中的像素进行对应时出现偏差,例如上面将2.86量化为2的时候就引入了0.86的偏差,这个偏差映射回原图就是0.86x32=27.52,可以看到这个像素偏差是很大的,而且这仅仅考虑了第二次的量化误差,所以这会影响整个算法的性能。
- 针对上图的流程是:
- 输入图片的大小为800x800,其中狗这个目标框的大小为665x665,经过VGG16网络之后获得的特征图尺寸为800/32x800/32=25x25,其中32代表VGG16中的5次下采样(步长为2)操作。那么,对于狗这个目标,我们将其对应到特征图上得到的结果是665/32x665/32=20.78x20.78,此时,没有像RoiPooling那样就行取整操作,而是保留浮点数。
- 接下来需要将这个20.78x20.78的ROI区域映射为7x7的ROI特征图,结果就是20.78/7x20.78/7=2.97x2.97,即每个小区域的大小为2.97x2.97。
- 假定每个小区域采样点数为4,也就是说,对于每个2.97*2.97的小区域,平分四份,每一份取其中心点位置,而中心点位置的像素,采用双线性插值法进行计算,这样,就会得到四个点的像素值,如下图:
2.2 实现效果
import tensorflow as tf
import matplotlib.pyplot as plt
# 图像读取
img = plt.imread('Trump.jpg')/255.
img2 = plt.imread('Trump2.jpg')/255.
# 图像展示
plt.figure(figsize=(10,8))
plt.subplot(1,2,1)
plt.imshow(img)
plt.subplot(1,2,2)
plt.imshow(img2)
# 对图像进行类型转换,并添加batch维
img = tf.convert_to_tensor(img, dtype=tf.float32)
img = tf.expand_dims(img, axis=0)
img = tf.image.resize(img, (500,500))
img2 = tf.convert_to_tensor(img2, dtype=tf.float32)
img2 = tf.expand_dims(img2, axis=0)
img2 = tf.image.resize(img2, (500,500))
# 将两个图像拼接在一起
img = tf.concat([img, img2], axis=0)
print('img:', img.shape)
# 进行ROIAlign处理:特征图,2个boxes,分别对应图像索引0和1,ROIAlign后的大小为50x50
out = tf.image.crop_and_resize(img, [[0.5, 0.5, 1.0, 1.0], [0.5, 0.5, 1.5, 1.5]], [0, 1], crop_size=(50, 50))
print('out:', a.shape)
plt.figure(figsize=(10,8))
# 尺寸调整后的图像
plt.subplot(2,2,1)
plt.imshow(img[0])
plt.subplot(2,2,2)
plt.imshow(img[1])
# ROIAlign的结果
plt.subplot(2,2,3)
plt.imshow(a[0])
plt.subplot(2,2,4)
plt.imshow(a[1])
plt.show()
3. 网络结构