目标检测-ssd

1、特点

将检测过程整个成一个 single deep neural network。便于训练与优化,同时提高检测速度。

相对于那些需要 object proposals 的检测模型, SSD 方法完全取消了 proposals generation、pixel resampling 或者 feature resampling 这些阶段,这样使得 SSD极大提高了检测速度,更容易去优化训练,也更容易地将检测模型融合进系统之中。

针对不同大小的目标检测,传统的做法是先将图像转换成不同大小(图像金字塔),然后分别检测,最后将结果综合起来(NMS)。而SSD 将输出一系列 离散化(discretization) 的 bounding boxes,为了得到高精度的检测结果,在不同层次的 feature maps 上去 predict objectbox offsets,同时,还得到不同 aspect ratio 的 predictions。

SSD 方法的核心之一是 predict object(物体),以及其 归属类别的 score(得分);同时,在 feature map 上使用小的卷积核,去 predict 一系列 bounding boxes 的 box offsets

SSD方法的核心之二是同时采用lower和upper的feature map做检测

2、概念理解

 

feature map cell:将 feature map 切分成 8×8 或者 4×4 之后的一个个 格子

default box :每一个格子上,一系列固定大小的 box,即图中虚线所形成的一系列 boxes。default box 很类似于 Faster R-CNN 中的 Anchor boxes,但是又不同于 Faster R-CNN 中的,在Faster RCNN中 anchor 只用在最后一个卷积层,ssd中的 Anchor boxes 用在了不同分辨率的 feature maps 上。有实验表明default box的shape数量越多,效果越好。

prior box:是指实际中选择的default box(每一个feature map cell 不是k个default box都取)。也就是说default box是一种概念,prior box则是实际的选取。训练中一张完整的图片送进网络获得各个feature map,对于正样本训练来说,需要先将prior boxground truth box匹配,匹配成功说明这个prior box所包含的是个目标,但离完整目标的ground truth box还有段距离,训练的目的是保证default box的分类confidence的同时将prior box尽可能回归到ground truth box。 举个列子:假设一个训练样本中有2个ground truth box,所有的feature map中获取的prior box一共有8732个。那个可能分别有10、20个prior box能分别与这2个ground truth box匹配上。训练的损失包含定位损失和回归损失两部分。

 

假设每个feature map cell有k个default box,那么对于每个default box都需要预测c个类别score和4个offset,那么如果一个feature map的大小是m×n,也就是有m*n个feature map cell,那么这个feature map就一共有(c+4)*k * m*n 个输出。

这些输出个数的含义是:采用3×3的卷积核对该层的feature map卷积时卷积核的个数,包含两部分(实际code是分别用不同数量的3*3卷积核对该层feature map进行卷积):数量c*k*m*nconfidence输出,表示每个default box的confidence,也就是类别的概率;数量4*k*m*nlocalization输出,表示每个default box回归后的坐标)。

3、model

算法的主网络结构是VGG16,将最后两个全连接层改成卷积层,并随后增加了4个卷积层来构造网络结构。对其中5种不同的卷积层的输出(feature map)分别用两个不同的 3×3 的卷积核进行卷积,一个输出分类用的confidence,每个default box 生成21个类别confidence;一个输出回归用的 localization,每个 default box 生成4个坐标值(x, y, w, h)。此外,这5个feature map还经过 PriorBox 层生成 prior box(生成的是坐标)。上述5个feature map中每一层的default box的数量是给定的(8732个)。最后将前面三个计算结果分别合并然后传给loss层。

 

SSD的结构在VGG16网络的基础上进行修改,训练时同样为conv1_1,conv1_2,conv2_1,conv2_2,conv3_1,conv3_2,conv3_3,conv4_1,conv4_2,conv4_3,conv5_1,conv5_2,conv5_3(512),fc6经过3*3*1024的卷积(原来VGG16中的fc6是全连接层,这里变成卷积层,下面的fc7层同理),fc7经过1*1*1024的卷积,conv6_1,conv6_2(对应上图的conv8_2),conv7_1,conv7_2,conv,8_1,conv8_2,conv9_1,conv9_2,loss。然后一方面:针对conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)(括号里数字是每一层选取的default box种类)中的每一个再分别采用两个3*3大小的卷积核进行卷积,这两个卷积核是并列的(括号里的数字代表prior box的数量,可以参考Caffe代码,所以上图中SSD结构的倒数第二列的数字8732表示的是所有prior box的数量,是这么来的38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+1*1*4=8732),这两个3*3的卷积核一个是用来做localization的(回归用,如果prior box是6个,那么就有6*4=24个这样的卷积核,卷积后map的大小和卷积前一样,因为pad=1,下同),另一个是用来做confidence的(分类用,如果prior box是6个,VOC的object类别有20个,那么就有6*(20+1)=126个这样的卷积核)。如下图是conv6_2的localizaiton的3*3卷积核操作,卷积核个数是24(6*4=24,由于pad=1,所以卷积结果的map大小不变,下同):这里的permute层就是交换的作用,比如你卷积后的维度是32×24×19×19,那么经过交换层后就变成32×19×19×24,顺序变了而已。而flatten层的作用就是将32×19×19×24变成32*8664,32是batchsize的大小。另一方面结合conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)中的每一个和数据层(ground truth boxes)经过priorBox层生成prior box。

经过上述两个操作后,对每一层feature的处理就结束了。对前面所列的5个卷积层输出都执行上述的操作后,就将得到的结果合并:采用Concat,类似googleNet的Inception操作,是通道合并而不是数值相加。 

SSD 是基于一个前向传播 CNN 网络,产生一系列 固定大小(fixed-size) 的 bounding boxes,以及每一个 box 中包含物体实例的可能性,即 score。之后,进行一个 非极大值抑制(Non-maximum suppression) 得到最终的 predictions。

Convolutional predictors for detection 
每一个添加的特征层(或者在基础网络结构中的特征层),可以使用一系列 convolutional filters,去产生一系列固定大小的 predictions。对于一个大小为 m×n,具有 p 通道的特征层,使用的 convolutional filters 就是 3×3×p的 kernels。产生的 predictions,要么就是归属类别的一个得分,要么就是相对于 default box coordinate 的 shape offsets。 
在每一个 m×n 的特征图位置上,使用上面的 3×3 的 kernel,会产生一个输出值。bounding box offset 值是输出的 default box 与此时 feature map location 之间的相对距离(YOLO 架构则是用一个全连接层来代替这里的卷积层)。

Default boxes and aspect ratios 
每一个 box 相对于与其对应的 feature map cell 的位置是固定的。 在每一个 feature map cell 中,我们要 predict 得到的 box 与 default box 之间的 offsets,以及每一个 box 中包含物体的 score(每一个类别概率都要计算出)。 
因此,对于一个位置上的 k 个boxes 中的每一个 box,我们需要计算出 c 个类,每一个类的 score,还有这个 box 相对于 它的默认 box 的 4 个偏移值(offsets)。于是,在 feature map 中的每一个 feature map cell 上,就需要有 (c+4)×k 个 filters。对于一张 m×n 大小的 feature map,即会产生 (c+4)×k×m×n 个输出结果。

4、训练过程

 在训练时,SSD 与那些用 region proposals + pooling 方法的区别是,SSD 训练图像中的 groundtruth 需要赋予到那些固定输出的 boxes 上。SSD 输出的是事先定义好的,一系列固定大小的 bounding boxes。

如下图中,狗狗的 groundtruth 是红色的 bounding boxes,但进行 label 标注的时候,要将红色的 groundtruth box 赋予 图(c)中一系列固定输出的 boxes 中的一个,即 图(c)中的红色虚线框。 

当这种将训练图像中的 groundtruth 与固定输出的 boxes 对应之后,就可以 end-to-end 的进行 loss function 的计算以及 back-propagation 的计算更新了。

相关问题及解决方式

如何将 groundtruth boxes 与 default boxes 进行配对,以组成 label 呢?

在开始的时候,用 MultiBox 中的 best jaccard overlap 来匹配每一个 ground truth box 与 default box,这样就能保证每一个 groundtruth box 与唯一的一个 default box 对应起来。

但是又不同于 MultiBox ,之后又将 default box 与任何的 groundtruth box 配对,只要两者之间的 jaccard overlap 大于一个阈值,这里的阈值为 0.5。 

目标函数

训练过程中的 prior boxes 和 ground truth boxes 的匹配,基本思路是:让每一个 prior box 回归并且到 ground truth box,这个过程的调控我们需要损失层的帮助,他会计算真实值和预测值之间的误差,从而指导学习的走向。

其中:

  • N 是与 ground truth box 相匹配的 default boxes 个数

  • localization loss(loc) 是 Fast R-CNN 中 Smooth L1 Loss,用在 predict box(l 与 ground truth box(g 参数(即中心坐标位置,width、height)中,回归 bounding boxes 的中心位置,以及 width、height

  • confidence loss(conf) 是 Softmax Loss,输入为每一类的置信度 c

  • 权重项 α,设置为 1

default boxes的 scales 和aspect ratios的选取

大部分 CNN 网络在越深的层,feature map 的尺寸(size)会越来越小。这样做不仅仅是为了减少计算与内存的需求,还有个好处就是,最后提取的 feature map 就会有某种程度上的平移与尺度不变性。

同时为了处理不同尺度的物体,一些文章将图像转换成不同的尺度,将这些图像独立的通过 CNN 网络处理,再将这些不同尺度的图像结果进行综合。

但是其实,如果使用同一个网络中的、不同层上的 feature maps,也可以达到相同的效果同时在所有物体尺度中共享参数。越底层的 layers,保留的图像细节越多。

一般来说,一个 CNN 网络中不同的 layers 有着不同尺寸的 感受野(receptive fields)。这里的感受野,指的是输出的 feature map 上的一个节点,其对应输入图像上尺寸的大小。

所幸的是,SSD 结构中,default boxes 不必要与每一层 layer 的 receptive fields 对应。ssd的设计中,feature map 中特定的位置,来负责图像中特定的区域,以及物体特定的尺寸。假如我们用 m 个 feature maps 来做 predictions,每一个 feature map 中 default box 的尺寸大小计算如下: 

 

 

可以看出这种default box在不同的feature层有不同的scale,在同一个feature层又有不同的aspect ratio,因此基本上可以覆盖输入图像中的各种形状和大小的object!

(训练自己的样本的时候可以在FindMatch()之后检查是否能覆盖了所有的 ground truth box)

源代码中的 ssd_pascal.py 设计了上面几个参数值,caffe 源码 prior_box_layer.cpp 中Forward_cpu()实现。

最后会得到(38*38*4 + 19*19*6 + 10*10*6 + 5*5*6 + 3*3*4 + 1*1*4)= 8732个prior box。

在结合 feature maps 上,所有 不同尺度、不同 aspect ratios 的 default boxes,它们预测的 predictions 之后。可以想见,我们有许多个 predictions,包含了物体的不同尺寸、形状。如下图,狗狗的 ground truth box 与 4×4 feature map 中的红色 box 吻合,所以其余的 boxes 都看作负样本。 

正负样本/Hard negative mining

prior box 和 grount truth box 按照IOU(JaccardOverlap)进行匹配,匹配成功则这个prior box就是positive example(正样本),如果匹配不上,就是negative example(负样本),显然这样产生的负样本的数量要远远多于正样本。这里将前向loss进行排序,选择最高的num_sel个prior box序号集合 DD。那么如果Match成功后的正样本序号集合P。那么最后正样本集为 PDP,负样本集为 DDP同时可以通过规范num_sel的数量(是正样本数量的三倍)来控制使得最后正、负样本的比例在 1:3 左右。

1.正样本获得

我们已经在图上画出了prior box,同时也有了ground truth,那么下一步就是将prior box匹配到ground truth上,这是在 src/caffe/utlis/bbox_util.cpp 的 FindMatches 以及子函数MatchBBox函数里完成的。值得注意的是先是从groudtruth box出发给每个groudtruth box找到了最匹配的prior box放入候选正样本集,然后再从prior box出发为prior box集中寻找与groundtruth box满足IOU>0.5的一个IOU最大的prior box(如果有的话)放入候选正样本集,这样显然就增大了候选正样本集的数量。

2.负样本获得

在生成一系列的 prior boxes 之后,会产生很多个符合 ground truth box 的 positive boxes(候选正样本集),但同时,不符合 ground truth boxes 也很多,而且这个 negative boxes(候选负样本集),远多于 positive boxes。这会造成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。

因此,本文采取,先将每一个物体位置上对应 predictions(prior boxes)loss 进行排序。 对于候选正样本集:选择最高的几个prior box与正样本集匹配(box索引同时存在于这两个集合里则匹配成功),匹配不成功则删除这个正样本(因为这个正样本不在难例里已经很接近ground truth box了,不需要再训练了);对于候选负样本集:选择最高的几个prior box与候选负样本集匹配,匹配成功则作为负样本。这就是一个难例挖掘的过程,举个例子,假设在这8732个prior box里,经过FindMatches后得到候选正样本P个,候选负样本那就有8732−P个。将prior box的prediction loss按照从大到小顺序排列后选择最高的M个prior box。如果这P个候选正样本里有a个box不在这M个prior box里,将这M个box从候选正样本集中踢出去。如果这8732−P个候选负样本集中包含的8732−P有M−a个在这M个prior box,则将这M−a个候选负样本作为负样本。SSD算法中通过这种方式来保证 positives、negatives 的比例。实际代码中有三种负样本挖掘方式:

如果选择HARD_EXAMPLE方式(源于论文Training Region-based Object Detectors with Online Hard Example Mining),则默认M=64,由于无法控制正样本数量,这种方式就有点类似于分类、回归按比重不同交替训练了。
如果选择MAX_NEGATIVE方式,则M=P∗neg_pos_ratio,这里当neg_pos_ratio=3的时候,就是论文中的正负样本比例1:3了。

数据增广策略

每一张训练图像,随机的进行如下几种选择:

  • 使用原始的图像
  • 采样一个 patch,与物体之间最小的 jaccard overlap 为:0.10.30.50.7 与 0.9
  • 随机的采样一个 patch

采样的 patch 与原始图像大小比例是 [0.11],aspect ratio 在 12 与 2 之间。

当 groundtruth box 的 中心(center)在采样的 patch 中时,我们保留重叠部分。

在这些采样步骤之后,每一个采样的 patch 被 resize 到固定的大小,并且以 0.5 的概率随机的 水平翻转(horizontally flipped)。

这样一个样本被诸多batch_sampler采样器采样后会生成多个候选样本,然后从中随机选一个样本送人网络训练。

5、检测应用实践

下面贴出一个使用caffe版ssd检测图片的python代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2017/10/24 9:29
# @Author  : Andes
# @Site    :
import sys
import config
import numpy as np
caffe_root = '/opt/qqw/caffe/'
sys.path.insert(0, caffe_root+'python')
import caffe
reload(sys)
sys.setdefaultencoding( "utf-8" )
from utility import get_label_dict as gld, get_path_list as gpl


class ImageDetection(object):

	def __init__(self, label_dict):
		self.net = self._init_model()
		self.transformer = self._image_transformer()
		self.label_dict = label_dict

	# SSD测试模型初始化
	def _init_model(self):
		caffe.set_device(0)
		caffe.set_mode_gpu()
		net = caffe.Net(config.DEPLOY_PATH, # deploy.prototxt文件地址
						config.WEIGHTS_PATH, # caffemodel地址
						caffe.TEST)
		# set net to batch size of 1
		net.blobs['data'].reshape(1, 3, config.IMG_RESIZE, config.IMG_RESIZE)
		return net

	# 测试图片转换
	def _image_transformer(self):
		# input preprocessing: 'data' is the name of the input blob == net.inputs[0]
		transformer = caffe.io.Transformer({'data': self.net.blobs['data'].data.shape})
		transformer.set_transpose('data', (2, 0, 1)) # python读取的图片文件格式为H×W×K,需转化为K×H×W
		transformer.set_mean('data', np.array([104, 117, 123]))  # mean pixel
		transformer.set_raw_scale('data',255) # python中将图片存储为[0, 1],而caffe中将图片存储为[0, 255],
		# 所以需要一个转换
		transformer.set_channel_swap('data', (2, 1, 0)) # caffe中图片是BGR格式,而原始格式是RGB,所以要转化
		return transformer

	def image_det(self, img_path):

		net = self.net
		try:
			image = caffe.io.load_image(img_path) # 读取的图片文件格式为H×W×K,需转化
			# print 'image:', image
		except Exception as e:
			print e
			print u'图片{}无法打开'.format(img_path)
			return False, None
		transformed_image = self.transformer.preprocess('data', image)
		net.blobs['data'].data[...] = transformed_image
		detections = net.forward()['detection_out']
		outputs = detections[0][0]
		pre_num = outputs.shape[0]
		for i in xrange(pre_num):
			if outputs[i][2] < config.CONF:
				continue
			else:
				label = int(outputs[i][1])
				score = outputs[i][2]
				xmin = outputs[i][3] # 坐标范围是0~1,需要给测试结果画框的自行百度,→_→
				ymin = outputs[i][4]
				xmax = outputs[i][5]
				ymax = outputs[i][6]
				label_name = self.label_dict[label]
				obj_list = [label, label_name, score, xmin, ymin, xmax, ymax]
				print obj_list

		return True, obj_list


def main():
	img_list = gpl.get_imglist_cnn(config.IMGS_ROOT) # 获取图片列表
	imgdet = ImageDetection(gld.get_coco80())
	for path in img_list:
		rval, obj_list = imgdet.image_det(path)
		print obj_list


if __name__ == '__main__':

	main()

get_label_dict模块代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2017/12/6 13:46
# @Author  : Andes
# @Site    : 
import config

def get_coco80(): label_dict = {} with open(config.COCO80_PATH, 'r') as f: for line in f: line = line.strip('\n') label, label_name = line.split(',') label = int(label) label_dict[label] = label_name return label_dict def main(): pass if __name__ == '__main__': main()

get_path_list模块代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2017/10/24 9:12
# @Author  : Andes
# @Site    : 
import os


def get_imglist_cnn(root_path):

	img_list = []
	for (r, d, f) in os.walk(root_path):
		for line in f:
			# 想要什么格式的图片自己定后缀就行,当然前提是后缀名和图片格式一致→_→
			if line.endswith('png') or line.endswith('jpg') or line.endswith('jpeg') \ 
					or line.endswith('PNG') or line.endswith('JPG') or line.endswith('JPEG'):
				img_list.append(os.path.join(r, line))
	return img_list


def main():

	pass


if __name__ == '__main__':

	main()

常量配置示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2017/12/20 10:59
# @Author  : Andes
# @Site    : 
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2017/12/6 15:01
# @Author  : Andes
# @Site    :
DEPLOY_PATH = '/opt/alpen/qqw/SSD_300x300/deploy.prototxt'
WEIGHTS_PATH = '/opt/alpen/qqw/SSD_300x300/VGG_coco_SSD_300x300_iter_400000.caffemodel'
IMGS_ROOT = '/opt/alpen/qqw/data_mass/ssd_imgs'
COCO80_PATH = '/opt/alpen/qqw/data_mass/label_files/label_coco80.txt'
CONF = 0.5
IMG_RESIZE = 300

label文件示例:

 

注意事项

1. 使用batch_sampler做data argument时要注意是否crop的样本只包含目标很小一部分。

2.检查对于你的样本来说回归和分类问题哪个更难,以此调整multibox_loss_param中loc_weight进行训练。

3.正负样本比例,HARD_EXAMPLE方式默认只取64个最高predictions loss来从中寻找负样本,检查你的样本集中正负样本比例是否合适。

posted on 2017-12-12 16:05  执剑长老  阅读(928)  评论(0编辑  收藏  举报