PSPNet :语义分割
随着卷积神经网络在目标检测任务上的推进,它也开始被用于更精细的图像处理任务:语义分割和实例分割。目标检测只需要预测图像中每个对象的位置和类别,语义分割还要把每个像素都进行分类,而实例分割的任务则更难,要进一步把每个对象的不同实例都区分开。

图像语义分割(semantic segmentation),从字面意思上理解就是让计算机根据图像的语义来进行分割。语义分割是对图像中的每一个像素进行分类,目前广泛应用于医学图像与无人驾驶等。语义在语音识别中指的是语音的意思,在图像领域,语义指的是图像的内容,对图片意思的理解。
一、VOC2012_AUG数据集简介
VOC12_AUG:基于voc扩充的一个语义分割数据集。其组成可参考:https://blog.csdn.net/lscelory/article/details/98180917。类别继承自voc,共20个类别。
下载地址: http://home.bharathh.info/pubs/codes/SBD/download.html
img文件夹:原始数据集,内含11355张rgb图片,20类目标 + 1类背景。
cls文件夹:原始数据集,内含11355个语义分割.mat文件,label信息用数值0-21表示,0代表背景信息, 1-20代表图片中目标物体种类。

二、PSPNet模型结构
全卷积网络FCN的缺点,在于缺少合适的策略来使用全局场景分类信息。金字塔场景分析网络PSPNet通过结合局部和全局信息来提高最终预测的可靠性。
模型通过金字塔池化模块在四个不同的粗细尺度上进行特征融合。最粗尺度对特征图进行全局平均池化,产生单格输出;加细尺度把特征图分成不同子区域,产生多格输出。不同尺度级别的输出对应不同大小的特征图,然后低维特征图通过双线性插值进行上采样获得相同大小的特征。最后,不同级别的特征被拼接为最终的金字塔池化全局特征。

一、第1部分:网络输入。
inputs_size = (473, 473, 3)
inputs = Input(shape=inputs_size)
二、第2部分:特征提取网络backbone,采用mobile_net_v2结构。
通过多次卷积、池化、跨层连接进行特征提取, 最后输出两个特征层:
f4为辅助分支 - (None, 30, 30, 96) 。(备注:此分支在代码中并未用上。)
o为主干部分 - (None, 30, 30, 320)。
f4 = _inverted_res_block(x, filters=96, alpha=alpha, stride=1, rate=block4_dilation,
expansion=6, block_id=12, skip_connection=True) # (None, 30, 30, 96)
o = _inverted_res_block(x, filters=320, alpha=alpha, stride=1, rate=block5_dilation,
expansion=6, block_id=16, skip_connection=False) # (None, 30, 30, 320)
三、第3部分:利用金字塔池化模块,在四个不同的粗细尺度上进行特征融合。
主干特征提取结果feature map,shape = (None, 30, 30, 320),记为1。
对主干特征o按pool_size = (30, 30)进行池化,shape = (None, 1, 1, 80),再利用双线性插值tf.image.resize函数上采样,得到shape = (None, 30, 30, 80)的特征提取结果feature map,记为2。
对主干特征o按pool_size = (15, 15)进行池化,shape = (None, 2, 2, 80),再利用双线性插值tf.image.resize函数上采样,得到shape = (None, 30, 30, 80)的特征提取结果feature map,记为3。
对主干特征o按pool_size = (10, 10)进行池化,shape = (None, 3, 3, 80),再利用双线性插值tf.image.resize函数上采样,得到shape = (None, 30, 30, 80)的特征提取结果feature map,记为4。
对主干特征o按pool_size = (5, 5)进行池化,shape = (None, 6, 6, 80),再利用双线性插值tf.image.resize函数上采样,得到shape = (None, 30, 30, 80)的特征提取结果feature map,记为5。
将1、2、3、4、5进行特征图拼接: o = Concatenate(axis=-1)(pool_outs)
(30, 30, 320) + (30, 30, 80) + (30, 30, 80) + (30, 30, 80) + (30, 30, 80) = (30, 30, 640)
最后得到多尺度特征融合结果:shape = (None, 30, 30, 640)
四、第4部分:网络输出。
先经过一轮卷积操作,再把通道数切换成n_classes,最后tf.image.resize函数上采样。
Conv2D(out_channel//4, (3, 3),padding=‘same’, use_bias=False) # (None, 30, 30, 80)
BatchNormalization() # (None, 30, 30, 80)
Activation(‘relu’) # (None, 30, 30, 80)
Dropout(0.1) # (None, 30, 30, 80)
Conv2D(n_classes, (1, 1), padding=‘same’) # (None, 30, 30, 21)
Lambda(resize_images)([o, img_input]) # (None, 473, 473, 21)
o = Activation(“softmax”) # (None, 473, 473, 21)
三、实验过程
网络模型共有175层,训练前先导入网上下载好的mobile_net_v2权重, by_name=True, skip_mismatch=True跳过不匹配结构,然后把其对应的前146层网络冰冻起来,开始训练。
优化器adam = Adam(lr=1e-4),训练50个epoch左右,val loss在0.2附近达到瓶颈。此时对于(473, 473)大小的rgb图片,所有像素值总计分类精度达到93%左右,效果还算不错。

测试集语义分割结果如下,背景像素默认为天蓝色:












四、深入思考
Ques1:PSPNet和FCN有什么区别?
PSPNet和U-Net、FCN相比,两者区别在于特征提取的方式不同。应该说,PSPNet特征提取的效果是更佳的,它采用了更多样化的卷积尺寸,提取到的特征更具多样性。而相比起来,U-Net从头到位都是在一个feature map模板上不断上采样做操作,相比起来提取到的特征更佳单一。
U-Net上采样是利用的是反卷积操作,而PSPNet用的是双线性插值进行上采样。
Ques2:可不可以拿语义分割来做目标检测?
语义分割是对每个像素都进行分类,而实例分割进一步把每个类别的不同实例的像素都区分开。

对于单对象目标检测问题,也就是每张rbg图片上每类目标物体最多只能出现一次的情况,可以拿语义分割进行目标检测,而且此时检测效果应该会不错。但对于多个对象的目标检测,语义分割会将这些像素点全分类到一起,无法间隔开来,此时必须要借助实例分割。
Ques3:四种最常见的上采样操作:
常见的上采样方法有双线性插值、转置卷积、上采样(unsampling)和上池化(unpooling)。其中前两种方法较为常见,后两种用得较少。
(1)双线性插值。
双线性插值,又称为双线性内插。在数学上,双线性插值是对线性插值在二维直角网格上的扩展,用于对双变量函数(例如 x 和 y)进行插值。其核心思想是在两个方向分别进行一次线性插值。
在FCN中上采样用的就是双线性插值,双线性插值方法中不需要学习任何参数。
(2)反卷积。
转置卷积像卷积一样需要学习参数。如果我们想要网络学习到最好地上采样的方法,这个时候就可以采用转置卷积,它具有可以学习的参数。
可以将一个卷积操作用一个矩阵表示,无非就是将卷积核重新排列到我们可以用普通的矩阵乘法进行矩阵卷积操作。从本质来说,我们通过在输入矩阵中的元素之间插入0进行补充,从而实现尺寸上采样,然后通过普通的卷积操作就可以产生和转置卷积相同的效果。
(3)上采样(Upsamppling)。
unsampling针对对应的上采样区域,全部填充的相同的值,比较粗糙。
(4)上池化(UpPooling)。
unpooling将原始值填充到上采样对应的位置上,其他位置则以0来进行填充,比较粗糙。
Ques4:源码中的上采样插值是否过于粗糙,直接由(30, 30)上采样扩充成(473, 473)尺寸?
o = Conv2D(n_classes, (1, 1), kernel_initializer=random_normal(stddev=0.02),
padding=‘same’)(o) # (None, 30, 30, 21)
o = Lambda(resize_images)([o, img_input]) # (None, 473, 473, 21)
刚开始的时候觉得这里的resize太过粗糙了,居然直接放大了16倍,特征非常不精密。
但后来仔细想想,本来语义分割里,属于同一类的区域也常常是一大片一大片聚集的,不太可能出现不同类别像素点非常零散的分布,本就是一大片区域密集出现,因此粗糙点应该也不影响效果。
Ques5:数据处理中碰到的一些问题。
(1)对于语义分割标注图片,利用Image.open函数能直接读出png文件的标注信息,得到一个单通道矩阵,对应位置处的像素值由0-20记录,正好对应不同目标物体类别。但不能利用opencv读取,此时得到的是一个三通道矩阵,标注信息反而丢失了。
(2)对语义分割标注label的resize操作,必须采用cv2.INTER_NEAREST最邻近插值。普通线性插值、样条插值会破坏原始像素点标记信息,产生新的类别数值,标注信息不再准确。在resize图像缩放时,新的像素内容应该和周边区域是一样的,利用最邻近插值,自然标记类别也一样。
cv2.INTER_NEAREST最邻近插值完美解决了语义分割的缩放问题,以后可以针对模型任意缩放尺寸,调整至最佳语义分割效果。
Ques6:源码复现中遇到的最大bug。
我利用10000张图片进行训练,1355张图片进行测试,训练20个epoch左右,val loss降低到0.2附近出现完全瓶颈,再也没办法降低下去。

利用此时的权重进行语义分割,效果却极差。我原以为是算法或数据集的问题,损失函数在0.2附近降低不下去了,导致效果不好,模型训练效果不佳。反复debug之后,发现是检测代码有一句出现了逻辑bug。
原本写的是:
psp_model.load_weights(‘Logs/2/epoch055-loss0.050-val_loss0.223.h5’, by_name=True, skip_mismatch=True)
应该改成:
psp_model.load_weights(‘Logs/2/epoch055-loss0.050-val_loss0.223.h5’)
原来问题出在,我加载训练好的权重进行语义分割预测时,并没有完整加载进所有权重,部分网络层被skip跳过了,采用的还是最开始随机初始化的权重,模型预测效果自然极差。修改完这个bug之后,VOC2012数据集每张图片的所有像素值分类精度能达到93%左右,非常不错。
而且我发现,在冰冻backbone特征提取网络层后,剩余部分只需要用600张左右就能训练出不错的效果,val loss也能降低到0.2附近,并不需要用到上万张图片。
五、源码
主函数:
import numpy as np
import cv2
import os
from read_data_path import make_data
from psp_model import get_psp_model
from train import SequenceData
from train import train_network
from train import load_network_then_train
from detect import detect_semantic
os.environ["CUDA_VISIBLE_DEVICES"] = "1"
class_dictionary = {
0: 'background', 1: 'aeroplane', 2: 'bicycle', 3: 'bird', 4: 'boat',
5: 'bottle', 6: 'bus', 7: 'car', 8: 'cat', 9: 'chair',
10: 'cow', 11: 'dining_table', 12: 'dog', 13: 'horse', 14: 'motorbike',
15: 'person', 16: 'potted_plant', 17: 'sheep', 18: 'sofa', 19: 'train',
20: 'TV_monitor'}
if __name__ == "__main__":
train_x, train_y, val_x, val_y, test_x, test_y = make_data()
psp_model = get_psp_model()
psp_model.summary()
train_generator = SequenceData(train_x, train_y, 32)
test_generator = SequenceData(test_x, test_y, 32)
# train_network(train_generator, test_generator, epoch=10)
# load_network_then_train(train_generator, test_generator, epoch=20, input_name='first_weights.hdf5',
# output_name='second_weights.hdf5')
# detect_semantic(test_x, test_y)
read_data_path:准备数据集
import numpy as np
import cv2
import os
class_dictionary = {
0: 'background', 1: 'aeroplane', 2: 'bicycle', 3: 'bird', 4: 'boat',
5: 'bottle', 6: 'bus', 7: 'car', 8: 'cat', 9: 'chair',
10: 'cow', 11: 'dining_table', 12: 'dog', 13: 'horse', 14: 'motorbike',
15: 'person', 16: 'potted_plant', 17: 'sheep', 18: 'sofa', 19: 'train',
20: 'TV_monitor'}
# VOC2012_AUG数据集简介:
# 两个文件夹: img文件夹包含11355张rgb图片,cls文件夹包含11355个语义分割.mat文件,id序号完全对应
# 利用scipy.io.loadmat函数读取cls中的.mat文件,可以得到标注信息。
# 读取得到 (h,w) 单通道矩阵,像素值总共有21个类别,由21个数字代替:0、1、2、...、20。
# 0代表背景信息
# 1-20代表图片中目标物体种类
def read_path():
data_x = []
data_y = []
filename = os.listdir('cls')
filename.sort()
for name in filename:
serial_number = name.split('.')[0]
img_path = 'img/' + serial_number + '.jpg'
seg_path = 'cls/' + serial_number + '.mat'
data_x.append(img_path)
data_y.append(seg_path)
return data_x, data_y
def make_data():
data_x, data_y = read_path()
print('all image quantity : ', len(data_y)) # 11355
train_x = data_x[:10000]
train_y = data_y[:10000]
val_x = data_x[10000:]
val_y = data_y[10000:]
test_x = data_x[10000:]
test_y = data_y[10000:]
return train_x, train_y, val_x, val_y, test_x, test_y
mobile_netv2:特征提取backbone
from keras.activations import relu
from keras.layers import Activation, Add, BatchNormalization, Conv2D, DepthwiseConv2D, Input
from keras.initializers import random_normal
inputs_size = (473, 473, 3)
down_sample = 16
block4_dilation = 1
block5_dilation = 2
block4_stride = 2
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
if new_v < 0.9 * v:
new_v += divisor
return new_v
def relu6(x):
return relu(x, max_value=6)
def _inverted_res_block(inputs, expansion, stride, alpha, filters, block_id, skip_connection, rate=1):
in_channels = inputs.shape[-1]
point_wise_filters = _make_divisible(int(filters * alpha), 8)
prefix = 'expanded_conv_{}_'.format(block_id)
x = inputs
# 利用1x1卷积根据输入进来的通道数进行通道数上升
if block_id:
x = Conv2D(expansion * in_channels, kernel_size=1, padding='same',
kernel_initializer=

本文详细介绍了PSPNet模型,一种利用金字塔池化融合全局信息的语义分割方法,涵盖了VOC2012_AUG数据集的使用、模型结构解析、实验流程和深入讨论。PSPNet通过多尺度特征融合提升预测准确性,适用于医学图像和无人驾驶等领域。

2004

被折叠的 条评论
为什么被折叠?



