基于Opean-PCDDet的SECOND模型

基于Opean-PCDDet的SECOND模型

本文主要讲解SEOND模型,同时介绍Opean-PCDDet项目的组织结构。作为初次学习Opean-PCDDet项目的一次记录。如果对你有所帮助,我很开心~

SECOND论文地址:Sensors | Free Full-Text | SECOND: Sparsely Embedded Convolutional Detection
SECOND代码地址:GitHub - traveller59/second.pytorch: SECOND for KITTI/NuScenes object detection

在这里插入图片描述

项目结构

Opean-PCDDet中主要分为两个包:pcdet项目文件包和tools工具包

  1. pcdet项目文件包
    在这里插入图片描述

    1. dataset:数据集处理文件,其中包含数据增强方法,数据处理方法以及数集构建方法。根据对应数据集配置文件,完成对于数据集的构建,处理和增强。能够处理的数据集包括以下几种常用3D目标检测数据集:
      在这里插入图片描述

    2. models:模型构建文件,包括2D骨干网络,3D骨干网络,检测头,整体检测器以及模型构建工具包。
      在这里插入图片描述

    3. ops:点处理包,包括3DNMS,PointNet++以及一些处理的包,在整体SECOND模型构建的过程中,只使用到了NMS。
      在这里插入图片描述

    4. utils:工具包,包含box处理工具,损失工具,通用工具等等。
      在这里插入图片描述

  2. tools工具包
    在这里插入图片描述
    tools包含以下几个包,我们主要介绍其中一个。
    在这里插入图片描述

  3. cfgs包,该包是配置文件。包含数据集配置,kitti应用数据集模型配置,lyft应用数据集模型配置,nuscenes应用数据集模型配置以及waymo应用数据集模型配置。具体配置内容,大家可以点进去自己看看,这里放一个KITTI数据集配置文件和SECOND模型配置文件图像:
    在这里插入图片描述
    在这里插入图片描述

零:SECOND模型概述

论文中模型结果如图所示:

在这里插入图片描述

SECOND模型总体包含6个步骤,完成数据集处理,模型构建,模型训练以及测试。这一节我们会给出整体构建过程。需要注意的是,Opean-PCDDet中各个模型的构建也是遵循这个流程的,大家想要看其他模型也可以按照这个流程看。之后我会每个步骤进行详细介绍,并且给出代码路径,以及代码详情。

0、Point2Voxle 点云体素化
1、MeanVFE 均值体素编码
2、VoxelBackBone8x 3D骨干网络特征提取
3、HeightCompression 高度压缩3D转2D
4、BaseBEVBackbone 2D骨干网络特征提取
5、AnchorHeadSingle 基于锚框的检测方法

这是一个详细一点的概述:

一: Point Cloud Grouping数据处理完成点云体素化
二: Mean VFE 体素特征编码
三: VoxelBackBone8x,3D骨干网络,用于提取体素特征
四: HeightCompression Z轴方向压缩,3D转2D
五: BaseBEVBackbone2D特征提取网络
六: AnchorHeadTemplate锚框检测头模板
6.1:generate_anchors锚框生成
6.2:AxisAlignedTargetAssigner锚框与目标框对齐
6.3:构建损失函数总体损失方法:get_loss
6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
6.4:预测生成方法:generate_predicted_boxes

七:Anchor_head_single利用2D特征进行检测分类以及回归

八:Detector3DTemplate构建模型

一: Point Cloud Grouping数据处理完成点云体素化

**作用:**Point Cloud Grouping数据处理完成点云体素化

路径:

当前Point Cloud Grouping路径:pcdet/datasets/processor/data_processor.py
下一步Mean VFE体素特征编码:pcdet/models/backbones_3d/vfe/mean_vfe.py

在这里插入图片描述

方法介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tan4zwLI-1684140019321)(C:\Users\zgh\AppData\Roaming\Typora\typora-user-images\image-20230515083052229.png)]

输入为点云数据,输出为体素特征数据,其中采用工厂模式对于不同数据集进行了不同处理
比如KITTI数据集,进行了:场景裁剪,随机打乱,体素化处理三个方案。
[
“mask_points_and_boxes_outside_range”,
“shuffle_points”,
“transform_points_to_voxels”
]

用VoxelGeneratorWrapper进行数据处理,输入点云数据,得到体素,体素位置,体素个数

另外,数据处理还包括:
sample_points:对点进行采样,采样点云,多了丢弃,少了补上
calculate_grid_size:计算场景网格尺寸

构造流程:

  1. 根据数据集配置文件构建数据处理方法,组成数据处理容器。如SECODN模型中处理的数据集是KITTI,根据其配置文件,得到数据集处理方法:

    工厂模式,根据不同的配置,只需要增加相应的方法即可实现不同的调用,其中KITTI使用的是
    [“mask_points_and_boxes_outside_range”,
    “shuffle_points”,
    transform_points_to_voxels"]
    将各个方法进行串联,根据con文件中DATA_PROCESSOR内容构造并且处理数据。

  2. 输入点云数据[n,4],以此以容器中处理方法对数据进行处理得到体素数据[m,t,c]。
    其中m是体素数目,t是一个体素中包含的最大点云数目,c是每个点云的特征。

    最后 transform_points_to_voxels返回以下几个结果:

    data_dict['voxels'] = voxels
    data_dict['voxel_coords'] = coordinates
    data_dict['voxel_num_points'] = num_points
    

    voxels代表了每个生成的voxel数据,维度是[M, 5, 4],总共M个体素,每个体素包含5个点,每个点4个特征信息。
    coordinates代表了每个生成的voxel所在的zyx轴坐标,维度是[M,3]。
    num_points代表了每个生成的voxel中有多少个有效的点维度是[m,],因为不满5会被0填充。

具体代码:

from functools import partial

import numpy as np
from skimage import transform

from ...utils import box_utils, common_utils

tv = None
try:
    import cumm.tensorview as tv
except:
    pass


class VoxelGeneratorWrapper():
    def __init__(self, vsize_xyz, coors_range_xyz, num_point_features, max_num_points_per_voxel, max_num_voxels):
        '''
        :param vsize_xyz: : 体素大小,每个体素网格的尺寸如(0.05, 0.05, 0.1)
        :param coors_range_xyz:点云数据的坐标范围,是一个元组,包含 x(-50,50)、y(-50,50)、z(-3,1) 方向的最小值和最大值。如 (-50, -50, -3, 50, 50, 1)
        :param num_point_features: 特征点个数点云数据中每个点的特征的维度。(x,y,z,r,g,b)则是6维
        :param max_num_points_per_voxel:每个体素中最大点的个数
        :param max_num_voxels:空间中最大体素个数
        '''

        #使用V2版本的spconv,VoxelGenerator为V2中的Point2VoxelCPU3d,版本号2
        try:
            from spconv.utils import VoxelGeneratorV2 as VoxelGenerator
            self.spconv_ver = 1
        except:
            try:
                from spconv.utils import VoxelGenerator
                self.spconv_ver = 1
            except:
                from spconv.utils import Point2VoxelCPU3d as VoxelGenerator
                self.spconv_ver = 2

        if self.spconv_ver == 1:
            self._voxel_generator = VoxelGenerator(
                voxel_size=vsize_xyz,
                point_cloud_range=coors_range_xyz,
                max_num_points=max_num_points_per_voxel,
                max_voxels=max_num_voxels
            )
        else:
            self._voxel_generator = VoxelGenerator(
                vsize_xyz=vsize_xyz,
                coors_range_xyz=coors_range_xyz,
                num_point_features=num_point_features,
                max_num_points_per_voxel=max_num_points_per_voxel,
                max_num_voxels=max_num_voxels
            )
    #调用generate方法使用创建的VoxelGenerator实例生成体素
    def generate(self, points):
        '''
        :param points: point是点云shape为(n,feature_num)
        :return:
            voxels 是稀疏 3D tensor,
            coords 是每个体素在稀疏 3D tensor 中的坐标,
            num_points_per_voxel 是每个体素中包含的点的数量。
        '''

        if self.spconv_ver == 1:
            voxel_output = self._voxel_generator.generate(points)
            if isinstance(voxel_output, dict):
                voxels, coordinates, num_points = \
                    voxel_output['voxels'], voxel_output['coordinates'], voxel_output['num_points_per_voxel']
            else:
                voxels, coordinates, num_points = voxel_output
        #版本为2
        else:
            #检查库 cumm 是否导入成功,并使用 _voxel_generator 对象的 point_to_voxel()
            #点云数据转换为稀疏 3D tensor 格式。
            #tv.from_numpy() 方法将点云数据转换为 cumm 库中的张量类型
            assert tv is not None, f"Unexpected error, library: 'cumm' wasn't imported properly."
            voxel_output = self._voxel_generator.point_to_voxel(tv.from_numpy(points))

            tv_voxels, tv_coordinates, tv_num_points = voxel_output
            # make copy with numpy(), since numpy_view() will disappear as soon as the generator is deleted
            voxels = tv_voxels.numpy()
            coordinates = tv_coordinates.numpy()
            num_points = tv_num_points.numpy()
        return voxels, coordinates, num_points


# 数据预处理类
"""
    一: Point Cloud Grouping数据处理完成点云体素化
    路径:pcdet/datasets/processor/data_processor.py
    下一步体素特征编码:pcdet/models/backbones_3d/vfe/mean_vfe.py
    输入为点云数据,输出为体素特征数据,其中采用工厂模式对于不同数据集进行了不同处理
    比如KITTI数据集,进行了,场景裁剪随机打乱,体素化处理三个方案
    [
    "mask_points_and_boxes_outside_range", 
    "shuffle_points", 
    "transform_points_to_voxels"
    ]用VoxelGeneratorWrapper进行数据处理,输入点云数据,得到体素,体素位置,体素个数
    另外,数据处理还包括:
    sample_points:对点进行采样,采样点云,多了丢弃,少了补上
    calculate_grid_size:计算场景网格尺寸
    
    构造流程:
    根据数据集配置文件构建数据处理方法,组成数据处理容器,
    输入点云数据[n,4],以此以容器中处理方法对数据进行处理得到体素数据[m,t,c]
    其中m是体素数目,t是一个体素中包含的最大点云数目,c是每个点云的特征:
     voxels代表了每个生成的voxel数据,维度是[M, 5, 4],总共M个体素,每个体素包含5个点,每个点4个特征信息
     coordinates代表了每个生成的voxel所在的zyx轴坐标,维度是[M,3]
     num_points代表了每个生成的voxel中有多少个有效的点维度是[m,],因为不满5会被0填充
"""
class DataProcessor(object):
    """
    数据预处理类
    Args:
        processor_configs: DATA_CONFIG.DATA_PROCESSOR参数文件 tools/cfgs/dataset_configs/kitti_dataset.yaml
        point_cloud_range: 点云范围
        training:训练模式
    """

    def __init__(self, processor_configs, point_cloud_range, training, num_point_features):
        self.point_cloud_range = point_cloud_range
        self.training = training
        self.num_point_features = num_point_features
        self.mode = 'train' if training else 'test'
        self.grid_size = self.voxel_size = None
        #数据处理方法队列
        self.data_processor_queue = []

        self.voxel_generator = None
        # 工厂模式,根据不同的配置,只需要增加相应的方法即可实现不同的调用,其中KITTI使用的是mask_points_and_boxes_outside_range
        # ["mask_points_and_boxes_outside_range", "shuffle_points", "transform_points_to_voxels"]
        #将各个方法进行串联,根据con文件中DATA_PROCESSOR内容构造并且处理数据
        #如SECOND中使用了:1,场景采样   2,随机打乱点  3,点转化体素
        for cur_cfg in processor_configs:
            cur_processor = getattr(self, cur_cfg.NAME)(config=cur_cfg)
            # 在forward函数中调用
            self.data_processor_queue.append(cur_processor)


    #场景裁剪
    def mask_points_and_boxes_outside_range(self, data_dict=None, config=None):
        """
        移除超出point_cloud_range的点,中心在范围内,角点在范围内
        """
        # 偏函数是将所要承载的函数作为partial()函数的第一个参数,
        # 原函数的各个参数依次作为partial()函数后续的参数
        # 以便函数能用更少的参数进行调用
        if data_dict is None:
            return partial(self.mask_points_and_boxes_outside_range, config=config)

        if data_dict.get('points', None) is not None:
            # mask为bool值,将x和y超过规定范围的点设置为0
            mask = common_utils.mask_points_by_range(data_dict['points'], self.point_cloud_range)
            # 根据mask提取点,获取在范围内的点
            data_dict['points'] = data_dict['points'][mask]
        # 当data_dict存在gt_boxes并且REMOVE_OUTSIDE_BOXES=True并且处于训练模式
        if data_dict.get('gt_boxes', None) is not None and config.REMOVE_OUTSIDE_BOXES and self.training:
            # mask为bool值,将box角点在范围内点个数大于最小阈值的设置为1
            mask = box_utils.mask_boxes_outside_range_numpy(
                data_dict['gt_boxes'], self.point_cloud_range, min_num_corners=config.get('min_num_corners', 1)
            )
            data_dict['gt_boxes'] = data_dict['gt_boxes'][mask]
        return data_dict

    #随机打乱
    def shuffle_points(self, data_dict=None, config=None):
        """将点云打乱"""
        if data_dict is None:
            return partial(self.shuffle_points, config=config)

        if config.SHUFFLE_ENABLED[self.mode]:
            points = data_dict['points']
            # 生成随机序列
            shuffle_idx = np.random.permutation(points.shape[0])
            points = points[shuffle_idx]
            data_dict['points'] = points

        return data_dict

    #调用VoxelGeneratorWrapper进行数据处理,输入点云数据,得到体素,体素位置,体素个数
    def transform_points_to_voxels(self, data_dict=None, config=None):
        """
        将点云转换为voxel,调用spconv的VoxelGeneratorV2
        """
        if data_dict is None:
            grid_size = (self.point_cloud_range[3:6] - self.point_cloud_range[0:3]) / np.array(config.VOXEL_SIZE)
            self.grid_size = np.round(grid_size).astype(np.int64)
            self.voxel_size = config.VOXEL_SIZE
            # just bind the config, we will create the VoxelGeneratorWrapper later,
            # to avoid pickling issues in multiprocess spawn
            return partial(self.transform_points_to_voxels, config=config)

        if self.voxel_generator is None:
            self.voxel_generator = VoxelGeneratorWrapper(
                # 给定每个voxel的长宽高  [0.05, 0.05, 0.1]
                vsize_xyz=config.VOXEL_SIZE,  # [0.16, 0.16, 4]
                # 给定点云的范围 [  0.  -40.   -3.   70.4  40.    1. ]
                coors_range_xyz=self.point_cloud_range,
                # 给定每个点云的特征维度,这里是x,y,z,r 其中r是激光雷达反射强度
                num_point_features=self.num_point_features,
                # 给定每个pillar/voxel中有采样多少个点,不够则补0
                max_num_points_per_voxel=config.MAX_POINTS_PER_VOXEL,  # 32
                # 最多选取多少个voxel,训练16000,推理40000
                max_num_voxels=config.MAX_NUMBER_OF_VOXELS[self.mode],  # 16000
            )

        # 使用spconv生成voxel输出
        points = data_dict['points']
        voxel_output = self.voxel_generator.generate(points)

        # 假设一份点云数据是N*4,那么经过pillar生成后会得到三份数据
        # voxels代表了每个生成的voxel数据,维度是[M, 5, 4],总共M个体素,每个体素包含5个点,每个点4个特征信息
        # coordinates代表了每个生成的voxel所在的zyx轴坐标,维度是[M,3]
        # num_points代表了每个生成的voxel中有多少个有效的点维度是[m,],因为不满5会被0填充
        voxels, coordinates, num_points = voxel_output

        # False
        if not data_dict['use_lead_xyz']:
            voxels = voxels[..., 3:]  # remove xyz in voxels(N, 3)

        data_dict['voxels'] = voxels
        data_dict['voxel_coords'] = coordinates
        data_dict['voxel_num_points'] = num_points
        return data_dict

    #对点进行采样
    def sample_points(self, data_dict=None, config=None):
        """
       采样点云,多了丢弃,少了补上
       """
        if data_dict is None:
            return partial(self.sample_points, config=config)

        num_points = config.NUM_POINTS[self.mode]
        if num_points == -1:
            return data_dict

        points = data_dict['points']
        # 如果采样点数 < 点云点数
        if num_points < len(points):
            # 计算点云深度
            pts_depth = np.linalg.norm(points[:, 0:3], axis=1)
            # 根据深度构造mask
            #小于40米为近点,大于40米为远点
            pts_near_flag = pts_depth < 40.0
            far_idxs_choice = np.where(pts_near_flag == 0)[0]
            near_idxs = np.where(pts_near_flag == 1)[0]
            choice = []
            # 如果采样点数 > 远点数量 : 远的全保留,近的取部分
            if num_points > len(far_idxs_choice):
                # 在近点中随机采样,因为近处稠密,近点采集n-远点个数,剩下远点全都要。
                near_idxs_choice = np.random.choice(near_idxs, num_points - len(far_idxs_choice), replace=False)
                # 如果远点不为0,则将采样的近点和远点拼接,如果为0,则直接返回采样的近点
                choice = np.concatenate((near_idxs_choice, far_idxs_choice), axis=0) \
                    if len(far_idxs_choice) > 0 else near_idxs_choice
            # 如果采样点数 < 远点数量,:远近随机采样
            else:
                choice = np.arange(0, len(points), dtype=np.int32)
                choice = np.random.choice(choice, num_points, replace=False)
            # 将点打乱
            np.random.shuffle(choice)
        # 如果采样点数 > 点云点数, 则随机采样点补全点云
        else:
            choice = np.arange(0, len(points), dtype=np.int32)
            if num_points > len(points):
                # 随机采样缺少的点云索引
                extra_choice = np.random.choice(choice, num_points - len(points), replace=False)
                # 拼接索引
                choice = np.concatenate((choice, extra_choice), axis=0)
            # 将索引打乱
            np.random.shuffle(choice)
        data_dict['points'] = points[choice]
        return data_dict

    #计算场景网格尺寸
    def calculate_grid_size(self, data_dict=None, config=None):
        """
        计算网格范围
        """
        if data_dict is None:
            #点云范围除以各个体素的尺寸,得到x,y,z方向上各个数目,如 10/0.5 = 20 x方向20个格子
            grid_size = (self.point_cloud_range[3:6] - self.point_cloud_range[0:3]) / np.array(config.VOXEL_SIZE)
            #取整
            self.grid_size = np.round(grid_size).astype(np.int64)
            self.voxel_size = config.VOXEL_SIZE
            return partial(self.calculate_grid_size, config=config)
        return data_dict

    #下采样得到深度
    def downsample_depth_map(self, data_dict=None, config=None):
        """降采样深度图"""
        #若为空,初始化
        if data_dict is None:
            self.depth_downsample_factor = config.DOWNSAMPLE_FACTOR
            return partial(self.downsample_depth_map, config=config)
        # skimage中类似平均池化的操作,进行图像将采样
        data_dict['depth_maps'] = transform.downscale_local_mean(
            image=data_dict['depth_maps'],
            factors=(self.depth_downsample_factor, self.depth_downsample_factor)
        )
        return data_dict

    #按顺序执行数据处理操作。
    def forward(self, data_dict):
        """
        Args:
            data_dict:
                points: (N, 3 + C_in)
                gt_boxes: optional, (N, 7 + C) [x, y, z, dx, dy, dz, heading, ...]
                gt_names: optional, (N), string
                ...

        Returns:
        """
        # 在for循环中逐个流程处理,最终都放入data_dict中
        for cur_processor in self.data_processor_queue:
            data_dict = cur_processor(data_dict=data_dict)

        return data_dict

二: Mean VFE 体素特征编码

**作用:**Mean VFE 体素特征编码,聚合各个体素的特征,将先前每个体素以体素中点特征表示转化为直接以每个体素表示。

路径:

上一步点云体素化数据处理:pcdet/datasets/processor/data_processor.py
路径:pcdet/models/backbones_3d/vfe/mean_vfe.py
下一步稀疏体素特征提取:pcdet/models/backbones_3d/spconv_backbone.py
在这里插入图片描述
方法介绍

方法很简单,与VoxelNet中的VFE对齐:

改进于VoxelNet中的 Stacked Voxel Feature Encoding(局部结合单独,堆叠后取平均),下图为VoxelNet的过程。
问题:VoxelNet中的SVFE堆叠多个VFE层和全连接层完成局部信息融合提取,局部结合单独,堆叠后取平均,但是这样的操作计算量较大。
改进:SECOND中直接取单个体素中的所有点特征的均值代表这个体素的特征,计算量大大减小。
效果:(Batch, 5, 4) --> (Batch, 4)
在这里插入图片描述

具体代码:

import torch

from .vfe_template import VFETemplate

"""
二: Mean VFE 体素特征编码

上一步点云体素化数据处理:pcdet/datasets/processor/data_processor.py
路径:pcdet/models/backbones_3d/vfe/mean_vfe.py
下一步稀疏体素特征提取:pcdet/models/backbones_3d/spconv_backbone.py

改进于VoxelNet中的 Stacked Voxel Feature Encoding(局部结合单独,堆叠后取平均)
问题:VoxelNet中的SVFE堆叠多个VFE层和全连接层完成局部信息融合提取,局部结合单独,堆叠后取平均,但是这样的操作计算量较大。
改进:SECOND中直接取单个体素中的所有点特征的均值代表这个体素的特征,计算量大大减小。
效果:(Batch*16000, 5, 4) --> (Batch*16000, 4)
"""
#体素特征编码,
class MeanVFE(VFETemplate):
    def __init__(self, model_cfg, num_point_features, **kwargs):
        super().__init__(model_cfg=model_cfg)
        # 每个点多少个特征(x,y,z,r)
        self.num_point_features = num_point_features

    def get_output_feature_dim(self):
        return self.num_point_features

    def forward(self, batch_dict, **kwargs):
        """
        Args:
            batch_dict:
                voxels: (num_voxels, max_points_per_voxel, C) 各个体素中各个点的特征
                voxel_num_points: optional (num_voxels) how many points in a voxel 单个体素包含的具体点数目
            **kwargs:

        Returns:
            体素特征
            vfe_features: (num_voxels, C)
        """
        #substitute代替PointNet中的特征提取结构
        # here use the mean_vfe module to substitute for the original pointnet extractor architecture
        voxel_features, voxel_num_points = batch_dict['voxels'], batch_dict['voxel_num_points']
        # 求每个voxel内 所有点的和,也就是把5个点的特征加了起来。代表这个体素的特征
        # eg:SECOND/PV-RCNN  shape (Batch*16000, 5, 4) -> (Batch*16000, 4)
        points_mean = voxel_features[:, :, :].sum(dim=1, keepdim=False)
        # 正则化项, 保证每个voxel中最少有一个点,防止除0
        normalizer = torch.clamp_min(voxel_num_points.view(-1, 1), min=1.0).type_as(voxel_features)
        # 求每个voxel内点坐标的平均值, 并用该值来代表该voxel 和/数目
        points_mean = points_mean / normalizer
        # 将处理好的voxel_feature信息重新加入batch_dict中
        batch_dict['voxel_features'] = points_mean.contiguous()
        return batch_dict

三: VoxelBackBone8x,3D骨干网络,用于提取体素特征

**作用:**VoxelBackBone8x作为3D骨干网络,对于VFE得到的体素特征进行在3维空间中进行特征提取。作为3D骨干网络目前常用的特征提取方法有以下几种:

方法名称描述优缺点
基于PointNet使用Point系列网络进行原始点云特征提取,进行多次sampling和grouping对点云进行采样和特征聚合。计算量大,但是能够直接应用在原始点云数据上,信息损失小。
基于3D卷积直接使用nn.conv3d的3D卷积方法对于体素特征进行特征提取。体素特征存在一定的信息损失,3D卷积计算量大,体素特征稀疏的状况下表现并不好。但是应用简单。
基于3D稀疏卷积利用3D稀疏卷积对于体素特征进行特征提取。稀疏卷积一定程度上缓解了计算量问题,只对有价值的区域进行卷积。信息也会产生损失。
基于Transformer的方法利用TF对于点或体素特征进行特征提取,获取带有互相关信息的体素特征。自注意力计算量过大,需要找到合适的处理方法,减少计算开销。得到的特征图质量高。

SECOND首次提出了使用稀疏3D卷积来解决3D卷积计算量大的问题,稀疏3D卷积的介绍可以参看我这一篇文章,包括计算过程和作用。

存在的问题:简单来说,3D卷积卷积会对空间中每个位置的体素进行卷积计算,但是空间中体素是较为离散的,大部分体素是没有价值的,在计算过程中,这些没有价值的体素也会参与运算,大大增加卷积过程的计算量。

解决的思路:能够在卷积过程中仅仅对于有价值的位置进行卷积,跳过无价值位置的卷积计算过程。由两种稀疏卷积构成,Sparse稀疏卷积只要卷积核覆盖到激活点(有价值点)时,就进行卷积操作,Submanifold稀疏卷积只有当激活点在卷积核中心的时候才进行卷积操作。在SECOND中,由这两种卷积构成卷积块,由多个卷积块足够3D骨干网络。

具体实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p2HNJuyb-1684140019323)(C:\Users\zgh\AppData\Roaming\Typora\typora-user-images\image-20230515094827274.png)]

路径:

上一步体素特征编码:pcdet/models/backbones_3d/vfe/mean_vfe.py
路径:pcdet/models/backbones_3d/spconv_backbone.py
下一步深度压缩:pcdet/models/backbones_2d/map_to_bev/height_compression.py
在这里插入图片描述

方法介绍
在这里插入图片描述
输入为VFE得到的体素特征数据,文件中post_act_block方法更具给定的参数,创建对应的稀疏卷积,SparseBasicBlock构造基本的稀疏卷积块。VoxelBackBone8x构建骨干网络,骨干网络由4层稀疏卷积层构成:

# [batch_size, 16, [41, 1600, 1408]] --> [batch_size, 16, [41, 1600, 1408]]
x_conv1 = self.conv1(x)
# [batch_size, 16, [41, 1600, 1408]] --> [batch_size, 32, [21, 800, 704]]
x_conv2 = self.conv2(x_conv1)
# [batch_size, 32, [21, 800, 704]] --> [batch_size, 64, [11, 400, 352]]
x_conv3 = self.conv3(x_conv2)
# [batch_size, 64, [11, 400, 352]] --> [batch_size, 64, [5, 200, 176]]
x_conv4 = self.conv4(x_conv3)

具体代码:

from functools import partial

import torch.nn as nn

from ...utils.spconv_utils import replace_feature, spconv


def post_act_block(in_channels, out_channels, kernel_size, indice_key=None, stride=1, padding=0,
                   conv_type='subm', norm_fn=None):
    '''
    :param in_channels: 输入通道数
    :param out_channels: 输出通道数
    :param kernel_size: 卷积核尺寸
    :param indice_key: indexkey是 SubMConv3d 中的一个参数,表示当前使用的稀疏张量格式的名称。
    :param stride: 步长
    :param padding: 填充
    :param conv_type: 卷积类型包括三种,subm , spare , inverseconv
    :param norm_fn: 归一化
    :return: 构建好的卷积块,包括,稀疏卷积,归一化,激活函数
    '''
    # 后处理执行块,根据conv_type选择对应的卷积操作并和norm与激活函数封装为块
    #subm卷积:中心对齐时进行卷积操作默认为该卷积
    if conv_type == 'subm':
        conv = spconv.SubMConv3d(in_channels, out_channels, kernel_size, bias=False, indice_key=indice_key)
    #spare卷积:激活点在卷积核内就进行卷积
    elif conv_type == 'spconv':
        conv = spconv.SparseConv3d(in_channels, out_channels, kernel_size, stride=stride, padding=padding,
                                   bias=False, indice_key=indice_key)
    #反向卷积
    elif conv_type == 'inverseconv':
        conv = spconv.SparseInverseConv3d(in_channels, out_channels, kernel_size, indice_key=indice_key, bias=False)
    else:
        raise NotImplementedError
    #构建卷积块
    m = spconv.SparseSequential(
        conv,
        norm_fn(out_channels),
        nn.ReLU(),
    )

    return m


class SparseBasicBlock(spconv.SparseModule):
    expansion = 1

    def __init__(self, inplanes, planes, stride=1, norm_fn=None, downsample=None, indice_key=None):
        super(SparseBasicBlock, self).__init__()

        assert norm_fn is not None
        bias = norm_fn is not None
        self.conv1 = spconv.SubMConv3d(
            inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=bias, indice_key=indice_key
        )
        self.bn1 = norm_fn(planes)
        self.relu = nn.ReLU()
        self.conv2 = spconv.SubMConv3d(
            planes, planes, kernel_size=3, stride=stride, padding=1, bias=bias, indice_key=indice_key
        )
        self.bn2 = norm_fn(planes)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = replace_feature(out, self.bn1(out.features))
        out = replace_feature(out, self.relu(out.features))

        out = self.conv2(out)
        out = replace_feature(out, self.bn2(out.features))

        if self.downsample is not None:
            identity = self.downsample(x)

        out = replace_feature(out, out.features + identity.features)
        out = replace_feature(out, self.relu(out.features))

        return out



"""
3D骨干网络,用于提取体素特征
三:VoxelBackBone8x
    上一步体素特征编码:pcdet/models/backbones_3d/vfe/mean_vfe.py 
    路径:pcdet/models/backbones_3d/spconv_backbone.py
    下一步深度压缩:pcdet/models/backbones_2d/map_to_bev/height_compression.py
    
    问题:原先VoxelNet中直接采用3D卷积获取体素特征,但是这样的卷积过程计算开销巨大,且计算过程中需要处理许多无价值体素
    思路:SECOND中采用稀疏卷积来替换3D卷积,并且修改了原先的稀疏卷积结构
    应对问题:原先稀疏卷积只有在卷积核中心处于激活点时才进行卷积计算,这样得到的特征信息不足
    处理方法:SECOND提出的Spare卷积只要激活点位于卷积核内就进行卷积操作,这样在减少非激活点卷积的计算开销的同时也保持了信息的完整性
    不至于造成巨大信息损失
"""
#卷积过程:
# k=3, s=1, p=1 : w'=w
# k=3, s=1, p=0 : w'=w-2
# k=3, s=2, p=1 : w'=w//2
# k=3, s=1, p=0 : w'=w//2 - 1
class VoxelBackBone8x(nn.Module):
    def __init__(self, model_cfg, input_channels, grid_size, **kwargs):
        super().__init__()
        self.model_cfg = model_cfg
        norm_fn = partial(nn.BatchNorm1d, eps=1e-3, momentum=0.01)

        self.sparse_shape = grid_size[::-1] + [1, 0, 0]
        #输入的3通道卷积到16通道,尺寸不发生变化,输入卷积使用sub
        self.conv_input = spconv.SparseSequential(
            spconv.SubMConv3d(input_channels, 16, 3, padding=1, bias=False, indice_key='subm1'),
            norm_fn(16),
            nn.ReLU(),
        )
        block = post_act_block
        # 16 -> 16 indice_key:'subm1'应该意思是说这是第一个卷积层的东西。第一层卷积采用sub
        self.conv1 = spconv.SparseSequential(
            #使用subm1稀疏卷积
            block(16, 16, 3, norm_fn=norm_fn, padding=1, indice_key='subm1'),
        )
        #稀疏卷积层构建稀疏卷积块,第二块卷积采用spare+sub+sub
        # 16->32
        # 32->32
        # 32->32
        #尺寸:第一层长宽高减半,后面不变
        self.conv2 = spconv.SparseSequential(
            # [1600, 1408, 41] <- [800, 704, 21]
            block(16, 32, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv2', conv_type='spconv'),
            block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),
            block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),
        )
        # 第三层卷积采用spare+sub+sub
        # 32->64
        # 64->64
        # 64->64
        # 尺寸:第一层长宽高减半,后面不变
        self.conv3 = spconv.SparseSequential(
            # [800, 704, 21] <- [400, 352, 11]
            block(32, 64, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv3', conv_type='spconv'),
            block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),
            block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),
        )
        # 第四层卷积采用spare+sub+sub
        # 64->64
        # 64->64
        # 64->64
        # 尺寸:第一层长宽高减半,后面不变
        self.conv4 = spconv.SparseSequential(
            # [400, 352, 11] <- [200, 176, 5]
            block(64, 64, 3, norm_fn=norm_fn, stride=2, padding=(0, 1, 1), indice_key='spconv4', conv_type='spconv'),
            block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),
            block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),
        )

        last_pad = 0
        last_pad = self.model_cfg.get('last_pad', last_pad)
        #进行输出
        #64->128
        #卷积核部分采用(3,1,1)的核,步长采用(2,1,1)步长目的:深度方向尺寸减小一半,长宽方向不变。采用spare
        #[w,h,d]->[w,h,d//2]
        self.conv_out = spconv.SparseSequential(
            # [200, 150, 5] -> [200, 150, 2]
            spconv.SparseConv3d(64, 128, (3, 1, 1), stride=(2, 1, 1), padding=last_pad,
                                bias=False, indice_key='spconv_down2'),
            norm_fn(128),
            nn.ReLU(),
        )
        #最终各个体素特征维度
        self.num_point_features = 128
        #每个卷积块体素特征维度
        self.backbone_channels = {
            'x_conv1': 16,
            'x_conv2': 32,
            'x_conv3': 64,
            'x_conv4': 64
        }

    def forward(self, batch_dict):
        """
        Args:
            batch_dict:
                batch_size: int
                vfe_features: (num_voxels, C) 体素特征
                voxel_coords: (num_voxels, 4), [batch_idx, z_idx, y_idx, x_idx] 体素位置
        Returns:
            batch_dict:
                encoded_spconv_tensor: sparse tensor 编码后的稀疏体素特征
        """
        # voxel_features, voxel_coords  shape (Batch * 16000, 4)
        voxel_features, voxel_coords = batch_dict['voxel_features'], batch_dict['voxel_coords']
        batch_size = batch_dict['batch_size']
        # 根据voxel坐标,并将每个voxel放置voxel_coor对应的位置,建立成稀疏tensor
        input_sp_tensor = spconv.SparseConvTensor(
            # (Batch * 16000, 4)特征
            features=voxel_features,
            # (Batch * 16000, 4) 位置,其中4为 batch_idx, x, y, z
            indices=voxel_coords.int(),
            # [41,1600,1408] ZYX 每个voxel的长宽高为0.05,0.05,0.1 点云的范围为[0, -40, -3, 70.4, 40, 1]
            spatial_shape=self.sparse_shape,
            # 4
            batch_size=batch_size
        )

        """
        稀疏卷积的计算中,feature,channel,shape,index这几个内容都是分开存放的,
        在后面用out.dense才把这三个内容组合到一起了,变为密集型的张量
        spconv卷积的输入也是一样,输入和输出更像是一个  字典或者说元组
        注意卷积中pad与no_pad的区别
        """

        # # 进行submanifold convolution
        # [batch_size, 4, [41, 1600, 1408]] --> [batch_size, 16, [41, 1600, 1408]]
        x = self.conv_input(input_sp_tensor)

        # [batch_size, 16, [41, 1600, 1408]] --> [batch_size, 16, [41, 1600, 1408]]
        x_conv1 = self.conv1(x)
        # [batch_size, 16, [41, 1600, 1408]] --> [batch_size, 32, [21, 800, 704]]
        x_conv2 = self.conv2(x_conv1)
        # [batch_size, 32, [21, 800, 704]] --> [batch_size, 64, [11, 400, 352]]
        x_conv3 = self.conv3(x_conv2)
        # [batch_size, 64, [11, 400, 352]] --> [batch_size, 64, [5, 200, 176]]
        x_conv4 = self.conv4(x_conv3)

        # for detection head
        # [200, 176, 5] -> [200, 176, 2]
        # [batch_size, 64, [5, 200, 176]] --> [batch_size, 128, [2, 200, 176]]
        out = self.conv_out(x_conv4)
        #记录最终卷积结果和下采样倍数
        batch_dict.update({
            'encoded_spconv_tensor': out,
            'encoded_spconv_tensor_stride': 8
        })
        #记录每一层卷积结果
        batch_dict.update({
            'multi_scale_3d_features': {
                'x_conv1': x_conv1,
                'x_conv2': x_conv2,
                'x_conv3': x_conv3,
                'x_conv4': x_conv4,
            }
        })
        #记录每一层卷积尺寸减小倍数
        batch_dict.update({
            'multi_scale_3d_strides': {
                'x_conv1': 1,
                'x_conv2': 2,
                'x_conv3': 4,
                'x_conv4': 8,
            }
        })
        # 记录最终卷积结果
        # 记录每一层卷积结果
        # 记录每一层卷积尺寸减小倍数
        return batch_dict


class VoxelResBackBone8x(nn.Module):
    def __init__(self, model_cfg, input_channels, grid_size, **kwargs):
        super().__init__()
        self.model_cfg = model_cfg
        norm_fn = partial(nn.BatchNorm1d, eps=1e-3, momentum=0.01)

        self.sparse_shape = grid_size[::-1] + [1, 0, 0]

        self.conv_input = spconv.SparseSequential(
            spconv.SubMConv3d(input_channels, 16, 3, padding=1, bias=False, indice_key='subm1'),
            norm_fn(16),
            nn.ReLU(),
        )
        block = post_act_block

        self.conv1 = spconv.SparseSequential(
            SparseBasicBlock(16, 16, norm_fn=norm_fn, indice_key='res1'),
            SparseBasicBlock(16, 16, norm_fn=norm_fn, indice_key='res1'),
        )

        self.conv2 = spconv.SparseSequential(
            # [1600, 1408, 41] <- [800, 704, 21]
            block(16, 32, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv2', conv_type='spconv'),
            SparseBasicBlock(32, 32, norm_fn=norm_fn, indice_key='res2'),
            SparseBasicBlock(32, 32, norm_fn=norm_fn, indice_key='res2'),
        )

        self.conv3 = spconv.SparseSequential(
            # [800, 704, 21] <- [400, 352, 11]
            block(32, 64, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv3', conv_type='spconv'),
            SparseBasicBlock(64, 64, norm_fn=norm_fn, indice_key='res3'),
            SparseBasicBlock(64, 64, norm_fn=norm_fn, indice_key='res3'),
        )

        self.conv4 = spconv.SparseSequential(
            # [400, 352, 11] <- [200, 176, 5]
            block(64, 128, 3, norm_fn=norm_fn, stride=2, padding=(0, 1, 1), indice_key='spconv4', conv_type='spconv'),
            SparseBasicBlock(128, 128, norm_fn=norm_fn, indice_key='res4'),
            SparseBasicBlock(128, 128, norm_fn=norm_fn, indice_key='res4'),
        )

        last_pad = 0
        last_pad = self.model_cfg.get('last_pad', last_pad)
        self.conv_out = spconv.SparseSequential(
            # [200, 150, 5] -> [200, 150, 2]
            spconv.SparseConv3d(128, 128, (3, 1, 1), stride=(2, 1, 1), padding=last_pad,
                                bias=False, indice_key='spconv_down2'),
            norm_fn(128),
            nn.ReLU(),
        )
        self.num_point_features = 128
        self.backbone_channels = {
            'x_conv1': 16,
            'x_conv2': 32,
            'x_conv3': 64,
            'x_conv4': 128
        }

    def forward(self, batch_dict):
        """
        Args:
            batch_dict:
                batch_size: int
                vfe_features: (num_voxels, C)
                voxel_coords: (num_voxels, 4), [batch_idx, z_idx, y_idx, x_idx]
        Returns:
            batch_dict:
                encoded_spconv_tensor: sparse tensor
        """
        voxel_features, voxel_coords = batch_dict['voxel_features'], batch_dict['voxel_coords']
        batch_size = batch_dict['batch_size']
        input_sp_tensor = spconv.SparseConvTensor(
            features=voxel_features,
            indices=voxel_coords.int(),
            spatial_shape=self.sparse_shape,
            batch_size=batch_size
        )
        x = self.conv_input(input_sp_tensor)

        x_conv1 = self.conv1(x)
        x_conv2 = self.conv2(x_conv1)
        x_conv3 = self.conv3(x_conv2)
        x_conv4 = self.conv4(x_conv3)

        # for detection head
        # [200, 176, 5] -> [200, 176, 2]
        out = self.conv_out(x_conv4)

        batch_dict.update({
            'encoded_spconv_tensor': out,
            'encoded_spconv_tensor_stride': 8
        })
        batch_dict.update({
            'multi_scale_3d_features': {
                'x_conv1': x_conv1,
                'x_conv2': x_conv2,
                'x_conv3': x_conv3,
                'x_conv4': x_conv4,
            }
        })

        return batch_dict

四: HeightCompression Z轴方向压缩,3D转2D

**作用:**将3维数据沿着Z轴方向压缩到2维。

点云数据时稀疏的,提取到的特征也是稀疏的,稀疏特征难以完成3D目标检测任务。因此在3D目标检测中,稀疏特征稠密化是一项重要的任务。稠密化的方法有以下几点:

方法名称描述
多种稀疏信息组合完成稠密化VoxelNet模型中组合各个体素的位置信息和内容特征信息完成点云信息的稠密化。
层层采样聚合点云信息PointNet++中,对点云数据进行多次采样和聚合,减少点的个数增加点的特征维度,完成点云特征的稠密化。
特征提取完成稠密化3维卷积对点云体素进行特征提取,拓展点云特征维度,压缩体素尺寸。
特征信息组合多模态融合中,组合点云信息以及图像信息,完成点云信息的稠密化。
减少特征维度,深度压缩点云3D检测方法中,常用的特征稠密化方法,压缩3维特征的深度维度,到平面上,形成鸟瞰图特征。深度方向没有重叠的目标,深度压缩带来稠密化的同时还有很多好处。

深度方向压缩包括以下几点好处:

好处:

  1. 简化了网络检测头的设计难度(将3D检测框降到2D,又回到了我们的2D目标检测老本行)
  2. 增加了高度方向上的感受野(多个深度方法进行合并,1x128 + 1x128 = 256 实际上把两层特征图和成了一层,感受野1->2)
  3. 加快了网络的训练、推理速度 (3维度点云数据降到了2维图像数据,各个处理都减少了一个维度的计算)

路径:

上一步稀疏体素特征提取:pcdet/models/backbones_3d/spconv_backbone.py
路径:pcdet/models/backbones_2d/map_to_bev/height_compression.py
下一步2D鸟瞰图特征提取:pcdet/models/backbones_2d/base_bev_backbone.py

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ro7FJNkk-1684140019324)(C:\Users\zgh\AppData\Roaming\Typora\typora-user-images\image-20230515134203871.png)]

方法介绍

方法很简单,我们得到的稀疏体素特征维度为[bs,128,(2,200,176)]。

我们希望在深度方向上进行压缩,因为点云空间深度方向不会重叠物体。输出(batch, 128*2, 200, 176)。

深度压缩的方式很多,早先都是直接将深度方向与特征维度进行堆叠,但是这样的压缩方法还是需要一定的计算量,之后的深度压缩可以直接将深度方向的信息进行求和。

具体代码:

import torch.nn as nn


# 在高度方向上进行压缩

"""
四、HeightCompression (Z轴方向压缩)
 上一步稀疏体素特征提取:pcdet/models/backbones_3d/spconv_backbone.py
 路径:pcdet/models/backbones_2d/map_to_bev/height_compression.py
 下一步2D鸟瞰图特征提取:pcdet/models/backbones_2d/base_bev_backbone.py
 
 方法很简单,我们得到的稀疏体素特征维度为[bs,128,(2,200,176)]
 我们希望在深度方向上进行压缩,因为点云空间深度方向不会重叠物体。输出(batch, 128*2, 200, 176)
 好处:
 1.简化了网络检测头的设计难度(将3D检测框降到2D,又回到了我们的2D目标检测老本行)
 2.增加了高度方向上的感受野(多个深度方法进行合并,1x128 + 1x128 = 256 实际上把两层特征图和成了一层,感受野1->2)
 3.加快了网络的训练、推理速度 (3维度点云数据降到了2维图像数据)
"""


class HeightCompression(nn.Module):
    def __init__(self, model_cfg, **kwargs):
        super().__init__()
        self.model_cfg = model_cfg
        # 高度的特征数
        self.num_bev_features = self.model_cfg.NUM_BEV_FEATURES

    def forward(self, batch_dict):

        """
        batch_dict:
            # 记录最终卷积结果
            # 记录每一层卷积结果
            # 记录每一层卷积尺寸减小倍数
        Args:
            encoded_spconv_tensor: sparse tensor
            [batch_size, 128, [2, 200, 176]]
        Returns:
            batch_dict:
                spatial_features:

        """
        # 得到VoxelBackBone8x的输出特征
        encoded_spconv_tensor = batch_dict['encoded_spconv_tensor']
        # 将稀疏的tensor转化为密集tensor, [bacth_size, 128, 2, 200, 176]
        # 结合batch,spatial_shape、indice和feature将特征还原到密集tensor中对应位置
        spatial_features = encoded_spconv_tensor.dense()
        # batch_size,128,2,200,176
        N, C, D, H, W = spatial_features.shape
        """
        将密集的3D tensor reshape为2D鸟瞰图特征    
        将两个深度方向内的voxel特征拼接成一个 shape : (batch_size, 256, 200, 176)
        z轴方向上没有物体会堆叠在一起,这样做可以增大Z轴的感受野,
        同时加快网络的速度,减小后期检测头的设计难度
        """
        # shape (batch, 128*2, 200, 176)
        spatial_features = spatial_features.view(N, C * D, H, W)
        # 将特征和采样尺度加入batch_dict
        batch_dict['spatial_features'] = spatial_features
        # 特征图的下采样倍数 8倍
        batch_dict['spatial_features_stride'] = batch_dict['encoded_spconv_tensor_stride']
        return batch_dict

五: BaseBEVBackbone2D特征提取网络

**作用:**BaseBEVBackbone2D作为3D骨干网络,对于深度压缩得到的BEV图,在2维空间中进行特征提取。

**具体实现:**2D检测又回到的了我们的老本行,当前也有成熟的2D特征提取的网络,比如resnet50等骨干网络,比如利用特征融合去结合浅层精细具体的局部信息和深层卷积层抽象完整的全局信息。这里SECOND就是采用的常规的CNN特征提取方法,将特征图进行下采样和上采用,之后聚合多个不同尺寸的特征体信息。这里直接才采用了VoxelNet网络的RNP网络。如下图所示:

在这里插入图片描述

路径:

上一步深度压缩:pcdet/models/backbones_2d/map_to_bev/height_compression.py
地址:pcdet/models/backbones_2d/base_bev_backbone.py
下一步利用2D特征进行检测分类以及回归:pcdet/models/dense_heads/anchor_head_single.py
在这里插入图片描述

方法介绍
在这里插入图片描述

这里采用了和VoxelNet类是的网络结构;分别对特征图进行不同尺度的下采样然后再进行上采用后在通道维度

行拼接。

下采样过程构建下采样卷积块,每个块中包含若干卷积层如下:
block1: cur_layers[conv2d(in,128) , connv2d(128,128) x 5] 接着
block2: cur_layers[conv2d(128,256) , connv2d(256,256) x 5]

之后是两次上采样块:
deblock1: 尺寸不变,通道数加倍
deblock2: 尺寸加倍,通道数不变

forward进行下采样和上采样,最后堆叠上采样结果作为2D特征提取结果。
LAYER_NUMS: [5, 5]
LAYER_STRIDES: [1, 2]
NUM_FILTERS: [128, 256]
UPSAMPLE_STRIDES: [1, 2]
NUM_UPSAMPLE_FILTERS: [256, 256]

具体代码:

import argparse

import numpy as np
import torch
import torch.nn as nn

from pcdet.config import cfg, cfg_from_yaml_file

"""
五:2D特征提取网络
    对于体素高度压缩得到的鸟瞰特征图进行特征提取
    
    上一步深度压缩:pcdet/models/backbones_2d/map_to_bev/height_compression.py
    地址:pcdet/models/backbones_2d/base_bev_backbone.py
    下一步利用2D特征进行检测分类以及回归:pcdet/models/dense_heads/anchor_head_single.py
    
    这里采用了和VoxelNet类是的网络结构;分别对特征图进行不同尺度的下采样然后再进行上采用后在通道维度进行拼接。
    下采样过程构建下采样卷积块,每个块中包含若干卷积层如下:
    block1: cur_layers[con(in,128) , con(128,128) x 5] 接着
    block2: cur_layers[con(128,256) , con(256,256) x 5]
    
    之后是两次上采样块:
    deblock1: 尺寸不变,通道数加倍
    deblock2: 尺寸加倍,通道数不变
    
    forward进行下采样和上采样,最后堆叠上采样结果作为2D特征提取结果。
        LAYER_NUMS: [5, 5]
        LAYER_STRIDES: [1, 2]
        NUM_FILTERS: [128, 256]
        UPSAMPLE_STRIDES: [1, 2]
        NUM_UPSAMPLE_FILTERS: [256, 256]
"""


# 鸟瞰图卷积
class BaseBEVBackbone(nn.Module):
    def __init__(self, model_cfg, input_channels):
        '''
        :param model_cfg: 模型参数
        :param input_channels: 输入通道数
        '''
        super().__init__()
        self.model_cfg = model_cfg
        # 读取下采样层参数
        # 如果不为空,确保一致后赋予参数,如果为空初始化
        if self.model_cfg.get('LAYER_NUMS', None) is not None:
            assert len(self.model_cfg.LAYER_NUMS) == len(self.model_cfg.LAYER_STRIDES) == len(
                self.model_cfg.NUM_FILTERS)
            layer_nums = self.model_cfg.LAYER_NUMS  # 层数  [5, 5]
            layer_strides = self.model_cfg.LAYER_STRIDES  # 步长  [1, 2]
            num_filters = self.model_cfg.NUM_FILTERS  # 输出通道数 [128, 256]
        else:
            layer_nums = layer_strides = num_filters = []
        # 读取上采样层参数
        # 如果不为空,确保一致后赋予参数,如果为空初始化
        if self.model_cfg.get('UPSAMPLE_STRIDES', None) is not None:
            assert len(self.model_cfg.UPSAMPLE_STRIDES) == len(self.model_cfg.NUM_UPSAMPLE_FILTERS)
            num_upsample_filters = self.model_cfg.NUM_UPSAMPLE_FILTERS  # [256, 256]
            upsample_strides = self.model_cfg.UPSAMPLE_STRIDES  #[1, 2]
        else:
            upsample_strides = num_upsample_filters = []
        # 层数
        num_levels = len(layer_nums)  # 2快卷积块,每块有5层卷积层
        # (256, 128) input_channels:256, num_filters[:-1]:64,128
        # 这里输入通道数组合,1为初始输入, 2为1输出通道 , 3为2输出通道
        # 64 - 128 , 128 - 256
        c_in_list = [input_channels, *num_filters[:-1]]
        self.blocks = nn.ModuleList()
        self.deblocks = nn.ModuleList()
        # (64,64)-->(64,128)-->(128,256) # 这里为cur_layers的第一层且stride=2
        #这里是卷积块 2 ,每块里面有若干层 5
        for idx in range(num_levels):
            #第idx块的 第一层卷积,标准卷积层 :填充,con,bn,relu
            # k=3, s规定, p=1
            cur_layers = [
                #上下左右进行一次0填充
                nn.ZeroPad2d(1),
                nn.Conv2d(
                    # in - 128  128-256尺寸减小一倍
                    c_in_list[idx], num_filters[idx], kernel_size=3,
                    stride=layer_strides[idx], padding=0, bias=False
                ),
                nn.BatchNorm2d(num_filters[idx], eps=1e-3, momentum=0.01),
                nn.ReLU()
            ]
            #第idx块的 后几层卷积,重复5次
            #k=3 ,s=1 ,p=1 尺寸不变
            for k in range(layer_nums[idx]):  # 根据layer_nums堆叠卷积层
                cur_layers.extend([
                    # 128 - 128
                    nn.Conv2d(num_filters[idx], num_filters[idx], kernel_size=3, padding=1, bias=False),
                    nn.BatchNorm2d(num_filters[idx], eps=1e-3, momentum=0.01),
                    nn.ReLU()
                ])
            # 在block中添加该层,没块con后设置对应的上采样块,注意这里
            # *作用是:将列表解开成几个独立的参数,传入函数 # 类似的运算符还有两个星号(**),是将字典解开成独立的元素作为形参
            self.blocks.append(nn.Sequential(*cur_layers))
            # 构造上采样层  # (1, 2, 4)
            if len(upsample_strides) > 0:
                stride = upsample_strides[idx] #[1]
                if stride >= 1:
                    #添加上采样层
                    self.deblocks.append(nn.Sequential(
                        nn.ConvTranspose2d(
                            #128->256  256-256
                            num_filters[idx], num_upsample_filters[idx],
                            upsample_strides[idx], #1  2
                            stride=upsample_strides[idx], bias=False
                        ),
                        nn.BatchNorm2d(num_upsample_filters[idx], eps=1e-3, momentum=0.01),
                        nn.ReLU()
                    ))
                else:
                    stride = np.round(1 / stride).astype(np.int)
                    self.deblocks.append(nn.Sequential(
                        nn.Conv2d(
                            num_filters[idx], num_upsample_filters[idx],
                            stride,
                            stride=stride, bias=False
                        ),
                        nn.BatchNorm2d(num_upsample_filters[idx], eps=1e-3, momentum=0.01),
                        nn.ReLU()
                    ))

        c_in = sum(num_upsample_filters)  # 256+256 = 512
        if len(upsample_strides) > num_levels:
            self.deblocks.append(nn.Sequential(
                nn.ConvTranspose2d(c_in, c_in, upsample_strides[-1], stride=upsample_strides[-1], bias=False),
                nn.BatchNorm2d(c_in, eps=1e-3, momentum=0.01),
                nn.ReLU(),
            ))

        self.num_bev_features = c_in

    def forward(self, data_dict):
        """
        Args:
            data_dict:
                spatial_features : (batch, c, W, H)
        Returns:
        """
        spatial_features = data_dict['spatial_features']
        ups = []
        ret_dict = {}
        x = spatial_features

        # 对不同的分支部分分别进行conv和deconv的操作
        for i in range(len(self.blocks)):
            """
            SECOND中一共存在两个下采样分支,
            输入: (batch, 128*2, 200, 176)
            分支一: (batch,256,200,176) 经过1in + 5con 尺寸不变,通道128
            分支二: (batch,2C,100,88) 经过1in + 5con 尺寸减半,通道256
            """
            x = self.blocks[i](x)

            stride = int(spatial_features.shape[2] / x.shape[2])
            ret_dict['spatial_features_%dx' % stride] = x

            # 如果存在deconv,则对经过conv的结果进行反卷积操作
            """
            SECOND中存在两个下采样,则分别对两个下采样分支进行反卷积操作
            分支一: (batch,C,200,176)-->(batch,2C,200,176)
            分支二: (batch,2C,100,88)-->(batch,2C,200,176)
            """
            #记录多次上采样结果
            if len(self.deblocks) > 0:
                ups.append(self.deblocks[i](x))
            else:
                ups.append(x)

        # 将上采样结果在通道维度拼接
        if len(ups) > 1:
            """
            最终经过所有上采样层得到的2个尺度的的信息
            每个尺度的 shape 都是 (batch, C, 200, 176)
            在第一个维度上进行拼接得到x  维度是 (batch, 2C, 200, 176)
            """
            x = torch.cat(ups, dim=1)
        elif len(ups) == 1:
            x = ups[0]

        # Fasle
        if len(self.deblocks) > len(self.blocks):
            x = self.deblocks[-1](x)

        # 将结果存储在spatial_features_2d中并返回
        data_dict['spatial_features_2d'] = x

        return data_dict


def parse_config():
    parser = argparse.ArgumentParser(description='arg parser')
    parser.add_argument('--cfg_file', type=str,
                        # default='cfgs/kitti_models/pointrcnn.yaml',
                        # default='cfgs/kitti_models/pv_rcnn.yaml',
                         default=r'D:\python\3D_Detection\open-pcddet-3d\tools\cfgs\kitti_models\second.yaml',
                        # default='/home/nathan/OpenPCDet/tools/cfgs/kitti_models/pointpillar.yaml',
                        # default='cfgs/kitti_models/CaDDN.yaml',
                        help='specify the config for demo')
    parser.add_argument('--data_path', type=str, default=r'D:\python\data\mkitti\training\velodyne',
                        help='specify the point cloud data file or directory')
    parser.add_argument('--ckpt', type=str,
                        # default="/home/nathan/OpenPCDet/tools/pointrcnn_7870.pth",
                        # default="/home/nathan/OpenPCDet/weights/caddn_pcdet.pth",
                        # default="/home/nathan/OpenPCDet/weights/pv_rcnn_8369.pth",
                        # default="/home/nathan/OpenPCDet/weights/voxel_rcnn_car_84.54.pth",
                        default="/home/nathan/OpenPCDet/weights/pointpillar_7728.pth",
                        help='specify the pretrained model')
    parser.add_argument('--ext', type=str, default='.bin', help='specify the extension of your point cloud data file')

    args = parser.parse_args()

    cfg_from_yaml_file(args.cfg_file, cfg)

    return args, cfg

if __name__ == '__main__':
    args, cfg = parse_config()
    bevBackBone = BaseBEVBackbone(model_cfg=cfg.MODEL,input_channels=4)
    print(bevBackBone)

六: AnchorHeadTemplate锚框检测头模板

​ 获取最后的2D全局特征以后,我们需要使用检测头得到最后的结果,对于检测任务而言,也就是分类结果和回归结果。当然SECOND在检测方法进行了创新添加了方向损失,简单来说就是对于车头方向进行二分类,在分类正确的情况下,在构建预测角与真实角度的损失,从两个方面约束了检测器,使得检测器能够检测到正确的方向,把角度限制在0-90度以内,在0-90度以内,预测偏差越小越好。

​ 第六部分使用了AnchorHeadTemplate锚框检测头模板这个类 ,其中包含锚框生成,锚框对齐,构建损失函预测生成等等方法。接下来在第六小结将会逐一进行介绍。以下是一个概要

  • 六: AnchorHeadTemplate锚框检测头模板

  • ​ 6.1:generate_anchors锚框生成

  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐

  • ​ 6.3:构建损失函数总体损失方法:get_loss

  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)

  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)

  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)

  • ​ 6.4:预测生成方法:generate_predicted_boxes

    方法汇总:

在这里插入图片描述

  • 检测头包括:
  • 锚框生成方法:generate_anchors(该方法生成初始锚框锚框)
  • 锚框对齐方法:assign_targets(该方法实现gt与锚框构建对应关系)
  • 角度转化方法:add_sin_difference(该方法将角度转化为sin cos形式,之后直接相减即可得到sin(a-b))
  • 方向对应方法:get_direction_target(该方法利用标签角度构建方向分类标签)
  • 构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • 分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • 回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • 总体损失方法:get_loss(调用分类损失和回归损失,得到模型总体损失)
  • 预测生成方法:generate_predicted_boxes(推断过程原始锚框加上预测偏移得到最终预测位置)

**地址:**pcdet/models/dense_heads/anchor_head_template.py

6.1:generate_anchors锚框生成

  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

**地址:**pcdet/models/dense_heads/target_assigner/anchor_generator.py

generate_anchors:生成锚框,在进行预测之前,需要在点云场景中生成锚框,同时每个类别(3)的anchor都有两个方向角为0度和90度。所以每个点对应6个锚框构建anchor生成对象:

  1. generate_anchors构建AnchorGenerator对象用于生成锚框,输入构建锚框信息

  2. 得到特征图尺寸,作为场景尺寸(1,200,176) (d,x,y)

  3. 检查锚框维度,要等于7不等的话进行填充。7表示:x,y,z,w,h,l,theta

  4. 调用AnchorGenerator对象中的generate_anchors方法得到锚框信息,包括anchors_list,每个位置anchors数目如下

    anchors:(1,248,216,1,2,7):(l,w,h,每个位置锚框数,朝向,特征)

    num_anchors_per_location:[2,2,2]

    每个位置俩锚框:

    车的(1,248,216,1,2,7)

    人的(1,248,216,1,2,7)

    骑手的(1,248,216,1,2,7)

构建AnchorGenerator方法:

@staticmethod
def generate_anchors(anchor_generator_cfg, grid_size, point_cloud_range, anchor_ndim=7):
    # 初始化AnchorGenerator类
    anchor_generator = AnchorGenerator(
        anchor_range=point_cloud_range,
        anchor_generator_config=anchor_generator_cfg
    )
    # config['feature_map_stride'] == 8
    # e.g. size [[176,200],[176,200],[176,200]] or [[216,248],[216,248],[216,248]]
    feature_map_size = [grid_size[:2] // config['feature_map_stride'] for config in
                        anchor_generator_cfg]
    # 计算所有3个类别的anchor和每个位置上的anchor数量
    anchors_list, num_anchors_per_location_list = anchor_generator.generate_anchors(feature_map_size)

    # 如果anchor的维度不等于7,则补0
    if anchor_ndim != 7:
        for idx, anchors in enumerate(anchors_list):
            pad_zeros = anchors.new_zeros([*anchors.shape[0:-1], anchor_ndim - 7])
            new_anchors = torch.cat((anchors, pad_zeros), dim=-1)
            anchors_list[idx] = new_anchors
    # list:3 [(1,248,216,1,2,7),(1,248,216,1,2,7),(1,248,216,1,2,7)], [2,2,2]
    # list:3 [(1,200,176,1,2,7),(1,200,176,1,2,7),(1,200,176,1,2,7)], [2,2,2]
    return anchors_list, num_anchors_per_location_list

详细介绍:

路径:pcdet/models/dense_heads/target_assigner/anchor_generator.py

后续锚框检测:pcdet/models/dense_heads/anchor_head_single.py

​ 由于在3D世界中,每个类别的物体大小相对固定,所以直接使用了基于KITTI数据集上每个类别的平均长宽高作

为anchor大小。同时每个类别的anchor都有两个方向角为0度和90度。 anchor的类别尺度大小(单位:米):

self.anchor_sizes,self.anchor_rotations,self.anchor_heights
分别是:

  • 车 [3.9, 1.6, 1.56],anchor的中心在Z轴的-1米、
  • 人[0.8, 0.6, 1.73],anchor的中心在Z轴的-0.6米、
  • 自行车[1.76, 0.6, 1.73],anchor的中心在Z轴的-0.6米
  • 旋转都为:[0, 1.57],[0, 1.57],[0, 1.57] 0-90

​ 每个anchro都有被指定两个个one-hot向量,一个用于方向分类,一个用于类别分类;还被指定一个7维的向量

用于anchor box的回归,分别是(x, y, z, l, w, h, θ)其中θ为PCDet坐标系下物体的朝向信息。

最终可以得到3个类别的anchor,维度都是[z, y, x, num_size, num_rot, 7],其中

  • num_size是每个类别有几个尺度(1个);
  • num_rot为每个anchor有几个方向类别(2个);
  • 7维向量表示为 [x, y, z, dx, dy, dz, rot](每个anchor box的信息)。

代码:

import torch



"""
6.1: 锚框生成

 路径:pcdet/models/dense_heads/target_assigner/anchor_generator.py
 后续锚框检测L:pcdet/models/dense_heads/anchor_head_single.py
 由于在3D世界中,每个类别的物体大小相对固定,
 所以直接使用了基于KITTI数据集上每个类别的平均长宽高作为anchor大小
 同时每个类别(3)的anchor都有两个方向角为0度和90度。
 
 anchor的类别尺度大小(单位:米): self.anchor_sizes,self.anchor_rotations,self.anchor_heights
    分别是
    车 [3.9, 1.6, 1.56],anchor的中心在Z轴的-1米、 
    人[0.8, 0.6, 1.73],anchor的中心在Z轴的-0.6米、
    自行车[1.76, 0.6, 1.73],anchor的中心在Z轴的-0.6米
    旋转都为:[0, 1.57],[0, 1.57],[0, 1.57] 0-90

    每个anchro都有被指定两个个one-hot向量,
    一个用于方向分类,一个用于类别分类;
    还被指定一个7维的向量用于anchor box的回归,分别是(x, y, z, l, w, h, θ)其中θ为PCDet坐标系下物体的朝向信息。
    最终可以得到3个类别的anchor,维度都是[z, y, x, num_size, num_rot, 7],
    其中num_size是每个类别有几个尺度(1个);
    num_rot为每个anchor有几个方向类别(2个);
    7维向量表示为 [x, y, z, dx, dy, dz, rot](每个anchor box的信息)。
"""
class AnchorGenerator(object):
    def __init__(self, anchor_range, anchor_generator_config):
        '''
        :param anchor_range: anchor在点云中的分布范围
        :param anchor_generator_config: 来自tools/cfgs/kitti_models/second.yaml,里面说明了SECOND锚框配置
        '''
        super().__init__()
        self.anchor_generator_cfg = anchor_generator_config  # list:3
        # [x,y,z,x',y',z']
        # 得到anchor在点云中的分布范围[0, -39.68, -3, 69.12, 39.68, 1], [0, -40, -3, 70.4, 40, 1]
        self.anchor_range = anchor_range
        # 得到配置参数中所有尺度anchor的长宽高,一个锚框共三个参数
        # list:3 --> 车、人、自行车[[[3.9, 1.6, 1.56]],[[0.8, 0.6, 1.73]],[[1.76, 0.6, 1.73]]]
        self.anchor_sizes = [config['anchor_sizes'] for config in anchor_generator_config]
        # 得到anchor的旋转角度,这是是弧度,也就是0度和90度,一个锚框共两个参数
        # list:3 --> [[0, 1.57],[0, 1.57],[0, 1.57]]
        self.anchor_rotations = [config['anchor_rotations'] for config in anchor_generator_config]
        # 得到每个anchor初始化在点云中z轴的位置,其中在kitti中点云的z轴范围是-3米到1米
        # list:3 -->  [[-1.78],[-0.6],[-0.6]]  一个锚框共一个参数说明中心位置
        self.anchor_heights = [config['anchor_bottom_heights'] for config in anchor_generator_config]
        # 每个先验框产生的时候是否需要在每个格子的中间,
        # 例如坐标点为[1,1],如果需要对齐中心点的话,需要加上0.5变成[1.5, 1.5]
        # 默认为False
        # list:3 --> [False, False, False]
        self.align_center = [config.get('align_center', False) for config in anchor_generator_config]

        assert len(self.anchor_sizes) == len(self.anchor_rotations) == len(self.anchor_heights)
        self.num_of_anchor_sets = len(self.anchor_sizes)  # 3

    def generate_anchors(self, grid_sizes):
        assert len(grid_sizes) == self.num_of_anchor_sets
        # 1.初始化
        # all_anchors: [(1,248,216,1,2,7),(1,248,216,1,2,7),(1,248,216,1,2,7)]
        # num_anchors_per_location:[2,2,2]
        all_anchors = []
        num_anchors_per_location = []
        # 2.三个类别的先验框逐类别生成
        for grid_size, anchor_size, anchor_rotation, anchor_height, align_center in zip(
                grid_sizes, self.anchor_sizes, self.anchor_rotations, self.anchor_heights, self.align_center):
            # 2 = 2x1x1 --> 每个位置产生2个anchor,这里的2代表两个方向
            num_anchors_per_location.append(len(anchor_rotation) * len(anchor_size) * len(anchor_height))
            #  不需要对齐中心点来生成先验框
            if align_center:
                #[x,y,z,x',y',z']将长宽划分到我们设置的网格度量中 70-0 // 180 步长
                x_stride = (self.anchor_range[3] - self.anchor_range[0]) / grid_size[0]
                y_stride = (self.anchor_range[4] - self.anchor_range[1]) / grid_size[1]
                # 中心对齐,平移半个网格
                x_offset, y_offset = x_stride / 2, y_stride / 2
            else:
                # 2.1计算每个网格的在点云空间中的实际大小
                # 用于将每个anchor映射回实际点云中的大小
                # (69.12 - 0) / (216 - 1) = 0.3214883848678234  单位:米每个格子尺寸
                x_stride = (self.anchor_range[3] - self.anchor_range[0]) / (grid_size[0] - 1)
                # (39.68 - (-39.68.)) / (248 - 1) = 0.3212955490297634  单位:米
                y_stride = (self.anchor_range[4] - self.anchor_range[1]) / (grid_size[1] - 1)
                # 由于没有进行中心对齐,所有每个点相对于左上角坐标的偏移量都是0
                x_offset, y_offset = 0, 0

            # 2.2 生成单个维度x_shifts,y_shifts和z_shifts
            # 以x_stride为step,在self.anchor_range[0] + x_offset和self.anchor_range[3] + 1e-5,
            # 产生x坐标 --> 216个点 [0, 69.12] , 按照每个锚框大小将x方向场景进行网格划分
            x_shifts = torch.arange(
                self.anchor_range[0] + x_offset, self.anchor_range[3] + 1e-5, step=x_stride, dtype=torch.float32,
            ).cuda()
            # 产生y坐标 --> 248个点 [0, 79.36]
            y_shifts = torch.arange(
                self.anchor_range[1] + y_offset, self.anchor_range[4] + 1e-5, step=y_stride, dtype=torch.float32,
            ).cuda()
            """
            new_tensor函数可以返回一个新的张量数据,该张量数据与指定的tensor具有相同的属性
            如拥有相同的数据类型和张量所在的设备情况等属性;
            并使用anchor_height数值个来填充这个张量
            """
            # [-1.78]
            z_shifts = x_shifts.new_tensor(anchor_height)
            # num_anchor_size = 1
            # num_anchor_rotation = 2
            num_anchor_size, num_anchor_rotation = anchor_size.__len__(), anchor_rotation.__len__()  # 1, 2
            #  [0, 1.57] 弧度制
            anchor_rotation = x_shifts.new_tensor(anchor_rotation)
            # [[3.9, 1.6, 1.56]]
            anchor_size = x_shifts.new_tensor(anchor_size)

            # 2.3 调用meshgrid生成网格坐标
            x_shifts, y_shifts, z_shifts = torch.meshgrid([
                x_shifts, y_shifts, z_shifts
            ])
            # meshgrid可以理解为在原来的维度上进行扩展,例如:
            # x原来为(216,)-->(216,1, 1)--> (216,248,1)
            # y原来为(248,)--> (1,248,1)--> (216,248,1)
            # z原来为 (1, )  --> (1,1,1)    --> (216,248,1)

            # 2.4.anchor各个维度堆叠组合,生成最终anchor(1,432,496,1,2,7)
            # 2.4.1.堆叠anchor的位置 
            # [x, y, z, 3]-->[216, 248, 1, 3] 代表了每个anchor空间位置,x方向第2,y方向第3,z方向第5个锚框,
            # xyz是锚框位置3是锚框位置的值,绝对位置信息,空间中(x,y,z)值
            # 其中3为该点所在映射tensor中的(z, y, x)数值
            anchors = torch.stack((x_shifts, y_shifts, z_shifts), dim=-1)  
            # 2.4.2.将anchor的位置和大小进行组合,编程为将anchor扩展并复制为相同维度(除了最后一维),然后进行组合

            #接下来两次repeat统一锚框位置信息和尺寸信息维度,方便cat
            # (216, 248, 1, 3) --> (216, 248, 1 , 1, 3)
            # 维度分别代表了: z,y,x, 该类别anchor的尺度数量,该个anchor的位置信息
            anchors = anchors[:, :, :, None, :].repeat(1, 1, 1, anchor_size.shape[0], 1)
            # (1, 1, 1, 1, 3) --> (216, 248, 1, 1, 3)
            anchor_size = anchor_size.view(1, 1, 1, -1, 3).repeat([*anchors.shape[0:3], 1, 1])
            # anchors生成的最终结果需要有位置信息和大小信息 --> (216, 248, 1, 1, 6)
            # 最后一个纬度中表示(z, y, x, l, w, h)
            anchors = torch.cat((anchors, anchor_size), dim=-1)
            # 2.4.3.将anchor的位置和大小和旋转角进行组合

            #统一旋转角信息和前两个信息的维度,锚框信息加上两个维度(旋转角有俩) 旋转信息直接重复锚框信息维度
            # 在倒数第二个维度上增加一个维度,然后复制该维度一次
            # (216, 248, 1, 1, 2, 6)        长, 宽, 深, anchor尺度数量, 该尺度旋转角个数,anchor的6个参数
            anchors = anchors[:, :, :, :, None, :].repeat(1, 1, 1, 1, num_anchor_rotation, 1)
            # (216, 248, 1, 1, 2, 1)        两个不同方向先验框的旋转角度
            anchor_rotation = anchor_rotation.view(1, 1, 1, 1, -1, 1).repeat(
                [*anchors.shape[0:3], num_anchor_size, 1, 1])
            # [z, y, x, num_size, num_rot, 7] --> (216, 248, 1, 1, 2, 7)
            # 最后一个纬度表示为anchors的位置+大小+旋转角度(z, y, x, l, w, h, theta)
            anchors = torch.cat((anchors, anchor_rotation), dim=-1)  # [z, y, x, num_size, num_rot, 7]

            # 2.5 置换anchor的维度
            # [z, y, x, num_anchor_size, num_rot, 7]-->[x, y, z, num_anchor_zie 1, num_rot 2, 7]
            # 最后一个纬度代表了 : [x, y, z, dx, dy, dz, rot]
            anchors = anchors.permute(2, 1, 0, 3, 4, 5).contiguous()
            # 使得各类anchor的z轴方向从anchor的底部移动到该anchor的中心点位置
            # 车 : -1.78 + 1.56/2 = -1.0
            # 人、自行车 : -0.6 + 1.73/2 = 0.23
            anchors[..., 2] += anchors[..., 5] / 2
            all_anchors.append(anchors)
        # all_anchors: [车的(1,248,216,1,2,7),人的(1,248,216,1,2,7),骑手的(1,248,216,1,2,7)]
        # num_anchors_per_location:[2,2,2]
        return all_anchors, num_anchors_per_location


if __name__ == '__main__':
    from easydict import EasyDict

    config = [
        EasyDict({
            'anchor_sizes': [[2.1, 4.7, 1.7], [0.86, 0.91, 1.73], [0.84, 1.78, 1.78]],
            'anchor_rotations': [0, 1.57],
            'anchor_bottom_heights': [0, 0.5]
        })
    ]

    A = AnchorGenerator(
        anchor_range=[-75.2, -75.2, -2, 75.2, 75.2, 4],
        anchor_generator_config=config
    )
    # import pdb
    #
    # pdb.set_trace()
    #分成188 x 188 个格子
    all_anchors, num_anchors_per_location = A.generate_anchors([[188, 188]])
    print(all_anchors[0].shape,num_anchors_per_location.__len__())

6.2:AxisAlignedTargetAssigner锚框与目标框对齐

  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

共有两种对齐方法,SECOND中使用的是第一种。

**地址:**pcdet/models/dense_heads/target_assigner/axis_aligned_target_assigner.py
**地址:**pcdet/models/dense_heads/target_assigner/atss_target_assigner.py

**Target assignment:**将不同类别的anchor堆叠在了一个点进行预测,逐帧逐类别对生成的anchor进行匹配

也就是单个点对应了6个框,我们要选出最适合的那个作为预测框去拟合标签框。

构建anchor匹配对象:

  1. init中调用self.target_assigner = self.get_target_assigner(anchor_target_cfg)构建对象
  2. get_target_assigner中有两种对齐器类:ATSSTargetAssigner,AxisAlignedTargetAssigner
  3. 使用过程assign_targets方法调用self.target_assigner完成锚框匹配
def assign_targets(self, gt_boxes):
    """
    Args:
        gt_boxes: (B, M, 8)
    Returns:
        all_targets_dict = {
            'box_cls_labels': cls_labels, # (4,321408)
            'box_reg_targets': bbox_targets, # (4,321408,7)
            'reg_weights': reg_weights # (4,321408)
        }
    """
    # anchors-->list:3 [(1,248,216,1,2,7),(1,248,216,1,2,7),(1,248,216,1,2,7)]
    targets_dict = self.target_assigner.assign_targets(
        self.anchors, gt_boxes
    )
    return targets_dict

详细描述:

被anchor_head_template调用用在训练模式完成标签与锚框对应。

路径:pcdet/models/dense_heads/target_assigner/axis_aligned_target_assigner.py

引用路径:pcdet/models/dense_heads/anchor_head_template.py

引用路径:pcdet/models/dense_heads/anchor_head_single.py

​ 将不同类别的anchor堆叠在了一个点进行预测,逐帧逐类别对生成的anchor进行匹配也就是单个点对应了6个

框,我们要选出最适合的那个作为预测框去拟合标签框。

​ 由于预测的时候,将不同类别的anchor堆叠在了一个点进行预测,所有进行Target assignment时候,要分类别

进行Target assignment操作。这里与2D 的SSD或YOLO的匹配不同。因此在匹配的时候,需要逐帧逐类别对生成

的anchor进行匹配;

  • 函数assign_targets负责一帧的匹配
  • 函数assign_targets_single负责一帧中单个类别的匹配

具体代码:

import numpy as np
import torch

from ....ops.iou3d_nms import iou3d_nms_utils
from ....utils import box_utils


"""
7,目标对齐
 被anchor_head_template调用用在训练模式完成标签与锚框对应
 路径:pcdet/models/dense_heads/target_assigner/axis_aligned_target_assigner.py
 引用路径:pcdet/models/dense_heads/anchor_head_template.py
 引用路径:pcdet/models/dense_heads/anchor_head_single.py
 
 
 将不同类别的anchor堆叠在了一个点进行预测,逐帧逐类别对生成的anchor进行匹配
 也就是单个点对应了6个框,我们要选出最适合的那个作为预测框去拟合标签框。
 
 由于预测的时候,将不同类别的anchor堆叠在了一个点进行预测,
 所有进行Target assignment时候,要分类别进行Target assignment操作。
 这里与2D 的SSD或YOLO的匹配不同。
 因此在匹配的时候,需要逐帧逐类别对生成的anchor进行匹配;
 其中函数assign_targets负责一帧的匹配,
 函数assign_targets_single负责一帧中单个类别的匹配


"""
class AxisAlignedTargetAssigner(object):
    def __init__(self, model_cfg, class_names, box_coder, match_height=False):
        super().__init__()
        # anchor生成配置参数,包括锚框尺寸,类名,高度,旋转等信息。
        anchor_generator_cfg = model_cfg.ANCHOR_GENERATOR_CONFIG
        # 为预测box找对应anchor的参数
        anchor_target_cfg = model_cfg.TARGET_ASSIGNER_CONFIG
        # 编码box的7个残差参数(x, y, z, w, l, h, θ) --> pcdet.utils.box_coder_utils.ResidualCoder
        self.box_coder = box_coder
        # 在PointPillars中指定正负样本的时候由BEV视角计算GT和先验框的iou,不需要进行z轴上的高度的匹配,
        # 想法是:1、点云中的物体都在同一个平面上,没有物体在Z轴发生重叠的情况
        #        2、每个类别的高度相差不是很大,直接使用SmoothL1损失就可以达到很好的高度回归效果
        self.match_height = match_height
        # 类别名称['Car', 'Pedestrian', 'Cyclist']
        self.class_names = np.array(class_names)
        # ['Car', 'Pedestrian', 'Cyclist']
        self.anchor_class_names = [config['class_name'] for config in anchor_generator_cfg]
        # anchor_target_cfg.POS_FRACTION = -1 < 0 --> None
        # 前景、背景采样系数 PointPillars、SECOND不考虑
        self.pos_fraction = anchor_target_cfg.POS_FRACTION if anchor_target_cfg.POS_FRACTION >= 0 else None
        # 总采样数  PointPillars不考虑
        self.sample_size = anchor_target_cfg.SAMPLE_SIZE  # 512
        # False 前景权重由 1/前景anchor数量 PointPillars不考虑
        self.norm_by_num_examples = anchor_target_cfg.NORM_BY_NUM_EXAMPLES
        self.matched_thresholds = {}
        # 类别iou匹配为正样本阈值{'Car':0.6, 'Pedestrian':0.5, 'Cyclist':0.5}
        # 类别iou匹配为负样本阈值{'Car':0.45, 'Pedestrian':0.35, 'Cyclist':0.35}
        self.unmatched_thresholds = {}
        for config in anchor_generator_cfg:
            self.matched_thresholds[config['class_name']] = config['matched_threshold']
            self.unmatched_thresholds[config['class_name']] = config['unmatched_threshold']

        self.use_multihead = model_cfg.get('USE_MULTIHEAD', False)  # False
        # self.separate_multihead = model_cfg.get('SEPARATE_MULTIHEAD', False)
        # if self.seperate_multihead:
        #     rpn_head_cfgs = model_cfg.RPN_HEAD_CFGS
        #     self.gt_remapping = {}
        #     for rpn_head_cfg in rpn_head_cfgs:
        #         for idx, name in enumerate(rpn_head_cfg['HEAD_CLS_NAME']):
        #             self.gt_remapping[name] = idx + 1

    def assign_targets(self, all_anchors, gt_boxes_with_classes):
        """
        处理一批数据中所有点云的anchors和gt_boxes,
        计算每个anchor属于前景还是背景,
        为每个前景的anchor分配类别和计算box的回归残差和回归权重
        Args:
            all_anchors: [(N, 7), ...]
            gt_boxes_with_classes: (B, M, 8)  # 最后维度数据为 (x, y, z, l, w, h, θ,class)
        Returns:
            all_targets_dict = {
                # 每个anchor的类别
                'box_cls_labels': cls_labels, # (batch_size,num_of_anchors)
                # 每个anchor的回归残差 -->(∆x, ∆y, ∆z, ∆l, ∆w, ∆h, ∆θ)
                'box_reg_targets': bbox_targets, # (batch_size,num_of_anchors,7)
                # 每个box的回归权重
                'reg_weights': reg_weights # (batch_size,num_of_anchors)
            }
        """
        # 1.初始化结果list并提取对应的gt_box和类别
        bbox_targets = []
        cls_labels = []
        reg_weights = []

        # 得到批大小
        batch_size = gt_boxes_with_classes.shape[0]  # 4
        # 得到所有GT的类别
        gt_classes = gt_boxes_with_classes[:, :, -1]  # (4,num_of_gt)
        # 得到所有GT的7个box参数
        gt_boxes = gt_boxes_with_classes[:, :, :-1]  # (4,num_of_gt,7)
        # 2.对batch中的所有数据逐帧匹配anchor的前景和背景
        for k in range(batch_size):
            cur_gt = gt_boxes[k]  # 取出当前帧中的 gt_boxes (num_of_gt,7)
            """
            由于在OpenPCDet的数据预处理时,以一批数据中拥有GT数量最多的帧为基准,
            其他帧中GT数量不足,则会进行补0操作,使其成为一个矩阵,例:
            [
                [1,1,2,2,3,2],
                [2,2,3,1,0,0],
                [3,1,2,0,0,0]
            ]
            因此这里从每一行的倒数第二个类别开始判断,
            截取最后一个非零元素的索引,来取出当前帧中真实的GT数据
            """
            cnt = cur_gt.__len__() - 1  # 得到一批数据中最多有多少个GT,最后一个
            # 这里的循环是找到最后一个非零的box,因为预处理的时候会按照batch最大box的数量处理,不足的进行补0
            while cnt > 0 and cur_gt[cnt].sum() == 0:
                cnt -= 1
            # 2.1提取当前帧非零的box和类别
            cur_gt = cur_gt[:cnt + 1]
            # cur_gt_classes 当前帧所有gt的类别,例: tensor([1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3], device='cuda:0', dtype=torch.int32)
            cur_gt_classes = gt_classes[k][:cnt + 1].int()

            target_list = []
            # 2.2 对每帧中的anchor和GT分类别,单独计算前背景
            # 计算时候 每个类别的anchor是独立计算的
            #这里类名对应,anchor的类名与该类别的anchor组成一批,对所有同类锚框处理
            for anchor_class_name, anchors in zip(self.anchor_class_names, all_anchors):
                # anchor_class_name : 车 | 行人 | 自行车
                # anchors : (1, 200, 176, 1, 2, 7)  7 --> (x, y, z, l, w, h, θ)
                if cur_gt_classes.shape[0] > 1:
                    # self.class_names : ["car", "person", "cyclist"]
                    # 这里减1是因为列表索引从0开始,
                    # 目的是得到属于列表中gt中哪些类别是与当前处理的了锚框类别相同,得到类别mask

                    # mask应用在两个地方:类别和边界框
                    # selected_classes = cur_gt_classes[mask] 当前帧所有gt的类别,加mask等于当前帧该类gt
                    # cur_gt[mask] 当前帧中的该类别的box:gt_boxes (num_of_gt,7)
                    #self.class_names[cur_gt_classes.cpu() - 1] :self.class_names名称[索引] 如self.class_names[0] = car
                    mask = torch.from_numpy(self.class_names[cur_gt_classes.cpu() - 1] == anchor_class_name)
                else:
                    mask = torch.tensor([self.class_names[c - 1] == anchor_class_name
                                         for c in cur_gt_classes], dtype=torch.bool)
                # 在检测头中是否使用多头,是的话 此处为True,默认为False
                if self.use_multihead:  # False
                    anchors = anchors.permute(3, 4, 0, 1, 2, 5).contiguous().view(-1, anchors.shape[-1])
                    # if self.seperate_multihead:
                    #     selected_classes = cur_gt_classes[mask].clone()
                    #     if len(selected_classes) > 0:
                    #         new_cls_id = self.gt_remapping[anchor_class_name]
                    #         selected_classes[:] = new_cls_id
                    # else:
                    #     selected_classes = cur_gt_classes[mask]
                    selected_classes = cur_gt_classes[mask]
                else:
                    # 2.2.1 计算所需的变量 得到特征图的大小
                    feature_map_size = anchors.shape[:3]  # (1, 248, 216)
                    # 将所有的anchors展平  shape : (216, 248, 1, 1, 2, 7) -->  (107136, 7)
                    anchors = anchors.view(-1, anchors.shape[-1])
                    # List: 根据累呗mask索引得到该帧中当前需要处理的类别  --> 车 | 行人 | 自行车
                    # 得到GT中与当前处理锚框类别相同的GT
                    selected_classes = cur_gt_classes[mask]

                # 2.2.2 使用assign_targets_single来单独为某一类别的anchors分配gt_boxes,
                # 并为前景、背景的box设置编码和回归权重
                single_target = self.assign_targets_single(
                    anchors,  # 该类的所有anchor
                    cur_gt[mask],  # GT_box边界框  shape : (num_of_GT_box, 7)
                    gt_classes=selected_classes,  # 当前选中的类别
                    matched_threshold=self.matched_thresholds[anchor_class_name],  # 当前类别anchor与GT匹配为正样本的阈值
                    unmatched_threshold=self.unmatched_thresholds[anchor_class_name]  # 当前类别anchor与GT匹配为负样本的阈值
                )
                target_list.append(single_target)
                # 到目前为止,处理完该帧单个类别和该类别anchor的前景和背景分配

            if self.use_multihead:
                target_dict = {
                    'box_cls_labels': [t['box_cls_labels'].view(-1) for t in target_list],
                    'box_reg_targets': [t['box_reg_targets'].view(-1, self.box_coder.code_size) for t in target_list],
                    'reg_weights': [t['reg_weights'].view(-1) for t in target_list]
                }

                target_dict['box_reg_targets'] = torch.cat(target_dict['box_reg_targets'], dim=0)
                target_dict['box_cls_labels'] = torch.cat(target_dict['box_cls_labels'], dim=0).view(-1)
                target_dict['reg_weights'] = torch.cat(target_dict['reg_weights'], dim=0).view(-1)
            else:
                #对应这一批数据,所有锚框类别的标签GT,现在还是分成3组的情况下 车 | 行人 | 自行车
                target_dict = {
                    # feature_map_size:(1,200,176, 2)
                    'box_cls_labels': [t['box_cls_labels'].view(*feature_map_size, -1) for t in target_list],
                    # (1,248,216, 2, 7)
                    'box_reg_targets': [t['box_reg_targets'].view(*feature_map_size, -1, self.box_coder.code_size)
                                        for t in target_list],
                    # (1,248,216, 2)
                    'reg_weights': [t['reg_weights'].view(*feature_map_size, -1) for t in target_list]
                }
                # 在同一组数据下,将三类的box组合cat一下
                # list box信息: 3*anchor (1, 248, 216, 2, 7) --> (1, 248, 216, 6, 7) -> (321408, 7)
                target_dict['box_reg_targets'] = torch.cat(
                    target_dict['box_reg_targets'], dim=-2
                ).view(-1, self.box_coder.code_size)
                # list:3 (1, 248, 216, 2) --> (1,248, 216, 6) -> (1*248*216*6, )
                target_dict['box_cls_labels'] = torch.cat(target_dict['box_cls_labels'], dim=-1).view(-1)
                # list:3 (1, 200, 176, 2) --> (1, 200, 176, 6) -> (1*248*216*6, )
                target_dict['reg_weights'] = torch.cat(target_dict['reg_weights'], dim=-1).view(-1)

            # 将当前批的这一类结果填入对应的容器,总bc批,总共三类结果 车 | 行人 | 自行车 cat之后了
            bbox_targets.append(target_dict['box_reg_targets'])
            cls_labels.append(target_dict['box_cls_labels'])
            reg_weights.append(target_dict['reg_weights'])
            # 到这里该batch的点云全部处理完

        # 3.将结果stack并返回,将每一批中的三类结果堆叠中的box标签
        bbox_targets = torch.stack(bbox_targets, dim=0)  # (batch_size,321408,7)
        cls_labels = torch.stack(cls_labels, dim=0)  # (batch_size,321408)
        reg_weights = torch.stack(reg_weights, dim=0)  # (batch_size,321408)
        all_targets_dict = {
            'box_cls_labels': cls_labels,  # (batch_size,321408)
            'box_reg_targets': bbox_targets,  # (batch_size,321408,7)
            'reg_weights': reg_weights  # (batch_size,321408)

        }
        return all_targets_dict

    def assign_targets_single(self, anchors, gt_boxes, gt_classes, matched_threshold=0.6, unmatched_threshold=0.45):
        """
        针对某一类别的anchors和gt_boxes,计算前景和背景anchor的类别,box编码和回归权重
        Args:

            anchors: (107136, 7)该类所有的锚框
            gt_boxes: (该帧中该类别的GT数量,7) 该类tg的box
            gt_classes: (该帧中该类别的GT数量, 1) 该类gt的类别
            matched_threshold: 0.6 该类正样本阈值
            unmatched_threshold: 0.45 该类负样本阈值
        Returns:
        前景anchor
            ret_dict = {
                'box_cls_labels': labels, # (107136,)
                'box_reg_targets': bbox_targets,  # (107136,7)
                'reg_weights': reg_weights, # (107136,)
            }
        """
        # ----------------------------1.初始化-------------------------------#
        num_anchors = anchors.shape[0]  # 216 * 248 = 107136
        num_gt = gt_boxes.shape[0]  # 该帧中该类别的GT数量

        # 初始化anchor对应的label和gt_id ,并置为 -1,-1表示loss计算时候不会被考虑,背景的类别被设置为0
        labels = torch.ones((num_anchors,), dtype=torch.int32, device=anchors.device) * -1
        gt_ids = torch.ones((num_anchors,), dtype=torch.int32, device=anchors.device) * -1

        # ---------------------2.计算该类别中anchor的前景和背景------------------------#
        if len(gt_boxes) > 0 and anchors.shape[0] > 0:
            # 1.计算该帧中某一个类别gt和对应anchors之间的iou(jaccard index)
            # anchor_by_gt_overlap    shape : (107136, num_gt)
            # anchor_by_gt_overlap代表当前类别的所有anchor和当前类别中所有GT的iou
            # ***->如一共10个anchor , 3个gt  则得到(10,3) 每个a对应的g的IOU
            # 这里计算3D IOU在match_height考虑高度的情况下,否则计算2D的iou
            anchor_by_gt_overlap = iou3d_nms_utils.boxes_iou3d_gpu(anchors[:, 0:7], gt_boxes[:, 0:7]) \
                if self.match_height else box_utils.boxes3d_nearest_bev_iou(anchors[:, 0:7], gt_boxes[:, 0:7])

            # NOTE: The speed of these two versions depends the environment and the number of anchors
            # anchor_to_gt_argmax = torch.from_numpy(anchor_by_gt_overlap.cpu().numpy().argmax(axis=1)).cuda()

            # 2.得到每一个anchor与哪个的GT的的iou最大
            # anchor_to_gt_argmax表示数据维度是anchor的长度,索引是gt
            #  ***->如一共10个anchor , 3个gt  则得到(10,1) 每个a对应的g,以IOU最大为准 索引对应
            anchor_to_gt_argmax = anchor_by_gt_overlap.argmax(dim=1)
            # anchor_to_gt_max得到每一个anchor最匹配的gt的iou数值
            # ***->如一共10个anchor , 3个gt  则得到(10,1) 每个a对应的g,以IOU最大为准 值对应
            # 取得anchor_by_gt_overlap[10,3]的值就是iou值
            anchor_to_gt_max = anchor_by_gt_overlap[
                torch.arange(num_anchors, device=anchors.device), anchor_to_gt_argmax]

            # gt_to_anchor_argmax = torch.from_numpy(anchor_by_gt_overlap.cpu().numpy().argmax(axis=0)).cuda()

            # 3.找到每个gt最匹配anchor的索引和iou
            # (num_of_gt,) 得到每个gt最匹配的anchor索引
            #  ***->如一共10个anchor , 3个gt  则得到(1,3) 每个g对应的a,以IOU最大为准 索引对应
            gt_to_anchor_argmax = anchor_by_gt_overlap.argmax(dim=0)
            # (num_of_gt,)找到每个gt最匹配anchor的iou数值
            # ***->如一共10个anchor , 3个gt  则得到(1,3) 每个g对应的a,以IOU最大为准 值对应
            gt_to_anchor_max = anchor_by_gt_overlap[gt_to_anchor_argmax, torch.arange(num_gt, device=anchors.device)]
            # 4.将GT中没有匹配到的anchor的iou数值设置为-1
            empty_gt_mask = gt_to_anchor_max == 0  # 得到没有匹配到anchor的gt的mask
            gt_to_anchor_max[empty_gt_mask] = -1  # 将没有匹配到anchor的gt的iou数值设置为-1

            # 5.找到anchor中和gt存在最大iou的anchor索引,即前景anchor
            """
            由于在前面的实现中,仅仅找出来每个GT和anchor的最大iou索引,但是argmax返回的是索引最小的那个,
            在匹配的过程中可能一个GT和多个anchor拥有相同的iou大小,
            所以此处要找出这个GT与所有anchors拥有相同最大iou的anchor
            """
            # 以gt为基础,逐个anchor对应,比如第一个gt的最大iou为0.9,则在所有anchor中找iou为0.9的anchor
            # nonzero函数是numpy中用于得到数组array中非零元素的位置(数组索引)的函数
            """
            矩阵比较例子 :
            anchors_with_max_overlap = torch.tensor([[0.78, 0.1, 0.9, 0],
                                                      [0.0, 0.5, 0, 0],
                                                      [0.0, 0, 0.9, 0.8],
                                                      [0.78, 0.1, 0.0, 0]])
            gt_to_anchor_max = torch.tensor([0.78, 0.5, 0.9,0.8]) 
            anchors_with_max_overlap = anchor_by_gt_overlap == gt_to_anchor_max
            
            # 返回的结果中包含了在anchor中与该GT拥有相同最大iou的所有anchor
            anchors_with_max_overlap = tensor([[ True, False,  True, False],
                                                [False,  True, False, False],
                                                [False, False,  True,  True],
                                                [ True, False, False, False]])
            在torch中nonzero返回的是tensor中非0元素的位置,此函数在numpy中返回的是非零元素的行列表和列列表。
            torch返回结果tensor([[0, 0],
                                [0, 2],
                                [1, 1],
                                [2, 2],
                                [2, 3],
                                [3, 0]])
            numpy返回结果(array([0, 0, 1, 2, 2, 3]), array([0, 2, 1, 2, 3, 0]))     
            所以可以得到第一个GT同时与第一个anchor和最后一个anchor最为匹配                     
            """
            """所以在实际的一批数据中可以到得到结果为
            tensor([[33382,     9],
                    [43852,    10],
                    [47284,     5],
                    [50370,     4],
                    [58498,     8],
                    [58500,     8],
                    [58502,     8],
                    [59139,     2],
                    [60751,     1],
                    [61183,     1],
                    [61420,    11],
                    [62389,     0],
                    [63216,    13],
                    [63218,    13],
                    [65046,    12],
                    [65048,    12],
                    [65478,    12],
                    [65480,    12],
                    [71924,     3],
                    [78046,     7],
                    [80150,     6]], device='cuda:0')
            在第1维度拥有相同gt索引的项,在该类所有anchor中同时拥有多个与之最为匹配的anchor
            """
            # (num_of_multiple_best_matching_for_per_GT,)
            anchors_with_max_overlap = (anchor_by_gt_overlap == gt_to_anchor_max).nonzero()[:, 0]
            # 得到这些最匹配anchor与该类别的哪个GT索引相对应
            # 其实和(anchor_by_gt_overlap == gt_to_anchor_max).nonzero()[:, 1]的结果一样
            gt_inds_force = anchor_to_gt_argmax[anchors_with_max_overlap]  # (35,)
            # 将gt的类别赋值到对应的anchor的label中
            labels[anchors_with_max_overlap] = gt_classes[gt_inds_force]
            # 将gt的索引也赋值到对应的anchors的gt_ids中
            gt_ids[anchors_with_max_overlap] = gt_inds_force.int()

            # 6.根据matched_threshold和unmatched_threshold以及anchor_to_gt_max计算前景和背景索引,并更新labels和gt_ids
            """这里对labels和gt_ids的操作应该已经包含了上面的anchors_with_max_overlap"""
            # 找到最匹配的anchor中iou大于给定阈值的mask #(107136,)
            pos_inds = anchor_to_gt_max >= matched_threshold
            # 找到最匹配的anchor中iou大于给定阈值的gt的索引 #(105,)
            gt_inds_over_thresh = anchor_to_gt_argmax[pos_inds]
            # 将pos anchor对应gt的类别赋值到对应的anchor的label中
            labels[pos_inds] = gt_classes[gt_inds_over_thresh]
            # 将pos anchor对应gt的索引赋值到对应的anchor的gt_id中
            gt_ids[pos_inds] = gt_inds_over_thresh.int()

            bg_inds = (anchor_to_gt_max < unmatched_threshold).nonzero()[:, 0]  # 找到背景anchor索引
        else:
            bg_inds = torch.arange(num_anchors, device=anchors.device)

        # 找到前景anchor的索引--> (num_of_foreground_anchor,)
        # 106879 + 119 = 106998 < 107136 说明有一些anchor既不是背景也不是前景,
        # iou介于unmatched_threshold和matched_threshold之间
        fg_inds = (labels > 0).nonzero()[:, 0]
        # 到目前为止得到哪些anchor是前景和哪些anchor是背景

        # ------------------3.对anchor的前景和背景进行筛选和赋值--------------------#
        # 如果存在前景采样比例,则分别采样前景和背景anchor,PointPillar中没有前背景采样操作,前背景均衡使用了focal loss损失函数
        if self.pos_fraction is not None:  # anchor_target_cfg.POS_FRACTION = -1 < 0 --> None
            num_fg = int(self.pos_fraction * self.sample_size)  # self.sample_size=512
            # 如果前景anchor大于采样前景数
            if len(fg_inds) > num_fg:
                # 计算要丢弃的前景anchor数目
                num_disabled = len(fg_inds) - num_fg
                # 在前景数目中随机产生索引值,并取前num_disabled个关闭索引
                # 比如:torch.randperm(4)
                # 输出:tensor([ 2,  1,  0,  3])
                disable_inds = torch.randperm(len(fg_inds))[:num_disabled]
                # 将被丢弃的anchor的iou设置为-1
                labels[disable_inds] = -1
                # 更新前景索引
                fg_inds = (labels > 0).nonzero()[:, 0]

            # 计算所需背景数
            num_bg = self.sample_size - (labels > 0).sum()
            # 如果当前背景数大于所需背景数
            if len(bg_inds) > num_bg:
                # torch.randint在0到len(bg_inds)之间,随机产生size为(num_bg,)的数组
                enable_inds = bg_inds[torch.randint(0, len(bg_inds), size=(num_bg,))]
                # 将enable_inds的标签设置为0
                labels[enable_inds] = 0
            # bg_inds = torch.nonzero(labels == 0)[:, 0]
        else:
            # 如果该类别没有GT的话,将该类别的全部label置0,即所有anchor都是背景类别
            if len(gt_boxes) == 0 or anchors.shape[0] == 0:
                labels[:] = 0
            else:
                # anchor与GT的iou小于unmatched_threshold的anchor的类别设置类背景类别
                labels[bg_inds] = 0
                # 将前景赋对应类别
                """
                此处分别使用了anchors_with_max_overlap和
                anchor_to_gt_max >= matched_threshold来对该类别的anchor进行赋值
                但是我个人觉得anchor_to_gt_max >= matched_threshold已经包含了anchors_with_max_overlap的那些与GT拥有最大iou的
                anchor了,所以我对这里的计算方式有一点好奇,为什么要分别计算两次,
                如果知道这里原因的小伙伴希望可以给予解答,谢谢!
                """
                labels[anchors_with_max_overlap] = gt_classes[gt_inds_force]

        # ------------------4.计算bbox_targets和reg_weights--------------------#
        # 初始化每个anchor的7个回归参数,并设置为0数值
        bbox_targets = anchors.new_zeros((num_anchors, self.box_coder.code_size))  # (107136,7)
        # 如果该帧中有该类别的GT时候,就需要对这些设置为正样本类别的anchor进行编码操作了
        if len(gt_boxes) > 0 and anchors.shape[0] > 0:
            # 使用anchor_to_gt_argmax[fg_inds]来重复索引每个anchor对应前景的GT_box
            fg_gt_boxes = gt_boxes[anchor_to_gt_argmax[fg_inds], :]
            # 提取所有属于前景的anchor
            fg_anchors = anchors[fg_inds, :]
            """
            PointPillar编码gt和前景anchor,并赋值到bbox_targets的对应位置
            7个参数的编码的方式为
            ∆x = (x^gt − xa^da)/d^a , ∆y = (y^gt − ya^da)/d^a , ∆z = (z^gt − za^ha)/h^a
            ∆w = log (w^gt / w^a) ∆l = log (l^gt / l^a) , ∆h = log (h^gt / h^a)
            ∆θ = sin(θ^gt - θ^a) 
            """
            bbox_targets[fg_inds, :] = self.box_coder.encode_torch(fg_gt_boxes, fg_anchors)

        # 初始化回归权重,并设置值为0
        reg_weights = anchors.new_zeros((num_anchors,))  # (107136,)

        if self.norm_by_num_examples:  # PointPillars回归权重中不需要norm_by_num_examples
            num_examples = (labels >= 0).sum()
            num_examples = num_examples if num_examples > 1.0 else 1.0
            reg_weights[labels > 0] = 1.0 / num_examples
        else:
            reg_weights[labels > 0] = 1.0  # 将前景anchor的回归权重设置为1

        ret_dict = {
            'box_cls_labels': labels,  # (107136,)
            'box_reg_targets': bbox_targets,  # (107136,7)编码后的结果
            'reg_weights': reg_weights,  # (107136,)
        }
        return ret_dict

6.3:构建损失函数总体损失方法:get_loss

  • 六: AnchorHeadTemplate锚框检测头模板

  • ​ 6.1:generate_anchors锚框生成

  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐

  • ​ 6.3:构建损失函数总体损失方法:get_loss

  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)

  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)

  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)

  • ​ 6.4:预测生成方法:generate_predicted_boxes

在这里插入图片描述

**地址:**pcdet/models/dense_heads/anchor_head_template.py

总体损失函数构建:包括分类损失,回归损失

细分 : Loss = cw * cls_loss + bw * box_loss + dw * dir_loss

调用下述损失函数方法计算损失函数,作为后续RPN损失。

具体代码:

def get_loss(self):
    # 计算classfiction layer的loss,tb_dict内容和cls_loss相同,形式不同,一个是torch.tensor一个是字典值
    cls_loss, tb_dict = self.get_cls_layer_loss()
    # 计算regression layer的loss
    box_loss, tb_dict_box = self.get_box_reg_layer_loss()
    # 在tb_dict中添加tb_dict_box,在python的字典中添加值,
    # 如果添加的也是字典,用update方法,如果是键值对则采用赋值的方式
    tb_dict.update(tb_dict_box)
    # rpn_loss是分类和回归的总损失
    rpn_loss = cls_loss + box_loss

    # 在tb_dict中添加rpn_loss,此时tb_dict中包含cls_loss,reg_loss和rpn_loss
    tb_dict['rpn_loss'] = rpn_loss.item()
    return rpn_loss, tb_dict
6.3.1构建损失方法:buildloss
  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

**地址:**pcdet/models/dense_heads/anchor_head_template.py

**介绍:**buildloss该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等,相当于初始化一个损失工具类,之后在分类损失和回归损失中需要用到什么直接从这里面拿。

具体代码:

def build_losses(self, losses_cfg):
    # 添加loss模块,包括分类损失,回归损失和方向损失并初始化
    # 使用focu loss 损失来处理正负样本不均衡的状况
    self.add_module(
        'cls_loss_func',
        loss_utils.SigmoidFocalClassificationLoss(alpha=0.25, gamma=2.0)
    )
    #使用带有权重的L1损失作为回归损失,对于x,y,z,w,l,g,o计算损失
    reg_loss_name = 'WeightedSmoothL1Loss' if losses_cfg.get('REG_LOSS_TYPE', None) is None \
        else losses_cfg.REG_LOSS_TYPE  # reg_loss_name:WeightedSmoothL1Loss
    self.add_module(
        'reg_loss_func',
        getattr(loss_utils, reg_loss_name)(code_weights=losses_cfg.LOSS_WEIGHTS['code_weights'])
    )
    #方向角损失使用加权交叉熵损失,相当于进行了一次前向分类
    self.add_module(
        'dir_loss_func',
        loss_utils.WeightedCrossEntropyLoss()
    )
6.3.2:分类损失方法:get_cls_layer_loss
  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

分类损失构建流程:

  1. 预测数据和标签数据的获取,得到预测分类情况cls_preds,得到所有anchor类别情况box_cls_labels。

  2. 根据标签anchor类别得到关注的anchor,正负样本的mask,正负样本赋予不同的权重-<负样本:0>- 0.45 -<不考虑:-1> - 0.65 -<正样本:1,2,3>-。

  3. 进行一个batch中样本权重计算,正样本数目越多,其样本权重越大。此权重为样本权重。

  4. 获取所有anchor标签中收到关注的anchor也就是值大于等于0的,排除高低阈值之间的不关注的anchor

  5. 构建one-hot形式编码,对于标签类别进行编码。

  6. 使用cls_preds, one_hot_targets类别预测结果和one-hot编码标签计算类别损失调用self.cls_loss_func中的带权重的foculoss损失计算类别损失,缓解正负样本不均衡带来的问题,权重采用样本权重。

  7. 最后得到的分类损失进行损失加权,添加损失函数中整体分类损失占比。

    return cls_loss, tb_dict

其中self.cls_loss_func在初始化中的build_loss构建,build_loss中调用SigmoidFocalClassificationLoss构建fucoloss.

路径:pcdet/utils/loss_utils.py , SigmoidFocalClassificationLoss

具体代码:

def get_cls_layer_loss(self):
    # (batch_size, 248, 216, 18) 网络类别预测类别
    cls_preds = self.forward_ret_dict['cls_preds']
    # (batch_size, 321408) 前景anchor类别
    box_cls_labels = self.forward_ret_dict['box_cls_labels']
    batch_size = int(cls_preds.shape[0])
    # [batch_szie, num_anchors]--> (batch_size, 321408)
    # 关心的anchor  选取出前景背景anchor, 在0.45到0.6之间的设置为仍然是-1,不参与loss计算
    cared = box_cls_labels >= 0
    # (batch_size, 321408) 前景anchor
    positives = box_cls_labels > 0
    # (batch_size, 321408) 背景anchor
    negatives = box_cls_labels == 0
    # 背景anchor赋予权重
    negative_cls_weights = negatives * 1.0
    # 将每个anchor分类的损失权重都设置为1
    cls_weights = (negative_cls_weights + 1.0 * positives).float()
    # 每个正样本anchor的回归损失权重,设置为1
    reg_weights = positives.float()
    # 如果只有一类
    if self.num_class == 1:
        # class agnostic
        box_cls_labels[positives] = 1

    # 正则化并计算权重     求出每个数据中有多少个正例,即shape=(batch, 1)
    pos_normalizer = positives.sum(1, keepdim=True).float()  # (4,1) 所有正例的和 eg:[[162.],[166.],[155.],[108.]]
    # 正则化回归损失-->(batch_size, 321408),最小值为1,根据论文中所述,用正样本数量来正则化回归损失
    reg_weights /= torch.clamp(pos_normalizer, min=1.0)
    # 正则化分类损失-->(batch_size, 321408),根据论文中所述,用正样本数量来正则化分类损失
    cls_weights /= torch.clamp(pos_normalizer, min=1.0)
    # care包含了背景和前景的anchor,但是这里只需要得到前景部分的类别即可不关注-1和0
    # cared.type_as(box_cls_labels) 将cared中为False的那部分不需要计算loss的anchor变成了0
    # 对应位置相乘后,所有背景和iou介于match_threshold和unmatch_threshold之间的anchor都设置为0
    cls_targets = box_cls_labels * cared.type_as(box_cls_labels)
    # 在最后一个维度扩展一次
    cls_targets = cls_targets.unsqueeze(dim=-1)

    cls_targets = cls_targets.squeeze(dim=-1)
    one_hot_targets = torch.zeros(
        *list(cls_targets.shape), self.num_class + 1, dtype=cls_preds.dtype, device=cls_targets.device
    )  # (batch_size, 321408, 4),这里的类别数+1是考虑背景

    # target.scatter(dim, index, src)
    # scatter_函数的一个典型应用就是在分类问题中,
    # 将目标标签转换为one-hot编码形式 https://blog.csdn.net/guofei_fly/article/details/104308528
    # 这里表示在最后一个维度,将cls_targets.unsqueeze(dim=-1)所索引的位置设置为1

    """
        dim=1: 表示按照列进行填充
        index=batch_data.label:表示把batch_data.label里面的元素值作为下标,
        去下标对应位置(这里的"对应位置"解释为列,如果dim=0,那就解释为行)进行填充
        src=1:表示填充的元素值为1
    """
    # (batch_size, 321408, 4)
    one_hot_targets.scatter_(-1, cls_targets.unsqueeze(dim=-1).long(), 1.0)
    # (batch_size, 248, 216, 18) --> (batch_size, 321408, 3)
    cls_preds = cls_preds.view(batch_size, -1, self.num_class)
    # (batch_size, 321408, 3) 不计算背景分类损失
    one_hot_targets = one_hot_targets[..., 1:]

    # 计算分类损失 # [N, M] # (batch_size, 321408, 3)
    cls_loss_src = self.cls_loss_func(cls_preds, one_hot_targets, weights=cls_weights)
    # 求和并除以batch数目
    cls_loss = cls_loss_src.sum() / batch_size
    # loss乘以分类权重 --> cls_weight=1.0
    cls_loss = cls_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['cls_weight']
    tb_dict = {
        'rpn_loss_cls': cls_loss.item()
    }
    return cls_loss, tb_dict
6.3.3:回归损失方法:get_box_reg_layer
  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

方向损失介绍:SECOND中还有一个重要的创新,就是对物体方向估计进行了重新的建模。

这里,作者在最后的RPN层(原来是两个分支,用来物体分类和位置回归)多引入了一个分支,用来对物体方向进行分类。为什么是分类不是回归呢?

在这里插入图片描述

这里我们简单聊聊在VoxelNet中是如何训练模型以达到确定方向的目的的。

在VoxelNet中,一个3D BBox被建模为一个7维向量表示,分别为(x,y,z,l,w,h)

训练过程中,对这7个变量采用Smooth L1损失进行回归训练。

这会造成什么问题呢?

大家设想下,当同一个3D检测框的预测方向恰好与真实方向相反的时候,上述的7个变量的前6个变量的回归损失较小,而最后一个方向的回归损失会很大,这其实并不利于模型训练。为了解决这个问题,作者引入了对方向角的回归损失,定义如下:
在这里插入图片描述

其中 Θ P 是预测角度, Θ t 是真实角度,计算过程两个角度相减再进行 s i n , s i n 函数在 0 − Π 内先单调递增再单调递减, 其中\Theta_P是预测角度,\Theta_t是真实角度,计算过程两个角度相减再进行sin,sin函数在0-\Pi内先单调递增再单调递减, 其中ΘP是预测角度,Θt是真实角度,计算过程两个角度相减再进行sinsin函数在0Π内先单调递增再单调递减,

也就是说如果两个预测角为 90 度时,该损失函数最大,两个预测角为 0 或 Π 时,损失值最小。 也就是说如果两个预测角为90度时,该损失函数最大,两个预测角为0或\Pi时,损失值最小。 也就是说如果两个预测角为90度时,该损失函数最大,两个预测角为0Π时,损失值最小。

这时候还会产生一个问题,那么我两个预测相反时,也就是预测朝向和真是朝向相反,我损失函数依然不大,这不符合我们希望的结果。

这时候作者提出了另一种解决方法(也就是RPN中的direction classifer分支,作者将车头是否区分正确直接通过一个softmax loss来进行约束。如果theta>0则为正,theta<0则为负。那么这就是个简单的二分类问题了,也就是结构图中direction classifer。

简单来说就是对车头进行单独的分类,车头分类正确的情况下才继续进行方向交损失的计算,此时loss中只针对角度偏移为0-90的目标,也就是偏移越大损失越大,达到了我们希望的效果。

使用数据介绍:

这里的forward_ret_dict['box_preds'],self.forward_ret_dict[dir_cls_preds]来自
self.forward_ret_dict和pcdet/models/dense_heads/anchor_head_single.py
self.forward_ret_dict['cls_preds']:类别预测 
shape :(batch_size, 200, 176, 18)
self.forward_ret_dict['dir_cls_preds']:方向分类 
shape :(batch_size, 200, 176, 12)
self.forward_ret_dict['box_preds']:box回归
shape:(batch_size, 200, 176, 42)
是各个锚框的预测结果,包括box预测结果,方向预测结果。

forward_ret_dict['box_reg_targets']和forward_ret_dict['box_cls_labels']来自
box_cls_labels:取值为-1,0,1,2,3. 
-1表示负样本初始全-1不进行loss计算,这部分既不是正样本也不是负样本,在低阈值和高阈值之间
0表示背景类别,负样本类别,在低阈值之下
1,2,3为anchor类别为正样本
-<负样本:0>- 0.45 -<不考虑:-1> - 0.65 -<正样本:1,2,3>-    

anchor_head_template.py中的self.target_assigner构建get_target_assigner,an-tar对齐
self.forward_ret_dict['box_reg_targets']:每个anchor的回归残差 -->(∆x, ∆y, ∆z, ∆l, ∆w, ∆h, ∆θ)
shape :(batch_size,num_of_anchors,7)
self.forward_ret_dict['box_cls_labels']:每个anchor的类别,
shape :(batch_size,num_of_anchors)
self.forward_ret_dict['reg_weights']:每个box的回归权重
shape :(batch_size,num_of_anchors)

构建流程:

  1. 获得预测数据中,类别预测和回归预测,带有pred的。
  2. 获得标签数据中,每个anchor的类别和回归残差,带_targets的。
  3. 使用标签数据中每个anchor的类别来构建正负样本mask。-1,0,1,2,3。
    -<负样本:0>- 0.45 -<不考虑:-1> - 0.65 -<正样本:1,2,3>-
  4. 样本权重reg_weights,表示的是每个batch中样本的权重,样本数量越多权重越大。 该样本数目/总样本数目
  5. 对于角度简单处理,处理成sc cs 形式,一会就可以直接减得到角度损失
  6. 传入box_preds_sin, reg_targets_sin预测和标签值计算WeightedSmoothL1Loss损失,权重为样本权重对于回归损失进行加权,这里的权重为损失权重,在整个损失函数中,回归损失占比
  7. 如果需要计算方向损失,简单理解就是车头是否分类正确,首先调用get_direction_target得到标签方向数据
  8. 获取预测方向数据,并且进行样本权重计算,样本数目越多权重越大。
  9. 计算预测方向与标签方向的加权交叉熵损失,相当于二分类问题。
  10. 对于方向损失进行损失加权,与box_loss合并。

**创新点:**使用了方向角损失 sin(a-b)对方向角进行约束,角度偏差越大,损失越大。但是当角度为180,也就是相反时,该损失也小,为了解决这个问题,又添加了方向损失,简单来说这就是个车头分类是否正确的二分类问题,去约束方向角到0-90在两个损失约束下:预测会在方向预测正确的同时判定偏差角度的大小,确保整体方向预测正确,角度偏差越小越好。

具体代码:

角度转化sin,cos形式:

@staticmethod
# 添加方向角损失,这里直接计算两个角度的 sin1 x cos2给1 cos1 x sin2给2
# 这样后续直接进行L1损失的时候相减就是 s1c2 - c1s2 = sin(1-2)
def add_sin_difference(boxes1, boxes2, dim=6):
    # 针对角度添加sin损失,有效防止-pi和pi方向相反时损失过大
    assert dim != -1  # 角度=180°×弧度÷π   弧度=角度×π÷180°
    #sin1 x cos2
    # (batch_size, 321408, 1)  torch.sin() - torch.cos() 的 input (Tensor) 都是弧度制数据,不是角度制数据。
    rad_pred_encoding = torch.sin(boxes1[..., dim:dim + 1]) * torch.cos(boxes2[..., dim:dim + 1])
    #cos1 x sin2
    # (batch_size, 321408, 1)
    rad_tg_encoding = torch.cos(boxes1[..., dim:dim + 1]) * torch.sin(boxes2[..., dim:dim + 1])
    # (batch_size, 321408, 7) 将编码后的结果放回
    boxes1 = torch.cat([boxes1[..., :dim], rad_pred_encoding, boxes1[..., dim + 1:]], dim=-1)
    # (batch_size, 321408, 7) 将编码后的结果放回
    boxes2 = torch.cat([boxes2[..., :dim], rad_tg_encoding, boxes2[..., dim + 1:]], dim=-1)
    return boxes1, boxes2

构建方向回归损失:

def get_box_reg_layer_loss(self):
    # (batch_size, 248, 216, 42) anchor_box的7个回归参数预测box值
    # (x, y, z, w, l, h, θ) x 6个锚框
    box_preds = self.forward_ret_dict['box_preds']
    # (batch_size, 248, 216, 12) anchor_box的方向预测
    box_dir_cls_preds = self.forward_ret_dict.get('dir_cls_preds', None)
    # (batch_size, 321408, 7) 每个anchor和GT编码的结果 标签box偏移
    box_reg_targets = self.forward_ret_dict['box_reg_targets']
    # (batch_size, 321408) 得到每个box的类别
    box_cls_labels = self.forward_ret_dict['box_cls_labels']
    batch_size = int(box_preds.shape[0])
    # 获取所有anchor中属于前景anchor的mask  shape : (batch_size, 321408)
    positives = box_cls_labels > 0
    # 设置回归参数为1.    [True, False] * 1. = [1., 0.]
    reg_weights = positives.float()  # (4, 211200) 只保留标签>0的值
    # 同cls处理 # (batch_size, 1) 所有正例的和 eg:[[162.],[166.],[155.],[108.]]
    # 每个batch中的正样本总数
    pos_normalizer = positives.sum(1,keepdim=True).float()
    #这里的reg_weights表示的是每个batch中样本的权重,样本数量越多权重越大。 该样本数目/总样本数目
    #将输入input张量每个元素的范围限制到区间 [min,max],返回结果到一个新张量。最小限制为1
    reg_weights /= torch.clamp(pos_normalizer, min=1.0)  # (batch_size, 321408)

    if isinstance(self.anchors, list):
        if self.use_multihead:
            #这里的anchors是所有初始生成的anchor
            # all_anchors: [车的(1,248,216,1,2,7),人的(1,248,216,1,2,7),骑手的(1,248,216,1,2,7)]
            # num_anchors_per_location:[2,2,2]
            anchors = torch.cat(
                [anchor.permute(3, 4, 0, 1, 2, 5).contiguous().view(-1, anchor.shape[-1]) for anchor in
                 self.anchors], dim=0)
        else:
            #所有类别cat起来
            anchors = torch.cat(self.anchors, dim=-3)  # (1, 248, 216, 3, 2, 7)
    else:
        anchors = self.anchors
    #统一预测和标签数据的维度。
    # (1, 248*216, 7) --> (batch_size, 248*216, 7)第一个维度重复到batchsize数目
    anchors = anchors.view(1, -1, anchors.shape[-1]).repeat(batch_size, 1, 1)
    # (batch_size, 248*216, 7)
    box_preds = box_preds.view(batch_size, -1,
                               box_preds.shape[-1] // self.num_anchors_per_location if not self.use_multihead else
                               box_preds.shape[-1])
    # 对角度进行处理sin(a - b) = sinacosb-cosasinb
    # (batch_size, 321408, 7)    分别得到sina cosb和cosa sinb 赋值到原始角度的位置  相减就得到sin(a-b)
    box_preds_sin, reg_targets_sin = self.add_sin_difference(box_preds, box_reg_targets)
    #计算回归损失
    loc_loss_src = self.reg_loss_func(box_preds_sin, reg_targets_sin, weights=reg_weights)
    loc_loss = loc_loss_src.sum() / batch_size
    #回归损失与权重相乘
    loc_loss = loc_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['loc_weight']  # loc_weight = 2.0 损失乘以回归权重
    box_loss = loc_loss
    tb_dict = {
        # pytorch中的item()方法,返回张量中的元素值,与python中针对dict的item方法不同
        'rpn_loss_loc': loc_loss.item()
    }

    # 如果存在方向预测,则添加方向损失
    if box_dir_cls_preds is not None:
        # (batch_size, 321408, 2) 此处生成每个anchor的方向分类
        #anchors每个anchor
        #box_reg_targets每个anchor的回归残差
        dir_targets = self.get_direction_target(
            anchors, box_reg_targets,
            dir_offset=self.model_cfg.DIR_OFFSET,  # 方向偏移量 0.78539 = π/4
            num_bins=self.model_cfg.NUM_DIR_BINS  # BINS的方向数 = 2
        )
        # 方向预测值 (batch_size, 321408, 2)
        dir_logits = box_dir_cls_preds.view(batch_size, -1, self.model_cfg.NUM_DIR_BINS)
        # 只要正样本的方向预测值 (batch_size, 321408)
        # weights也是单独每个锚框的方向权重,计算方向损失过程进行单独锚框加权
        weights = positives.type_as(dir_logits)
        # (4, 211200) 除正例数量,使得每个样本的损失与样本中目标的数量无关
        weights /= torch.clamp(weights.sum(-1, keepdim=True), min=1.0)
        # 方向损失计算 , 方向角损失使用加权交叉熵损失
        dir_loss = self.dir_loss_func(dir_logits, dir_targets, weights=weights)
        dir_loss = dir_loss.sum() / batch_size
        # 损失权重,dir_weight: 0.2
        dir_loss = dir_loss * self.model_cfg.LOSS_CONFIG.LOSS_WEIGHTS['dir_weight']
        # 将方向损失加入box损失
        box_loss += dir_loss

        tb_dict['rpn_loss_dir'] = dir_loss.item()
    return box_loss, tb_dict

6.4:预测生成方法:generate_predicted_boxes

  • 六: AnchorHeadTemplate锚框检测头模板
  • ​ 6.1:generate_anchors锚框生成
  • ​ 6.2:AxisAlignedTargetAssigner锚框与目标框对齐
  • ​ 6.3:构建损失函数总体损失方法:get_loss
  • ​ 6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
  • ​ 6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
  • ​ 6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
  • ​ 6.4:预测生成方法:generate_predicted_boxes

方法介绍:推断过程推理模式下,在预测出每个anchor的分类和回归后,需要根据anchor和预测的结果进行解码操作,再进行NMS去除冗余测检测框。其中调用来源于pcdet/models/dense_heads/anchor_head_single.py,在推断模式下,直接使用预测的数据产生预测结果。

输入数据介绍:

self.forward_ret_dict['cls_preds']:类别预测 
shape :(batch_size, 200, 176, 18)
self.forward_ret_dict['dir_cls_preds']:方向分类 
shape :(batch_size, 200, 176, 12)
self.forward_ret_dict['box_preds']:box回归
shape:(batch_size, 200, 176, 42)

具体流程:

  1. 获取预测头的预测结果,包括类别预测,方向分类预测以及回归预测结果,维度如上。

  2. 获取num_anchors以及batch_anchors,前者表示预测结果中所有的锚框个数,后者表示所有bs所有锚框的7个维度参数:

    num_anchors:(1, 248, 216, 3, 2, 7) -> (1x248x216x3x2 , 7)

    batch_anchors:(1, 248, 216, 3, 2, 7) ->(bs, 248x216x3x2, 7)

  3. 分类和回归预测解码,实际上就是利用num_anchors将预测数据进行resahpe,得到所有锚框的分类和回归预测结果:

    batch_cls_preds:(batch_size, 200, 176, 18)->(bs, 1x200x176x3x2, 3)

    batch_box_preds:(batch_size, 200, 176, 42)-> (bs, 1x200x176x3x2, 7)

    类别解码不用后处理,box解码结果还需要进行后处理self.box_coder.decode_torch(batch_box_preds, batch_anchors)

  4. 如果还需要进行方向预测解码,对于方向预测结果进行reshape,得到所有锚框方向预测结果

    dir_cls_preds:(batch_size, 200, 176, 12)-> (bs, 1x200x176x3x2, 2)

  5. 取得预测结果中最大概率的方向角,进行角度处理,赋予到box的预测结果batch_box_preds中的最后一个维度

  6. 最后返回类别预测结果以及回归预测结果:batch_cls_preds,batch_box_preds

具体代码:

def generate_predicted_boxes(self, batch_size, cls_preds, box_preds, dir_cls_preds=None):
    """
    Args:
        batch_size:
        cls_preds: (N, H, W, C1) C1 = 18 = 3 x 6
        box_preds: (N, H, W, C2) C2 = 42 = 7 x 6
        dir_cls_preds: (N, H, W, C3) C3=12=2 x 6

    Returns:
        batch_cls_preds: (B, num_boxes, num_classes)
        batch_box_preds: (B, num_boxes, 7+C)

    """
    if isinstance(self.anchors, list):
        # 是否使用多头预测,默认否
        if self.use_multihead:
            anchors = torch.cat([anchor.permute(3, 4, 0, 1, 2, 5).contiguous().view(-1, anchor.shape[-1])
                                 for anchor in self.anchors], dim=0)
        else:
            """
            每个类别anchor的生成情况:
            [(Z, Y, X, anchor尺度, 该尺度anchor方向, 7个回归参数)
            (Z, Y, X, anchor尺度, 该尺度anchor方向, 7个回归参数)
            (Z, Y, X, anchor尺度, 该尺度anchor方向, 7个回归参数)]
            在倒数第三个维度拼接
            anchors 维度 (Z, Y, X, 3个anchor尺度, 每个尺度两个方向, 7)
                        (1, 248, 216, 3, 2, 7)
            """
            anchors = torch.cat(self.anchors, dim=-3)
    else:
        anchors = self.anchors
    # 计算一共有多少个anchor Z*Y*X*num_of_anchor_scale*anchor_rot
    # (1, 248, 216, 3, 2, 7) -> (1x248x216x3x2 , 7)
    num_anchors = anchors.view(-1, anchors.shape[-1]).shape[0]
    # (batch_size, Z*Y*X*num_of_anchor_scale*anchor_rot, 7)
    # (1, 248, 216, 3, 2, 7) ->(bs , 248x216x3x2, 7)所有锚框信息
    batch_anchors = anchors.view(1, -1, anchors.shape[-1]).repeat(batch_size, 1, 1)

    # 将预测结果都flatten为一维的,这里中间直接使用num_anchors,最后一维得到的就剩下分类3.回归7维数据
    # (batch_size, Z*Y*X*num_of_anchor_scale*anchor_rot, 3)
    # (N, H, W, C1)  (batch_size, 200, 176, 18)->(bs, 1x200x176x3x2, 3)
    batch_cls_preds = cls_preds.view(batch_size, num_anchors, -1).float() \
        if not isinstance(cls_preds,
                          list) else cls_preds
    # (batch_size, Z*Y*X*num_of_anchor_scale*anchor_rot, 7)
    # (N, H, W, C2) (batch_size, 200, 176, 42)-> (bs, 1x200x176x3x2, 7)
    batch_box_preds = box_preds.view(batch_size, num_anchors, -1) if not isinstance(box_preds, list) \
        else torch.cat(box_preds, dim=1).view(batch_size, num_anchors, -1)
    # 对7个预测的box参数进行解码操作
    batch_box_preds = self.box_coder.decode_torch(batch_box_preds, batch_anchors)
    # 每个anchor的方向预测
    if dir_cls_preds is not None:
        # 0.78539 方向偏移
        dir_offset = self.model_cfg.DIR_OFFSET
        # 0
        dir_limit_offset = self.model_cfg.DIR_LIMIT_OFFSET  # 0
        # 将方向预测结果flatten为一维的
        # (batch_size, Z*Y*X*num_of_anchor_scale*anchor_rot, 2)
        # (N, H, W, C3) (batch_size, 200, 176, 12)-> (bs, 1x200x176x3x2, 2)
        dir_cls_preds = dir_cls_preds.view(batch_size, num_anchors, -1) if not isinstance(dir_cls_preds, list) \
            else torch.cat(dir_cls_preds, dim=1).view(batch_size, num_anchors, -1)  # (1, 321408, 2)
        # (batch_size, Z*Y*X*num_of_anchor_scale*anchor_rot)
        # 取出所有anchor的方向分类 : 正向和反向
        dir_labels = torch.max(dir_cls_preds, dim=-1)[1]
        # pi
        period = (2 * np.pi / self.model_cfg.NUM_DIR_BINS)
        # 将角度在0到pi之间    在OpenPCDet中,坐标使用的是统一规范坐标,x向前,y向左,z向上
        # 这里参考训练时候的原因,现将角度角度沿着x轴的逆时针旋转了45度得到dir_rot
        dir_rot = common_utils.limit_period(
            batch_box_preds[..., 6] - dir_offset, dir_limit_offset, period
        )
        """
        从新将角度旋转回到激光雷达坐标系中,所以需要加回来之前减去的45度,
        如果dir_labels是1的话,说明方向在是180度的,因此需要将预测的角度信息加上180度,
        否则预测角度即是所得角度
        """
        batch_box_preds[..., 6] = dir_rot + dir_offset + period * dir_labels.to(batch_box_preds.dtype)

    # PointPillars、SECOND、PV-RCNN中无此项
    if isinstance(self.box_coder, box_coder_utils.PreviousResidualDecoder):
        batch_box_preds[..., 6] = common_utils.limit_period(
            -(batch_box_preds[..., 6] + np.pi / 2), offset=0.5, period=np.pi * 2
        )
    # batch_cls_preds shape(batch, H*W*num_anchor, 3)
    # batch_box_preds shape(batch, H*W*num_anchor, 7)
    return batch_cls_preds, batch_box_preds

其中3中对于box进行解码:调用self.box_coder对象的decode_torch方法 self.box_coder.decode_torch
self.box_coder.decode_torch(batch_box_preds, batch_anchors)

传入box经过reshape结果和全部anchor信息


调用关系:
anchor_head_single.py->anchor_head_template.py->box_coder_utils.py:class ResidualCoder(object).decode_torch
传入参数:所有锚框的回归预测结果,所有所有锚框信息
box_encodings:(bs, 248x216x3x2, 7) 所有锚框的回归预测结果
batch_anchors:(bs , 248x216x3x2, 7) 所有锚框信息

1,对于传入的数据进行分割,使用torch.split分割box_encodings信息以及batch_anchors信息得到:
  batch_anchors:xa, ya, za, dxa, dya, dza, ra, *cas 锚框位置信息
  box_encodings:xt, yt, zt, dxt, dyt, dzt, rt, *cts 预测偏移信息
2,计算对角线长度,根据偏移计算进行还原坐标信息,锚框位置加上预测偏移位置得到绝对位置。
    # ∆x = (x^gt − xa^da)/diagonal --> x^gt = ∆x * diagonal + x^da
    xg = xt * diagonal + xa
    yg = yt * diagonal + ya
    zg = zt * dza + za
    # ∆l = log(l^gt / l^a)的逆运算 --> l^gt = exp(∆l) * l^a
    dxg = torch.exp(dxt) * dxa
    dyg = torch.exp(dyt) * dya
    dzg = torch.exp(dzt) * dza
3,根据模型要求还原角度信息,使用sin表达或者直接使用角度表达。
 sin角度:rg_cos = cost + torch.cos(ra)
         rg_sin = sint + torch.sin(ra)
         rg = torch.atan2(rg_sin, rg_cos)
 角度:   rg = rt + ra
4,整合所有解码数据进行返回return torch.cat([xg, yg, zg, dxg, dyg, dzg, rg, *cgs], dim=-1)

具体代码:

def decode_torch(self, box_encodings, anchors):
    """
    Args:
        box_encodings: (B, N, 7 + C) or (N, 7 + C) [x, y, z, dx, dy, dz, heading or *[cos, sin], ...]
        anchors: (B, N, 7 + C) or (N, 7 + C) [x, y, z, dx, dy, dz, heading, ...]

    Returns:

    """
    # 这里指torch.split的第二个参数   torch.split(tensor, split_size, dim=)
    # split_size是切分后每块的大小,不是切分为多少块!,多余的参数使用*cags接收
    xa, ya, za, dxa, dya, dza, ra, *cas = torch.split(anchors, 1, dim=-1)
    # 分割编码后的box PointPillar为False
    if not self.encode_angle_by_sincos:
        xt, yt, zt, dxt, dyt, dzt, rt, *cts = torch.split(box_encodings, 1, dim=-1)
    else:
        xt, yt, zt, dxt, dyt, dzt, cost, sint, *cts = torch.split(box_encodings, 1, dim=-1)
    # 计算anchor对角线长度
    diagonal = torch.sqrt(dxa ** 2 + dya ** 2)  # (B, N, 1)-->(1, 321408, 1)
    #进行还原,锚框位置加上偏移位置得到绝对位置。
    # loss计算中anchor与GT编码的运算:g表示gt,a表示anchor
    # ∆x = (x^gt − xa^da)/diagonal --> x^gt = ∆x * diagonal + x^da
    # 下同
    xg = xt * diagonal + xa
    yg = yt * diagonal + ya
    zg = zt * dza + za
    # ∆l = log(l^gt / l^a)的逆运算 --> l^gt = exp(∆l) * l^a
    # 下同
    dxg = torch.exp(dxt) * dxa
    dyg = torch.exp(dyt) * dya
    dzg = torch.exp(dzt) * dza

    # 如果角度是cos和sin编码,采用新的解码方式 PointPillar为False
    if self.encode_angle_by_sincos:
        rg_cos = cost + torch.cos(ra)
        rg_sin = sint + torch.sin(ra)
        rg = torch.atan2(rg_sin, rg_cos)
    else:
        # rts = [rg - ra] 角度的逆运算
        rg = rt + ra
    # PointPillar无此项
    cgs = [t + a for t, a in zip(cts, cas)]
    return torch.cat([xg, yg, zg, dxg, dyg, dzg, rg, *cgs], dim=-1)

七: anchor_head_single利用2D特征进行检测分类以及回归

地址:

上一步:2D特征提取得到下采样上采样后cat的特征图:pcdet/models/backbones_2d/base_bev_backbone.py

地址:pcdet/models/dense_heads/anchor_head_single.py

预先包括锚框生成:pcdet/models/dense_heads/target_assigner/anchor_generator.py

预先包括锚框GT对齐:pcdet/models/dense_heads/target_assigner/axis_aligned_target_assigner.py

统一到:锚框检测头模板类,构建基于锚框的方法的检测头:pcdet/models/dense_heads/anchor_head_temp

**概述:**经过BaseBEVBackbone后得到的特征图为(batch, 256 * 2, 200, 176);在SECOND中,作者提出了方向分类,将原来VoxelNet的两个预测头上增加了一个方向分类头,来解决角度训练过程中一个预测的结果与GTBox的方向相反导致大loss的情况。目前总共三个检测头:分类头,回归头,方向头(转化成2分类)

方法介绍:利用AnchorHeadTemplate中基于锚框的检测模板,完成2D全局特征图的分类预测和回归预测。在训练模式下,构建训练损失,在推断模式下,直接生成预测框。

在这里插入图片描述

具体流程:

流程:
init中初始化三个预测1x1卷积核,分别是
分类头:Conv2d(512,18,kernel_size=(1,1),stride=(1,1)) 分类检测头 类别3x 框6 = 18
回归头:Conv2d(512,42,kernel_size=(1,1),stride=(1,1)) 回归检测头 位置7x
框6 = 42
方向头:Conv2d(512,12,kernel_size=(1,1),stride=(1,1)) 方向检测头 方向2x框6 = 12
注意我们针对的是每个位置3个先验框[车,人,骑手],每个先验框两个方向,所以 3x2 = 6,总共6个框

forward中输入数据来自于2D特征提取网络(b,c,w,h),该特征作为点云场景最终特征,用于预测核训练
训练过程:分别使用三个检测头得到每个位置,6个先验框的类别,位置,方向预测形象,加入到self.forward_ret_dict中
类别预测 shape :(batch_size, 200, 176, 18)
方向分类 shape :(batch_size, 200, 176, 12)
box回归shape:(batch_size, 200, 176, 42)
最后将标签进行处理,使用self.assign_targets得到标签的表示,同样加入到self.forward_ret_dict中,
用于后续对于各个损失的计算。
推断过程:分别使用三个检测头得到每个位置,使用self.generate_predicted_boxes直接产生最后的预测结果

最后附上预测核标签的shape
这里是标签,类别标签(b,w*h*6,1),位置标签(b,w*h*6,7)
        targets_dict = {
            'box_cls_labels': cls_labels, # (4,211200)
            'box_reg_targets': bbox_targets, # (4,211200, 7)
            'reg_weights': reg_weights # (4,211200)
        }
这里的预测结果,如类别是每个位置 200 * 176 = 36200 对应3个框,每个框2个方向 3 * 2 = 6
        综合起来,每个位置,每个框的类别 36200 * 6 = 211200 , 3
        同理,每个位置,每个框的偏移 36200 * 6 = 211200 , 7 
        data_dict['batch_cls_preds'] = batch_cls_preds  # (1, 211200, 3) 70400*3=211200
        data_dict['batch_box_preds'] = batch_box_preds  # (1, 211200, 7)

具体代码:

import numpy as np
import torch.nn as nn

from .anchor_head_template import AnchorHeadTemplate

"""
6.2:利用2D特征进行检测分类以及回归
    上一步:2D特征提取得到下采样上采样后cat的特征图:pcdet/models/backbones_2d/base_bev_backbone.py
    地址:pcdet/models/dense_heads/anchor_head_single.py
    预先包括锚框生成:pcdet/models/dense_heads/target_assigner/anchor_generator.py
    预先包括锚框GT对齐:pcdet/models/dense_heads/target_assigner/axis_aligned_target_assigner.py
    统一到:锚框检测头模板类,构建基于锚框的方法的检测头:pcdet/models/dense_heads/anchor_head_template.py
    
    经过BaseBEVBackbone后得到的特征图为(batch, 256 * 2, 200, 176);在SECOND中,
    作者提出了方向分类,将原来VoxelNet的两个预测头上增加了一个方向分类头,
    来解决角度训练过程中一个预测的结果与GTBox的方向相反导致大loss的情况。
    目前总共三个检测头:分类头,回归头,方向头(转化成2分类)
    
    流程:
    init中初始化三个预测1x1卷积核,分别是
    分类头:Conv2d(512,18,kernel_size=(1,1),stride=(1,1)) 分类检测头 类别3*框6 = 18
    回归头:Conv2d(512,42,kernel_size=(1,1),stride=(1,1)) 回归检测头 位置7*框6 = 42
    方向头:Conv2d(512,12,kernel_size=(1,1),stride=(1,1)) 方向检测头 方向2*框6 = 12   
    注意我们针对的是每个位置3个先验框[车,人,骑手],每个先验框两个方向,所以 3x2 = 6,总共6个框
    
    forward中输入数据来自于2D特征提取网络(b,c,w,h),该特征作为点云场景最终特征,用于预测核训练
    训练过程:分别使用三个检测头得到每个位置,6个先验框的类别,位置,方向预测形象,加入到self.forward_ret_dict中
    类别预测 shape :(batch_size, 200, 176, 18)
    方向分类 shape :(batch_size, 200, 176, 12)
    box回归shape:(batch_size, 200, 176, 42)
    最后将标签进行处理,使用self.assign_targets得到标签的表示,同样加入到self.forward_ret_dict中,
    用于后续对于各个损失的计算。
    推断过程:分别使用三个检测头得到每个位置,使用self.generate_predicted_boxes直接产生最后的预测结果
    
    最后附上预测核标签的shape
    这里是标签,类别标签(b,w*h*6,1),位置标签(b,w*h*6,7)
            targets_dict = {
                'box_cls_labels': cls_labels, # (4,211200)
                'box_reg_targets': bbox_targets, # (4,211200, 7)
                'reg_weights': reg_weights # (4,211200)
            }
    这里的预测结果,如类别是每个位置 200 * 176 = 36200 对应3个框,每个框2个方向 3 * 2 = 6
            综合起来,每个位置,每个框的类别 36200 * 6 = 211200 , 3
            同理,每个位置,每个框的偏移 36200 * 6 = 211200 , 7 
            data_dict['batch_cls_preds'] = batch_cls_preds  # (1, 211200, 3) 70400*3=211200
            data_dict['batch_box_preds'] = batch_box_preds  # (1, 211200, 7)
"""
class AnchorHeadSingle(AnchorHeadTemplate):
    """
    Args:
        model_cfg: AnchorHeadSingle的配置
        input_channels: 384 | 512 输入通道数
        num_class: 3
        class_names: ['Car','Pedestrian','Cyclist']
        grid_size: (X, Y, Z)
        point_cloud_range: (0, -39.68, -3, 69.12, 39.68, 1) ,[0, -40, -3, 70.4, 40, 1]
        predict_boxes_when_training: False
    """

    def __init__(self, model_cfg, input_channels, num_class, class_names, grid_size, point_cloud_range,
                 predict_boxes_when_training=True, **kwargs):
        super().__init__(
            model_cfg=model_cfg, num_class=num_class, class_names=class_names, grid_size=grid_size,
            point_cloud_range=point_cloud_range,
            predict_boxes_when_training=predict_boxes_when_training
        )
        #(重要) 每个点有3个尺度的个先验框  每个先验框都有两个方向(0度,90度) num_anchors_per_location:[2, 2, 2]
        self.num_anchors_per_location = sum(self.num_anchors_per_location)  # sum([2, 2, 2]) = 6
        # Conv2d(512,18,kernel_size=(1,1),stride=(1,1)) 分类检测头
        self.conv_cls = nn.Conv2d(
            #self.num_class=3 三种类别,  self.num_anchors_per_location = 6
            input_channels, self.num_anchors_per_location * self.num_class,
            kernel_size=1
        )
        # Conv2d(512,42,kernel_size=(1,1),stride=(1,1)) 回归检测头
        self.conv_box = nn.Conv2d(
            # self.num_anchors_per_location = 6 , self.box_coder.code_size=7位置信息  6x7=42
            input_channels, self.num_anchors_per_location * self.box_coder.code_size,
            kernel_size=1
        )
        # 如果存在方向损失,则添加方向卷积层Conv2d(512,12,kernel_size=(1,1),stride=(1,1))
        if self.model_cfg.get('USE_DIRECTION_CLASSIFIER', None) is not None:
            self.conv_dir_cls = nn.Conv2d(
                input_channels,
                # self.num_anchors_per_location=6 , self.model_cfg.NUM_DIR_BINS = 2 两个方向
                self.num_anchors_per_location * self.model_cfg.NUM_DIR_BINS,
                kernel_size=1
            )
        else:
            self.conv_dir_cls = None
        self.init_weights()

    # 初始化参数
    def init_weights(self):
        pi = 0.01
        # 初始化分类卷积偏置
        nn.init.constant_(self.conv_cls.bias, -np.log((1 - pi) / pi))
        # 初始化分类卷积权重
        nn.init.normal_(self.conv_box.weight, mean=0, std=0.001)

    def forward(self, data_dict):
        # 从字典中取出经过backbone处理过的信息
        # spatial_features_2d 维度 (batch_size, C, W, H)
        spatial_features_2d = data_dict['spatial_features_2d']
        # 类别预测:每个坐标点上面6个先验框的类别预测 --> (batch_size, 18, W, H)
        cls_preds = self.conv_cls(spatial_features_2d)
        # 位置预测:每个坐标点上面6个先验框的参数预测 --> (batch_size, 42, W, H)
        # 其中每个先验框需要预测7个参数,分别是(x, y, z, w, l, h, θ)
        box_preds = self.conv_box(spatial_features_2d)
        # 维度调整,将类别放置在最后一维度   [N, H, W, C] --> (batch_size, W, H, 18)
        cls_preds = cls_preds.permute(0, 2, 3, 1).contiguous()
        # 维度调整,将先验框调整参数放置在最后一维度   [N, H, W, C] --> (batch_size ,W, H, 42)
        box_preds = box_preds.permute(0, 2, 3, 1).contiguous()
        # 将类别和先验框调整预测结果放入前向传播字典中
        self.forward_ret_dict['cls_preds'] = cls_preds
        self.forward_ret_dict['box_preds'] = box_preds
        # 进行方向分类预测
        if self.conv_dir_cls is not None:
            # # 每个先验框都要预测为两个方向中的其中一个方向 --> (batch_size, 12, W, H)
            dir_cls_preds = self.conv_dir_cls(spatial_features_2d)
            # 将类别和先验框方向预测结果放到最后一个维度中   [N, H, W, C] --> (batch_size, W, H, 12)
            dir_cls_preds = dir_cls_preds.permute(0, 2, 3, 1).contiguous()
            # 将方向预测结果放入前向传播字典中
            self.forward_ret_dict['dir_cls_preds'] = dir_cls_preds
        else:
            dir_cls_preds = None

        """
        如果是在训练模式的时候,需要对每个先验框分配GT来计算loss
        """
        if self.training:
            #这里是标签,类别标签(b,w*h*6,1),位置标签(b,w*h*6,7)
            # targets_dict = {
            #     'box_cls_labels': cls_labels, # (4,211200)
            #     'box_reg_targets': bbox_targets, # (4,211200, 7)
            #     'reg_weights': reg_weights # (4,211200)
            # }
            targets_dict = self.assign_targets(
                gt_boxes=data_dict['gt_boxes']  # (4,39,8)
            )
            # 将GT分配结果放入前向传播字典中
            self.forward_ret_dict.update(targets_dict)

        # 如果不是训练模式,则直接生成进行box的预测,在PV-RCNN和Voxel-RCNN中在训练时候也要生成bbox用于refinement
        if not self.training or self.predict_boxes_when_training:
            # 根据预测结果解码生成最终结果
            batch_cls_preds, batch_box_preds = self.generate_predicted_boxes(
                batch_size=data_dict['batch_size'],
                cls_preds=cls_preds, box_preds=box_preds, dir_cls_preds=dir_cls_preds
            )

            # 这里的预测结果,如类别是每个位置 200 * 176 = 36200 对应3个框,每个框2个方向 3 * 2 = 6
            # 综合起来,每个位置,每个框的类别 36200 * 6 = 211200 , 3
            # 同理,每个位置,每个框的偏移 36200 * 6 = 211200 , 7
            data_dict['batch_cls_preds'] = batch_cls_preds  # (1, 211200, 3) 70400*3=211200
            data_dict['batch_box_preds'] = batch_box_preds  # (1, 211200, 7)
            data_dict['cls_preds_normalized'] = False

        return data_dict

八: Detector3DTemplate构建模型

一: Point Cloud Grouping数据处理完成点云体素化
二: Mean VFE 体素特征编码
三: VoxelBackBone8x,3D骨干网络,用于提取体素特征
四: HeightCompression Z轴方向压缩,3D转2D
五: BaseBEVBackbone2D特征提取网络
六: AnchorHeadTemplate锚框检测头模板
6.1:generate_anchors锚框生成
6.2:AxisAlignedTargetAssigner锚框与目标框对齐
6.3:构建损失函数总体损失方法:get_loss
6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
6.4:预测生成方法:generate_predicted_boxes

七:Anchor_head_single利用2D特征进行检测分类以及回归

八:Detector3DTemplate构建模型

路径:pcdet/models/detectors/detector3d_template.py

综合上述所有的部分,进行模型的构建,具体构建流程也是上述组织流程,最后在检测结果后添加了后处理模块,对于预测结果进行非极大化抑制。

这里主要介绍NMS的内容:

1,初始化数据,得到配置文件,bs,初始化预测字典,初始化结果字典
2,对于每一帧数据进行处理
    2.1获取各个锚框的类别预测信息和偏移预测信息。
    data_dict['batch_cls_preds']每个位置6个框(176x200x6=211200)的类别预测(1, 211200, 3)
    data_dict['batch_box_preds']每个位置6个框(176x200x6=211200)的类别偏移预测(1, 211200, 7)
    进行复制以计算召回率src_cls_preds,src_box_preds
    2.2得到各个类别预测最大的概率和索引
    2.3传入类别cls_preds预测box_preds偏移预测到model_nms_utils.class_agnostic_nms进行非最大化抑制
       抑制过程如下:1.过滤掉执置信度分数(类别概率得分)低的锚框,减少后续计算
                  2.置信度从高到低排序,计算两两框之间的IOU,
                    IOU大说明预测的同一个目标,保留最大分数锚框,进行下一个IOU低的锚框与之后锚框的判断
                  3.返回保留的锚框索引和置信度得分selected, selected_scores
    2.4根据返回的保留索引结果在box_preds偏移预测中取得对应框偏移,在lable_preds类别预测中取得该框类别
       final_labels = label_preds[selected]
       final_boxes = box_preds[selected] 
    2.5传入最终预测框,总召回字典,等信息到generate_recall_record计算该帧的召回率更新召回字典recall_dict
    2.6最终结果字典记录该帧NMS后剩余锚框的定位结果,各个框的分类成绩,各个框的最终类别。并且添加到总体预测字典
    record_dict = {
            'pred_boxes': final_boxes,
            'pred_scores': final_scores,
            'pred_labels': final_labels
        }
    2.7返回总体预测字典和召回率字典。

具体代码:

import os

import torch
import torch.nn as nn

from ...ops.iou3d_nms import iou3d_nms_utils
from ...utils.spconv_utils import find_all_spconv_keys
from .. import backbones_2d, backbones_3d, dense_heads, roi_heads
from ..backbones_2d import map_to_bev
from ..backbones_3d import pfe, vfe
from ..model_utils import model_nms_utils

"""
3D检测器模板,整合先前所有模块,构建2D,3D骨干网络,使用检测头检测等等。

一: Point Cloud Grouping数据处理完成点云体素化
二: Mean VFE 体素特征编码
三: VoxelBackBone8x,3D骨干网络,用于提取体素特征
四: HeightCompression Z轴方向压缩,3D转2D
五: BaseBEVBackbone2D特征提取网络
六: AnchorHeadTemplate锚框检测头模板
    6.1:generate_anchors锚框生成
    6.2:AxisAlignedTargetAssigner锚框与目标框对齐
    6.3:构建损失函数总体损失方法:get_loss
        6.3.1:构建损失方法:buildloss(该方法构建的是损失函数中叶子节点损失,精确到交叉熵损失,L1损失,focalLoss等等)
        6.3.2:分类损失方法:get_cls_layer_loss(构建分类损失,采用focalLoss)
        6.3.3:回归损失方法:get_box_reg_layer(构建回归算,采用L1损失,其中方法分类构建方向标签,采用交叉熵损失)
    6.4:预测生成方法:generate_predicted_boxes
"""


class Detector3DTemplate(nn.Module):
    def __init__(self, model_cfg, num_class, dataset):
        '''
        :param model_cfg: 模型配置文件tools/cfgs/kitti_models/second.yaml
        :param num_class: 类别个数
        :param dataset: 数据集
        '''
        super().__init__()

        self.model_cfg = model_cfg
        self.num_class = num_class
        self.dataset = dataset
        self.class_names = dataset.class_names
        self.register_buffer('global_step', torch.LongTensor(1).zero_())

        self.module_topology = [
            'vfe', 'backbone_3d', 'map_to_bev_module', 'pfe',
            'backbone_2d', 'dense_head', 'point_head', 'roi_head'
        ]

    @property
    def mode(self):
        return 'TRAIN' if self.training else 'TEST'

    def update_global_step(self):
        self.global_step += 1
    #调用build_networks根据配置文件内容构建网络模型
    def build_networks(self):
        model_info_dict = {
            'module_list': [],
            'num_rawpoint_features': self.dataset.point_feature_encoder.num_point_features,
            'num_point_features': self.dataset.point_feature_encoder.num_point_features,
            'grid_size': self.dataset.grid_size,
            'point_cloud_range': self.dataset.point_cloud_range,
            'voxel_size': self.dataset.voxel_size,
            'depth_downsample_factor': self.dataset.depth_downsample_factor
        }
        for module_name in self.module_topology:
            module, model_info_dict = getattr(self, 'build_%s' % module_name)(
                model_info_dict=model_info_dict
            )
            self.add_module(module_name, module)
        return model_info_dict['module_list']

    # 二: Mean VFE 体素特征编码
    def build_vfe(self, model_info_dict):
        if self.model_cfg.get('VFE', None) is None:
            return None, model_info_dict

        vfe_module = vfe.__all__[self.model_cfg.VFE.NAME](
            model_cfg=self.model_cfg.VFE,
            num_point_features=model_info_dict['num_rawpoint_features'],
            point_cloud_range=model_info_dict['point_cloud_range'],
            voxel_size=model_info_dict['voxel_size'],
            grid_size=model_info_dict['grid_size'],
            depth_downsample_factor=model_info_dict['depth_downsample_factor']
        )
        model_info_dict['num_point_features'] = vfe_module.get_output_feature_dim()
        model_info_dict['module_list'].append(vfe_module)
        return vfe_module, model_info_dict

    # 构建3D骨干网络:VoxelBackBone8x,3D骨干网络,用于提取体素特征
    def build_backbone_3d(self, model_info_dict):
        if self.model_cfg.get('BACKBONE_3D', None) is None:
            return None, model_info_dict

        backbone_3d_module = backbones_3d.__all__[self.model_cfg.BACKBONE_3D.NAME](
            model_cfg=self.model_cfg.BACKBONE_3D,
            input_channels=model_info_dict['num_point_features'],
            grid_size=model_info_dict['grid_size'],
            voxel_size=model_info_dict['voxel_size'],
            point_cloud_range=model_info_dict['point_cloud_range']
        )
        model_info_dict['module_list'].append(backbone_3d_module)
        model_info_dict['num_point_features'] = backbone_3d_module.num_point_features
        model_info_dict['backbone_channels'] = backbone_3d_module.backbone_channels \
            if hasattr(backbone_3d_module, 'backbone_channels') else None
        return backbone_3d_module, model_info_dict

    #四: HeightCompression Z轴方向压缩,3D转2D
    def build_map_to_bev_module(self, model_info_dict):
        if self.model_cfg.get('MAP_TO_BEV', None) is None:
            return None, model_info_dict

        map_to_bev_module = map_to_bev.__all__[self.model_cfg.MAP_TO_BEV.NAME](
            model_cfg=self.model_cfg.MAP_TO_BEV,
            grid_size=model_info_dict['grid_size']
        )
        model_info_dict['module_list'].append(map_to_bev_module)
        model_info_dict['num_bev_features'] = map_to_bev_module.num_bev_features
        return map_to_bev_module, model_info_dict

    #五: BaseBEVBackbone2D特征提取网络
    def build_backbone_2d(self, model_info_dict):
        if self.model_cfg.get('BACKBONE_2D', None) is None:
            return None, model_info_dict

        backbone_2d_module = backbones_2d.__all__[self.model_cfg.BACKBONE_2D.NAME](
            model_cfg=self.model_cfg.BACKBONE_2D,
            input_channels=model_info_dict['num_bev_features']
        )
        model_info_dict['module_list'].append(backbone_2d_module)
        model_info_dict['num_bev_features'] = backbone_2d_module.num_bev_features
        return backbone_2d_module, model_info_dict

    def build_pfe(self, model_info_dict):
        if self.model_cfg.get('PFE', None) is None:
            return None, model_info_dict

        pfe_module = pfe.__all__[self.model_cfg.PFE.NAME](
            model_cfg=self.model_cfg.PFE,
            voxel_size=model_info_dict['voxel_size'],
            point_cloud_range=model_info_dict['point_cloud_range'],
            num_bev_features=model_info_dict['num_bev_features'],
            num_rawpoint_features=model_info_dict['num_rawpoint_features']
        )
        model_info_dict['module_list'].append(pfe_module)
        model_info_dict['num_point_features'] = pfe_module.num_point_features
        model_info_dict['num_point_features_before_fusion'] = pfe_module.num_point_features_before_fusion
        return pfe_module, model_info_dict

    #六: AnchorHeadTemplate锚框检测头模板
    def build_dense_head(self, model_info_dict):
        if self.model_cfg.get('DENSE_HEAD', None) is None:
            return None, model_info_dict
        dense_head_module = dense_heads.__all__[self.model_cfg.DENSE_HEAD.NAME](
            model_cfg=self.model_cfg.DENSE_HEAD,
            input_channels=model_info_dict['num_bev_features'],
            num_class=self.num_class if not self.model_cfg.DENSE_HEAD.CLASS_AGNOSTIC else 1,
            class_names=self.class_names,
            grid_size=model_info_dict['grid_size'],
            point_cloud_range=model_info_dict['point_cloud_range'],
            predict_boxes_when_training=self.model_cfg.get('ROI_HEAD', False),
            voxel_size=model_info_dict.get('voxel_size', False)
        )
        model_info_dict['module_list'].append(dense_head_module)
        return dense_head_module, model_info_dict

    def build_point_head(self, model_info_dict):
        if self.model_cfg.get('POINT_HEAD', None) is None:
            return None, model_info_dict

        if self.model_cfg.POINT_HEAD.get('USE_POINT_FEATURES_BEFORE_FUSION', False):
            num_point_features = model_info_dict['num_point_features_before_fusion']
        else:
            num_point_features = model_info_dict['num_point_features']

        point_head_module = dense_heads.__all__[self.model_cfg.POINT_HEAD.NAME](
            model_cfg=self.model_cfg.POINT_HEAD,
            input_channels=num_point_features,
            num_class=self.num_class if not self.model_cfg.POINT_HEAD.CLASS_AGNOSTIC else 1,
            predict_boxes_when_training=self.model_cfg.get('ROI_HEAD', False)
        )

        model_info_dict['module_list'].append(point_head_module)
        return point_head_module, model_info_dict

    def build_roi_head(self, model_info_dict):
        if self.model_cfg.get('ROI_HEAD', None) is None:
            return None, model_info_dict
        point_head_module = roi_heads.__all__[self.model_cfg.ROI_HEAD.NAME](
            model_cfg=self.model_cfg.ROI_HEAD,
            input_channels=model_info_dict['num_point_features'],
            backbone_channels=model_info_dict['backbone_channels'],
            point_cloud_range=model_info_dict['point_cloud_range'],
            voxel_size=model_info_dict['voxel_size'],
            num_class=self.num_class if not self.model_cfg.ROI_HEAD.CLASS_AGNOSTIC else 1,
        )

        model_info_dict['module_list'].append(point_head_module)
        return point_head_module, model_info_dict

    def forward(self, **kwargs):
        raise NotImplementedError

    #10.2 无类别NMS 用于构建模型后处理
    """
    1,初始化数据,得到配置文件,bs,初始化预测字典,初始化结果字典
    2,对于每一帧数据进行处理
        2.1获取各个锚框的类别预测信息和偏移预测信息。
        data_dict['batch_cls_preds']每个位置6个框(176x200x6=211200)的类别预测(1, 211200, 3)
        data_dict['batch_box_preds']每个位置6个框(176x200x6=211200)的类别偏移预测(1, 211200, 7)
        进行复制以计算召回率src_cls_preds,src_box_preds
        2.2得到各个类别预测最大的概率和索引
        2.3传入类别cls_preds预测box_preds偏移预测到model_nms_utils.class_agnostic_nms进行非最大化抑制
           抑制过程如下:1.过滤掉执置信度分数(类别概率得分)低的锚框,减少后续计算
                      2.置信度从高到低排序,计算两两框之间的IOU,
                        IOU大说明预测的同一个目标,保留最大分数锚框,进行下一个IOU低的锚框与之后锚框的判断
                      3.返回保留的锚框索引和置信度得分selected, selected_scores
        2.4根据返回的保留索引结果在box_preds偏移预测中取得对应框偏移,在lable_preds类别预测中取得该框类别
           final_labels = label_preds[selected]
           final_boxes = box_preds[selected] 
        2.5传入最终预测框,总召回字典,等信息到generate_recall_record计算该帧的召回率更新召回字典recall_dict
        2.6最终结果字典记录该帧NMS后剩余锚框的定位结果,各个框的分类成绩,各个框的最终类别。并且添加到总体预测字典
        record_dict = {
                'pred_boxes': final_boxes,
                'pred_scores': final_scores,
                'pred_labels': final_labels
            }
        2.7返回总体预测字典和召回率字典。
                    
           
    
    """
    def post_processing(self, batch_dict):
        """
        Args:
            batch_dict:
                batch_size: 批数目
                batch_cls_preds: 分类预测 (B, num_boxes, num_classes | 1) or (N1+N2+..., num_classes | 1)
                                or [(B, num_boxes, num_class1), (B, num_boxes, num_class2) ...]
                multihead_label_mapping: 多头标签[(num_class1), (num_class2), ...]
                batch_box_preds: 边界框预测(B, num_boxes, 7+C) or (N1+N2+..., 7+C)
                cls_preds_normalized: indicate whether batch_cls_preds is normalized
                batch_index: optional (N1+N2+...)
                has_class_labels: True/False
                roi_labels: (B, num_rois)  1 .. num_classes
                batch_pred_labels: (B, num_boxes, 1)
        Returns:

        """
        # post_process_cfg后处理参数,包含了nms类型、阈值、使用的设备、nms后最多保留的结果和输出的置信度等设置
        post_process_cfg = self.model_cfg.POST_PROCESSING
        # 推理默认为1
        batch_size = batch_dict['batch_size']
        # 保留计算recall的字典
        recall_dict = {}
        # 预测结果存放在此
        pred_dicts = []
        # 逐帧进行处理
        for index in range(batch_size):
            if batch_dict.get('batch_index', None) is not None:
                assert batch_dict['batch_box_preds'].shape.__len__() == 2
                batch_mask = (batch_dict['batch_index'] == index)
            else:
                assert batch_dict['batch_box_preds'].shape.__len__() == 3
                # 得到当前处理的是第几帧
                batch_mask = index
            # box_preds shape (所有anchor的数量, 7)
            box_preds = batch_dict['batch_box_preds'][batch_mask]
            # 复制后,用于recall计算
            src_box_preds = box_preds

            if not isinstance(batch_dict['batch_cls_preds'], list):
                # (所有anchor的数量, 3)
                cls_preds = batch_dict['batch_cls_preds'][batch_mask]
                # 同上 复制后,用于recall计算
                src_cls_preds = cls_preds
                assert cls_preds.shape[1] in [1, self.num_class]

                if not batch_dict['cls_preds_normalized']:
                    # 损失函数计算使用的BCE,所以这里使用sigmoid激活函数得到类别概率
                    cls_preds = torch.sigmoid(cls_preds)
            else:
                cls_preds = [x[batch_mask] for x in batch_dict['batch_cls_preds']]
                src_cls_preds = cls_preds
                if not batch_dict['cls_preds_normalized']:
                    cls_preds = [torch.sigmoid(x) for x in cls_preds]

            # 是否使用多类别的NMS计算,否,不考虑不同类别的物体会在3D空间中重叠
            if post_process_cfg.NMS_CONFIG.MULTI_CLASSES_NMS:
                if not isinstance(cls_preds, list):
                    cls_preds = [cls_preds]
                    multihead_label_mapping = [torch.arange(1, self.num_class, device=cls_preds[0].device)]
                else:
                    multihead_label_mapping = batch_dict['multihead_label_mapping']

                cur_start_idx = 0
                pred_scores, pred_labels, pred_boxes = [], [], []
                for cur_cls_preds, cur_label_mapping in zip(cls_preds, multihead_label_mapping):
                    assert cur_cls_preds.shape[1] == len(cur_label_mapping)
                    cur_box_preds = box_preds[cur_start_idx: cur_start_idx + cur_cls_preds.shape[0]]
                    cur_pred_scores, cur_pred_labels, cur_pred_boxes = model_nms_utils.multi_classes_nms(
                        cls_scores=cur_cls_preds, box_preds=cur_box_preds,
                        nms_config=post_process_cfg.NMS_CONFIG,
                        score_thresh=post_process_cfg.SCORE_THRESH
                    )
                    cur_pred_labels = cur_label_mapping[cur_pred_labels]
                    pred_scores.append(cur_pred_scores)
                    pred_labels.append(cur_pred_labels)
                    pred_boxes.append(cur_pred_boxes)
                    cur_start_idx += cur_cls_preds.shape[0]

                final_scores = torch.cat(pred_scores, dim=0)
                final_labels = torch.cat(pred_labels, dim=0)
                final_boxes = torch.cat(pred_boxes, dim=0)
            else:
                # 得到类别预测的最大概率,和对应的索引值
                cls_preds, label_preds = torch.max(cls_preds, dim=-1)
                if batch_dict.get('has_class_labels', False):
                    # 如果有roi_labels在里面字典里面,
                    # 使用第一阶段预测的label为改预测结果的分类类别
                    label_key = 'roi_labels' if 'roi_labels' in batch_dict else 'batch_pred_labels'
                    label_preds = batch_dict[label_key][index]
                else:
                    # 类别预测值加1
                    label_preds = label_preds + 1

                # 无类别NMS操作
                # selected : 返回了被留下来的anchor索引
                # selected_scores : 返回了被留下来的anchor的置信度分数
                selected, selected_scores = model_nms_utils.class_agnostic_nms(
                    # 每个anchor的类别预测概率和anchor回归参数
                    box_scores=cls_preds, box_preds=box_preds,
                    nms_config=post_process_cfg.NMS_CONFIG,
                    score_thresh=post_process_cfg.SCORE_THRESH
                )
                # 无此项
                if post_process_cfg.OUTPUT_RAW_SCORE:
                    max_cls_preds, _ = torch.max(src_cls_preds, dim=-1)
                    selected_scores = max_cls_preds[selected]

                # 得到最终类别预测的分数
                final_scores = selected_scores
                # 根据selected得到最终类别预测的结果
                final_labels = label_preds[selected]
                # 根据selected得到最终box回归的结果
                final_boxes = box_preds[selected]

            # 如果没有GT的标签在batch_dict中,就不会计算recall值
            recall_dict = self.generate_recall_record(
                box_preds=final_boxes if 'rois' not in batch_dict else src_box_preds,
                recall_dict=recall_dict, batch_index=index, data_dict=batch_dict,
                thresh_list=post_process_cfg.RECALL_THRESH_LIST
            )
            # 生成最终预测的结果字典
            record_dict = {
                'pred_boxes': final_boxes,
                'pred_scores': final_scores,
                'pred_labels': final_labels
            }
            #这一帧的结果添加到总体结果中
            pred_dicts.append(record_dict)
        #返回总体NMS后的预测结果,box,scores,pred_labels 和召回率字典
        return pred_dicts, recall_dict

    @staticmethod
    def generate_recall_record(box_preds, recall_dict, batch_index, data_dict=None, thresh_list=None):
        if 'gt_boxes' not in data_dict:
            return recall_dict

        rois = data_dict['rois'][batch_index] if 'rois' in data_dict else None
        gt_boxes = data_dict['gt_boxes'][batch_index]

        if recall_dict.__len__() == 0:
            recall_dict = {'gt': 0}
            for cur_thresh in thresh_list:
                recall_dict['roi_%s' % (str(cur_thresh))] = 0
                recall_dict['rcnn_%s' % (str(cur_thresh))] = 0

        cur_gt = gt_boxes
        k = cur_gt.__len__() - 1
        while k > 0 and cur_gt[k].sum() == 0:
            k -= 1
        cur_gt = cur_gt[:k + 1]

        if cur_gt.shape[0] > 0:
            if box_preds.shape[0] > 0:
                iou3d_rcnn = iou3d_nms_utils.boxes_iou3d_gpu(box_preds[:, 0:7], cur_gt[:, 0:7])
            else:
                iou3d_rcnn = torch.zeros((0, cur_gt.shape[0]))

            if rois is not None:
                iou3d_roi = iou3d_nms_utils.boxes_iou3d_gpu(rois[:, 0:7], cur_gt[:, 0:7])

            for cur_thresh in thresh_list:
                if iou3d_rcnn.shape[0] == 0:
                    recall_dict['rcnn_%s' % str(cur_thresh)] += 0
                else:
                    rcnn_recalled = (iou3d_rcnn.max(dim=0)[0] > cur_thresh).sum().item()
                    recall_dict['rcnn_%s' % str(cur_thresh)] += rcnn_recalled
                if rois is not None:
                    roi_recalled = (iou3d_roi.max(dim=0)[0] > cur_thresh).sum().item()
                    recall_dict['roi_%s' % str(cur_thresh)] += roi_recalled

            recall_dict['gt'] += cur_gt.shape[0]
        else:
            gt_iou = box_preds.new_zeros(box_preds.shape[0])
        return recall_dict

    def _load_state_dict(self, model_state_disk, *, strict=True):
        state_dict = self.state_dict()  # local cache of state_dict

        spconv_keys = find_all_spconv_keys(self)

        update_model_state = {}
        for key, val in model_state_disk.items():
            if key in spconv_keys and key in state_dict and state_dict[key].shape != val.shape:
                # with different spconv versions, we need to adapt weight shapes for spconv blocks
                # adapt spconv weights from version 1.x to version 2.x if you used weights from spconv 1.x

                val_native = val.transpose(-1, -2)  # (k1, k2, k3, c_in, c_out) to (k1, k2, k3, c_out, c_in)
                if val_native.shape == state_dict[key].shape:
                    val = val_native.contiguous()
                else:
                    assert val.shape.__len__() == 5, 'currently only spconv 3D is supported'
                    val_implicit = val.permute(4, 0, 1, 2, 3)  # (k1, k2, k3, c_in, c_out) to (c_out, k1, k2, k3, c_in)
                    if val_implicit.shape == state_dict[key].shape:
                        val = val_implicit.contiguous()

            if key in state_dict and state_dict[key].shape == val.shape:
                update_model_state[key] = val
                # logger.info('Update weight %s: %s' % (key, str(val.shape)))

        if strict:
            self.load_state_dict(update_model_state)
        else:
            state_dict.update(update_model_state)
            self.load_state_dict(state_dict)
        return state_dict, update_model_state

    def load_params_from_file(self, filename, logger, to_cpu=False):
        if not os.path.isfile(filename):
            raise FileNotFoundError

        logger.info('==> Loading parameters from checkpoint %s to %s' % (filename, 'CPU' if to_cpu else 'GPU'))
        loc_type = torch.device('cpu') if to_cpu else None
        checkpoint = torch.load(filename, map_location=loc_type)
        model_state_disk = checkpoint['model_state']

        version = checkpoint.get("version", None)
        if version is not None:
            logger.info('==> Checkpoint trained from version: %s' % version)

        state_dict, update_model_state = self._load_state_dict(model_state_disk, strict=False)

        for key in state_dict:
            if key not in update_model_state:
                logger.info('Not updated weight %s: %s' % (key, str(state_dict[key].shape)))

        logger.info('==> Done (loaded %d/%d)' % (len(update_model_state), len(state_dict)))

    def load_params_with_optimizer(self, filename, to_cpu=False, optimizer=None, logger=None):
        if not os.path.isfile(filename):
            raise FileNotFoundError

        logger.info('==> Loading parameters from checkpoint %s to %s' % (filename, 'CPU' if to_cpu else 'GPU'))
        loc_type = torch.device('cpu') if to_cpu else None
        checkpoint = torch.load(filename, map_location=loc_type)
        epoch = checkpoint.get('epoch', -1)
        it = checkpoint.get('it', 0.0)

        self._load_state_dict(checkpoint['model_state'], strict=True)

        if optimizer is not None:
            if 'optimizer_state' in checkpoint and checkpoint['optimizer_state'] is not None:
                logger.info('==> Loading optimizer parameters from checkpoint %s to %s'
                            % (filename, 'CPU' if to_cpu else 'GPU'))
                optimizer.load_state_dict(checkpoint['optimizer_state'])
            else:
                assert filename[-4] == '.', filename
                src_file, ext = filename[:-4], filename[-3:]
                optimizer_filename = '%s_optim.%s' % (src_file, ext)
                if os.path.exists(optimizer_filename):
                    optimizer_ckpt = torch.load(optimizer_filename, map_location=loc_type)
                    optimizer.load_state_dict(optimizer_ckpt['optimizer_state'])

        if 'version' in checkpoint:
            print('==> Checkpoint trained from version: %s' % checkpoint['version'])
        logger.info('==> Done')

        return it, epoch

最后继承Detector3DTemplate模板类,具体构建SECOND模型,构建就很简单了,直接向模板类中传递SECOND模型的配置文件,以及KITTI数据集的配置文件即可。

地址:pcdet/models/detectors/second_net.py

具体代码如下:

from .detector3d_template import Detector3DTemplate



"""
构建SECOND模型
继承Detector3DTemplate构建模型

"""
class SECONDNet(Detector3DTemplate):
    def __init__(self, model_cfg, num_class, dataset):
        '''
        :param model_cfg: 模型配置文件
        :param num_class: 类个数
        :param dataset: 数据集
        '''
        super().__init__(model_cfg=model_cfg, num_class=num_class, dataset=dataset)
        #根据配置文件构建模型
        self.module_list = self.build_networks()

    """
    0、Point2Voxle 点云体素化
    1、MeanVFE 均值体素编码
    2、VoxelBackBone8x 3D骨干网络特征提取
    3、HeightCompression 高度压缩3D转2D
    4、BaseBEVBackbone 2D骨干网络特征提取
    5、AnchorHeadSingle 基于锚框的检测方法
    """
    def forward(self, batch_dict):
        #将数据送到每一层模块中
        for cur_module in self.module_list:
            batch_dict = cur_module(batch_dict)
        #训练情况下,计算损失
        if self.training:
            loss, tb_dict, disp_dict = self.get_training_loss()

            ret_dict = {
                'loss': loss
            }
            return ret_dict, tb_dict, disp_dict
        #预测情况下,进行后处理,得到预测框和类别以及预测分数。
        else:
            pred_dicts, recall_dicts = self.post_processing(batch_dict)
            return pred_dicts, recall_dicts

    def get_training_loss(self):
        disp_dict = {}
        #调用检测头获取损失方法得到损失
        loss_rpn, tb_dict = self.dense_head.get_loss()
        tb_dict = {
            'loss_rpn': loss_rpn.item(),
            **tb_dict
        }

        loss = loss_rpn
        return loss, tb_dict, disp_dict

好啦,到此使用Opean-PCDDet构建SECOND模型的整体流程就结束了,大家在看其他Opean-PCDDet模型的时候也可按照上述顺序,作者是第一次看Opean-PCDDet构建模型的相关代码,在这里记录一下,也给之后想看单模态模型Opean-PCDDet中的项目的萌新一个指引。

在看模型的过程中,也参考了很大大佬的博客,放在这里:

Opean-PCDDet链接:https://github.com/open-mmlab/OpenPCDet

主要参考博主,本文结构与之类似:(89条消息) SECOND点云检测代码详解_second 点云_NNNNNathan的博客-CSDN博客

创新点介绍:【3D目标检测】SECOND算法解析 - 知乎 (zhihu.com)

稀疏卷积介绍:通俗易懂的解释Sparse Convolution过程 - 知乎 (zhihu.com)

to %s’ % (filename, ‘CPU’ if to_cpu else ‘GPU’))
loc_type = torch.device(‘cpu’) if to_cpu else None
checkpoint = torch.load(filename, map_location=loc_type)
epoch = checkpoint.get(‘epoch’, -1)
it = checkpoint.get(‘it’, 0.0)

    self._load_state_dict(checkpoint['model_state'], strict=True)

    if optimizer is not None:
        if 'optimizer_state' in checkpoint and checkpoint['optimizer_state'] is not None:
            logger.info('==> Loading optimizer parameters from checkpoint %s to %s'
                        % (filename, 'CPU' if to_cpu else 'GPU'))
            optimizer.load_state_dict(checkpoint['optimizer_state'])
        else:
            assert filename[-4] == '.', filename
            src_file, ext = filename[:-4], filename[-3:]
            optimizer_filename = '%s_optim.%s' % (src_file, ext)
            if os.path.exists(optimizer_filename):
                optimizer_ckpt = torch.load(optimizer_filename, map_location=loc_type)
                optimizer.load_state_dict(optimizer_ckpt['optimizer_state'])

    if 'version' in checkpoint:
        print('==> Checkpoint trained from version: %s' % checkpoint['version'])
    logger.info('==> Done')

    return it, epoch



最后继承Detector3DTemplate模板类,具体构建SECOND模型,构建就很简单了,直接向模板类中传递SECOND模型的配置文件,以及KITTI数据集的配置文件即可。

地址:pcdet/models/detectors/second_net.py

具体代码如下:

```python
from .detector3d_template import Detector3DTemplate



"""
构建SECOND模型
继承Detector3DTemplate构建模型

"""
class SECONDNet(Detector3DTemplate):
    def __init__(self, model_cfg, num_class, dataset):
        '''
        :param model_cfg: 模型配置文件
        :param num_class: 类个数
        :param dataset: 数据集
        '''
        super().__init__(model_cfg=model_cfg, num_class=num_class, dataset=dataset)
        #根据配置文件构建模型
        self.module_list = self.build_networks()

    """
    0、Point2Voxle 点云体素化
    1、MeanVFE 均值体素编码
    2、VoxelBackBone8x 3D骨干网络特征提取
    3、HeightCompression 高度压缩3D转2D
    4、BaseBEVBackbone 2D骨干网络特征提取
    5、AnchorHeadSingle 基于锚框的检测方法
    """
    def forward(self, batch_dict):
        #将数据送到每一层模块中
        for cur_module in self.module_list:
            batch_dict = cur_module(batch_dict)
        #训练情况下,计算损失
        if self.training:
            loss, tb_dict, disp_dict = self.get_training_loss()

            ret_dict = {
                'loss': loss
            }
            return ret_dict, tb_dict, disp_dict
        #预测情况下,进行后处理,得到预测框和类别以及预测分数。
        else:
            pred_dicts, recall_dicts = self.post_processing(batch_dict)
            return pred_dicts, recall_dicts

    def get_training_loss(self):
        disp_dict = {}
        #调用检测头获取损失方法得到损失
        loss_rpn, tb_dict = self.dense_head.get_loss()
        tb_dict = {
            'loss_rpn': loss_rpn.item(),
            **tb_dict
        }

        loss = loss_rpn
        return loss, tb_dict, disp_dict

好啦,到此使用Opean-PCDDet构建SECOND模型的整体流程就结束了,大家在看其他Opean-PCDDet模型的时候也可按照上述顺序,作者是第一次看Opean-PCDDet构建模型的相关代码,在这里记录一下,也给之后想看单模态模型Opean-PCDDet中的项目的萌新一个指引。

在看模型的过程中,也参考了很大大佬的博客,放在这里:

Opean-PCDDet链接:https://github.com/open-mmlab/OpenPCDet

主要参考博主,本文结构与之类似:(89条消息) SECOND点云检测代码详解_second 点云_NNNNNathan的博客-CSDN博客

创新点介绍:【3D目标检测】SECOND算法解析 - 知乎 (zhihu.com)

稀疏卷积介绍:通俗易懂的解释Sparse Convolution过程 - 知乎 (zhihu.com)

最后是附上自己之前的一篇SECOND模型的理论:(89条消息) SECOND模型_'十月’的博客-CSDN博客

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值