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

简介: 笔记

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


 

目录
相关文章
|
人工智能 自然语言处理 搜索推荐
彻底摒弃人工标注,AutoAlign方法基于大模型让知识图谱对齐全自动化
【8月更文挑战第18天】知识图谱作为结构化语义库,在AI领域应用广泛,但构建中实体对齐难题一直存在。近期,AutoAlign提供了一种全自动对齐方案,由张锐等人研发并发布于arXiv。此方法摒弃传统的人工标注依赖,利用大型语言模型实现全自动化对齐。AutoAlign包括谓词与实体对齐两部分,通过构建谓词邻近图及计算实体嵌入,有效提升对齐性能。实验显示其性能超越现有方法,尤其适用于大规模数据集。尽管如此,AutoAlign仍面临计算资源消耗及不同领域适应性等挑战,未来需进一步优化以增强鲁棒性和泛化能力。
392 7
|
存储 安全 Linux
|
移动开发 C#
数据库系统概论期末经典大题讲解(范式提升、求闭包、求主码)
数据库系统概论期末经典大题讲解(范式提升、求闭包、求主码)
492 0
R语言错误处理与调试:如何高效调试R代码
【8月更文挑战第28天】调试R代码是一项需要不断练习和提高的技能。通过理解常见的错误类型、使用`traceback()`查看错误路径、逐步执行代码、利用`tryCatch()`捕获和处理错误、设置更严格的警告级别、利用RStudio的调试工具以及编写可复现的示例,你可以更加高效地调试R代码,并快速解决遇到的问题。
|
数据可视化 API 算法框架/工具
Python用T-SNE非线性降维技术拟合和可视化高维数据iris鸢尾花、MNIST 数据
Python用T-SNE非线性降维技术拟合和可视化高维数据iris鸢尾花、MNIST 数据
|
PyTorch 算法框架/工具 索引
PyTorch 2.2 中文官方教程(七)(2)
PyTorch 2.2 中文官方教程(七)
424 0
|
机器学习/深度学习 存储 人工智能
算法金 | LSTM 原作者带队,一个强大的算法模型杀回来了
**摘要:** 本文介绍了LSTM(长短期记忆网络)的发展背景和重要性,以及其创始人Sepp Hochreiter新推出的xLSTM。LSTM是为解决传统RNN长期依赖问题而设计的,广泛应用于NLP和时间序列预测。文章详细阐述了LSTM的基本概念、核心原理、实现方法和实际应用案例,包括文本生成和时间序列预测。此外,还讨论了LSTM与Transformer的竞争格局。最后,鼓励读者深入学习和探索AI领域。
293 7
算法金 | LSTM 原作者带队,一个强大的算法模型杀回来了
|
SQL 缓存 关系型数据库
MySQL常见问题解决和自动化安装脚本
这篇内容包含了两个主要部分:解决MySQL登录问题和处理GPG密钥问题。当MySQL密码正确但无法登录时,可以通过执行SQL命令`ALTER USER`和`flush privileges`来修改和重置密码。对于MySQL安装时的GPG密钥错误,首先需要强制删除旧的MySQL仓库包,导入新的GPG公钥,然后安装MySQL服务器。如果遇到GPG检查错误,可以使用`--nogpgcheck`参数忽略检查来安装。最后,提供了一个自动化安装MySQL的脚本,用于检查旧版本、卸载残留、安装MySQL8并启动服务。
1069 1
MySQL常见问题解决和自动化安装脚本
|
数据可视化
8个常见的数据可视化错误以及如何避免它们
本文揭示了8个数据可视化常见错误:误导色彩对比、过多的数据图表、省略基线、误导性标签、错误的可视化方法、不实的因果关系、放大有利数据和滥用3D图形。强调清晰、准确和洞察力的重要性,提醒制作者避免使用过多颜色、一次性展示大量数据、错误图表类型以及展示无关相关性等。正确可视化能有力支持决策,不应牺牲真实性以追求视觉效果。
1263 6