一、目标分割简介
计算机视觉旨在识别和理解图像中的内容,包含三大基本任务:图像分类(图a)、目标检测(图b)和图像分割,其中图像分割又可分为:语义分割(图c)和实例分割(图d)。
这三个任务对图像的理解逐步深入。假设给定一张输入图像,
- 图像分类旨在判断该图像所属类别。
- 目标检测是在图像分类的基础上,进一步判断图像中的目标具体在图像的什么位置,通常是以外包矩形(bounding box)的形式表示。
- 图像分割是目标检测更进阶的任务,目标检测只需要框出每个目标的包围盒,语义分割需要进一步判断图像中哪些像素属于哪个目标。但是,语义分割不区分属于相同类别的不同实例。如上图所示,当图像中有多个cube时,语义分割会将所有立方体整体的所有像素预测为“cube”这个类别。与此不同的是,实例分割需要区分出哪些像素属于第一个cube、哪些像素属于第二个cube……。
1.1 图像分割的定义
- 定义:在计算机视觉领域,图像分割(Object Segmentation)指的是将数字图像细分为多个图像子区域(像素的集合)的过程,并且同一个子区域内的特征具有一定相似性,不同子区域的特征呈现较为明显的差异。
图像分割的目标就是为图像中的每个像素分类。应用领域非常的广泛:自动驾驶、医疗影像,图像美化、三维重建等等。
-
自动驾驶(Autonomous vehicles):汽车需要安装必要的感知系统以了解它们的环境,这样自动驾驶汽车才能够安全地驶入现有的道路
-
医疗影像诊断(Medical image diagnostics):机器在分析能力上比放射科医生更强,而且可以大大减少诊断所需时间。
图像分割是一个非常困难的问题,尤其是在深度学习之前。深度学习使得图像分割的准确率提高了很多,接下来我们主要围绕深度学习方法给大家介绍图像分割的内容。
1.2 任务类型
1.2.1 任务描述
简单来说,我们的目标是输入一个RGB彩色图片 ( h e i g h t × w i d t h × 3 ) (height \times width \times 3) (height×width×3)或者一个灰度图 ( h e i g h t × w i d t h × 1 ) (height \times width \times 1) (height×width×1),然后输出一个包含各个像素类别标签的分割图 ( h e i g h t × w i d t h × 1 ) (height \times width \times 1) (height×width×1)。如下图所示:
与我们处理分类值的方式类似,预测目标可以采用one-hot编码,即为每一个可能的类创建一个输出通道。通过取每个像素点在各个channel的argmax可以得到最终的预测分割图,(如下图所示):
比如:person的编码为:10000,而Grass的编码为:00100
当将预测结果叠加到单个channel时,称这为一个掩膜mask,它可以给出一张图像中某个特定类的所在区域:
1.2.2 任务类型
目前的图像分割任务主要有两类: 语义分割和实例分割
我们以下图为例,来介绍这两种分割方式:
- 语义分割就是把图像中每个像素赋予一个类别标签,如下图我们将图像中的像素分类为人,羊,狗,草地即可。
- 实例分割,相对于语义分割来讲,不仅要区分不同类别的像素,还需要需要对同一类别的不同个体进行区分。如下图所示,不仅需要进行类别的划分,还要将各个个体划分出来:羊1,羊2,羊3,羊4,羊5等。
目前图像分割的任务主要集中在语义分割,而目前的难点也在于“语义”,表达某一语义的同一物体并不总是以相同的形象出现,如包含不同的颜色、纹理等,这对精确分割带来了很大的挑战。而且以目前的模型表现来看,在准确率上还有很大的提升空间。而实例分割的思路主要是目标检测+语义分割,即用目标检测方法将图像中的不同实例框出,再用语义分割方法在不同检测结果内进行逐像素标记。
1.3 常用的开源数据集
图像分割常用的数据集是PASCAL VOC,城市风光数据集,coco数据集等。
1.3.1 VOC数据集
VOC数据集共有20类数据,目录结构如下图所示:
这个在目标检测概述一节中已经给大家介绍过,接下来我们主要介绍目标分割相关的内容:
- JPEGImages中存放图片文件
- imagesets中的segmentation中记录了用于分割的图像信息
- SegmentationClass中是语义分割的标注信息
- SegmentationObject中是实例分割的标注信息
VOC中的图片并不是所有都用于分割,用于分割比赛的图片实例都记录在txt文件中,如下图所示:
相关的图像记录在相应的文本文件中,如训练集数据记录在train.txt文件中,其中内容如下:
那我们就可以使用这些数据进行图像分割模型的训练。图像的标注效果如下图所示:
原图像002378.jpg的语义分割和实例分割的标注结果如上所示,背景是黑色的,不同类别的像素标注为不同的颜色。可以看出,语义分割只标注了像素的类别,而实例分割不仅标注了类别,还对同一类别的不同个体进行了区分。
在写程序的时候就利用 train.txt 对图片进行挑选,因为不是所有的图片都有分割真实值,获取图片及其对应的真实值,对网络进行训练即可。
1.3.2 城市风光Cityscapes数据集
Cityscapes是由奔驰于2015年推出的,提供无人驾驶环境下的图像分割数据集。它包含50个城市不同场景、不同背景、不同季节的街景,提供了5000张在城市环境中驾驶场景的高质量像素级注释图像(其中 2975 for train,500 for val,1525 for test)。
Cityscapes是目前公认的自动驾驶领域内最具权威性和专业性的图像语义分割评测集之一,其关注真实场景下的城区道路环境理解,任务难度更高且更贴近于自动驾驶等热门需求。接下来我们看下数据的内容,数据集的文件目录结构如下图所示:
其中txt文件中保存了相关样本图片的路径和文件名,便于查找相应的数据,我们主要使用数据是leftImg8bit和gtFine中的内容,如下所示:
- leftImg8bit文件夹有三个子目录:test, train以及val,分别为测试集,训练集以及验证集图片。这三个子目录的图片又以城市为单元来存放,如下图所示:
这里解释下leftImg8bit的含义,因为cityscapes实际上来源于双摄像头拍摄的立体视频序列,所以这里的leftImg就是来自于左摄像头的图片,而8bit意味着该图片集都为RGB每个分量为8bit的图片。
- gtFine是样本图片对应的标注信息,gtFine下面也是分为train, test以及val,然后它们的子目录也是以城市为单位来放置图片。这些都是和leftImg8bit的一一对应。 不同的是,在城市子目录下面,每张样本图片对应有6个标注文件,如下所示:
精细标注数据集里面每张图片只对应四张标注文件:xxx_gtFine_color.png, xxx_gtFine_instanceIds.png, xxx_gtFine_labelsIds.png以及xxx_gtFine_polygons.json。 xxx_color.png是标注的可视化图片,真正对训练有用的是后面三个文件。xxx_instanceIds.png是用来做实例分割训练用的,而xxx_labelsIds.png是语义分割训练需要的。而最后一个文件xxx_polygons.json主要记录了每个多边形标注框上的点集坐标。
该数据集的标注效果可视化如下所示:
1.4 评价指标
图像分割中通常使用许多标准来衡量算法的精度。这些标准通常是像素精度及IoU的变种,以下我们将会介绍常用的几种逐像素标记的精度标准。
为了便于解释,假设如下:共有 k + 1 k+1 k+1个类(从 L 0 L_0 L0到 L k L_k Lk,其中包含一个背景类), p i j p_{ij} pij表示本属于类 i i i但被预测为类 j j j的像素。即 p i i p_{ii} pii表示预测正确的像素。
1.4.1 像素精度
Pixel Accuracy(PA,像素精度):这是最简单的度量,为预测正确的像素占总像素的比例。
对于样本不均衡的情况,例如医学图像分割中,背景与标记样本之间的比例往往严重失衡。因此并不适合使用这种方法进行度量。
1.4.2 平均像素精度
Mean Pixel Accuracy(MPA,平均像素精度):是PA的一种简单提升,计算每个类内被正确分类像素数的比例,之后求所有类的平均。
1.4.3 平均交并比
Mean Intersection over Union(MIoU,平均交并比):为语义分割的标准度量,其计算两个集合的交集和并集之比,在语义分割的问题中,这两个集合为真实值(ground truth)和预测值(predicted segmentation)。交集为预测正确的像素数(intersection),并集为预测或真实值为 i i i类的和减去预测正确的像素,在每个类上计算IoU,之后求平均即可。
那么,如何理解这里的公式呢?如下图所示,红色圆代表真实值,黄色圆代表预测值。橙色部分红色圆与黄色圆的交集,即预测正确的部分,红色部分表示假负(真实值为该类预测错误)的部分,黄色表示假正(预测值为i类,真实值为其他)的部分。
MIoU计算的是计算A与B的交集(橙色部分)与A与B的并集(红色+橙色+黄色)之间的比例,在理想状态下A与B重合,两者比例为1 。
在以上所有的度量标准中,MIoU由于其简洁、代表性强而成为最常用的度量标准,大多数研究人员都使用该标准报告其结果。PA对于样本不均衡的情况不适用。
二、语义分割:FCN和UNet
1. FCN网络
FCN(Fully Convolutional Networks) 用于图像语义分割,自从该网络提出后,就成为语义分割的基本框架,后续算法基本都是在该网络框架中改进而来。
对于一般的分类CNN网络,如VGG和Resnet,都会在网络的最后加入一些全连接层,经过softmax后就可以获得类别概率信息。
但是这个概率只能标识整个图片的类别,不能标识每个像素点的类别,所以这种全连接方法不适用于图像分割。
而FCN提出可以把后面几个全连接都换成卷积,这样就可以获得一张2维的feature map,后接softmax获得每个像素点的分类信息,从而解决了分割问题,如下图所示:
简而言之,FCN和CNN的区别就是:CNN卷积层之后连接的是全连接层;FCN卷积层之后仍连接卷积层,输出的是与输入大小相同的特征图。
1.1 网络结构
FCN是一个端到端,像素对像素的全卷积网络,用于进行图像的语义分割。整体的网络结构分为两个部分:全卷积部分和上采样部分。
1.1.1 全卷积部分
全卷积部分使用经典的CNN网络(以AlexNet网络为例),并把最后的全连接层换成1\times 1卷积,用于提取特征。
-
在传统的Alex结构中,前5层是卷积层,第6层和第7层分别是一个长度为4096的一维向量,第8层是长度为1000的一维向量,分别对应1000个不同类别的概率。
-
FCN将最后的3层转换为卷积层,卷积核的大小 (通道数,宽,高) 分别为 (4096,1,1)、(4096,1,1)、(1000,1,1),虽然参数数目相同,但是计算方法就不一样了,这时还可使用预训练模型的参数。
-
CNN中输入的图像固定成227x227大小,第一层pooling后为55x55,第二层pooling后图像大小为27x27,第五层pooling后的图像大小为13x13, 而FCN输入的图像是H*W大小,第一层pooling后变为原图大小的½,第二层变为原图大小的¼,第五层变为原图大小的⅛,第八层变为原图大小的1/16,如下所示:
-
经过多次卷积和pooling以后,得到的图像越来越小,分辨率越来越低。对最终的特征图进行upsampling,把图像进行放大到原图像的大小,就得到原图像的分割结果。
1.1.2 上采样部分
上采样部分将最终得到的特征图上采样得到原图像大小的语义分割结果。
在这里采用的上采样方法是反卷积(Deconvolution),也叫做转置卷积(Transposed Convolution):
- 反卷积是一种特殊的正向卷积
- 通俗的讲,就是输入补0+卷积。先按照一定的比例通过补0来扩大输入图像的尺寸,再进行正向卷积即可。
如下图所示:输入图像尺寸为3x3,卷积核kernel为3x3,步长strides=2,填充padding=1
假设反卷积的输入是n x n ,反卷积的输出为mxm ,padding=p,stride=s,kernel_size = k。
那么此时反卷积的输出就为:
m = s ( n − 1 ) + k − 2 p m=s(n-1)+k-2p m=s(n−1)+k−2p
与正向卷积不同的是,要先根据步长strides对输入的内部进行填充,这里strides可以理解成输入放大的倍数,而不能理解成卷积移动的步长。
这样我们就可以通过反卷积实现上采样。
1.2 跳层连接
如果只利用反卷积对最后一层的特征图进行上采样的到原图大小的分割,由于最后一层的特征图太小,会损失很多细节。因而提出增加Skips结构将最后一层的预测(有更富的全局信息)和更浅层(有更多的局部细节)的预测结合起来。
那么:
-
对于FCN-32s,直接对pool5 feature进行32倍上采样获得32x upsampled feature,再对32x upsampled feature每个点做softmax prediction获得32x upsampled feature prediction(即分割图)。
-
对于FCN-16s,首先对pool5 feature进行2倍上采样获得2x upsampled feature,再把pool4 feature和2x upsampled feature逐点相加,然后对相加的feature进行16倍上采样,并softmax prediction,获得16x upsampled feature prediction。
-
对于FCN-8s,首先进行pool4+2x upsampled feature逐点相加,然后又进行pool3+2x upsampled逐点相加,即进行更多次特征融合。具体过程与16s类似,不再赘述。
下面有一张32倍,16倍和8倍上采样得到的结果图对比:
可以看到随着上采样做得越多,分割结果越来越精细。
1.3 总结
-
优点
端到端的,可以接受任意大小的输入图像尺寸,比较高效。 -
局限性
得到的结果还是不够精细。进行8倍上采样虽然比32倍的效果好了很多,但是上采样的结果还是比较模糊的,对图像中的细节不敏感。而且在对各个像素进行分类时,没有考虑像素与像素之间的关系。
2. Unet网络
Unet网络是建立在FCN网络基础上的,它的网络架构如下图所示,总体来说与FCN思路非常类似。
整个网络由编码部分(左) 和 解码部分(右)组成,类似于一个大大的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通道的特征图转化为特定类别数量(分类数量)的结果。(图中青色箭头)
三、UNet案例
1.1 任务及数据集简介
使Oxford-IIIT Pet Dataset宠物图像分割数据集,包含37种宠物类别,其中有12种猫的类别和25种狗的类别,每个类别大约有200张图片,所有图像都具有品种,头部ROI和像素级分割的标注,如下图所示:
图像分割时共分为前景,背景和不确定3种,图像数据包含的类别及对应的数量如下图所示:
数据集的目录结果如下所示:\segdata
1、Images:存储数据集的图片数据,其中图片文件名是以大写开头为“cat”,小写开头为“dog”。
2、Annotations:标注信息,内容如下所示:
-
list.txt中的内容格式如下所示,其中Class ID对应着37类中的某一类,SPECIES是总分类,1是猫,2是狗;BreedID是猫狗分类中的子分类,猫的子分类为12类,而狗的子分类为25类。
-
trimaps是图像的像素级标注信息,是我们的目标值
接下来我们利用UNET网络进行宠物数据集分割。
1.2 数据集获取
在进行模型构建之前,我们将读取数据集,导入相应的工具包:
import os
from IPython.display import Image, display
from tensorflow.keras.preprocessing.image import load_img
import PIL
from PIL import ImageOps
1.2.1 路径及相关参数设置
在这里我们设置数据的路径,图像的大小,batch_size和类别数量,在这里使用了一个技巧,图像分割时共分为前景,背景和不确定3种,分别标注为:1,2,3,对类别进行热编码时,我们编码为:1:0010;2:0100;3:1000,这样在设置类别个数时设为4即可。
# 图片位置
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(".")
]
)
1.2.2 数据展示
将图像及对应的结果进行展示:
# 显示一个图像
display(Image(filename=input_img_paths[10]))
标注信息中只有3个值,我们使用PIL.ImageOps.autocontrast进行展示,该方法计算输入图像的直方图,然后重新映射图像,最暗像素变为黑色,即0,最亮的变为白色,即255,其他的值以其他的灰度值进行显示,在这里前景,背景和不确定分别标注为:1,2,3,所以前景最小显示为黑色,不确定的区域最大显示为白色。
# 显示标注图像
img = PIL.ImageOps.autocontrast(load_img(target_img_paths[10]))
display(img)
1.2.3 构建数据集生成器
利用keras.utils.Sequence构建图像生成器来读取数据,每个Sequence必须实现 getitem 和 len 方法,通过 getitem 应返回完整的批次, Sequence是进行多处理的更安全方法。这种结构保证了网络在每个时间段的每个样本上只会训练一次。主要实现3个方法;init,len和getitem即可。
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
接下来,我们就可以使用该方法来获取数据。
1.3 模型构建
Unet的网络的结构如下图所示,主要分为两部分:编码和解码部分,我们分别进行构建
导入相关的工具包:
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
1.3.1 编码部分
编码部分的特点是:
-
架构中含有着一种重复结构,每次重复中都有2个 3 x 3卷积层、非线性ReLU层和一个 2 x 2 max pooling层(stride为2)。
-
每一次下采样后我们都把特征通道的数量加倍
-
每次重复都有两个输出:一个用于编码部分进行特征提取,一个用于解码部分的特征融合
构建的代码如下所示:
# 输入:输入张量,卷积核个数
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
1.3.2 解码部分
解码部分也使用了重复模块:
每一个模块有两个输入:一个是编码阶段的特征图,一个是解码部分的特征图
每一步都首先使用反卷积(up-convolution),每次使用反卷积都将特征通道数量减半,特征图大小加倍。(图中绿箭头)
反卷积过后,将反卷积的结果与编码部分中对应步骤的特征图拼接起来。(白/蓝块)
编码部分中的特征图尺寸稍大,将其修剪过后进行拼接。(左边深蓝虚线)
对拼接后的map再进行2次3 x 3的卷积。(右侧蓝箭头)
编码实现如下:
# 输入:输入张量,特征融合的张量,卷积核个数
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
1.3.3 模型构建
将编码部分和解码部分组合一起,就可构建unet网络,在这里unet网络的深度通过depth进行设置,并设置第一个编码模块的卷积核个数通过filter进行设置,通过以下模块将编码和解码部分进行组合:
# 使用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)
进行可视化。
1.4 模型训练
1.4.1 数据集划分
数据集中的图像是按顺序进行存储的,在这里我们将数据集打乱后,验证集的数量1000,剩余的为训练集,划分训练集和验证集:
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:]
1.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)
1.4.3 模型编译
进行模型编译,设置:
- 优化方法:使用rmsprop优化方法
- 损失函数:使用交叉熵损失函数,因为没有对目标值进行热编码,所以使用sparse_categorical_crossentropy
# 模型编译
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
1.4.4 模型训练
设置epoch对模型进行训练,指明验证集数据:
# 模型训练,epoch设为5
epochs = 15
model.fit(train_gen, epochs=epochs, validation_data=val_gen)
训练过程如下:
Epoch 1/15
199/199 [==============================] - 44s 223ms/step - loss: 0.9539 - val_loss: 13.5056
Epoch 2/15
199/199 [==============================] - 44s 221ms/step - loss: 0.5145 - val_loss: 2.2228
Epoch 3/15
199/199 [==============================] - 44s 222ms/step - loss: 0.4318 - val_loss: 0.4182
Epoch 4/15
199/199 [==============================] - 44s 221ms/step - loss: 0.4027 - val_loss: 0.4100
Epoch 5/15
199/199 [==============================] - 44s 223ms/step - loss: 0.3551 - val_loss: 0.3894
Epoch 6/15
199/199 [==============================] - 44s 220ms/step - loss: 0.3226 - val_loss: 0.4020
Epoch 7/15
199/199 [==============================] - 44s 219ms/step - loss: 0.3195 - val_loss: 0.4273
Epoch 8/15
199/199 [==============================] - 44s 220ms/step - loss: 0.2789 - val_loss: 0.3707
Epoch 9/15
199/199 [==============================] - 43s 219ms/step - loss: 0.2599 - val_loss: 0.4059
Epoch 10/15
199/199 [==============================] - 44s 222ms/step - loss: 0.2440 - val_loss: 0.3799
Epoch 11/15
199/199 [==============================] - 43s 218ms/step - loss: 0.2297 - val_loss: 0.4244
Epoch 12/15
199/199 [==============================] - 43s 218ms/step - loss: 0.2179 - val_loss: 0.4320
Epoch 13/15
199/199 [==============================] - 43s 218ms/step - loss: 0.2081 - val_loss: 0.4034
Epoch 14/15
199/199 [==============================] - 44s 220ms/step - loss: 0.1977 - val_loss: 0.4034
Epoch 15/15
199/199 [==============================] - 44s 222ms/step - loss: 0.1901 - val_loss: 0.4150
<tensorflow.python.keras.callbacks.History at 0x110063898>
随着迭代次数的增加,训练集和验证集的损失函数变换如下图所示:
1.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)
- 选择某一个图像进行预测
# 选中验证集的第10个图像
i = 10
- 原图像展示
# 输入图像显示
display(Image(filename=val_input_img_paths[i]))
- 目标值展示
# 真实值显示
img = PIL.ImageOps.autocontrast(load_img(val_target_img_paths[i]))
display(img)
- 模型预测结果
# 显示预测结果
display_mask(i)
四、实例分割 Mask RCNN
上图是MaskRCNN预测的结果
1.1 Mask RCNN流程
Mask-RCNN是一个实例分割(Instance segmentation)网络框架,通过增加不同的分支可以完成目标分类,目标检测,实例分割等多种任务。具体来讲,就是在Faster-RCNN的基础上增加了一个分支,在实现目标检测的同时分割目标像素,其分支结构如下图所示:
掩码分支是作用于每个RoI区域(候选区域),以像素到像素的方式预测分割掩码,得到实例分割的结果。
Mask RCNN的整体结构如下图所示:
整体的流程是:
- 输入要处理的图片。
- 将图片送入到CNN特征提取网络得到特征图。
- 然后对特征图的每一个像素位置设定固定个数的ROI(对应于在FasterRCNN中的Anchor),然后将ROI区域送入RPN网络进行二分类(前景和背景)以及坐标回归,以获得精炼后的ROI区域(对应于FasterRCNN中的候选区域)。
- 对上个步骤中获得的ROI区域执行ROIAlign操作(是对ROIPooling的改进),即先将原图和feature map的pixel对应起来,然后将feature map和固定大小的feature对应起来。
- 最后对这些ROI区域进行多类别分类,候选框回归和引入FCN生成Mask,完成实例分割任务。
整个过程中与FasterRCNN中不同的是ROIAlign和分割分支,其他都是相同的,接下来我们着重介绍这两个内容。
1.2 ROIAlign
1.2.1 原理介绍
FasterRCNN中的ROIPooling过程如下所示:
它的流程是:
-
输入图片的大小为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,可以看到这个像素偏差是很大的,而且这仅仅考虑了第二次的量化误差,所以这会影响整个算法的性能。
为了缓解ROI Pooling量化误差过大的问题,MaskRCNN提出了ROIAlign,ROIAlign没有使用量化操作,而是使用了双线性插值估计非整数点的像素值。这一过程如下图所示:
针对上图的流程是:
-
输入图片的大小为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.97x2.97大小的区域)的像素值,如此类推,同样是49个小区域得到49个像素值,组成7x7大小的特征图。
双线性插值是一种图像缩放填充算法,它充分的利用了原图中虚拟点(比如20.56这个浮点数,像素位置不是整数值,而是浮点值)四周的真实存在的像素值来共同决定目标图中的一个像素值,即可以将20.56这个虚拟的位置点对应的像素值估计出来。
1.2.2 实现效果
在tensorFlow中实现时使用:
tf.image.crop_and_resize(
image, boxes, box_indices, crop_size, method='bilinear', extrapolation_value=0,
name=None
)
参数介绍:
-
image: 表示特征图
-
boxes:指需要划分的ROI区域,输入格式为[ymin,xmin,ymax,xmax],注意是归一化的结果。
假设候选区域坐标是[y1,x1,y2,x2],那么想要得到相应正确的crop图形就一定要归一化,即图片的长度是[w,h],则实际输入的boxes为[y1/h,x1/w,y2/h,x2/w],超出1的部分使用黑色0进行填充。
-
box_indice: 是boxes和image之间的索引,即box对应的图像索引
-
crop_size: 表示RoiAlign之后的候选区域的大小。
-
method:插值方法,默认是双线性插值
下面我们利用两张图片看下ROIAlign的效果:
- 导入工具包
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_size数据(batch_size=2)
# 对图像进行类型转换,并添加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)
输出为:
img: (2, 500, 500, 3)
一个batch中包含2个图像,每个图像的大小为500x500x3,理解为两个特征图
- ROIAlign
# 进行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)
输出为:
out: (2, 50, 50, 3)
效果展示
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()
1.3 网络结构
上述已经介绍了Mask-RCNN 的结构与FasterRCNN是相同的,增加了一个分割的头,如下所示:
骨干网络ResNet-FPN用于特征提取,RPN网络进行候选区域的提取,ROIAlign获取固定大小的特征图,头部网络包括边界框识别(分类和回归)+mask预测,具体如下所示:
mask分支是一个全卷积网络,实际工作中我们使用右图较多一些,其中使用2x2的反卷积进行上采样。预测时 mask 分支输出结果 resize 到 RoI 的大小, 然后应用 0.5 的阈值进行二值化得到最终的分割结果。
1.4 损失函数
Mask-RCNN在Faster-RCNN的基础上多了一个ROIAligin和Mask预测分支,因此Mask R-CNN的损失也是多任务损失:
L c l s L_{cls} Lcls 与faster rcnn的定义没有区别。具体来看下 L m a s k L_{mask} Lmask。
Mask分支对每个ROI区域产生一个mxmxK的输出特征图,即K个的二值掩膜图像,其中K代表目标种类数。对于预测的二值掩膜输出,对每一个像素点应用sigmoid函数,整体损失定义为平均二值交叉损失熵。对于真实类别为𝑘的𝑅𝑜𝐼,仅在第k个掩码上计算损失(其他掩码输出不计入)。这样做解耦了掩膜和种类预测。
不像FCN的做法,在每个像素点上应用softmax函数,整体采用的多任务交叉熵,这样会导致类间竞争,最终导致分割效果差。
转载:https://blog.csdn.net/mengxianglong123/article/details/125827912