SSD项目
1.数据集
以VOC2007为例:
-
Annotation存放图像标注信息的文件(后缀为".xml")
-
ImageSets存放训练和测试的图像列比信息.
-
JPEGImages存放的是图像文件.
-
SegmentationClass和SegmentationObjec文件夹存放的是与图像分割相关的数据.
-
显式图像及标注信息
import mxnet as mx
import xml.etree.ElementTree as ET
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import random
#xml文件解析
def parse_xml(xml_path):
bbox= []
tree = ET.parse(xml_path)
root = tree.getroot()
objects = root.findall('object')
for object in objects:
name = object.find('name').text
bndbox = object.find('bndbox')
xmin = int(bndbox.find('xmin').text)
ymin = int(bndbox.find('ymin').text)
xmax = int(bndbox.find('xmax').text)
ymax = int(bndbox.find('ymax').text)
bbox_i = [name, xmin, ymin, xmax, ymax]
bbox.append(bbox_i)
#返回目标类别名和坐标信息
return bbox
#图像可视化
def visiualize_bbox(image, bbox, name):
fig, ax = plt.subplots()
plt.imshow(image)
colors = dict()
for bbox_i in bbox:
cls_name = bbox_i[0]
if cls_name not in colors:
colors[cls_name] = (random.random(), random.random(),
random.random())
xmin = bbox_i[1]
ymin = bbox_i[2]
xmax = bbox_i[3]
ymax = bbox_i[4]
rect = patches.Rectangle(xy=(xmin,ymin), width=xmax-xmin,
height=ymax-ymin,
edgecolor=colors[cls_name],
facecolor='None',
linewidth=3.5)
plt.text(xmin, ymin-2, '{:s}'.format(cls_name),
bbox=dict(facecolor=colors[cls_name],
alpha=0.5))
ax.add_patch(rect)
plt.axis('off')
plt.savefig('VOC_image_demo/{}_gt.png'.format(name))
plt.close()
if __name__ == '__main__':
name = '000001'
xml_path = 'VOC_image_demo/{}.xml'.format(name)
img_path = 'VOC_image_demo/{}.jpg'.format(name)
bbox = parse_xml(xml_path=xml_path)
image_string = open(img_path, 'rb').read()
image = mx.image.imdecode(image_string, flag=1).asnumpy()
visiualize_bbox(image, bbox, name)
运行结果:
由000001.jpg和000001.xml产生000001_gt.png.
2.SSD算法简介
网络结构图如下:
该算法采用的是修改后的16层VGG网络做为特征提取网络,修改的主要内容是将两个全连接层(fc6和fc7)conv6和conv7,另外将5个池化层pool5修改为不改变输入特征图的尺寸.然后在网络的后面添加一系列卷积模块(如图conv8_2,conv9_2,conv10_2,conv11_2),这样就构成了SSD的主体结构.
SSD算法采用基于多个特征层进行预测的方式来预测目标框的位置,具体而言就是conv4_3,conv7,conv8_2,conv9_2,conv10_2,conv11_2.如图,每个特征层都会有目标类别的分类支路和回归支路,这两个支路都是由特定卷积核数量的卷积层构成的,假设在某个特征层的特征图上,每个点设置了K个anchor,目标的类别数一共是N,那么分类支路的卷积核数量就是K(N+1),其中1代表背景类别;回归支路上的卷积核数量就是K*4,其中4表示坐标信息最终将这6个预测层的分类结果和回归结果分别汇总到一起就构成了整个网络的分类回归结果.
2.1 anchor
anchor是一系列固定大小,宽高比的框,这些框均匀的分布在输入图像上,而检测的目的就是基于这些anchor得到预测框的偏置信息(offset),使得anchor加上偏置信息后得道的预测框能够尽可能的接近真实目标框。
- mx.nd.contrib.MultiBoxPrior()接口的size参数可设置anchor的大小,ratio可设置anchor的宽高比。
2.1.1 特征图2×2,size=0.3,ratio=1的anchor
import mxnet as mx
import matplotlib.pyplot as plt
import matplotlib.patches as patches
input_h = 2
input_w = 2
#定义一个3通道2×2大小的特征图(此处通道数可以随意设置)
input = mx.nd.random.uniform(shape=(1,3,input_h, input_w))
print(input)
#利用mx.nd.contrib.MultiBoxPrior()接口,实现大小为0.3,宽高比为1的anchor
#因为输入特征图大小是2*2,设定的大小和宽高比只有一种,因此一共得到了4个anchor
anchors = mx.nd.contrib.MultiBoxPrior(data=input, sizes=[0.3], ratios=[1])
print(anchors)
anchors = anchors.reshape((input_h, input_w, -1, 4))
print(anchors)
#说明在2*2大小的特征图上,每个位置都生成了1个anchor
#显示4个anchor在图像上的具体位置信息
def plot_anchors(anchors, sizeNum, ratioNum):
#读取图片
img = mx.img.imread("9.1-basis/anchor_demo/000001.jpg")
#获取图像height, width和通道数(550,353,3)
height, width, _ = img.shape
#得到一个空白的框
fig, ax = plt.subplots(1)
#将图片填充到空白框内
ax.imshow(img.asnumpy())
edgecolors = ['r', 'g', 'y', 'b']
for h_i in range(anchors.shape[0]): #anchors.shape[0]=2
for w_i in range(anchors.shape[1]): #anchors.shape[1]=2
#anchors[0,0,:,:].asnumpy()=[[0.09999999 0.09999999 0.4 0.4 ]]
for index, anchor in enumerate(anchors[h_i,w_i,:,:].asnumpy()): #anchor[0]
xmin = anchor[0]*width
ymin = anchor[1]*height
xmax = anchor[2]*width
ymax = anchor[3]*height
rect = patches.Rectangle(xy=(xmin,ymin), width=xmax-xmin,
height=ymax-ymin,
edgecolor=edgecolors[index],
facecolor='None',
linewidth=1.5)
ax.add_patch(rect)
plt.savefig("9.1-basis/anchor_demo/mapSize_{}*{}_sizeNum_{}_ratioNum{}.png".format\
(anchors.shape[0],anchors.shape[1],sizeNum,ratioNum))
plot_anchors(anchors=anchors,sizeNum=1,ratioNum=1)
2.1.2 特征图2×2,size=0.3,ratio=(1,2,0.5)的anchor
input_h = 2
input_w = 2
input = mx.nd.random.uniform(shape=(1,3,input_h, input_w))
#利用mx.nd.contrib.MultiBoxPrior()接口,实现大小为0.3,宽高比为1,2,0.5的anchor
#在2*2的特征图上,每个点生成3个anchor,一共得到了12个anchor
anchors = mx.nd.contrib.MultiBoxPrior(data=input, sizes=[0.3], ratios=[1, 2, 0.5])
print(anchors)
anchors = anchors.reshape((input_h, input_w, -1, 4))
print(anchors)
print(anchors.shape)
#说明在2*2大小的特征图上,每个位置都生成了3个anchor
plot_anchors(anchors=anchors,sizeNum=1,ratioNum=3)
2.1.3 特征图2×2,size=(0.3,0.4),ratio=(1,2,0.5)的anchor
- anchor数量计算方法:
- 假设sizes[1,2,…m],ratios[1,2…n];
- sizes[0]和所有ratios组合得到n个anchors;
- size[1:]和ratio[0]组合得到m-1anchors ;
最终anchors数量=m+n-1
input_h = 2
input_w = 2
input = mx.nd.random.uniform(shape=(1,3,input_h, input_w))
#利用mx.nd.contrib.MultiBoxPrior()接口,实现大小为0.3,宽高比为1,2,0.5的anchor
#在2*2的特征图上,每个点生成4个anchor,一共16个anchor
anchors = mx.nd.contrib.MultiBoxPrior(data=input, sizes=[0.3,0.4], ratios=[1, 2, 0.5])
anchors = anchors.reshape((input_h, input_w, -1, 4))
print(anchors)
print(anchors.shape)
plot_anchors(anchors=anchors,sizeNum=2,ratioNum=3)
2.1.4 特征图5×5,size=(0.1,0.15),ratio=(1,2,0.5)的anchor
得道5×5×4=100个anchor。在实际的SSD算法中,特征图上不同点之间的anchor重叠非常多,因此基本上能覆盖所有物体。一般而言,在网络的浅层,特征图尺寸较大,比如(3838,1919),此时设置的anchor尺寸较小比如(0.1,0.2),其主要用来检测小尺寸目标,在网络的浅层,特征图尺寸较大,比如(33,11),此时设置的anchor尺寸较小比如(0.8,0.9),其主要用来检测大尺寸目标。
2.2 IoU(Intersection over Union)
IoU(Intersection over Union)是目标检测算法中用来评价2个矩形框之间相似度的指标。
IoU的值为两个矩形框相交的面积/相并的面积,简称交并比。在设定好anchor之后,需要判断每个anchor的标签,而判断的依据就是anchor和真实目标框的IoU。假设某个anchor和某个真实的目标框的IoU大于设定的阀值,则说明该anchor基本覆盖了这个目标,因此就可以认为这个anchor的类别就是这个目标的类别。
在目标检测算法中,网络的回归支路训练的目标是offset,这个offset是基于真实坐标框和anchor坐标计算的偏置,而回归支路的输出值也是这个偏置。因此回归的目的就是让预测的偏置接近于anchor和真实坐标框之间的偏置。
import mxnet as mx
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#绘制anchors
def plot_anchors(anchors, img, text, linestyle="-"):
"""
anchors为:
[[[0.136 0.48 0.552 0.742]
[0.023 0.024 0.997 0.996]]]
<NDArray 1x2x5 @cpu(0)>
"""
height, width,_ = img.shape #500 353 3
colors = ['r', 'y', 'b', 'c', 'm']
for num_i in range(anchors.shape[0]): #anchors.shape[0]=1,num_i只能取0
#anchors[0,:,:].asnumpy()为:
"""
[[0.136 0.48 0.552 0.742]
[0.023 0.024 0.997 0.996]]
"""
for index, anchor in enumerate(anchors[num_i,:,:].asnumpy()):
xmin = anchor[0] * width
ymin = anchor[1] * height
xmax = anchor[2] * width
ymax = anchor[3] * height
rect = patches.Rectangle(xy=(xmin,ymin), width=xmax-xmin,
height=ymax-ymin,edgecolor=colors[index],
facecolor='None',linestyle=linestyle,
linewidth=0.5)
#rect为:
"""
Rectangle(xy=(48.008, 240), width=146.848, height=131, angle=0)
Rectangle(xy=(8.119, 12), width=343.822, height=486, angle=0)
"""
ax.text(xmin,ymin,text[index],
bbox=dict(facecolor=colors[index],alpha=0.5))
ax.add_patch(rect)
#读入图片
img = mx.img.imread("9.1-basis/anchor_demo/000001.jpg")
#得到空白框
fig, ax = plt.subplots(1)
#将图片填充到空白框
ax.imshow(img.asnumpy())
#将图像的真实框画在图像上,ground_truth.shape为<NDArray 1x2x5 @cpu(0)>
ground_truth = mx.nd.array([[[0, 0.136, 0.48, 0.552, 0.742],
[1, 0.023, 0.024, 0.997, 0.996]]])
#此时传入的anchors.shape为<NDArray 1x2x4 @cpu(0)>
plot_anchors(anchors=ground_truth[:,:,1:], img=img,
text=['dog', 'persion'])
#接下来定义5个anchor,并将5个anchor显示在图像上
anchor = mx.nd.array([[[0.1, 0.3, 0.4, 0.6],
[0.15, 0.1, 0.85, 0.8],
[0.1, 0.2, 0.6, 0.4],
[0.25, 0.5, 0.55, 0.7],
[0.05, 0.08, 0.95, 0.9]]])
#<NDArray 1x5x4 @cpu(0)>
plot_anchors(anchors=anchor, img=img, text=['1','2','3','4','5'],
linestyle=':')
plt.savefig("9.1-basis/target_demo/anchor_gt.png")
#初始化一个分类预测值,维度是1×2×5,1代表1张图像,2代表两个类别,5代表anchor数量。
cls_pred = mx.nd.array([[[0.4, 0.3, 0.2, 0.1, 0.1],
[0.6, 0.7, 0.8, 0.9, 0.9]]])
#通过 mx.nd.contrib.MultiBoxTarget()接口获取模型训练的目标值。
tmp = mx.nd.contrib.MultiBoxTarget(anchor=anchor, #该参数在计算回归目标(offset)时用到
label=ground_truth, #该参数在计算回归目标(offset)和分类时都会用到
cls_pred=cls_pred, #该参数内容在这里并未用到,只要维度符合要求即可
overlap_threshold=0.5, #当预测框和真实框的IoU大于0.5时,预测的分类和回归目标就与真实值对应
"""
训练过程中,一个批次有多张图像,每张图像真实框数量不一定相同,采用“-1”填充
保证每张图像的真实标签相同。
"""
ignore_label=-1, #忽略掉真实框的填充值标签-1
negative_mining_ratio=3, #在对负样本过滤时,正负样本比例是1:3
"""
该参数表示计算回归目标中心点坐标(x,y)的权重是0.1,宽和高的offset权重是0.2。
"""
variances=[0.1,0.1,0.2,0.2])
print("location target: {}".format(tmp[0]))
print("location target mask: {}".format(tmp[1]))
print("classification target: {}".format(tmp[2]))
结果解析:
通过mx.nd.contrib.MultiBoxTarget()返回三个值赋给tmp。
- tmp[0]:输出的是回归支路的训练目标,我们希望回归支路的输出值和真实值的smooth L1损失值越小越好。tmp[0]的维度是1×20,1代表一张图片,20代表5个anchor,每个anchor有4个位置坐标信息,其中0代表负样本,也就是背景,结果中1号和3号anchor是背景。
- tmp[1]:输出是回归支路的mask,与正样本anchor对应的坐标用1填充,负样本anchor对应的坐标用0填充,该变量在计算回归损失时会用到,负样本不参加计算。
- tmp[2]:输出的是每个anchor的分类目标,0代表背景类,其他类别依次加1因此,dog用类别1表示,人用类别2表示。类别根据IoU值来判定。
回归坐标的计算方法:将极坐标转换为中心坐标,预测与真实框的中心点坐标相减,除以预测框的宽度/高度,再分别除以对应的权重系数,均值默认为0,权重系数对应variances设置为[0.1,0.1,0.2,0.2]。
源论文这样:
实际加了权重参数:
以2号anchor为例(0.51-0.5)/0.7/0.1约等于0.142857,与tmp[0][4]相等。
2.3 NMS(Non Maximum Suppresion)
NMS简称非极大抑制。针对一个目标,可能有几个甚至几十个预测对的候选框,NMS目的就是去掉重复的预测框。假设输入图像中类别为person的真实框有一个,这个真实框的坐标为[0.1,0.1,0.45,0.45].网络输出的预选框中,预测类别为person的框有K个,每个预测框都有一个预测类别,一个类别置信度,和4个坐标相关的值。
算法过程如下:
1.假设K个预测框中有N个预测框类别的置信大于0,找出类别置信度最大的那个框;
2.计算剩下的N-1个框和选出来的这个框的IoU值,IoU值大于预先设定阈值的框即为重复预测框(假设重复预测框有M个),剔除这M个预测框(将这M个预测框的置信度设为0,表示剔除),保留IoU值小于阈值的预测框。
3.接下来从N-1-M个候选框里,找出里信度最大的预测框,然后计算剩下的N-2-M个预测框与选出来的预测框的IoU值,同样将IoU值大于预先设定阈值的预测框剔除,保留IoU值小于阈值的预测框。
4.重复3的步骤,进行下一轮过滤,一直到所有框都过滤结束为止。最终保留的预测框就是输出结果。
5.结果,任意两个预测框的IoU值都小于设定的IoU阈值,即达到了去掉重复预测框的目的。
如下图所示:
假设预测框有7个,bbox1与bbox2,bbox4的IoU值>0.5,与bbox3和bbox5的IoU值<0.5.
如下图,踢出bbox2和bbox4,将其置信度设为0.计算bbox3与bbox5的IoU值>0.5。
如下图,剔除bbox5,将其置信度设为0.
最终输出为bbox1和bbox3.将其它预测框过滤掉。
mAP(mean Average Precision)
Precision= TP/(TP+FP);
Recall = TP/(TP+FN);