补充内容--目标检测之边界框解码【附代码】

这篇文章是对之前搭建目标检测框架的补充内容,【搭建自己的目标检测网络】从零开始,搭建自己的基于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等代码详解,我将会在后面抽空整理一下。 

你可能感兴趣的:(搭建自己的目标检测,目标检测,人工智能,计算机视觉,深度学习)