这篇文章是对之前搭建目标检测框架的补充内容,【搭建自己的目标检测网络】从零开始,搭建自己的基于VGG16的目标检测网络【附代码】_z240626191s的博客-CSDN博客。主要是讲解如何进行边界框的解码过程获得最终的结果。将一行一行的理解检测代码。
在目标检测预测阶段, 可以定义Detect()对预测结果进行解码,
参数说明:
num_classes:类的数量【包括背景类】
0指的背景类标签
200指的top_k:取出预测结果的前200个
confidence:置信度
nms_iou:NMS阈值
self.softmax = nn.Softmax(dim=-1) # 所有类的概率相加为1
# Detect(num_classes,bkg_label,top_k,conf_thresh,nms_thresh)
# top_k:一张图片中,每一类的预测框的数量
# conf_thresh 置信度阈值
# nms_thresh:值越小表示要求的预测框重叠度越小,0.0表示不允许重叠
self.detect = Detect(num_classes, 0, 200, confidence, nms_iou)
在forward()中调用该函数:
output = self.detect(
loc.view(loc.size(0), -1, 4),
self.softmax(conf.view(conf.size(0), -1, self.num_classes)),
self.priors
其中loc和conf在之前的代码有定义,现在再来看一下:
loc和conf是存储边界回归预测和分类回归预测的列表,featrue是存放预测特征的列表。
featrue中的特征层大小为:(1,1024,19,19)
self.loc是一个卷积:Conv2d(1024, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
self.conf: Conv2d(1024, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
这里在说一下为什么self.loc中的输出通道为16和为什么self.conf中的输出是8
由于预测特征层19*19中,每个网格中的先验框数量我设置的是4,并且!每个先验框可以用左上角和右小角两对坐标表示,就有4个数,所以是4*4=16,每个坐标输出通道占一个维度,所以是16维度。
而在self.conf中,由于每个先验框都会负责预测当前网格中的类【包含背景类】,我的类数量为2,所以是4*2=8,即每个先验框预测两个类【我这里是两个类,大家的不一定一样,根据自己的数据集】。
zip()是可以将传入的参数打包成一个元组,这时的x就还是等于featrue内容,l=self.loc,c=self.conf。表示对featrue进行卷积,获得边界框和分类预测。【卷积以后通过permute()改变维度,将通道这一维度放在后面】
最终得到loc和conf的shape为:[1,5776],[1,2888]。这个维度,是通过函数view()得到的,将预测结果按行铺平。则5776=19*19*16,2888=19*19*8
def forward(self, x):
loc = list()
conf = list()
featrue = list()
for k in range(len(self.backbone)):
x = self.backbone[k](x)
featrue.append(x) # 最后一个特征层为batch_size,1024,19,19
for (x, l, c) in zip(featrue, self.loc, self.conf):
loc.append(l(x).permute(0, 2, 3, 1).contiguous())
conf.append(c(x).permute(0, 2, 3, 1).contiguous())
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(
loc.view(loc.size(0), -1, 4),
self.softmax(conf.view(conf.size(0), -1, self.num_classes)),
self.priors
)
现在已经获得了conf和loc结果,接下来是对预测结果进一步处理,进行解码工作。 定义Detect函数。
class Detect(nn.Module):
def __init__(self, num_classes, bkg_label, top_k, conf_thresh, nms_thresh):
super().__init__()
self.num_classes = num_classes # 类别数量
self.background_label = bkg_label # 背景类标签
self.top_k = top_k # 预测结果取前k个结果
self.nms_thresh = nms_thresh # NMS阈值
if nms_thresh <= 0:
raise ValueError('nms_threshold must be non negative.')
self.conf_thresh = conf_thresh # 置信度
self.variance = Config['variance'] # [0.1, 0.2]
在forward()函数中,将上面得到loc和conf传进去。
参数说明:
loc_data即上面得到的loc,形状为:(1,1444,16)
conf_data即上面得到的conf,形状为:(1,1444,8)
prior_data,表示设置的默认先验框,形状为:(1444,4)
注意:这里的prior_data和loc_data虽然都是边界框信息,但表示的内容不一样!前者是每个网格中设置的默认先验框,后者是预测后的边界框回归。
def forward(self, loc_data, conf_data, prior_data):
# loc_data shape=(1,1444,4), conf_data shape=(1,1444,2), prior_data shape=(1444,4)
# prior_data 和loc_data不一样,虽然都是表示边界,但loc是预测,prior_data是默认先验框。后面预测框和默认框要匹配
loc_data = loc_data.cpu() # shape is (batch_size,1444,4)后面的两维是先验框数量和边界框坐标
conf_data = conf_data.cpu() # shape is (batch_size,1444,num_classes[包含背景类])
num = loc_data.size(0) # shape is batch_size=1
num_priors = prior_data.size(0) # 先验框数量,1444
output = torch.zeros(num, self.num_classes, self.top_k,
5) # 建立一个output阵列存放输出 shape[batch_size,num_classes,200,5] 5是包含了类[预测的是什么类]和边界框信息
# --------------------------------------#
# 对分类预测结果进行reshape
# num, num_classes, num_priors
# conf_preds shape = [batch_size,num_classes,1444]
# --------------------------------------#
conf_preds = conf_data.view(num, num_priors, self.num_classes).transpose(2,
1) # transpose是将num_priors,num_classes维度进行反转
# 对每一张图片进行处理正常预测的时候只有一张图片,所以只会循环一次
for i in range(num):
# --------------------------------------#
# 对先验框解码获得预测框
# 解码后,获得的结果的shape为
# num_priors, 4
# --------------------------------------#
# decode传入参数:loc_data[0]【得到的预测边界回归的整个矩阵信息】,(1444,4)[所有先验框的坐标信息],[0.1,0.2]
decoded_boxes = decode(loc_data[i], prior_data, self.variance) # 回归预测loc_data的结果对先验框进行调整
conf_scores = conf_preds[i].clone()
# --------------------------------------#
# 获得每一个类对应的分类结果
# num_priors,
# --------------------------------------#
for cl in range(1, self.num_classes):
# --------------------------------------#
# 首先利用门限进行判断
# 然后取出满足门限的得分
# --------------------------------------#
c_mask = conf_scores[cl].gt(self.conf_thresh)
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, self.nms_thresh, self.top_k)
output[i, cl, :count] = torch.cat((scores[ids[:count]].unsqueeze(1), boxes[ids[:count]]), 1)
return output
上述代码中的decode:
为什么要乘variances,因为在encode中除以了一个variances。除以variance是对预测box和真实box的误差进行放大,从而增加loss,增加梯度,加快收敛速度
def decode(loc, priors, variances):
# loc的shape = [1444,4],预测值
# priors的shape = (1444,4),默认先验框
# variances = [0.1, 0.2]
# priors[:, :2]取前两列 为什么要乘variances,因为在encode中除了一个variances。除以variance是对预测box和真实box的误差进行放大,从而增加loss,增加梯度,加快收敛速度
boxes = torch.cat((
priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:], # 调整先验框中心的位置 loc[:,:2]回归预测结果前两位的结果,priors[:, 2:]先验框的长和宽
priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1) # 调整后先验框的宽和高
boxes[:, :2] -= boxes[:, 2:] / 2 # 先验框的左上角
boxes[:, 2:] += boxes[:, :2] # 先验的右下角
return boxes
得到的decode结果为:
tensor([[0.0170, 0.0725, 0.2099, 0.2345],
[0.0242, 0.0514, 0.2397, 0.3136],
[0.0022, 0.0093, 0.2496, 0.1957],
...,
[0.9842, 1.0297, 0.2650, 0.4561],
[1.0425, 0.9871, 0.2142, 0.2067],
[0.9617, 0.9852, 0.1966, 0.3825]])
In [19]: boxes.shape
Out[19]: torch.Size([1444, 4])
再看decode后的下一行代码:
conf_scores = conf_preds[i].clone()
其中conf_preds是shape为[batch_size,num_classes,1444],表示的是每个类中含1444个先验框进行预测,由于我加上背景类是两个类,所以可以输出一下结果,共两行【第一行为背景类,后面的才是自己的类】,1444列:其中的数值为每个先验框中预测得到概率值【是经过了softmax得到的结果】。
tensor([[[0.9887, 0.9798, 0.9932, ..., 0.9954, 0.9979, 0.9935],
[0.0113, 0.0202, 0.0068, ..., 0.0046, 0.0021, 0.0065]]])
再看这行代码:
c_mask = conf_scores[cl].gt(self.conf_thresh)
是将上述获得的分类分布情况,通过置信度进行过滤,gt(self.conf_thresh),返回的是True或Flase。即这1444个框中,哪个框中的概率值大于阈值。
In [7]: c_mask
Out[7]: tensor([False, False, False, ..., False, False, False])
scores = conf_scores[cl][c_mask]这句代码的意思是,由于我们已经知道哪个框中的值最大,所以在conf_scores中寻找这个值。这个值就是预测的概率值
In [10]: scores
Out[10]: tensor([0.8631])
接下来的一行的代码中,我将分开讲解展示一下输出的到底是什么:
l_mask = c_mask.unsqueeze(1).expand_as(decoded_boxes)
通过unsqueeze(1)变成了列的形式
c_mask.unsqueeze(1)
Out[13]:
tensor([[False],
[False],
[False],
...,
[False],
[False],
[False]])
In [15]: decoded_boxes
Out[15]:
tensor([[-0.0879, -0.0447, 0.1220, 0.1898],
[-0.0956, -0.1054, 0.1441, 0.2082],
[-0.1226, -0.0885, 0.1270, 0.1072],
...,
[ 0.8517, 0.8016, 1.1167, 1.2577],
[ 0.9354, 0.8838, 1.1496, 1.0904],
[ 0.8635, 0.7940, 1.0600, 1.1765]])
通过 expand_as()函数,将c_mask变成和decoded_boxes一样形状的tensor.一共1444行,4列,其中一行都为True的先验框即表示预测的目标,4列的内容就是该框的坐标信息
In [14]: c_mask.unsqueeze(1).expand_as(decoded_boxes)
Out[14]:
tensor([[False, False, False, False],
[False, False, False, False],
[False, False, False, False],
...,
[False, False, False, False],
[False, False, False, False],
[False, False, False, False]])
boxes = decoded_boxes[l_mask].view(-1, 4)
获得预测框的边界信息。
Out[20]: tensor([0.3035, 0.2699, 0.8175, 0.9886])
最后会经过NMS进行过滤,筛选出最终的结果。NMS我将会在后面的文章进行补充,欢迎关注我~
ids, count = nms(boxes, scores, self.nms_thresh, self.top_k)
output[i, cl, :count] = torch.cat((scores[ids[:count]].unsqueeze(1), boxes[ids[:count]]), 1)
打印一下输出,里面的有坐标信息的,其实就是前面对应都是True的那一行 。但不同的是,现在是5列。第一列是概率值,后面四列是预测的边界框坐标值【这里的坐标值是0~1之间的,是归一化后的,最终显示在图上的时候需要修改一下的】
In [20]: output
Out[20]:
tensor([[[[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
...,
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000]],[[0.8631, 0.3035, 0.2699, 0.8175, 0.9886],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
...,
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000]]]])
同时,我们也打印一下output的形状:
torch.Size([1, 2, 200, 5])
好了,整个解码过程就基本这些了,NMS等代码详解,我将会在后面抽空整理一下。