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

简介: 笔记

这篇文章是对之前搭建目标检测框架的补充内容,【搭建自己的目标检测网络】从零开始,搭建自己的基于VGG16的目标检测网络【附代码】。主要是讲解如何进行边界框的解码过程获得最终的结果。将一行一行的理解检测代码。


在目标检测预测阶段, 可以定义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()中调用该函数:

1.
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等代码详解,我将会在后面抽空整理一下。  


 

目录
相关文章
|
6月前
|
编解码 自动驾驶 测试技术
【论文速递】PETR: 用于多视图 3D 对象检测的位置嵌入变换
【论文速递】PETR: 用于多视图 3D 对象检测的位置嵌入变换
|
6月前
|
机器学习/深度学习 缓存 人工智能
大语言模型中常用的旋转位置编码RoPE详解:为什么它比绝对或相对位置编码更好?
Transformer的基石自2017年后历经变革,2022年RoPE引领NLP新方向,现已被顶级模型如Llama、Llama2等采纳。RoPE融合绝对与相对位置编码优点,解决传统方法的序列长度限制和相对位置表示问题。它通过旋转矩阵对词向量应用角度与位置成正比的旋转,保持向量稳定,保留相对位置信息,适用于长序列处理,提升了模型效率和性能。RoPE的引入开启了Transformer的新篇章,推动了NLP的进展。[[1](https://avoid.overfit.cn/post/9e0d8e7687a94d1ead9aeea65bb2a129)]
956 0
|
移动开发 文字识别 算法
论文推荐|[PR 2019]SegLink++:基于实例感知与组件组合的任意形状密集场景文本检测方法
本文简要介绍Pattern Recognition 2019论文“SegLink++: Detecting Dense and Arbitrary-shaped Scene Text by Instance-aware Component Grouping”的主要工作。该论文提出一种对文字实例敏感的自下而上的文字检测方法,解决了自然场景中密集文本和不规则文本的检测问题。
1947 0
论文推荐|[PR 2019]SegLink++:基于实例感知与组件组合的任意形状密集场景文本检测方法
|
1月前
|
机器学习/深度学习 JSON 算法
实例分割笔记(一): 使用YOLOv5-Seg对图像进行分割检测完整版(从自定义数据集到测试验证的完整流程)
本文详细介绍了使用YOLOv5-Seg模型进行图像分割的完整流程,包括图像分割的基础知识、YOLOv5-Seg模型的特点、环境搭建、数据集准备、模型训练、验证、测试以及评价指标。通过实例代码,指导读者从自定义数据集开始,直至模型的测试验证,适合深度学习领域的研究者和开发者参考。
388 3
实例分割笔记(一): 使用YOLOv5-Seg对图像进行分割检测完整版(从自定义数据集到测试验证的完整流程)
|
5月前
|
机器学习/深度学习 自动驾驶 机器人
【机器学习知识点】3. 目标检测任务中如何在图片上的目标位置绘制边界框
【机器学习知识点】3. 目标检测任务中如何在图片上的目标位置绘制边界框
|
1月前
|
机器学习/深度学习 JSON 算法
语义分割笔记(二):DeepLab V3对图像进行分割(自定义数据集从零到一进行训练、验证和测试)
本文介绍了DeepLab V3在语义分割中的应用,包括数据集准备、模型训练、测试和评估,提供了代码和资源链接。
186 0
语义分割笔记(二):DeepLab V3对图像进行分割(自定义数据集从零到一进行训练、验证和测试)
|
5月前
|
机器学习/深度学习 自然语言处理 算法
【CV大模型SAM(Segment-Anything)】真是太强大了,分割一切的SAM大模型使用方法:可通过不同的提示得到想要的分割目标
【CV大模型SAM(Segment-Anything)】真是太强大了,分割一切的SAM大模型使用方法:可通过不同的提示得到想要的分割目标
|
6月前
|
存储 传感器 编解码
CVPR 2023 最全分割类论文整理:图像/全景/语义/实例分割等【附PDF+代码】
CVPR 2023 最全分割类论文整理:图像/全景/语义/实例分割等【附PDF+代码】
986 1
|
机器学习/深度学习 编解码 数据可视化
ConvNeXt V2:与屏蔽自动编码器共同设计和缩放ConvNets,论文+代码+实战
ConvNeXt V2:与屏蔽自动编码器共同设计和缩放ConvNets,论文+代码+实战
|
JSON 算法 数据格式
优化cv2.findContours()函数提取的目标边界点,使语义分割进行远监督辅助标注
可以看到cv2.findContours()函数可以将目标的所有边界点都进行导出来,但是他的点存在一个问题,太过密集,如果我们想将语义分割的结果重新导出成labelme格式的json文件进行修正时,这就会存在点太密集没有办法进行修改,这里展示一个示例:没有对导出的结果进行修正,在labelme中的效果图。
212 0