基于SSD目标检测网络的树莓派控制系统(二)

基于PyQt5的SSD目标检测软件(二)

SSD 目标检测网络详解

一、前言

​ 这次SSD目标检测网络是基于pytorch版本的并且我的代码中去掉了cuda加速(部分代码残留没有删除),原因很简单,因为我得AMD不YES了,等有机会用intel的显卡再说吧,b站up主的原版代码有加速部分可以参考增加,不过仅使用cpu硬算这样效率确实降低了很多,有待以后更改。

二、SSD是什么?

​ 目标检测近年来已经取得了很多重要的发展,那么主流的算法也主要分为两个部分:

  • Two-stage方法,如R-CNN系列算法,其主要思路是先通过启发式方法( selective search ) 或者CNN网络(RPN)产生一系列稀疏的候选框,然后对这些候选框进行分类与回归,因为中间使用了一些图像分割方法判别特征后进行候选框的生成,所以two-stage方法的优势是准确度高;
  • One-stage方法,如Yolo 和SSD , YOLO在卷积层后接全连接层,即检测时只利用了最高层Feature maps(包括Faster RCNN也是如此),SSD主要思路是对图片进行均匀的分割如38*38而后对每一块区域进行抽样,抽样时可以采用不同尺度和长宽比, 然后利用CNN提取特征后同时进行分类与回归,整个过程只需要一步,所以其优势是速度快,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低。

SSD全称为 Single Shot MultiBox Detector,Single shot说明SSD算法属于one-stage方法,MultiBox说明SSD算法基于多框预测。 SSD是Wei Liu在ECCV 2016上提出的一种目标检测算法, 截至目前是主要的检测框架之一,相比Faster RCNN有明显的速度优势,相比YOLO又有明显的mAP优势(不过已经被CVPR 2017的YOLO9000超越)。

三、代码实现及原理介绍

1.预测部分

1.1 主干网络原理介绍

​ SSD网络模型是由VGG16网络修改而来,而VGG是由Simonyan和Ziss而man在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》提出的卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组( Visual Geometry Group )的缩写。

VGG16的原理图如下所示:

VGG16

1、一张原始图片被resize到(224,224,3)。
2、conv1两次[3,3]卷积网络,输出的特征层为64,输出为(224,224,64),再2X2最大池化,输出net为(112,112,64)。
3、conv2两次[3,3]卷积网络,输出的特征层为128,输出net为(112,112,128),再2X2最大池化,输出net为(56,56,128)。
4、conv3三次[3,3]卷积网络,输出的特征层为256,输出net为(56,56,256),再2X2最大池化,输出net为(28,28,256)。
5、conv4三次[3,3]卷积网络,输出的特征层为512,输出net为(28,28,512),再2X2最大池化,输出net为(14,14,512)。
6、conv5三次[3,3]卷积网络,输出的特征层为512,输出net为(14,14,512),再2X2最大池化,输出net为(7,7,512)。
7、利用卷积的方式模拟全连接层,效果等同,输出net为(1,1,4096)。共进行两次。
8、利用卷积的方式模拟全连接层,效果等同,输出net为(1,1,1000)。
最后输出的就是每个类的预测。

SSD网络模型图

SSD网络模型

其中不同的地方在于:

  • 将VGG16的FC6和FC7层转化为卷积层
  • 去掉所有的Dropout层和FC8层
  • 新增了Conv6、Conv7、Conv8、Conv9层
1.2 SSD_VGG(前)

SSD原理图

如图所示,当图片输入进网络模型时,首先经过VGG16网络(Conv1-FC7)然后进行特征提取进行分类和回归预测,紧接着进入增加的几个卷积层(Conv6-Conv9),每经过一层便进行一次特征提取进行分类和回归预测

a、输入一张图片后,被Resize到300x300x3的shape

b、Conv1,经过两次 [3,3] 卷积,输出的特征层为64,输出为(300,300,64),再2X2最大池化,输出net为(150,150,64)。

c、Conv2,经过两次 [3,3] 卷积,输出的特征层为128,输出net为(150,150,128),再2X2最大池化,输出net为(75,75,128)。

d、Conv3,经过三次 [3,3] 卷积,输出的特征层为256,输出net为(75,75,256),再2X2最大池化,输出net为(38,38,256)。

e、Conv4,经过三次 [3,3] 卷积,输出的特征层为512,输出net为(38,38,512),再2X2最大池化,输出net为(19,19,512)。

f、Conv5,经过三次 [3,3] 卷积,输出的特征层为512,输出net为(19,19,512),再2X2最大池化,输出net为(19,19,512)。

g、利用卷积代替全连接层,进行了两次 [3,3] 卷积,输出的特征层为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)

SSD_VGG

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
512, 512, 512]
# 300,300,3-> input
# 实际上从0层开始算 输入层不算 整体减1
# Conv1_1 300,300,64 ->1
# ReLU ->2
# Conv1_2 300,300,64 ->3
# ReLU ->4
# Pooling1 150,150,64 ->5

# Conv2_1 150,150,128 ->6
# ReLU ->7
# Conv2_2 150,150,128 ->8
# ReLU ->9
# Pooling2 75,75,128 ->10

# Conv3_1 75,75,256 ->11
# ReLU ->12
# Conv3_2 75,75,256 ->13
# ReLU ->14
# Conv3_3 75,75,256 -> 15 注意75是单数 最大池化默认ceil==false 会省略单数边缘->37
# ReLU ->16
# Pooling3 38,38,256 ->17

# Conv4_1 38,38,512 -> 18
# ReLU -> 19
# Conv4_2 38,38,512 -> 20
# ReLU -> 21
# Conv4_3 38,38,512 -> 22 做分类预测
# ReLU -> 23
# L2Norm -> 24
# Pooling4 19,19,512 -> 25 同上理

# Conv5_1 19,19,512 -> 26
# ReLU -> 27
# Conv5_2 19,19,512 -> 28
# ReLU -> 29
# Conv5_3 19,19,512 -> 30
# ReLU -> 31

# Pools 19,19,512 -> 32
# Conv6 19,19,1024 -> 33 具有膨胀率 模拟全连接
# ReLU -> 34
# Conv7 19,19,1024 -> 35 做分类预测
# ReLU -> 36

def vgg(i):
layers = []
# i = 3 三通道
in_channels = i
for v in base:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
elif v == 'C':
layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6) # 具有膨胀率 模拟全连接
conv7 = nn.Conv2d(1024, 1024, kernel_size=1) # 全连接
layers += [pool5, conv6,
nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
return layers
1.3 追加层(后)

h、Conv6,经过一次 [1,1] 卷积,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为512,因此输出的net为(10,10,512)。

i、Conv7,经过一次 [1,1] 卷积,调整通道数,一次步长为2的[3,3]卷积网络,输出的特征层为256,因此输出的net为(5,5,256)。

j、Conv8,经过一次 [1,1] 卷积,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(3,3,256)。

k、Conv9,经过一次 [1,1] 卷积,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(1,1,256)。

SSD追加层

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def add_extras(i, batch_norm=False):
# 向VGG添加了额外的图层以进行特征缩放
layers = []
in_channels = i

# Block 6
# 19,19,1024 -> 10,10,512
layers += [nn.Conv2d(in_channels, 256, kernel_size=1, stride=1)] # [0]
# C6_2
layers += [nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)]

# Block 7
# 10,10,512 -> 5,5,256
layers += [nn.Conv2d(512, 128, kernel_size=1, stride=1)]
layers += [nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)]

# Block 8
# 5,5,256 -> 3,3,256
layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]

# Block 9
# 3,3,256 -> 1,1,256
layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]

return layers
1.4.从特征获取预测结果

特征预测

​ 红框部分圈出SSD网络模型在预测时与yolo最大得差别就是:YOLO在卷积层后接全连接层,即检测时只利用了最高层Feature maps(包括Faster RCNN也是如此),而SSD是取 Conv4的第三次卷积(Conv4_3即第21层)的特征、FC7的特征、Conv6的第二次卷积(Conv6_2)的特征、Conv7的第二次卷积(Conv7_2)的特征、Conv8的第二次卷积(Conv8_2)的特征、Conv9的第二次卷积(Conv9_2)的特征,一共6个特征层,为了和普通特征层区分,我们称之为有效特征层,来获取预测结果。Relu激活函数和L2Norm并不影响特征。

对获取到的每一个有效特征层,我们分别对其进行一次num_priors x 4的卷积、一次num_priors x num_classes的卷积、并需要计算每一个有效特征层对应的先验框。而num_priors指的是该特征层所拥有的先验框数量。

其中:
num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为ssd的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。

所有的特征层对应的预测结果的shape如下:
回归和分类特征提取shape

注:表格中num_classes 为 21(20类+1背景) 所以 卷积后 num_priors x num_classes = 4x21 =84
**代码实现:**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class SSD(nn.Module):
def __init__(self, phase, base, extras, head, num_classes):
super(SSD, self).__init__()
self.phase = phase
self.num_classes = num_classes
self.cfg = Config
self.vgg = nn.ModuleList(base)
self.L2Norm = L2Norm(512, 20)
self.extras = nn.ModuleList(extras)
self.priorbox = PriorBox(self.cfg)
with torch.no_grad():
self.priors = self.priorbox.forward()
self.loc = nn.ModuleList(head[0])
self.conf = nn.ModuleList(head[1])
if phase == 'test':
self.softmax = nn.Softmax(dim=-1)
self.detect = Detect(self)

def forward(self, x):
sources = list()
loc = list()
conf = list()

# 获得conv4_3的内容 relu层也算 Pooling不进行relu 一共36层 0-22=1-23
for k in range(23):# 22层
x = self.vgg[k](x)

s = self.L2Norm(x)# L2标准化 原因:深度不够 24层
sources.append(s)

# 获得fc7的内容
for k in range(23, len(self.vgg)):#23-34=24-35层
x = self.vgg[k](x)
sources.append(x) #FC7_1

# 获得后面的内容
for k, v in enumerate(self.extras):
x = F.relu(v(x), inplace=True)# 这里加了relu所以在网络中没有显示
if k % 2 == 1:
sources.append(x)

# [batch_size,channel
# 添加回归层和分类层
for (x, l, c) in zip(sources, self.loc, self.conf):
loc.append(l(x).permute(0, 2, 3, 1).contiguous()) # permute 通道数翻转
conf.append(c(x).permute(0, 2, 3, 1).contiguous())

# 进行resize
loc = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
conf = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
if self.phase == "test":
output = self.detect.apply(loc.view(loc.size(0), -1, 4),
self.softmax(conf.view(conf.size(0), -1, self.num_classes)),
self.priors)
else:
output = (
loc.view(loc.size(0), -1, 4),
conf.view(conf.size(0), -1, self.num_classes),
self.priors
)
return output

mbox = [4, 6, 6, 6, 4, 4]


def get_ssd(phase, num_classes):
vgg, extra_layers = add_vgg(3), add_extras(1024)

loc_layers = []
conf_layers = []
vgg_source = [21, -2]
for k, v in enumerate(vgg_source):
# k是索引,v是枚举出来得对象(0 21) (1 -2)提取出21层和33层对应(4,6)
loc_layers += [nn.Conv2d(vgg[v].out_channels,
mbox[k] * 4, kernel_size=3, padding=1)]
conf_layers += [nn.Conv2d(vgg[v].out_channels,
mbox[k] * num_classes, kernel_size=3, padding=1)]

for k, v in enumerate(extra_layers[1::2], 2): # (2,1)(3,3)(4,5)(5,7)
loc_layers += [nn.Conv2d(v.out_channels, mbox[k]
* 4, kernel_size=3, padding=1)]
conf_layers += [nn.Conv2d(v.out_channels, mbox[k]
* num_classes, kernel_size=3, padding=1)]

SSD_MODEL = SSD(phase, vgg, extra_layers, (loc_layers, conf_layers), num_classes)
return SSD_MODEL
1.5 预测结果得解码

feature

我们通过对每一个特征层的处理,可以获得三个内容,分别是:

线路1: 经过一次卷积后,生成了 **[1, num_class*num_priorbox, layer_height, layer_width]**大小的feature用于softmax分类目标和非目标(其中num_class是目标类别,SSD300中num_class = 21,即20个类别+1个背景)

num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

线路2 : 经过一次卷积后, 生成了**[1, 4*num_priorbox, layer_height, layer_width]**大小的feature用于bounding box regression(边界框回归 在训练中 微调边界框大小匹配真实框)(即每个点一组[dxmin,dymin,dxmax,dymax])

线路3: 生成了**[1, 2, 4*num_priorbox*layer_height*layer_width]**大小的prior box blob,其中2个channel分别存储prior box的4个点坐标(x1, y1, x2, y2)和对应的4个参数variance

num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。

每一个有效特征层所对应的先验框 是对应着该特征层上 每一个网格点上 预先设定好的多个框(如:Conv4_3->5776个)

我们利用 num_priors x 4 的卷积每一个有效特征层对应的先验框 匹配获得框的预测位置。

每一个有效特征层对应的先验框就是,如图所示的作用:

​ 每一个有效特征层将整个图片分成与其长宽对应的网格,如conv4-3的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,如conv4-3的特征层就是建立了4个先验框;对于conv4-3的特征层来讲,整个图片被分成38x38个网格,每个网格中心对应4个先验框,一共包含了,38x38x4个,5776个先验框。

prior

先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,ssd利用num_priors x 4的卷积的结果对先验框进行调整。

num_priors x 4中的num_priors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

x_offset与y_offset代表了真实框中心距离先验框中心的xy轴偏移情况。
h和w代表了真实框的宽与高相对于先验框的变化情况。

SSD解码过程就是将每个网格的中心点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选这一部分基本上是所有目标检测通用的部分。

  • 取出每一类得分大于Config["nms_thresh"]的框和得分。
  • 利用框的位置和得分进行非极大抑制(NMS)生成预测框。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Detect(Function):
# 回归预测结果,分类预测结果,先验框 静态方法
@staticmethod
def forward(self, loc_data, conf_data, prior_data):
if Config['nms_thresh'] <= 0:
raise ValueError('nms_threshold must be non negative.')
loc_data = loc_data.cpu()
conf_data = conf_data.cpu()
# 图片数量 预测一般一张
num = loc_data.size(0) # batch size 一张图片
# 先验框数量 8732
num_priors = prior_data.size(0)
# 存放输出(1,类别,200)
output = torch.zeros(num, Config['num_classes'], Config["top_k"], 5)

# 分类预测结果转换(1,8732,种类)torch.transpose(input, dim0, dim1, out=None) → Tensor 返回输入矩阵input的转置。交换维度dim0和dim1。 输出张量与输入张量共享内存,所以改变其中一个会导致另外一个也被修改。
conf_preds = conf_data.view(num, num_priors,Config['num_classes']).transpose(2, 1)
# 对每一张图片进行处理
for i in range(num):
# 对先验框解码获得预测框
decoded_boxes = decode(loc_data[i], prior_data, Config['variance'])
# 取出某一图片所有先验框种类
conf_scores = conf_preds[i].clone()

for cl in range(1, Config['num_classes']):
# 对每一类进行非极大抑制
# gt(a,b) 相当于 a > b conf_thresh阈值0.01 返回(True,False)
c_mask = conf_scores[cl].gt(Config["conf_thresh"])
# 两组合并去除false对应数据数据
scores = conf_scores[cl][c_mask]
if scores.size(0) == 0:
continue
l_mask = c_mask.unsqueeze(1).expand_as(decoded_boxes)
boxes = decoded_boxes[l_mask].view(-1, 4)
# 进行非极大抑制
ids, count = nms(boxes, scores, Config['nms_thresh'], Config["top_k"])
output[i, cl, :count] = \
torch.cat((scores[ids[:count]].unsqueeze(1),
boxes[ids[:count]]), 1)
# 进行排序
flt = output.contiguous().view(num, -1, 5)
_, idx = flt[:, :, 0].sort(1, descending=True)
_, rank = idx.sort(1)
# 取出top_K框返回
flt[(rank < Config["top_k"]).unsqueeze(-1).expand_as(flt)].fill_(0)
return output
1.6 在原图上进行绘制

我们可以获得预测框在原图上的位置,而且这些预测框都是经过(NMS)筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。