论文:DETRs with Collaborative Hybrid Assignments Training

代码**:https://github.com/Sense-X/Co-DETR


摘要

在这篇论文中,作者观察到在DETR中将过少的 Query 分配为正样本,采用一对一的集合匹配,会导致对编码器输出的监督稀疏,严重损害编码器的区分特征学习,反之亦然,也会影响解码器中的注意力学习。

为了缓解这个问题,作者提出了一种新颖的协同混合分配训练方案,名为Co-DETR,以从多样的标签分配方式中学习更高效、更有效的基于DETR的检测器。这种新的训练方案可以通过训练多个并行辅助 Head ,以一对多的标签分配方式(如ATSS和Faster RCNN)进行监督,轻松增强端到端检测器中编码器的学习能力。此外,作者通过从这些辅助 Head 中提取正样本坐标,为解码器中的正样本的训练效率进行额外的定制化正样本 Query 。在推理过程中,这些辅助 Head 被丢弃,因此作者的方法不会引入额外的参数和计算成本到原始检测器中,也不需要手工制定的非最大抑制(NMS)。

作者进行了大量实验,以评估所提方法在DETR变种上的有效性,包括DAB-DETR、Deformable-DETR和DINO-Deformable-DETR。在COCO val上,与ViT-L Backbone 网络结合,Co-DETR取得了66.0%的AP,在LVIS val上取得了67.9%的AP,明显优于以往的方法,且模型大小要小得多。

一、简介

目标检测 是计算机视觉中的一个基本任务,要求作者定位物体并对其进行分类。开创性的R-CNN家族和一系列变种,如ATSS、RetinaNet、FCOS和PAA,显著突破了目标检测任务。其中的核心方案是一对多的标签分配,即将每个 GT 框分配给检测器输出中的多个坐标,作为与Proposal、Anchor或窗口中心合作的监督目标。尽管这些检测器表现出有望的性能,但它们严重依赖于许多手工设计的组件,如非极大值抑制过程或Anchor生成。

为了进行更灵活的端到端检测器,DEtection TRansformer(DETR) 提出将目标检测视为一个集合预测问题,并引入了基于Transformer编码器-解码器架构的一对一集合匹配方案。通过这种方式,每个 GT 框只会被分配给一个特定的 Query ,不再需要多个手工设计的编码先验知识的组件。这种方法引入了灵活的检测流程,并鼓励许多DETR变种进一步改进它。然而,普通的端到端目标检测器的性能仍然不如具有一对多标签分配的传统检测器。

在本文中,作者试图使基于DETR的检测器优于传统检测器,同时保持其端到端的优点。为了解决这一挑战, 作者关注一对一集合匹配的直观缺点,即它探索了较少的正 Query 。这将导致严重的低效训练问题。 作者从两个方面详细分析了这一问题,即编码器生成的潜在表示和解码器中的注意力学习

作者首先比较了Deformable-DETR和一对多标签分配方法之间的潜在特征的可区分性得分,其中 作者简单地用ATSS Head替换了解码器。每个空间坐标中的特征 L2-范数用于表示可区分性得分。给定编码器的输出F∈RC×H×W ,作者可以得到可区分性得分图S∈R1×H×W 。当相应区域中的得分较高时,可以更好地检测到物体。

在这里插入图片描述

如图2所示,作者通过在可区分性得分上应用不同的阈值来展示IoF-IoB曲线(IoF:前景交叉,IoB:背景交叉)。ATSS中的更高IoF-IoB曲线表明更容易区分前景和背景。图2中的示例显示,过少的正 Query 也会影响注意力学习,增加解码器中的更多正 Query 可以稍微缓解这个问题。

作者进一步在图3中可视化了可区分性得分图S。显然,一对一集合匹配中的一些显著区域的特征被充分激活,但在一对一集合匹配中探索较少。这一重要观察激发了作者提出一个简单但有效的方法,即协同混合分配训练方案(Co-DETR)。Co-DETR的关键见解是使用多样化的一对多标签分配来提高编码器和解码器的训练效率和有效性

具体而言,作者将这些 Head 与Transformer编码器的输出集成在一起。这些 Head 可以通过多样化的一对多标签分配进行监督,例如ATSS、FCOS和Faster RCNN。不同的标签分配丰富了编码器输出的监督,迫使它具有足够的区分度,以支持这些 Head 的训练收敛

为了进一步提高解码器的训练效率,作者巧妙地编码了这些辅助 Head 中正样本的坐标,包括正Anchor和正Proposal。它们被发送到原始解码器作为多组正 Query ,以预测预分配的类别和边界框。每个辅助 Head 中的正坐标都作为一个独立的组,与其他组隔离。

多样化的一对多标签分配可以引入丰富的(正 Query , GT )对以提高解码器的训练效率。请注意,在推理过程中只使用原始解码器,因此所提出的训练方案只在训练期间引入额外的开销。

在这里插入图片描述
如图3所示,Co-DETR极大地缓解了一对一集合匹配中编码器特征学习不足的问题。作为一种即插即用的方法,作者轻松地将其与不同的DETR变种结合使用,包括DAB-DETR、Deformable-DETR和DINO-Deformable-DETR。

在这里插入图片描述
正如图1所示,Co-DETR实现了更快的训练收敛速度,甚至具有更高的性能。在12轮训练中,作者将基本的Deformable-DETR的性能提高了5.8%的平均精度(AP),在36轮训练中提高了3.2%的AP。最先进的DINO-Deformable-DETR与Swin-L结合,仍然可以在COCO val上将性能从58.5%提高到59.5%的AP。令人惊讶的是,结合ViT-L Backbone网络,作者在COCO test-dev上实现了66.0%的AP,在LVIS val上实现了67.9%的AP,建立了新的最先进的检测器,而模型规模要小得多


二、本文方法

2.1.概述

在这里插入图片描述
按照标准的DETR,输入image 经过 Backbone网络和编码器以生成潜在特征。然后,多个预定义的物体 Query 通过交叉注意力与它们在解码器中进行交互。作者引入Co-DETR来通过协同混合分配训练方案定制的正 Query 生成来改善编码器中的特征学习和解码器中的注意力学习。

2.2.协同混合分配训练

为了缓解解码器中较少的正 Query 而导致的对编码器输出的稀疏监督,作者结合了不同的一对多标签分配范式,例如ATSS和Faster R-CNN,与多功能的辅助 Head 。不同的标签分配丰富了对编码器输出的监督,迫使它具有足够的区分度来支持这些 Head 的训练收敛。

具体来说,给定编码器的潜在特征 F,通过多尺度适配器转化为特征金字塔 {F1,…FJ}J 表示具有22+J 下采样Stride的特征图。与ViTDet类似,特征金字塔由单尺度编码器中的单个特征图构建,比如双线性插值和3×3卷积进行上采样。至于多尺度编码器,只对多尺度编码器特征 F 中最粗糙的特征进行下采样,以构建特征金字塔。

定义具有相应标签分配方式AK 的K个Head :第 i 个Head ,将 {F1,…FJ} 输入获得预测值, Ai 用于计算 Pi 中的正负样本的监督目标。将G表示为 GT 集,这个过程可以被表达为:

在这里插入图片描述

其中{pos}和{neg}表示由Ai 确定的对集。j 表示在 {F1,…FJ} 中的特征索引。Bi{pos} 是正空间坐标的集合。Pi{pos}和Pi{neg} 是相应坐标中的监督目标,包括类别和回归偏移量

不同的head和标签分配方式如下表:
在这里插入图片描述

损失函数可以定义为:

在这里插入图片描述

负样本的回归损失被丢弃。K个辅助 Head 的优化的训练目标可以如下定义:

在这里插入图片描述

2.3. 定制的正 Query 生成

一对一的集合匹配范式,每个 GT 框只会被分配给一个特定的 Query 作为监督目标。正Query过少会导致Transformer解码器中的跨注意力学习效率低下Co-DETR 根据每个辅助 Head 中的标签分配 Ai 精心生成足够多的定制正 Query。
具体而言,第i 个辅助 Head 中的正坐标集合 Bi{Pos}∈RMix4,其中Mi 是正样本的数量额外的定制正Query Qi∈RMixC可以通过以下方式生成:

在这里插入图片描述

其中PE(·)代表位置编码,并根据索引对 (j,Fj 中的正坐标或负坐标)从E(·)中选择相应的特征。

结果,有K + 1组 Query 为单一的一对一集合匹配分支做出贡献,具体来说,第i 个辅助分支中第 l个解码器层的损失可以表达为:

在这里插入图片描述

P ‾ \overline{\text{P}} Pi,l 是第 i 个辅助分支中第个 l 解码器层的输出预测。最终,Co-DETR的训练目标是:

在这里插入图片描述
其中 L ‾ \overline{\text{L}} Ll dec 代表原始的一对一集合匹配分支中的损失,λ1和λ2 是平衡系数.

2.4. Co-DETR为何有效

Co-DETR显著改进了基于DETR的检测器。接下来,作者尝试从定性和定量两方面调查其有效性。作者基于使用ResNet-50 Backbone网络的Deformable-DETR,并使用36轮的设置进行了详细分析

1、丰富编码器的监督

直观地说,正 Query 过少会导致监督稀疏,因为每个 GT 只有一个 Query 通过回归损失进行监督。一对多标签分配方式中的正样本接收更多的位置监督,有助于增强潜在特征的学习

为了进一步探讨稀疏监督如何阻碍模型训练,作者详细研究了编码器产生的潜在特征。作者引入了IoF-IoB曲线来量化编码器输出的可区分性得分。具体来说,给定编码器的潜在特征F,受到图3中特征可视化的启发,作者计算了IoF (前景交叉) 和IB (背景交叉)。给定Level-j 处的编码器特征Fj∈RC×Hj×wj,首先计算L2范数 F ^ \widehat{F} F j∈R1×Hj×wj,然后将其调整为图像大小HxW。可区分性得分D(F)通过对所有Level的分数进行平均计算:

在这里插入图片描述
图3中可视化了ATSS、Deformable-DETR和作者的Co-Deformable-DETR的可区分性得分。与Deformable-DETR相比,ATSS和Co-Deformable-DETR都具有更强的区分关键目标区域的能力,而Deformable-DETR几乎被背景干扰

在这里插入图片描述

2、通过减少匈牙利匹配的不稳定性来改进跨注意力学习

匈牙利匹配是一对一集合匹配中的核心方案。跨注意力是帮助正 Query 编码丰富目标信息的重要操作。匈牙利匹配引入了不可控制的不稳定性,因为在训练过程中,分配给同一图像中特定正 Query 的 GT 会发生变化

在这里插入图片描述

图5提供了不稳定性的比较,本文方法有助于更稳定的匹配过程。此外,为了量化跨注意力优化的程度,作者还计算了关注分数的IoF-IoB曲线。与特征可区分度得分计算类似,作者为注意力分数设置不同的阈值,以获取多个IoF-IoB对。可以在图2中查看Deformable-DETR、Group-DETR和Co-Deformable-DETR之间的比较。作者发现,具有更多正 Query 的DETR的IoF-IoB曲线通常高于Deformable-DETR,这与作者的动机一致。

2.5. 与其他方法的比较

Group-DETR、H-DETR和SQR通过具有重复组和重复 GT 框的一对一匹配来执行一对多分配。Co-DETR明确为每个 GT 分配了多个空间坐标作为正 Query 。因此,这些密集的监督信号直接应用于潜在特征图,使其更具区分性。

虽然这些对手引入了更多的正 Query但由匈牙利匹配实现的一对多分配仍然受到一对一匹配的不稳定性问题的困扰。作者的方法受益于即插即用的一对多分配的稳定性,并继承了它们的正 Query 与 GT 框之间的特定匹配方式。

Group-DETR和H-DETR未能揭示一对一匹配和传统一对多分配之间的互补性。据作者所知,作者是第一个对具有传统一对多分配和一对一匹配的检测器进行定量和定性分析的研究,这有助于作者更好地理解它们的差异和互补性,从而可以自然地通过利用即插即用的一对多分配设计来提高DETR的学习能力,而不需要额外的专门的一对多设计经验。

重复的物体 Query 不可避免地会为解码器带来大量的负 Query 和显著增加GPU内存消耗。然而,作者的方法只处理解码器中的正坐标,因此内存消耗较少,如表7所示。

在这里插入图片描述

三、实验

在这里插入图片描述
表2和表3对Co-DETR在不同的DETR变种上的有效性和泛化能力进行了实证分析(结果由mmdetection复现)。

首先,作者将协作混合分配训练应用于具有C5特征的单尺度DETR,较长的训练过后,Conditional-DETR和DAB-DETR都比Baseline提高了2.4%和2.3%的AP。对于多尺度特征的DeformableDETR,检测性能从37.1%显著提高到42.9%的AP。

1 与先进方法的比较

与DeformableDETR++和DINO配对,其中K = 2。此外,作者采用了质量Focal Loss和NMS来进行作者的Co-DINO-Deformable-DETR。作者在COCO val上报告了比较结果,如表4所示。

在这里插入图片描述

与其他竞争对手相比,作者的方法收敛速度快得多。例如,只使用ResNet-50 Backbone网络的Co-DINO-Deformable-DETR在12个Epoch内就可以轻松达到52.1%的AP。作者的Swin-L方法可以在1×scheduler下获得58.9%的AP,甚至超过其他最先进的3×scheduler框架。

更重要的是,最佳模型Co-DINO-Deformable-DETR++在36个Epoch的训练下,使用ResNet-50可以实现54.8%的AP,使用Swin-L可以实现60.7%的AP,超越了所有使用相同 Backbone网络的现有检测器,差距明显。
在这里插入图片描述

作者还展示了Co-DETR在长尾LVIS检测数据集上的最佳结果。具体而言,作者使用了与COCO相同的Co-DINO-Deformable-DETR++作为模型,但选择了FedLoss作为分类损失,以弥补不平衡数据分布的影响。

在这里,作者只应用边界框监督并报告目标检测结果。比较结果见表6。Co-DETR与Swin-L在LVIS val和minival上分别取得了56.9%和62.3%的AP,超越了使用MAE预训练的ViT-H和GLIPv2的ViTDet分别+3.5%和+2.5%的AP。作者进一步在这个数据集上对Objects365预训练的Co-DETR进行了微调。
在这里插入图片描述

2.消融研究

消融实验在具有ResNet-50 Backbone网络的Deformable-DETR上进行的。作者默认选择辅助Head的数量K为1,并将总批量大小设置为32。

选择辅助Head的标准

在这里插入图片描述

作者进一步探讨了在表7和表8中选择辅助Head的标准。表8中的结果显示,任何具有一对多标签分配的辅助Head都可以稳定地提高Baseline性能,而ATSS获得了最佳性能。作者发现,当选择K小于3时,随着K的增加,准确性持续提高。值得注意的是,当K=6时性能下降,作者推测这是由于辅助Head之间的严重冲突引起的。如果特征学习在不同的辅助Head之间不一致,那么当K变大时,连续改进将被破坏。

总之,作者可以选择任何一个Head作为辅助Head,当K≤2时,作者将ATSS和Faster-RCNN视为实现最佳性能的常规做法。作者不使用太多不同的Head,例如6个不同的Head,以避免优化冲突。

3.冲突分析

当在不同的辅助Head中为相同的空间坐标分配不同的前景框或将其视为不同的背景时,会导致冲突,从而使检测器的训练变得混乱。作者首先定义Head Hi 和Head Hj 之间的距离,以及Hi 到平均距离来衡量优化冲突,如下所示:

在这里插入图片描述
在这里插入图片描述

其中,KL、D、I、C分别指的是KL散度、数据集、输入图像和类激活图(CAM)。作者计算了K>1的多个辅助Head之间的平均距离以及K=1时DETR Head和单一辅助Head之间的距离。作者发现当K=1时,每个辅助Head的距离度量是微不足道的,这与作者在表8中的结果一致:当K=1时,DETR Head可以与任何Head共同改进

当K增加到2时,距离度量略有增加,如表7所示,作者的方法实现了最佳性能。当K从3增加到6时,距离度量急剧增加,表明这些辅助Head之间的严重优化冲突导致了性能下降。然而,具有6个ATSS的Baseline可以达到49.5%的AP,并且通过将ATSS替换为6个不同的Head可以降低到48.9%的AP。因此,作者推测过多不同的辅助Head,例如超过3个不同的Head,会加剧冲突。总之,优化冲突受到不同辅助Head的数量以及这些Head之间的关系的影响。

01、是否应该添加不同的Head?

使用两个ATSS Head(49.2%的AP)进行协作训练仍然可以提高一个ATSS Head(48.7%的AP)的模型,因为根据作者的分析,ATSS是DETR Head的补充。

此外,引入一个不同于原始 Head 的多样化和互补的辅助Head,例如Faster-RCNN,可以带来更好的增益(49.5%的AP)。

02、每个组件的效果

在这里插入图片描述
每个组件的消融效果,如表9所示。引入辅助Head显著提高了性能,因为密集的空间监督使编码器特征更具判别性。另外,引入定制的正 Query 也对最终结果做出了显著贡献,同时提高了一对一集合匹配的训练效率。

03、与更长的训练计划的比较

在这里插入图片描述
如表10所示,作者发现Deformable-DETR不能从更长的训练中受益,因为性能会饱和。相反,Co-DETR大大加速了收敛速度,并提高了性能的峰值。

04、辅助分支的性能
在这里插入图片描述
令人惊讶的是,作者观察到Co-DETR对辅助Head也带来了持续的增益,如表11所示。这意味着作者的训练范式有助于更具判别性的编码器表示,从而提高了解码器和辅助Head的性能。

05、原始正 Query 和定制正 Query 的分布差异在这里插入图片描述
作者在图7a中可视化了原始正 Query 和定制正 Query 的位置。作者每张图像只显示一个对象(绿色框)。由解码器中的匈牙利匹配分配的正 Query 标记为红色。

用蓝色和橙色标记了从Faster-RCNN和ATSS中提取的正 Query ,这些定制 Query 分布在实例的中心区域周围,并为检测器提供了足够的监督信号。

作者在图7b中计算了原始 Query 和定制 Query 之间的平均距离。原始负 Query 和定制正 Query 之间的平均距离明显大于原始和定制正 Query 之间的距离。由于原始 Query 和定制 Query 之间的分布差距很小,因此在训练过程中不会遇到不稳定性问题。

四、代码分析

4.1 测试代码

第一步:针对不同backbone提取特征。这里参数配置以Resnet50为例(可选Swin Transformer、VIT)

x = self.extract_feat(img, img_metas)
#img:(bs,3,800,1333) x:5层特征图:(b,256,200,334)…(b,256,13,21)

第二步:生成mask、位置编码
主要代码:results_list = self.query_head.simple_test( x, img_metas, rescale=True),位于 projects/models/co_dino_head.py 中的CoDINOHead类。这里head可选DINO,也可选diformable DINO。下面是forward过程:

def forward(self,  mlvl_feats, img_metas, dn_label_query=None,dn_bbox_query=None, attn_mask=None):
    batch_size = mlvl_feats[0].size(0)       # 4
    input_img_h, input_img_w = img_metas[0]['batch_input_shape']  # 800 1333
        
    # 1.生成特征图大小的 mask-------------------------------------------
    img_masks = mlvl_feats[0].new_ones(  (batch_size, input_img_h, input_img_w))  # (4,800,1333)*[1]
    for img_id in range(batch_size):
        img_h, img_w, _ = img_metas[img_id]['img_shape']
        img_masks[img_id, :img_h, :img_w] = 0       # (4,800,1333)*[0]
 
    # 2. mask 位置编码(原理见博客Transformer位置编码)------------------------------------------
    mlvl_masks = []
    mlvl_positional_encodings = []
    for feat in mlvl_feats:
        mlvl_masks.append(F.interpolate(img_masks[None], size=feat.shape[-2:]).to(torch.bool).squeeze(0))   # (4,200,334*False
        mlvl_positional_encodings.append(self.positional_encoding(mlvl_masks[-1]))                        # (4,256,200,334128维的正余弦位置编码

其中,self.positional_encoding 为:

SinePositionalEncoding(num_feats=128, temperature=20, normalize=True, scale=6.28318, eps=1e-06)

第三步 :transformer过程。分为 Encoder自注意力Decoder交叉注意力 两部分。

# 3.通过mmdet 自带的Transformer 编码、解码器,对齐特征并回归出proposal坐标-------------------------------------------------------
hs, inter_references, topk_score, topk_anchor, enc_outputs = \
    self.transformer(
        mlvl_feats,                                # 5层特征
        mlvl_masks,                                # 5层 mask
        query_embeds,                              # None
        mlvl_positional_encodings,                 # 5256维的 mask位置编码
        dn_label_query,                            # None
        dn_bbox_query,                             # None
        attn_mask,                                 # None
        reg_branches=self.reg_branches if self.with_box_refine else None,  # noqa:E501
        cls_branches=self.cls_branches if self.as_two_stage else None)  # noqa:E501

# 5个输出:decoder特征(6,900,bs,256)decoder坐标(7,bs,900,4) (bs,900,80) xywh(bs,900,4)  encoder特征(89023,bs,256)            

以上代码步骤大致为:

  1. encoder过程:在特征图上部署密集的anchor,得到89023个 xywh(每层的分辨率相加);然后经历encoder对其特征,再进行分类和回归,取分数前900的proposal,跟anchor相加得到坐标和类别分数。这部分主要提供两个输出:memory(bs, 89023,256)的特征,reference_points 900个感兴趣区域的坐标。
  2. 在decoder中,query为可学习的参数(900,256),与特征memory进行解码。得到对齐的900个特征和900个坐标。self.encoder和self.decoder是重点。

以下是步骤3的具体代码,能看懂步骤的话可以跳过:

# a. 得到m拼接的 mask----------------------------------------------------------------------------------
for lvl, (feat, mask, pos_embed) in enumerate(
        zip(mlvl_feats, mlvl_masks, mlvl_pos_embeds)):
    bs, c, h, w = feat.shape                                         # (bs,256,200,334)
    spatial_shape = (h, w)
    spatial_shapes.append(spatial_shape)
    feat = feat.flatten(2).transpose(1, 2)
    mask = mask.flatten(1)
    pos_embed = pos_embed.flatten(2).transpose(1, 2)                 # (bs,66800,256)
    lvl_pos_embed = pos_embed + self.level_embeds[lvl].view(1, 1, -1)      # (bs,66800,256)
    lvl_pos_embed_flatten.append(lvl_pos_embed)
    feat_flatten.append(feat)
    mask_flatten.append(mask)
feat_flatten = torch.cat(feat_flatten, 1)           # (bs,89023,256)
mask_flatten = torch.cat(mask_flatten, 1)           # (bs,89023)*false
lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1)        # (bs,89023,256)
spatial_shapes = torch.as_tensor(
    spatial_shapes, dtype=torch.long, device=feat_flatten.device)   # (5,2):[200,334],[100,167],..[13,21] 
level_start_index = torch.cat((spatial_shapes.new_zeros(
    (1, )), spatial_shapes.prod(1).cumsum(0)[:-1]))                 # [0, 66800, 83500, 87700, 88750]
valid_ratios = torch.stack(
    [self.get_valid_ratio(m) for m in mlvl_masks], 1)               # (bs,5,2)*[1]


# b. 得到anchor坐标xywh,其中wh每层特征图固定:0.05 0.1 0.2 0.4 0.8-------------------------------------------
reference_points = self.get_reference_points(
    spatial_shapes, valid_ratios, device=feat.device)               # (bs,89023,5,2) 相当于(0,1)归一化的anchor坐标;维度5是直接复制得到的

feat_flatten = feat_flatten.permute(1, 0, 2)  # (H*W, bs, embed_dims) (89023,bs,256)
lvl_pos_embed_flatten = lvl_pos_embed_flatten.permute(
    1, 0, 2)  # (H*W, bs, embed_dims)



# c.自注意力:from mmdet.models.utils.transformer import DetrTransformerEncoder--------------------------
# 这部分是 特征图的自注意力,输入只有(展开的)特征图------------------------------------------------------------
memory = self.encoder(                                           
    query=feat_flatten,                        # (89023,bs,256)
    key=None,
    value=None,
    query_pos=lvl_pos_embed_flatten,           # (89023,bs,256)
    query_key_padding_mask=mask_flatten,       # (bs, 89023)
    spatial_shapes=spatial_shapes,             # 5个特征图shape
    reference_points=reference_points,         # anchor:(bs,89023,5,2) 
    level_start_index=level_start_index,       # [0, 66800, 83500, 87700, 88750]
    valid_ratios=valid_ratios,                 # (bs,5,2)*[1]
    **kwargs)
memory = memory.permute(1, 0, 2)               # (bs, 89023 ,256)
bs, _, c = memory.shape


# d.用卷积和linear 做特征回归和分类------------------------------------------------------------------------
output_memory, output_proposals = self.gen_encoder_output_proposals(
    memory, mask_flatten, spatial_shapes)            # memory:(bs,89023,256)false部分填充为0  xywh:(bs,89023,4)false部分填充为inf  mask来源于xy作为grid在(0.01~0.99)之间的坐标,wh的五层特征图分别为0.05 0.1 0.2 0.4 0.8
enc_outputs_class = cls_branches[self.decoder.num_layers](
    output_memory)                                   # (bs,89023,80)
enc_outputs_coord_unact = reg_branches[self.decoder.num_layers](
    output_memory) + output_proposals                # (bs,89023,4)  特征output_memory回归后,作为偏移量 + grid(xywh)
cls_out_features = cls_branches[self.decoder.num_layers].out_features    # 80
topk = self.two_stage_num_proposals                                      # 900
# NOTE In DeformDETR, enc_outputs_class[..., 0] is used for topk TODO
topk_indices = torch.topk(enc_outputs_class.max(-1)[0], topk, dim=1)[1]  #(bs,900) :对应89023个 proposal 的索引

topk_score = torch.gather( enc_outputs_class, 1,
    topk_indices.unsqueeze(-1).repeat(1, 1, cls_out_features))     # (bs,900,80)
topk_coords_unact = torch.gather( enc_outputs_coord_unact, 1,
    topk_indices.unsqueeze(-1).repeat(1, 1, 4))                    # (bs,900,4)
topk_anchor = topk_coords_unact.sigmoid()
topk_coords_unact = topk_coords_unact.detach()

query = self.query_embed.weight[:, None, :].repeat(1, bs, 1).transpose(0, 1)   # query_embed:(900,256)DETR中的可学习参数

# It is actually content query, which is named tgt in other
# DETR-like models

reference_points = topk_coords_unact                           # (bs,900,4)
reference_points = reference_points.sigmoid()



# d.decoder 部分--------------------------------------------------------------------
query = query.permute(1, 0, 2)                                # (900,bs,256)
memory = memory.permute(1, 0, 2)                              # (89023,bs,256)
inter_states, inter_references = self.decoder(
    query=query,                                              # (900,256),可学习参数
    key=None,
    value=memory,                                             # (89023,bs,256),密集特征
    attn_masks=attn_mask,                                     # None
    key_padding_mask=mask_flatten,                            # (bs,89023) *[False]
    reference_points=reference_points,                        # (bs,900,4)
    spatial_shapes=spatial_shapes,
    level_start_index=level_start_index,
    valid_ratios=valid_ratios,                                # 1
    reg_branches=reg_branches,
    **kwargs)                                                 # (6,900,bs,256) (7,bs,900,4)

inter_references_out = inter_references

return inter_states, inter_references_out, topk_score, topk_anchor, memory
# decoder特征(6,900,bs,256)decoder坐标(7,bs,900,4) (bs,900,80) xywh(bs,900,4)  encoder特征(89023,bs,256)

decoder有6层,其中每层都是一个DetrTransformer层,包含多头attention、多尺度可变形attention、FFN层:
在这里插入图片描述
详解decoder过程:

# 3.1 上步得到的900个proposal 坐标编码(具体代码在最后):----------------------------------------
query_sine_embed = self.gen_sineembed_for_position(
          reference_points_input[:, :, 0, :], self.embed_dims//2)    # (4,900,4) -->(4,900,512)  512 是坐标四个维度编码为128后拼接
query_pos = self.ref_point_head(query_sine_embed)                    # (4,900,256)  linear操作

## 3.2 MultiheadAttention----------------------------------------------------------------------
key = query                 # 输入query是可学习变量,key为None。需要给key赋值
key_pos = query_pos  if key_pos is None
key = key + key_pos
identity = query

out = nn.MultiheadAttention( query=query, key=key, value=value, attn_mask=attn_mask, key_padding_mask=key_padding_mask)[0]    
out = identity + self.dropout_layer(self.proj_drop(out))        # (89023,bs,256)

## 3.3 MultiScaleDeformableAttention---------------------------------------------------------------

identity = query                # query 是上步3.2的输出
query = query + query_pos

sampling_offsets = self.sampling_offsets(query).view(
                bs, num_query, self.num_heads, self.num_levels, self.num_points, 2)      # nn.linear()
attention_weights = self.attention_weights(query).view(
                bs, num_query, self.num_heads, self.num_levels * self.num_points)        # nn.linear()
attention_weights = attention_weights.softmax(-1)

sampling_locations = reference_points[:, :, None, :, None, :2] + sampling_offsets / self.num_points 
output = MultiScaleDeformableAttnFunction.apply(
                value, spatial_shapes, level_start_index, sampling_locations,
                attention_weights, self.im2col_step)


# 4.得到特征金字塔out(将encoder特征enc_outputs拆回5层,再加一层下采样)-------------------------------------------------
for lvl in range(num_level):
    bs, c, h, w = mlvl_feats[lvl].shape
    end = start + h*w
    feat = enc_outputs[start:end].permute(1, 2, 0).contiguous()
    start = end
    outs.append(feat.reshape(bs, c, h, w))
outs.append(self.downsample(outs[-1]))             # 6层特征


# 5.对decoder出的900个特征hs,做分类与回归(做了6次)-------------------------------
for lvl in range(hs.shape[0]):
    reference = inter_references[lvl]                     # (bs,900,4):decoder坐标
    reference = inverse_sigmoid(reference, eps=1e-3)
    outputs_class = self.cls_branches[lvl](hs[lvl])       # linear(256,80): 对decoder特征(900,bs,256)做分类,-->(bs,900,80)
    tmp = self.reg_branches[lvl](hs[lvl])                 # linear(256,4): 对decoder特征(900,bs,256)做回归,-->(bs,900,4)
    if reference.shape[-1] == 4:
        tmp += reference
    else:
        assert reference.shape[-1] == 2
        tmp[..., :2] += reference
    outputs_coord = tmp.sigmoid()
    outputs_classes.append(outputs_class)                 # (6,bs,900,80)
    outputs_coords.append(outputs_coord)                  # (6,bs,900,4)

outputs_classes = torch.stack(outputs_classes)
outputs_coords = torch.stack(outputs_coords)

return outputs_classes, outputs_coords, topk_score, topk_anchor, outs
# (6,bs,900,80)  (6,bs,900,4)  (bs,900,80)  anchor:xywh(bs,900,4) outs:6层特征
# 6.上步得到的bbox转为result(只取了6次结果的最后一次)

det_bboxes = bbox_cxcywh_to_xyxy(bbox_pred)
det_bboxes[:, 0::2] = det_bboxes[:, 0::2] * img_shape[1]
det_bboxes[:, 1::2] = det_bboxes[:, 1::2] * img_shape[0]

det_bboxes, keep_idxs = batched_nms(det_bboxes, scores, det_labels, cfg.nms)    # nms 筛选剩下800个proposal
det_labels = det_labels[keep_idxs][:cfg.max_per_img]


bbox_results = [bbox2result(det_bboxes, det_labels, self.query_head.num_classes)
            for det_bboxes, det_labels in results_list]

# 其实也就是去梯度,转为numpy:
            bboxes = bboxes.detach().cpu().numpy()
            labels = labels.detach().cpu().numpy()

4.2 训练代码

4.2.1.提取特征

x = self.extract_feat(img, img_metas)

4.2.2.DETR 的编码、解码loss

bbox_losses, x = self.query_head.forward_train(x, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None)
# 1.生成可学习变量query:--------------------------------------------------------------
dn_label_query, dn_bbox_query, attn_mask, dn_meta = self.dn_generator(gt_bboxes, gt_labels,
                                                                      self.label_embedding)  
 # label_embedding:(80,256) dn_query是随机在GroundTruth附近采样的查询向量,用来补充query(也是256维的可学习向量,数量随机)

query = torch.cat([dn_label_query, query], dim=1)                        # 900个可学习参数+196= 1096
reference_points = torch.cat([dn_bbox_query, topk_coords_unact], dim=1)


# 2.Encoder、Decoder过程同测试:---------------------------------------------------------------
hs, inter_references, topk_score, topk_anchor, enc_outputs = \
    self.transformer(
        mlvl_feats,
        mlvl_masks,
        query_embeds,
        mlvl_positional_encodings,
        dn_label_query,
        dn_bbox_query,
        attn_mask,
        reg_branches=self.reg_branches if self.with_box_refine else None,  # noqa:E501
        cls_branches=self.cls_branches if self.as_two_stage else None  # noqa:E501
    )
# 5个输出:hs:decoder特征(6,1092,bs,256), inter_references:decoder坐标(7,bs,1092,4)topk_score:encoder分类得到的score(bs,900,80)  xywh(bs,900,4)  encoder特征memory:(89023,bs,256)


# 2.5 上步transformer结果的进一步处理:-------------------------------------------------------

start = 0
for lvl in range(num_level):
    bs, c, h, w = mlvl_feats[lvl].shape
    end = start + h*w
    feat = enc_outputs[start:end].permute(1, 2, 0).contiguous()
    start = end
    outs.append(feat.reshape(bs, c, h, w))
outs.append(self.downsample(outs[-1]))            # encoder特征memory解码成6层特征图(下采样,5层再加一层)


outputs_classes = []
outputs_coords = []
# 随后将decoder特征解码为坐标,与decoder坐标相加,得到 outputs_classes 与 outputs_coords----------------
for lvl in range(hs.shape[0]):
    reference = inter_references[lvl]                     # (bs,1092,4):decoder坐标
    reference = inverse_sigmoid(reference, eps=1e-3)      # (bs,1092,4):decoder坐标
    outputs_class = self.cls_branches[lvl](hs[lvl])       # linear(256,80): 对decoder特征(900,bs,256)做分类,-->(bs,1092,80)
    tmp = self.reg_branches[lvl](hs[lvl])                 # linear(256,4): 对decoder特征(900,bs,256)做回归,-->(bs,1092,4)
    if reference.shape[-1] == 4:
        tmp += reference
    outputs_coord = tmp.sigmoid()
    outputs_classes.append(outputs_class)                 # (6,bs,900,80)
    outputs_coords.append(outputs_coord)                  # (6,bs,900,4)


outs : outputs_classes,  outputs_coords,  topk_score,  topk_anchor,  outs
#out包含5部分: decode_cls(6,bs,1092,80)decode_coord(6,bs,1092,4) proposal_score(bs,900,80)anchor(bs,900,4) encoder6层特征:memory

准备好 loss 的输入:

# 计算loss的输入:------------------------------------------------
loss_inputs = outs + (gt_bboxes, gt_labels, img_metas, dn_meta)

# 3.self.extract_dn_outputs:拆出outs中的特征:---------------------------------------------------------

all_cls_scores = all_cls_scores[:, :, :pad_size=192, :]      # denoising_cls_scores(6, bs, 192, 80)
all_bbox_preds = all_bbox_preds[:, :, :pad_size=192, :]      # denoising_bbox_preds(6, bs, 192, 4)
dn_cls_scores = all_cls_scores[:, :, pad_size=192:, :]      # matching_cls_scores(6, bs, 900, 80)
dn_bbox_preds = all_bbox_preds[:, :, pad_size=192:, :]      # matching_bbox_preds(6, bs, 900, 4

损失由3部分组成。
1.首先是encoder部分的 回归+分类损失*

# 4.计算900个 proposal 与 GroundTruth 的 loss_single
enc_loss_cls, enc_losses_bbox, enc_losses_iou = self.loss_single(enc_topk_scores, enc_topk_anchors,
                                                                 gt_bboxes_list, gt_labels_list)


    # 4.1 assign 分配GT给proposal:
	    # 4.1.1 计算类别损失focal_loss: focal_loss---------------------------------------------------
	            cls_pred = cls_pred.sigmoid()                        # 3个超参:eps=1e-12 alpha=0.25 gamma=2
	            neg_cost = -(1-cls_pred + self.eps).log()*(1-self.alpha)*cls_pred.pow(self.gamma)     # (900,80) 
	            pos_cost = -(cls_pred + self.eps).log()*self.alpha * (1-cls_pred).pow(self.gamma)     # (900,80)
	 
	            cls_cost = pos_cost[:, gt_labels] - neg_cost[:, gt_labels]    # (900,6) 6代表GT数量
	            return cls_cost * self.weight                                 # self.weight=2.0
	
	    # 4.1.2 计算 回归L1 loss:self.reg_cost:-----------------------------------------------------
	             normalize_gt_bboxes = gt_bboxes / factor            # 归一化:除以宽高
	             gt_bboxes = bbox_xyxy_to_cxcywh(gt_bboxes)
	             bbox_cost = torch.cdist(bbox_pred, gt_bboxes, p=1)    # (900,6return bbox_cost * self.weight                        # self.weight = 5.0
	
	    # 4.1.3 计算giou:--------------------------------------------------------------------------
	    iou_cost = -bbox_overlaps( bboxes, gt_bboxes, mode=giou, is_aligned=False)      # 计算GIOU(900,6)
	    return iou_cost * self.weight                                 # self.weight=2.0
	
	    # 4.1.4 根据总cost,分配标签:-----------------------------------------------------------------
	    cost = cls_cost + reg_cost + iou_cost             # (900,2)
	
	    matched_row_inds, matched_col_inds = linear_sum_assignment(cost)   
	    # SciPy库中的l函数,执行了匈牙利算法,用于根据cost分数,为每个真值找到最佳的匹配预测值

    # 4.2 得到sampling_result:分配正负样本(4GT为例)-------------------------------------------------

    self.pos_bboxes = bboxes[pos_inds]          # (4,4)
    self.neg_bboxes = bboxes[neg_inds]          # (896,4)
    self.pos_is_gt = gt_flags[pos_inds]         # [0,0,0,0]

    self.num_gts = gt_bboxes.shape[0]           # 4
    self.pos_assigned_gt_inds = assign_result.gt_inds[pos_inds] - 1         # [3,2,1,0]
    sampling_result.pos_gt_labels               # [0,0,0,0]

    # 5. 计算有所bs的 正样本vsGT 的iou: ---------------------------------------------------------------
    scores[pos_inds] = bbox_overlaps(pos_decode_bbox_pred.detach(), pos_decode_bbox_targets,is_aligned=True)      # bs*900[0],有样本的地方是IOU分数

    # 6. 计算900个样本的focal loss(结果是一个数)---------------------------------------------------------
    loss_cls = self.loss_cls(cls_scores, (labels, scores), weight=label_weights, avg_factor=cls_avg_factor)
    # cls_scores:(bs*900, 80) labels:900*[80],正样本处为0  scores见上步 label_weights:bs*900[1] cls_avg_factor:正样本数量

    # 7. 计算giou回归损失(结果是一个数)----------------------------------------------------------------
    factor = bbox_pred.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0).repeat(bbox_pred.size(0), 1)     # (bs*900,4) 图宽高

    bboxes = bbox_cxcywh_to_xyxy(bbox_preds) * factors                # (bs*900,4)
    bboxes_gt = bbox_cxcywh_to_xyxy(bbox_targets) * factors           # (bs*900,4)

    loss_iou = self.loss_iou( bboxes, bboxes_gt, bbox_weights, avg_factor=13)   # bs中共13GT
    # 计算giou:1.92  bbox_weights:正样本为1,其余0

    loss_bbox = self.loss_bbox(bbox_preds, bbox_targets, bbox_weights, avg_factor=num_total_pos)
    # 计算L1 loss

loss_dict['enc_loss_cls'] = enc_loss_cls
loss_dict['enc_loss_bbox'] = enc_losses_bbox
loss_dict['enc_loss_iou'] = enc_losses_iou

2.其次 decoder部分的 回归+分类损失*

# 计算6层 decoder损失
num_dec_layers = len(all_cls_scores)       # 6层
all_gt_bboxes_list = [gt_bboxes_list for _ in range(num_dec_layers)]
all_gt_labels_list = [gt_labels_list for _ in range(num_dec_layers)]

# 循环6遍,计算损失。具体损失过程同上(先assign,再sampler,回归、分类)--------------------------------------
losses_cls, losses_bbox, losses_iou = self.loss_single(all_cls_scores, all_bbox_preds, all_gt_bboxes_list, all_gt_labels_list)

loss_dict['loss_cls'] = losses_cls[-1]
loss_dict['loss_bbox'] = losses_bbox[-1]
loss_dict['loss_iou'] = losses_iou[-1]

for loss_cls_i, loss_bbox_i, loss_iou_i in zip(losses_cls[:-1], losses_bbox[:-1], losses_iou[:-1]):
    loss_dict[f'd{num_dec_layer}.loss_cls'] = loss_cls_i
    loss_dict[f'd{num_dec_layer}.loss_bbox'] = loss_bbox_i
    loss_dict[f'd{num_dec_layer}.loss_iou'] = loss_iou_i

3.最后 dn_query的 decoder部分的 回归+分类损失*

num_dec_layers = len(dn_cls_scores)
all_gt_bboxes_list = [gt_bboxes_list for _ in range(num_dec_layers)]
all_gt_labels_list = [gt_labels_list for _ in range(num_dec_layers)]

# 1.get_dn_target_single:生成密集的GT-----------------------------------------------------------------
   # 1.1 首先平均采样正负样本---------------------------------------------------------------------
   t = torch.range(0, len(gt_labels) - 1).long().cuda()   # [0,1,2]  3GT
   t = t.unsqueeze(0).repeat(num_groups, 1)               # (33,3) 重复num_groups=33次
   pos_assigned_gt_inds = t.flatten()
   pos_inds = (torch.tensor(range(num_groups))*single_pad).unsqueeze(1) + t     # single_pad=6
   pos_inds = pos_inds.flatten()           # 99个ind:[[0,1,2],[6,7,8],[12,13,14]...[192,193,194]]   dn_query在这个bs中是198个
   neg_inds = pos_inds + single_pad // 2      # 99个ind:pos_inds+3 :[3,4,5,  9,10,11,  15,16,17,...195,196,197]

   # 1.2 生成密集的GT_label---------------------------------------------------------------
   labels = gt_bboxes.new_full((self.num_classes=80)          # 198*[80]
   labels[pos_inds] = gt_labels[pos_assigned_gt_inds]         # 正样本处是gt的标签
   label_weights = gt_bboxes.new_ones(num_bboxes)             # 198*[1]

   # 1.3 生成密集的GT_box---------------------------------------------------------------
   bbox_targets = torch.zeros_like(dn_bbox_pred)
   bbox_weights = torch.zeros_like(dn_bbox_pred)
   bbox_weights[pos_inds] = 1.0                               # (198,4)*[0] 正样本处是1
   bbox_targets[pos_inds] = gt_bboxes_targets/(w,h,w,h).repeat(num_groups)

   # 1.4 计算正样本的box_iou:---------------------------------------------------------
   pos_bbox_targets = bbox_targets[pos_inds]
   pos_decode_bbox_targets = bbox_cxcywh_to_xyxy(pos_bbox_targets)      # (165,4)
   pos_bbox_pred = dn_bbox_preds.reshape(-1, 4)[pos_inds]
   pos_decode_bbox_pred = bbox_cxcywh_to_xyxy(pos_bbox_pred)            # (165,4)
   scores[pos_inds] = bbox_overlaps(pos_decode_bbox_pred.detach(),pos_decode_bbox_targets)  # 正样本iou:(165)
   
   # 1.5 计算focal_loss(权重为1)------------------------------------------------------
   loss_cls = self.loss_cls(cls_scores, (labels, scores),
                            weight=label_weights, avg_factor=cls_avg_factor)                # ->0.4841
              loss = F.binary_cross_entropy_with_logits(
                    pred, zerolabel, reduction='none') * scale_factor.pow(beta)    # (bs*198,80),然后求平均

   # 1.6 计算giou(权重为2,恢复原尺度):--------------------------------------------------
   loss_iou = self.loss_iou(bboxes, bboxes_gt, bbox_weights, avg_factor=num_total_pos)      # (bs*198,4) 负样本处坐标为0 

   # 1.7 计算 L1 loss(权重为5,归一化尺度计算)-------------------------------------------
        loss_bbox = self.loss_bbox(bbox_preds, bbox_targets, bbox_weights, avg_factor=num_total_pos)


# 2.记录loss(以上3个损失,计算了6次)----------------------------------------------------
loss_dict['dn_loss_cls'] = dn_losses_cls[-1]
loss_dict['dn_loss_bbox'] = dn_losses_bbox[-1]
loss_dict['dn_loss_iou'] = dn_losses_iou[-1]
num_dec_layer = 0
for loss_cls_i, loss_bbox_i, loss_iou_i in zip(
        dn_losses_cls[:-1], dn_losses_bbox[:-1],
        dn_losses_iou[:-1]):
    loss_dict[f'd{num_dec_layer}.dn_loss_cls'] = loss_cls_i
    loss_dict[f'd{num_dec_layer}.dn_loss_bbox'] = loss_bbox_i
    loss_dict[f'd{num_dec_layer}.dn_loss_iou'] = loss_iou_i
    num_dec_layer += 1

4.2.3.RPN过程和损失

RPN配置:nms_pre: 4000, max_per_img: 1000, nms: ‘iou_threshold’: 0.7

rpn_losses, proposal_list = self.rpn_head.forward_train( x, img_metas, gt_bboxes, gt_labels=None, proposal_cfg
outs = self(x)  # 输入6层x:(bs,256,334,144)  (bs,256,167,72)...(bs,256,11,5) -> conv(256,9) conv(256,36)
loss_inputs = outs + (gt_bboxes, img_metas)

# 2.生成bs个anchor,每个包含6层特征图维度的坐标(xyxy)
anchor_list, valid_flag_list = self.get_anchors( featmap_sizes, img_metas, device=device)

# 3.得到密集的gt_box-----------------------------------------------------------------
cls_reg_targets = self.get_targets( anchor_list, valid_flag_list, gt_bboxes, img_metas, gt_bboxes_ignore_list=None,
                                    gt_labels_list=gt_labels,   label_channels=label_channels)

      # 3.1 anchor和gt的iou-----------------------------------------------------------
      overlaps = self.iou_calculator(gt_bboxes, bboxes)              # (3,577296)
      
      # 3.2 根据iou阈值,采样正负样本----------------------------------------------------
      assign_result = self.assign_wrt_overlaps(overlaps, gt_labels)
      pos_inds = sampling_result.pos_inds            # (20) 个ind
      neg_inds = sampling_result.neg_inds            # (236)个ind

      # 3.gt与anchor编码(结果就是网络要预测的值,之间存在loss)---------------------------
      pos_bbox_targets = self.bbox_coder.encode(sampling_result.pos_bboxes, sampling_result.pos_gt_bboxes)        # (20,4)

      bbox_targets[pos_inds, :] = pos_bbox_targets                              # (577296,4)*[0] 其中20个正样本位置为encode值
      bbox_weights[pos_inds, :] = 1.0                                           # (577296,4)*[0] 其中20个正样本位置为1
      label_weights[pos_inds] = 1.0                                         # (577296)*[0] 其中20个正样本位置为1
      label_weights[neg_inds] = 1.0

      labels[pos_inds] = gt_labels[sampling_result.pos_assigned_gt_inds]

anchor_list.内容如下:可以看到有几层密集分布的坐标(宽高尺度不同):
在这里插入图片描述

# 4.计算密集损失(密集图像维度上一一对应)-----------------------------------------------------------------
losses_cls, losses_bbox = self.loss_single(cls_scores, bbox_preds, all_anchor_list, labels_list,
                                          label_weights_list, bbox_targets_list,bbox_weights_list,
                                          num_total_samples=512)
               # 4.1 交叉商损失(weights=12)-----------------------------------
               loss_cls = self.loss_cls(cls_score, labels, label_weights, avg_factor=512)
               
               # 4.2 L1损失(weights=12)-------------------------------------
               bbox_pred = self.bbox_coder.decode(anchors, bbox_pred)             # (865728,4)
               loss_bbox = self.loss_bbox(bbox_pred,bbox_targets, bbox_weights, avg_factor=512)

# 5. 计算proposal-----------------------------------------------------------------------------------
proposal_list = self.get_bboxes(*outs, img_metas=img_metas,proposal_cfg)
    # 5.1 生成密集anchor(逐层特征图)
		mlvl_priors = self.prior_generator.grid_priors(featmap_sizes, dtype=torch.float32, device)
    
    # 5.2 self._get_bboxes_single 得到最终结果
    for level_idx in range(len(cls_score_list)):                    # 循环6层特征图
	    rpn_cls_score = cls_score_list[level_idx].sigmoid()         # 第一层:(9,334,144) -> (432864)
	    rpn_bbox_pred = bbox_pred_list[level_idx]                   # 第一层:(36,334,144)-> (4328644)

        if 0 < nms_pre < scores.shape[0]:                           # 特征图分辨率>4000时,只选择前4000个
            ranked_scores, rank_inds = scores.sort(descending=True)
            topk_inds = rank_inds[:nms_pre]
            scores = ranked_scores[:nms_pre]
            rpn_bbox_pred = rpn_bbox_pred[topk_inds, :]
            anchors = anchors[topk_inds, :]

        mlvl_scores.append(scores)
        mlvl_bbox_preds.append(rpn_bbox_pred)
        mlvl_valid_anchors.append(anchors)                          # 前几层都是4000,后面就是w*h
        level_ids.append(scores.new_full((scores.size(0),),level_idx,dtype=torch.long))   # (4000)[0]

    # 5.3 self._bbox_post_process---------------------------------------------------------------------
    scores = torch.cat(mlvl_scores)              # (18196)
    anchors = torch.cat(mlvl_valid_anchors)      # (18196,4)
    rpn_bbox_pred = torch.cat(mlvl_bboxes)       # (18196,4)
    proposals = self.bbox_coder.decode(anchors, rpn_bbox_pred, max_shape=img_shape)      # (18196,4)
    # 上面调用了delta2bbox函数,主要就是anchor与预测值的解码,生成最终bbox。(详情可见后面拓展)

    # 5.4 NMS非极大值抑制(过滤重叠的边界框)-------------------------------------------------------------
    dets, _ = batched_nms(proposals, scores, ids, cfg.nms)     # (12871,5) ids:特征图索引[0,0,0,...1,1,1,...5,5,5]5维应该是分数

    return dets[:cfg.max_per_img]                   # 最终只取前1000个

losses.update(rpn_losses)





4.3 额外代码

1.对900个proposal的坐标做位置编码,将4维坐标转换为512维。

def gen_sineembed_for_position(pos_tensor, pos_feat=128):
    # n_query, bs, _ = pos_tensor.size()
    # sineembed_tensor = torch.zeros(n_query, bs, 256)
    scale = 2 * math.pi
    dim_t = torch.arange(
        pos_feat, dtype=torch.float32, device=pos_tensor.device)
    dim_t = 10000**(2 * (dim_t // 2) / pos_feat)
    x_embed = pos_tensor[:, :, 0] * scale
    y_embed = pos_tensor[:, :, 1] * scale
    pos_x = x_embed[:, :, None] / dim_t
    pos_y = y_embed[:, :, None] / dim_t
    pos_x = torch.stack((pos_x[:, :, 0::2].sin(), pos_x[:, :, 1::2].cos()),
                        dim=3).flatten(2)
    pos_y = torch.stack((pos_y[:, :, 0::2].sin(), pos_y[:, :, 1::2].cos()),
                        dim=3).flatten(2)

    w_embed = pos_tensor[:, :, 2] * scale
    pos_w = w_embed[:, :, None] / dim_t
    pos_w = torch.stack(
        (pos_w[:, :, 0::2].sin(), pos_w[:, :, 1::2].cos()),
        dim=3).flatten(2)

    h_embed = pos_tensor[:, :, 3] * scale
    pos_h = h_embed[:, :, None] / dim_t
    pos_h = torch.stack(
        (pos_h[:, :, 0::2].sin(), pos_h[:, :, 1::2].cos()),
        dim=3).flatten(2)

    pos = torch.cat((pos_y, pos_x, pos_w, pos_h), dim=2)
    else:
        raise ValueError('Unknown pos_tensor shape(-1):{}'.format(
            pos_tensor.size(-1)))
    return pos
  1. class DnQueryGenerator:
gt_bboxes=bbox_xyxy_to_cxcywh(bboxes) / wh








3.delta2bbox:anchor和预测框的解码过程,生成最终bbox

def delta2bbox(rois,
               deltas,
               means=(0., 0., 0., 0.),
               stds=(1., 1., 1., 1.),
               max_shape=None,
               wh_ratio_clip=16 / 1000,
               clip_border=True,
               add_ctr_clamp=False,
               ctr_clamp=32):
    """Apply deltas to shift/scale base boxes.

    Typically the rois are anchor or proposed bounding boxes and the deltas are
    network outputs used to shift/scale those boxes.
    This is the inverse function of :func:`bbox2delta`.

    Args:
        rois (Tensor): Boxes to be transformed. Has shape (N, 4).
        deltas (Tensor): Encoded offsets relative to each roi.
            Has shape (N, num_classes * 4) or (N, 4). Note
            N = num_base_anchors * W * H, when rois is a grid of
            anchors. Offset encoding follows [1]_.
        means (Sequence[float]): Denormalizing means for delta coordinates.
            Default (0., 0., 0., 0.).
        stds (Sequence[float]): Denormalizing standard deviation for delta
            coordinates. Default (1., 1., 1., 1.).
        max_shape (tuple[int, int]): Maximum bounds for boxes, specifies
           (H, W). Default None.
        wh_ratio_clip (float): Maximum aspect ratio for boxes. Default
            16 / 1000.
        clip_border (bool, optional): Whether clip the objects outside the
            border of the image. Default True.
        add_ctr_clamp (bool): Whether to add center clamp. When set to True,
            the center of the prediction bounding box will be clamped to
            avoid being too far away from the center of the anchor.
            Only used by YOLOF. Default False.
        ctr_clamp (int): the maximum pixel shift to clamp. Only used by YOLOF.
            Default 32.

    Returns:
        Tensor: Boxes with shape (N, num_classes * 4) or (N, 4), where 4
           represent tl_x, tl_y, br_x, br_y.

    References:
        .. [1] https://arxiv.org/abs/1311.2524

    Example:
        >>> rois = torch.Tensor([[ 0.,  0.,  1.,  1.],
        >>>                      [ 0.,  0.,  1.,  1.],
        >>>                      [ 0.,  0.,  1.,  1.],
        >>>                      [ 5.,  5.,  5.,  5.]])
        >>> deltas = torch.Tensor([[  0.,   0.,   0.,   0.],
        >>>                        [  1.,   1.,   1.,   1.],
        >>>                        [  0.,   0.,   2.,  -1.],
        >>>                        [ 0.7, -1.9, -0.5,  0.3]])
        >>> delta2bbox(rois, deltas, max_shape=(32, 32, 3))
        tensor([[0.0000, 0.0000, 1.0000, 1.0000],
                [0.1409, 0.1409, 2.8591, 2.8591],
                [0.0000, 0.3161, 4.1945, 0.6839],
                [5.0000, 5.0000, 5.0000, 5.0000]])
    """
    num_bboxes, num_classes = deltas.size(0), deltas.size(1) // 4
    if num_bboxes == 0:
        return deltas

    deltas = deltas.reshape(-1, 4)

    means = deltas.new_tensor(means).view(1, -1)
    stds = deltas.new_tensor(stds).view(1, -1)
    denorm_deltas = deltas * stds + means

    dxy = denorm_deltas[:, :2]
    dwh = denorm_deltas[:, 2:]

    # Compute width/height of each roi
    rois_ = rois.repeat(1, num_classes).reshape(-1, 4)
    pxy = ((rois_[:, :2] + rois_[:, 2:]) * 0.5)
    pwh = (rois_[:, 2:] - rois_[:, :2])

    dxy_wh = pwh * dxy

    max_ratio = np.abs(np.log(wh_ratio_clip))
    if add_ctr_clamp:
        dxy_wh = torch.clamp(dxy_wh, max=ctr_clamp, min=-ctr_clamp)
        dwh = torch.clamp(dwh, max=max_ratio)
    else:
        dwh = dwh.clamp(min=-max_ratio, max=max_ratio)

    gxy = pxy + dxy_wh
    gwh = pwh * dwh.exp()
    x1y1 = gxy - (gwh * 0.5)
    x2y2 = gxy + (gwh * 0.5)
    bboxes = torch.cat([x1y1, x2y2], dim=-1)
    if clip_border and max_shape is not None:
        bboxes[..., 0::2].clamp_(min=0, max=max_shape[1])
        bboxes[..., 1::2].clamp_(min=0, max=max_shape[0])
    bboxes = bboxes.reshape(num_bboxes, -1)
    return bboxes

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐