简述

  之所以想写这篇博客,主要原因在于阅读别人的代码时候,首先希望把流程架构弄清楚,然后才方便进行修改。second.pytorch代码量比较大,刚开始拿到时候,我也是一头雾水,硬着头皮往下面去看,配置环境(没有跑起来的建议去下载我的docker镜像,深度学习的利器,避免二次配置软件环境问题),让其跑起来方便调试来进行阅读。话不多说,现在开始进行简要分析一下second.pytorch点云检测这部分代码。

框架分析

  通过pycharm打开second.pytorch代码之后,左边的工程树显示一堆代码,看着就有点郁闷。下面逐个进行简单的说明,最后通过pointpillars算法为例,来进行简单的pipeline流程阐述。

second.pytorch --------
                 |---images
                 |---second ----|---apex
                 |---torchplus  |---builder
                                |---configs
                                |---core
                                |---data
                                |---framework
                                |---kittiviewer
                                |---protos
                                |---pytorch ------|---builder
                                |---spconv        |---core
                                |---utils         |---models
                                                  |---utils

上述的代码主要工程树,简单说下每个文件夹下面的代码功能:
  apexspconv是进行second.pytorch安装的第三方依赖库;
  
  builder为基础网络的构建基础代码;
  
  configs为网络参数配置文件夹;
  
  core为基础功能文件夹,包括anchor、box_coder等些实现;
  
  data文件夹为数据处理模块;
  
  framework文件暂不清楚,好像是测试模块;
  
  kittiviewer很显然为可视化功能模块;
  
  protos模块读取内部的proto才构成py文件,具体不太清楚(理解后来填坑);
  
  pytorch文件夹为second.pytorch的核心,涉及训练、预测、网络等代码;
  
  utils为基础功能文件夹;

  下面就以pointpillars算法为例,来进行pipeline的说明,以快速加深对该框架的理解。下面看下pointpillars算法流程图:

在看pointpillars算法流程图之前,首先来看一下论文的pipeline:

从上图可以看出pointpillars算法与之前的VoxelNet的框架基本一致,主要有三个部分组成:

  1. PillarFeatureNet:特征编码网络,在Bird-View的图下,将点云数据编码成为稀疏的伪图像;

  2. PillarFeatureScatter:卷积中间网络,对伪图像进行backbone网络提取特征信息;

  3. RPN:区域生成网络,用于分类与回归3D框,不过需要注意的是点云的Bird-View下最后一层特征层feature_map不能很小;

  在Review这部分代码时候,second.pytorch代码主要涉及比较错乱,代码之间的交错比较多,可能需要仔细的梳理一下关系。voxel_builder、target_assign_builder、second_builder、box_coder_builder、optimizer_builder等等。在这里如果你想将该算法最终转换至TensorRT推理引擎下进行加速的话,那么可以参考我的那部分代码,修改网络之间的数据结构以此保证模型转换能够使用trt加速推理:传送门。当然你如果只是想阅读该算法在pytorch下的性能等来部署的话,可以参考second.pytorch的官方code:传送门,因为我的也是在这个代码基础上改动一下。

接下来主要分析几个主要的py文件:

---------->
         |---------> train.py
         |---------> voxelnet.py
         |---------> pointpillars.py
         |---------> rpn.py

  对于pointpillars算法最主要的几个代码文件,从程序运行的流程来简要解释一下。由于输入voxel为动态的变化网络,那么在中间的子网络就会随着动态的改变输入的维度。但是,使用TensorRT进行onnx中间件模型优化加速时候,模型的输入维度必须固定一个系数。那么,就需要对网络设置一个最大的维度,如果voxel的维度小于设置的最大维度,那么进行补零来进行输入维度的对齐。

second.pytorch代码相关tricks及其bugs解决

Anchors生成参数配置:

target_assigner: {
  anchor_generators: {
     anchor_generator_stride: {
           sizes: [1.6, 3.9, 1.56] # wlh
           strides: [0.32, 0.32, 0.0] # if generate only 1 z_center, z_stride will be ignored
           offsets: [0.16, -39.52, -1.78] # origin_offset + strides / 2
           rotations: [0, 1.57] # 0, pi/2
           matched_threshold : 0.6
           unmatched_threshold : 0.45
      }
}
...
}

  在对代码进行review的时候,你会发现anchors是固定的生成,与点云的输入无关;所以,你要想更加适配的生成对应的anchors系数,你需要对target_assigner里面的相关系数进行设置,其中anchor_generator_stride包括三维bbox相关信息,长宽高、中心点的偏移量、旋转角度等。这部分anchors的设置对最终的目标判定有很大的影响,设置错误的容易不收敛。

NMS加速predict模块:

  在目录second/pytorch/core/box_torch_ops.py中使用nms功能模块函数,原始采取的cpu版本的nms,可以使用gpu模块的nms进行加速。最粗暴的一种改变方式:

def rotate_nms(): # row 507
# ret = np.array(rotate_nms_cc(dets_np, iou_threshold), dtype=np.int64)
ret = np.array(rotate_nms_gpu(dets_np, iou_threshold), dtype=np.int64)

RPN系列网络输入feature_map维度对应不上:

  如果运行时候遇见RPN相关网络的输入feature_map的维度不是[1, 64, 496, 432]的话,请查看你的训练配置文件里面的voxel_generator中的point_cloud_range与voxel_size的范围是否与之前的相同。当然,适当的修改voxel中的范围可以减少输入pointcloud的个数与pillars的个数,从而降低输入到后面网络的tensor张量,提升预测的speed。

model: {
  second: {
    network_class_name: "VoxelNet"
    voxel_generator {
      full_empty_part_with_mean: false
      point_cloud_range : [0, -39.68, -3, 69.12, 39.68, 1]
      voxel_size : [0.16, 0.16, 4]
      max_number_of_points_per_voxel : 100
    }
    ......

在train还是在evaluate功能部分:

  为什么要这么说呢?是希望你时刻记住自己是在做训练还是预测模块,主要是代码量比较大,有涉及模型的onnx导出部分的修改,切记不同的功能时候有些代码是需要进行修改的。训练时候,需要把网络的pfelayer的返回return voxel_feature进行注释掉,相反导出onnx时候需要使用。还有就是pillarscatter在forward时候,训练与导出的batch_size不同,所以需要记住或者直接传入变量batch_size进行修改。

在train时候相关tricks:

  相信你打开训练的config文件时候,觉得也有点多的参数,但是并不是所有的参数都会使用到。有些时候会出现训练卡住无法继续往下训练的情况,主要的原因可能在于学习率initial_learning_rate设置与类别中的database_sample与database_prep_steps这些变量的设置。每次训练的时候model_dir是需要没有创建的,当然你也可以修改代码去掉这一功能。最后,想说的是每个变量的意义仍然需要进一步的确定与研究,改变参数所带来的影响等等。如果你想看一下网络导出成onnx后,使用tensorrt的加速效果,那么请你参考PointPillars-TensorRT加速

  如果你的point_cloud_range设置比较小,voxel_size的比较大的话,那么相对应的你的database_sampler里面对于car或者person的name_to_max_num与min_num_points_pairs阈值需要相对应的进行修改。理由很简单,不同的range与划分voxel时候,获取每个pillar点云是不同的。

database_sampler {
    database_info_path: "/data/sets/kitti_second/kitti_dbinfos_train.pkl"
    sample_groups {
      name_to_max_num {
        key: "Car"
        value: 15
      }
    }
    database_prep_steps {
      filter_by_min_num_points {
        min_num_point_pairs {
          key: "Car"
          value: 5
        }
      }
    }
    database_prep_steps {
      filter_by_difficulty {
        removed_difficulties: [-1]
      }
    }
    global_random_rotation_range_per_object: [0, 0]
    rate: 1.0
  }
小结

  说实话second.pytorch中的代码比较庞大,涉及的不仅仅只是一个pointpillars算法,还有voxelnet不同功能模块的算法,所以涉及的参数变量较多,整体上理解整个的代码框架只能根据文件夹的分布来简单理解一下。所以代码都是一样,需要理解整个框架的分布功能,然后找一个算法理解整个pipeline的部分,最后进行细节逐个击破。关于点云的密集程度对pointpillars算法的影响,请参考这篇博客:Robustness of PointPillars Under Noisy Attack。最后,由于代码比较错综复杂,希望你可以早点享受该算法的效果吧。

参考文献

https://arxiv.org/abs/1812.05784
https://github.com/traveller59/second.pytorch
https://towardsdatascience.com/the-state-of-3d-object-detection-f65a385f67a8
https://paperswithcode.com/paper/pointpillars-fast-encoders-for-object
https://deeplearn.org/arxiv/104450/pointpainting:-sequential-fusion-for-3d-object-detection
http://carina.cse.lehigh.edu/PointPillars_Robustness/

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐