PointPillar 3D目标检测模型详解

一、参考资料

pointpillars 论文
pointpillars 论文
PointPillars - gitbook_docs
使用 NVIDIA CUDA-Pointpillars 检测点云中的对象
3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现
模型部署入门教程(三):PyTorch 转 ONNX 详解
PointPillar代码解析-OpenPCDet
pointpillars deployment 学习
模型部署——pointpillars转一个onnx

二、重要说明

pointpillars算法最突出的是提出一种柱形的编码功能,点云依然采取常用的体素组织起来。VoxelNet 直接采用体素3D卷积SECOND采用稀疏卷积pointpillars采用pillar方式转换成为2D卷积来加深网络,以此来提高效率与精度至于后面接SSD还是RPN等网络,只是相对于2d卷积下的网络根据应用场景与需求来进行选取。

三、相关介绍

1. 3D目标检测

3D检测论文阅读简记

自动驾驶中基于Lidar的object检测,简单的说,就是从3D点云数据中定位到object的框类别。具体地,输入是点云 $X∈R^{N×c} ( 一般 c = 4 ) ,输出是 n 个检测框 b b o x e s , 以第 i 个检测框 b b o x 为例 , 它 = = 包括 ∗ ∗ 位姿信息 ∗ ∗ (一般 c=4 ),输出是 n 个检测框bboxes, 以第 i 个检测框bbox为例, 它==包括**位姿信息** (一般c=4),输出是n个检测框bboxes,以第i个检测框bbox为例,==包括位姿信息(x_i,y_i,z_i,w_i,l_i,h_i,θ_i) $和类别信息 ( l a b e l i , s c o r e i ) (label_i,score_i) (labeli,scorei)。==

基于Lidar的object检测模型包括:Point-based,Voxel-base,Point-Voxel-based,Multi-view-based

1.1 Point-based

经典模型:PointNet,PointNet++,[PointRCNN(CVPR19),IA-SSD(CVPR22)等]。

基于 Point-based 的模型,直接对点云进行处理,可以减少位置信息的损失,但同时也带来了巨大的计算资源消耗,使其很难做到实时

1.2 Voxel-based

经典模型:[PointPillars(CVPR19),CenterPoint(CVPR21)等]。

基于 Voxel-base 的模型,相较于 Point-base 的模型在推理速度上有所提升,但是由于模型中使用了三维卷积的 backbone,所以也仍然很难做到实时

相较于其他的模型,PointPillars 在推理速度方面有着明显的优势(遥遥领先),同时又能保持着不错的准确性。

1.3 Point-Voxel-based

经典模型:[PV-RCNN(CVPR20),HVPR(CVPR21)等]。

1.4 Multi-view-based

经典模型:[PIXOR(CVPR18)等]。

2. voxelization(体素化)

2.1 体素化简介

voxel-base 的模型中常常使用 voxelization(体素化)。在实际使用过程中我们都希望我们的模型又快有准,所以为了可以权衡速度和精度,VoxelNet 提出了使用 voxelization(体素化)的方法来处理点云。

点云是三维空间中的物体表示,因此一个自然的思路是将空间在长宽高三个方向划分格子,每个格子称为 voxel(体素),通过处理将其转换为 3 维数组的形式,再使用 3D 卷积和 2D 卷积的网络处理,如下图所示:
在这里插入图片描述

2.2 体素化存在的问题

体素化也会带来一些问题,例如不可避免的会造成一些信息的丢失,对体素参数较为敏感,以及转换成 3 维数组后提取特征时通常需要用到 3 维卷积。3 维的卷积是一个相当耗时的操作,所以当我们设置体素化的粒度过大时会导致较多的信息丢失,但如果粒度过小又会导致计算时间几何增加

2.3 改进体素化

PointPillars 在 VoxelNet 中的 voxel 的基础上提出了一种改进版本的点云表征方法 pillar,可以将点云转换成伪图像的形式,进而通过 2D 卷积实现目标检测,相较于 VoxelNet 将点云转换成 voxel 形式然后使用相当耗时的 3 维卷积来处理特征,PointPillars 这种使用 2 维卷积的网络在推理速度上有很大的优势。

什么是 pillar?原文中的描述是“ a pillar is a voxel with unlimited spatial extent in the z direction ”,其实很简单,将空间的 x,y 轴两个方向上划分格子,然后再将每个格子在 z 轴上拉伸,使其可以覆盖整个空间 z 轴,就可以得到一个 pillar,且空间中的每个点都可以划分到某个 pillar 中

3. 点云配准

点云批准论文

4. 相机标定

3D雷达与相机的标定方法详细教程

5. 体素特征编码层(VFE)

VFE(Voxel Feature Encode),体素特征编码层,其实是简化版的pointnet。

四、PointPillar相关介绍

【模型加速】PointPillars模型TensorRT加速实验(1)
【模型加速】PointPillars模型TensorRT加速实验(2)
【模型加速】PointPillars模型TensorRT加速实验(3)

1. 问题引入(背景)

3D卷积太昂贵了,不适合边缘设备部署。

PointPillar延续了VoxelNet和SECOND的思路,VoxelNet的方法是直接用体素做3D卷积,SECOND用了稀疏卷积,而PointPillar使用了pillar的方式,直接转成2维卷积进行提速。

2. PointPillar贡献

PointPillars 是一个既简单又实用的模型,在保持较高精度的同时又有很高的推理速度,同时部署也很友好,是一个十分常用的模型。

  1. 提出了一种新的点云编码器和新网络pointpillar,实现对三维目标检测网络的端到端训练;
  2. 将三维点云处理为二维伪图像,用传统CNN对伪图像进行特征提取,推理速度显著提升,是其他方法(含3维卷积)的2-4倍。

3. PointPillar网络结构

整个算法逻辑包含3个部分:数据预处理,神经网络,后处理。其中神经网络部分,原论文将其结构描述为3个部分:

  • PFN(Pillar Feature Net):将输入的点云转换为稀疏的伪图像的特征形式。
  • Backbone(2D CNN):使用 2D 的 CNN 处理伪图像特征得到高维度的特征。
  • Detection Head(SSD):检测和回归 3D 边界框。
    在这里插入图片描述
    在实际部署的时候,结构拆分和论文中的稍微有些出入。主要是分成PFN(Pillar Feature Network)MFNRPN。其中MFN是用来将PFN提取的Pillar级的点云深度特征进一步转化为伪点云图像。RPN就是Backbone,而检测头的部分功能被包含在后处理的逻辑里面。

4. PFN模块

PFN(Pillar Feature Network)。

因为不同点云帧的点云数量是变化的,非空Pillar的数量自然也是不同的,在考虑将PFN导出为ONNX模型时,需要采用dynamic shape。

从PFN的8个输入可知,num_points表示每个Pillar包含的实际点云数量,这个轴是dynamic的。
在这里插入图片描述

  1. 首先将一个样本的点云空间划分成(在 X 轴方向上点云空间的范围 / pillar size,在 Y 轴方向上点云空间的范围 / pillar size)pillar 网格,样本中的点根据会被包含在各个 pillar 中,没有点的 pillar 则视为空 pillar。
  2. 假设样本中包含的非空 pillar 数量为 P,同时限制每个 pillar 中的点的最大数量为 N,如果一个 pillar 中点的数量不及 N,则用 0 补全,若超过 N,则从 pillar 内的点中采样出 N 个点来。并对 pillar 中的每个点进行编码,其中每个点的表示会包括点的坐标,反射强度,pillar 的几何中心,点与 pillar 几何中心的相对位置,将每个点的表示的长度记为 D。这样我们的一个点云样本就可以用一个(P,N,D)的张量来表示。
  3. 得到点云的 pillar 表示的张量后,我们对其进行处理提取特征,通过使用简化版的 PointNet 中的 SA 模块来处理每个 pillar。即先对每个 pillar 中的点使用多层 MLP 来使得每个点的维度从 D 变成 C,这样张量变成了(P,N,C),然后对每个 pillar 中的点使用 Max Pooling,得到每个 pillar 的特征向量,也使得张量中的 N 的维度消失,得到了(P,C)维度的特征图。
  4. 最后将(P,C)的特征根据 pillar 的位置展开成伪图像特征,将 P 展开为(H,W)。这样我们就获得了类似图像的(C,H,W)形式的特征表示。

总结:shape变化,(P,N,D) ——》(P,N,C)——》(P,C)——》(C,H,W)

4.1 PFN的输入

PFN有8个输入:

  1. pillar_x:包含Pillar化后的点云x坐标,shape为(1,1,P,100);
  2. pillar_y:包含Pillar化后的点云y坐标,shape为(1,1,P,100);
  3. pillar_z:包含Pillar化后的点云z坐标, shape为(1,1,P,100);
  4. pillar_i:包含Pillar化后的点云强度值,shape为(1,1,P,100);
  5. num_points: 保存每个Pillar包含的实际点云数量,shape为(1,P);
  6. x_sub_shaped:保存Pillar的中心x坐标,shape为(1,1,P,100);
  7. y_sub_shaped:保存Pillar的中心y坐标,shape为(1,1,P,100);
  8. mask:pillar点云掩码,shape为(1,1,P,100);
#准备输入
pillar_x = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
pillar_y = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
pillar_z = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
pillar_i = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
num_points_per_pillar = torch.ones([1, 9918], dtype=torch.float32, device="cpu")
x_sub_shaped = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
y_sub_shaped = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")
mask = torch.ones([1, 1, 9918, 100], dtype=torch.float32, device="cpu")

4.2 PFN的输出

PFN的输出shape为(1,64,pillar_num,1),pillar_num表示非空pillar的数量,是dynamic shape。因为不同点云帧的点云数量是变化的,非空Pillar的数量自然也是不同的。

5. MFN模块

MFN(Middle Feature Extractor Network)是用来将PFN提取的Pillar级的点云深度特征进一步转化为伪点云图像

5.1 MFN的输入

MFN有2个输入,且都是dynamic的

  1. voxel_features:voxel_features是PFN的输出,经过PFN后每个非空pillar用表示为一个64维的深度特征,其shape为(1,64,P,1),p为pillar的数量,是dynamic的
  2. coords:pillar在x-y网格中的坐标,shape为(P,4),p为pillar的数量,是dynamic的

5.2 MFN输出

MFN的输出spatial_features是一个固定尺寸(1,64,496,432)的特征图,是RPN网络的唯一的输入。

6. RPN(Backbone)模块

RPN的输入

RPN有一个输入,且是固定尺寸的。

rpn_input = torch.ones([1, 64, 496, 432], dtype=float_type, device=device)
torch.onnx.export(net.rpn, rpn_input, rpn_onnx_file, verbose=verbose)
print('rpn.onnx transfer success ...')

五、PointPillar源码解析

代码阅读 :SECOND pytorch版本
3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现
GitHub - zhulf0804/PointPillars: A Simple PointPillars PyTorch Implenmentation for 3D Lidar(KITTI) Detection.
GitHub - jjw-DL/OpenPCDet-Noted: OpenPCDet代码分析与注释

1. 重要说明

参数项 参数含义
P 非空 pillar 数量为P,训练集中最多16000, 测试集中最多40000
N 每个 pillar 中存储的最大点云数量为 N,如果一个 pillar 中点的数量不及 N,则用 0 补全,若超过 N,则从 pillar 内的点中采样出 N 个点来
C 每个pillar encoder后的channel数量为C
D 对 pillar 中的每个点进行编码,其中每个点的表示会包括点的坐标,反射强度,pillar 的几何中心,点与 pillar 几何中心的相对位置,将每个点的表示的长度记为 D
(P,N,D) 一个点云样本用一个(P,N,D)的张量表示
M 32

2. 项目结构

second.pytorch --------
                 |---images
                 |---second ----|---apex
                 |---torchplus  |---builder
                                |---configs
                                |---core
                                |---data
                                |---framework
                                |---kittiviewer
                                |---protos
                                |---pytorch ------|---builder
                                |---spconv        |---core
                                |---utils         |---models
                                                  |---utils
  • apex与spconv是进行second.pytorch安装的第三方依赖库;
  • builder为基础网络的构建基础代码;
  • configs为网络参数配置文件夹;
  • core为基础功能文件夹,包括anchor、box_coder等些实现;
  • data文件夹为数据处理模块;
  • framework文件暂不清楚,好像是测试模块;
  • kittiviewer很显然为可视化功能模块;
  • protos模块读取内部的proto才构成py文件,具体不太清楚(理解后来填坑);
  • pytorch文件夹为second.pytorch的核心,涉及训练、预测、网络等代码;
  • utils为基础功能文件夹;

3. 代码流程

在这里插入图片描述

4. 坐标系转换

因为gt label中提供的bbox信息是Camera坐标系的,因此在训练时需要使用外参等将其转换到Lidar坐标系; 有时想要把3d bbox映射到图像坐标系中的2d bbox方便可视化,此时需要内参。具体转换关系如Figure 2。坐标系转换的代码见utils/process.py
在这里插入图片描述
在这里插入图片描述

5. 数据增强

数据增强应该是Lidar检测中很重要的一环。发现其与2D检测中的增强差别较大,比如3D中会做database sampling(我理解的是把gt bbox进行cut-paste), 会做碰撞检测等。在本库中主要使用了采用了5种数据增强, 相关代码在dataset/data_aug.py

  • 采样gt bbox并将其复制到当前帧的点云
    • 因为当前帧点云中objects(gt_bboxes)可能比较少, 不利于训练; 因此从Car, Pedestrian, Cyclist的database数据集中随机采集一定数量的bbox及inside points, 使当前帧中每类gt_bboxes的数量分别达到15, 10, 10.
    • 但因为在实际情况中, gt_bboxes是没有overlap的(若存在overlap, 就表示有碰撞了); 因此需要将采样的bboxes先与当前帧点云中的gt_bboxes进行碰撞检测, 通过碰撞检测的bboxes和对应labels加到gt_bboxes_3d, gt_labels; 同时把当前帧点云中位于这些采样bboxes内的点删除掉, 替换成采样的bboxes(包括inside points).
  • bbox 随机旋转平移
    • 以某个bbox为例, 随机产生num_try个平移向量t和旋转角度r, 旋转角度可以转成旋转矩阵(mat).
    • 对bbox进行旋转和平移, 找到num_try中第一个通过碰撞测试的平移向量t和旋转角度r(mat).
    • 对bbox内部的点进行旋转和平移.
    • 对bbox进行旋转和平移.
  • 随机水平翻转
    • points水平翻转
    • bboxes水平翻转
  • 整体旋转/平移/缩放
    • object旋转, 缩放和平移
    • point旋转, 缩放和平移
  • 对points进行shuffle: 打乱点云数据中points的顺序。

Figure3是对上述前4种数据增强的可视化结果。
在这里插入图片描述

6. 网络结构

对于输入点云 X ∈ R N × 4 X∈R^{N×4} XRN×4 , PointPillars是如何一步步地得到bbox的呢 ? 相关代码见model/pointpillars.py
在这里插入图片描述

输入项 含义 shape
voxels(pillars) [20000, 32, 4]
coors(coors_batch) [20000, 4]
num_points(npoints_per_pillars) [20000]

6.1 PillarLayer

Lidar的range是[0, -39.68, -3, 69.12, 39.68, 1], 即(xmin, ymin, zmin, xmax, ymax, zmax)。

  • 基于预先设定好的voxel_size=(0.16, 0.16, 4), 将点云 X 中的 N 个点划分到(432, 496, 1)个Pillars里。
    • voxel_size中的0.16是根据KITTI数据集和经验得到的,是先验值;h=4是因为point_range=[0, -39.68, -3, 69.12, 39.68, 1]中的[-3, 1],差值为4。
    • (432, 496, 1)是根据point_range除以voxel_size得到的。
  • 选择 P (训练集中最多16000, 测试集中最多40000)个Pillars, 并且每个Pillar选择 M=32 个点, 不足32个点时补(0, 0, 0, 0)。
  • 数据shape的变化: (N,4) -> (P,M,4) , 同时记录这 P 个Pillars在(432, 496)的map中的位置(coors)和每个Pillar中有效点的数量(npoints_per_pillar)。

6.2 PillarEncoder

  • 对每个Pillar中的点进行去均值编码: (P,M,4) -> (P,M,3)
  • 对每个Pillar中有效点进行去中心编码: (P,M,4) -> (P,M,2)
  • 合并编码: 将原始的 (P,M,4) 同去均值编码和去中心编码的结果进行cat, 得到 (P,M,9) 的向量。这里有两点需要注意: (1)每个Pillar中只对有效点(npoints_per_pillar)进行操作, 即(0, 0, 0, 0)还是保持(0, 0, 0, 0); (2)这应该是一个trick, 把9维编码向量中的前2维换成去中心编码的向量, 详情见https://github.com/open-mmlab/mmdetection3d/issues/1150
  • 进行embedding(卷积核池化): (P,M,9) -> (P,M,64) -> (P,64) 。
  • Pillar scatter: 根据Pillars在map中的位置(coors), 将 P 个pillars的特征scatter到(432, 496)的特征图上(没有Pillar的位置补0向量), 得到 (64,496,432) 的特征图, 这里不妨记为 (C,H,W)。
  • 数据shape的变化: (P,M,4) -> (C,H,W) 。

6.3 Backbone

  • 在得到了 (C,H,W) 的特征图后, Backbone及接下来的Neck, Head都是在2D上进行操作了,基本是Conv2d + BN + ReLU的组合,所以接下来主要介绍tensor的shape变化。
    • block1: (C,H,W) -> (C,H/2,W/2) , 即 (64,496,432) -> (64,248,216) 。
    • block2: (C,H/2,W/2) -> (2∗C,H/4,W/4) , 即 (64,248,216) -> (128,124,108) 。
    • block3: (2∗C,H/4,W/4) -> (4∗C,H/8,W/8) , 即 (128,124,108) -> (256,62,54) 。
    • 数据shape的变化: (C,H,W) -> [(C,H/2,W/2),(2∗C,H/4,W/4),(4∗C,H/8,W/8)] 。

6.4 Neck

  • decoder1: (C,H/2,W/2) -> (2∗C,H/2,W/2) 。
  • decoder2: (2∗C,H/4,W/4) -> (2∗C,H/2,W/2) 。
  • decoder3: (4∗C,H/8,W/8) -> (2∗C,H/2,W/2) 。
  • cat: [(2∗C,H/2,W/2),(2∗C,H/2,W/2),(2∗C,H/2,W/2)] -> (6∗C,H/2,W/2) 。此时,得到的特征图为(384,248,216) 。
  • 数据shape的变化: [(C,H/2,W/2),(2∗C,H/4,W/4),(4∗C,H/8,W/8)] -> (6∗C,H/2,W/2) 。

6.5 Head(Anchor3DHead)

  • PointPillars共有3个不同尺寸的anchors(详情见2.2小节), 每个尺寸的anchor有2个角度, 因此共有6个anchors。网络训练了3个类别: Pedestrian, Cyclist和Car。
  • 类别分类branch: (6∗C,H/2,W/2) -> (6∗3,H/2,W/2) , 即 (384,248,216) -> (18,248,216) 。
  • bbox回归branch: (6∗C,H/2,W/2) -> (6∗7,H/2,W/2) , 即 (384,248,216) -> (42,248,216) 。
  • 朝向分类branch: (6∗C,H/2,W/2) -> (6∗2,H/2,W/2) , 即 (384,248,216) -> (12,248,216) 。
  • 数据shape的变化: (6∗C,H/2,W/2) -> [(6∗3,H/2,W/2),(6∗7,H/2,W/2),(6∗2,H/2,W/2)] 。
输出项 含义 shape
bbox_pred(bbox_preds) bbox回归 [1, 42, 248, 216]
bbox_cls_pred(cls_scores) 类别分类 [1, 18, 248, 216]
bbox_dir_cls_pred(dir_cls_preds) 朝向分类 [1, 12, 248, 216]

7. GT值生成

3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现

Head的3个分支是基于anchor分别预测了类别, bbox框(相对于anchor的偏移量和尺寸比)和旋转角度的类别, 那么在训练时, 如何得到每一个anchor对应的GT值呢 ? 相关代码见model/anchors.py

8. 损失函数

3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现

现在知道了类别分类head, bbox回归head和朝向分类head的预测值和GT值, 接下来介绍损失函数。相关代码见loss/loss.py

9. 单帧预测和可视化

3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现

基于Head的预测值和anchors, 如何得到最后的候选框呢 ? 相关代码见model/pointpillars.py

10. 模型评估

评估指标同2D检测类似, 也是采用AP, 即Precison-Recall曲线下的面积。不同的是, 在3D中可以计算3D bbox, BEV bbox 和 (2D bbox, AOS)的AP。

11. configs文件

car.fhd.config

model: {
    
    
  second: {
    
    
    network_class_name: "VoxelNet"
    # 体素生成
    voxel_generator {
    
    
      point_cloud_range : [0, -40, -3, 70.4, 40, 1]  # 点云范围
      # point_cloud_range : [0, -32.0, -3, 52.8, 32.0, 1]
      voxel_size : [0.05, 0.05, 0.1]  # 体素大小
      max_number_of_points_per_voxel : 5  # 每个体素的最大点数
    }
    # 体素特征提取器
    voxel_feature_extractor: {
    
    
      module_class_name: "SimpleVoxel"
      num_filters: [16]
      with_distance: false
      num_input_features: 4
    }
    # 中间特征提取器
    middle_feature_extractor: {
    
    
      module_class_name: "SpMiddleFHD"
      # num_filters_down1: [] # protobuf don't support empty list.
      # num_filters_down2: []
      downsample_factor: 8
      num_input_features: 4
    }
    # RPN网络
    rpn: {
    
    
      module_class_name: "RPNV2"
      layer_nums: [5]
      layer_strides: [1]
      num_filters: [128]
      upsample_strides: [1]
      num_upsample_filters: [128]
      use_groupnorm: false
      num_groups: 32
      num_input_features: 128
    }
    # 损失函数
    loss: {
    
    
      classification_loss: {
    
    
        weighted_sigmoid_focal: {
    
    
          alpha: 0.25
          gamma: 2.0
          anchorwise_output: true
        }
      }
      localization_loss: {
    
    
        weighted_smooth_l1: {
    
    
          sigma: 3.0
          code_weight: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
        }
      }
      classification_weight: 1.0
      localization_weight: 2.0
    }
    num_point_features: 4 # model's num point feature should be independent of dataset
    # Outputs
    use_sigmoid_score: true
    encode_background_as_zeros: true
    encode_rad_error_by_sin: true
    sin_error_factor: 1.0

    use_direction_classifier: true # this can help for orientation benchmark
    direction_loss_weight: 0.2 # enough.
    num_direction_bins: 2
    direction_limit_offset: 1

    # Loss
    pos_class_weight: 1.0
    neg_class_weight: 1.0

    loss_norm_type: NormByNumPositives
    # Postprocess
    post_center_limit_range: [0, -40, -2.2, 70.4, 40, 0.8]
    nms_class_agnostic: false # only valid in multi-class nms

    box_coder: {
    
    
      ground_box3d_coder: {
    
    
        linear_dim: false
        encode_angle_vector: false
      }
    }
    target_assigner: {
    
    
      class_settings: {
    
    
        anchor_generator_range: {
    
    
          sizes: [1.6, 3.9, 1.56] # wlh
          anchor_ranges: [0, -40.0, -1.00, 70.4, 40.0, -1.00] # carefully set z center
          rotations: [0, 1.57] # DON'T modify this unless you are very familiar with my code.
        }
        matched_threshold : 0.6
        unmatched_threshold : 0.45
        class_name: "Car"
        use_rotate_nms: true
        use_multi_class_nms: false
        nms_pre_max_size: 1000
        nms_post_max_size: 100
        nms_score_threshold: 0.3 # 0.4 in submit, but 0.3 can get better hard performance
        nms_iou_threshold: 0.01

        region_similarity_calculator: {
    
    
          nearest_iou_similarity: {
    
    
          }
        }
      }
      # anchor_generators: {
    
    
      #   anchor_generator_stride: {
    
    
      #     sizes: [1.6, 3.9, 1.56] # wlh
      #     strides: [0.4, 0.4, 0.0] # if generate only 1 z_center, z_stride will be ignored
      #     offsets: [0.2, -39.8, -1.00] # origin_offset + strides / 2
      #     rotations: [0, 1.57] # DON'T modify this unless you are very familiar with my code.
      #     matched_threshold : 0.6
      #     unmatched_threshold : 0.45
      #   }
      # }
      sample_positive_fraction : -1
      sample_size : 512
      assign_per_class: true
    }
  }
}

#训练输入读取器,原batch_size=8,num_workers=3
train_input_reader: {
    
    
  dataset: {
    
    
    dataset_class_name: "KittiDataset"
    # kitti_info_path: "/media/yy/960evo/datasets/kitti/kitti_infos_train.pkl"
    # kitti_root_path: "/media/yy/960evo/datasets/kitti"
    kitti_info_path: "/home/cv/文档/datasets/KITTI_PP/kitti_infos_train.pkl"
    kitti_root_path: "/home/cv/文档/datasets/KITTI_PP"
  }

  batch_size: 8
  preprocess: {
    
    
    max_number_of_voxels: 17000
    shuffle_points: true
    num_workers: 1
    groundtruth_localization_noise_std: [1.0, 1.0, 0.5]
    # groundtruth_rotation_uniform_noise: [-0.3141592654, 0.3141592654]
    # groundtruth_rotation_uniform_noise: [-1.57, 1.57]
    groundtruth_rotation_uniform_noise: [-0.78539816, 0.78539816]
    global_rotation_uniform_noise: [-0.78539816, 0.78539816]
    global_scaling_uniform_noise: [0.95, 1.05]
    global_random_rotation_range_per_object: [0, 0] # pi/4 ~ 3pi/4
    global_translate_noise_std: [0, 0, 0]
    anchor_area_threshold: -1
    remove_points_after_sample: true
    groundtruth_points_drop_percentage: 0.0
    groundtruth_drop_max_keep_points: 15
    remove_unknown_examples: false
    sample_importance: 1.0
    random_flip_x: false
    random_flip_y: true
    remove_environment: false

    #数据库采样器
    database_sampler {
    
    
      # database_info_path: "/media/yy/960evo/datasets/kitti/kitti_dbinfos_train.pkl"
      database_info_path: "/home/cv/文档/datasets/KITTI_PP/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
    }
  }
}

train_config: {
    
    
  optimizer: {
    
    
    adam_optimizer: {
    
    
      learning_rate: {
    
    
        one_cycle: {
    
    
          lr_max: 2.25e-3
          moms: [0.95, 0.85]
          div_factor: 10.0
          pct_start: 0.4
        }
      }
      weight_decay: 0.01
    }
    fixed_weight_decay: true
    use_moving_average: false
  }
  # steps: 99040 # 1238 * 120
  # steps: 49520 # 619 * 80
  # steps: 30950 # 619 * 80
  # steps_per_eval: 3095 # 619 * 5
  steps: 23200 # 464 * 50
  steps_per_eval: 2320 # 619 * 5

  save_checkpoints_secs : 1800 # half hour
  save_summary_steps : 10
  enable_mixed_precision: false 
  loss_scale_factor: -1
  clear_metrics_every_epoch: true
}

#测试输入读取器,原batch_size=8,num_workers=3
eval_input_reader: {
    
    
  dataset: {
    
    
    dataset_class_name: "KittiDataset"
    # kitti_info_path: "/media/yy/960evo/datasets/kitti/kitti_infos_val.pkl"
    # # kitti_info_path: "/media/yy/960evo/datasets/kitti/kitti_infos_test.pkl"
    # kitti_root_path: "/media/yy/960evo/datasets/kitti"

    kitti_info_path: "/home/cv/文档/datasets/KITTI_PP/kitti_infos_val.pkl"
    # kitti_info_path: "/home/cv/文档/datasets/KITTI_PP/kitti_infos_test.pkl"
    kitti_root_path: "/home/cv/文档/datasets/KITTI_PP"
  }
  batch_size: 8
  preprocess: {
    
    
    max_number_of_voxels: 40000
    shuffle_points: false
    num_workers: 3
    anchor_area_threshold: -1
    remove_environment: false
  }
}

12. 总结

点云检测, 相比于点云中其它任务(分类, 分割和配准等), 逻辑和代码都更加复杂, 但这并不是体现在网络结构上, 更多的是体现在数据增强, Anchors和GT生成, 单帧推理等。

点云检测, 相比于2D图像检测任务, 不同的是坐标系变换, 数据增强(碰撞检测, 点是否在立方体判断等), 斜长方体框IoU的计算等; 评估方式因为考虑到DontCare, difficulty等, 也更加复杂一些。

六、关键步骤(CPU版本)

OpenPCDet
mmdetection3d
second.pytorch
PointPillars-TF
simple-pointpillar
PointPillars
nutonomy_pointpillars

可用版本

项目代码 版本 测试人 是否通过
PointPillars 李梓和
PointPillar(MindSpore版本) MindSpore
PointPillars (train with CUDA and inference with TensorRT) TensorRT
3d Object Detection for Pedestrian with Pointpillars, Tensorflow, Intel Realsense d435i at 120 HZ ROS
TensorFlow2.X
second.pytorch PyTorch 刘林军
PointPillars Pytorch Model Convert To ONNX, And Using TensorRT to Load this IR(ONNX) for Fast Speeding Inference ONNX
TensorRT
PointPillars Inference with TensorRT TensorRT
CUDA
GitHub - zhulf0804/PointPillars: A Simple PointPillars PyTorch Implenmentation for 3D Lidar(KITTI) Detection. PyTorch 待验证

1. 下载代码

git clone https://github.com/traveller59/second.pytorch.git
cd ./second.pytorch/second

2. 安装环境

推荐用Anaconda管理虚拟环境。

conda install scikit-image scipy numba pillow matplotlib
pip install fire tensorboardX protobuf opencv-python
pip install torchplus
pip install pycamia
pip install spconv

安装boost库

apt-get install libboost-all-dev 

2.1 安装SparseConvNet

GitHub - facebookresearch/SparseConvNet: Submanifold sparse convolutional networks

git clone https://github.com/facebookresearch/SparseConvNet.git
cd SparseConvNet/
bash develop.sh

2.2 配置环境变量

添加second.pytorch至PYTHONPATH。

export  PYTHONPATH=$PYTHONPATH:/your_second.pytorch_path/ 

3. 下载预训练模型

simple-pointpillar

4. 下载KITTI数据集

4.1 KITTI数据集目录结构

└── KITTI_DATASET_ROOT
       ├── training    <-- 7481 train data
       |   ├── image_2 <-- for visualization
       |   ├── calib
       |   ├── label_2
       |   ├── velodyne
       |   └── velodyne_reduced <-- empty directory
       └── testing     <-- 7580 test data
           ├── image_2 <-- for visualization
           ├── calib
           ├── velodyne
           └── velodyne_reduced <-- empty directory

4.2 方法一(Kaggle)

推荐:不需要科学上网,下载速度挺快的。

kitti-3d-object-detection-dataset
在这里插入图片描述

4.3 方法二(百度网盘)

测试数据集为KITTI数据集,KITTI官网需要翻墙,且访问速度慢。网上有很多百度网盘链接,推荐用网盘下载。

Kitti数据集百度网盘链接 00-21全

5. 数据预处理

5.1 create_data.py指令用法

Usage: create_data.py <group|command>
  available groups:      copy | pathlib | pickle | fire | np | imgio | sys |
                         box_np_ops | kitti
  available commands:    bound_points_jit | prog_bar | create_kitti_info_file |
                         create_reduced_point_cloud |
                         create_groundtruth_database

For detailed information on this command, run:
  create_data.py --help

5.2 创建 kitti infos

python create_data.py create_kitti_info_file --data_path=KITTI_DATASET_ROOT

5.3 创建 reduced point cloud

python create_data.py create_reduced_point_cloud --data_path=KITTI_DATASET_ROOT

5.4 创建 groundtruth-database infos

python create_data.py create_groundtruth_database --data_path=KITTI_DATASET_ROOT

5.5 完成数据预处理

kitti
    |- training
        |- calib (#7481 .txt)
        |- image_2 (#7481 .png)
        |- label_2 (#7481 .txt)
        |- velodyne (#7481 .bin)
        |- velodyne_reduced (#7481 .bin)
    |- testing
        |- calib (#7518 .txt)
        |- image_2 (#7518 .png)
        |- velodyne (#7518 .bin)
        |- velodyne_reduced (#7518 .bin)
    |- kitti_gt_database (# 19700 .bin)
    |- kitti_infos_train.pkl
    |- kitti_infos_val.pkl
    |- kitti_infos_trainval.pkl
    |- kitti_infos_test.pkl
    |- kitti_dbinfos_train.pkl

6. 修改配置

train_input_reader: {
    
    
  ...
  database_sampler {
    
    
    database_info_path: "/path/to/kitti_dbinfos_train.pkl"
    ...
  }
  kitti_info_path: "/path/to/kitti_infos_train.pkl"
  kitti_root_path: "KITTI_DATASET_ROOT"
}
...
eval_input_reader: {
    
    
  ...
  kitti_info_path: "/path/to/kitti_infos_val.pkl"
  kitti_root_path: "KITTI_DATASET_ROOT"
}

7. 训练模型

cd ~/second.pytorch/second
python ./pytorch/train.py train --config_path=./configs/pointpillars/car/xyres_16.proto --model_dir=/path/to/model_dir

8. 评估模型

cd ~/second.pytorch/second/
python pytorch/train.py evaluate --config_path= configs/pointpillars/car/xyres_16.proto --model_dir=/path/to/model_dir
python ./pytorch/train.py evaluate --config_path=configs/pointpillars/car/xyres_16.proto --model_dir=/mnt/d/datasets/archive/car_fhd --measure_time=True --batch_size=1
python ./pytorch/train.py evaluate --config_path=configs/pointpillars/car/xyres_16.proto --model_dir=/mnt/d/datasets/archive/car_fhd --measure_time=True --batch_size=1
middle_class_name PointPillarsScatter
remain number of infos: 3769
Generate output labels...
/mnt/d/MyDocuments/cache/second.pytorch/second/../second/pytorch/models/voxelnet.py:786: UserWarning: indexing with dtype torch.uint8 is now deprecated, please use a dtype torch.bool instead. (Triggered internally at  /pytorch/aten/src/ATen/native/IndexingUtils.h:25.)
  box_preds = box_preds[a_mask]
/mnt/d/MyDocuments/cache/second.pytorch/second/../second/pytorch/models/voxelnet.py:787: UserWarning: indexing with dtype torch.uint8 is now deprecated, please use a dtype torch.bool instead. (Triggered internally at  /pytorch/aten/src/ATen/native/IndexingUtils.h:25.)
  cls_preds = cls_preds[a_mask]
/mnt/d/MyDocuments/cache/second.pytorch/second/../second/pytorch/models/voxelnet.py:790: UserWarning: indexing with dtype torch.uint8 is now deprecated, please use a dtype torch.bool instead. (Triggered internally at  /pytorch/aten/src/ATen/native/IndexingUtils.h:25.)
  dir_preds = dir_preds[a_mask]
[100.0%][===================>][0.70it/s][44:38>00:01]
generate label finished(1.41/s). start eval:
avg forward time per example: 0.679
avg postprocess time per example: 0.012
Car [email protected], 0.70, 0.70:
bbox AP:0.00, 0.00, 0.00
Car [email protected], 0.50, 0.50:
bbox AP:0.00, 0.00, 0.00

资源占用情况
在这里插入图片描述

9. Kitti Viewer Web

详细步骤,请参考:second.pytorch

# 运行server
python ./kittiviewer/backend/main.py main --port=xxxx

# 启动本地web server
cd ./kittiviewer/frontend && python -m http.server

# 浏览器打开
http://127.0.0.1:8000

七、FAQ

Q:加载.h5模型错误

Traceback (most recent call last):
  File "point_pillars_training_run.py", line 112, in <module>
    pillar_net.load_weights(os.path.join(MODEL_ROOT, "model.h5"))
  File "/home/yoyo/miniconda3/envs/ppillar/lib/python3.8/site-packages/keras/utils/traceback_utils.py", line 70, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "/home/yoyo/miniconda3/envs/ppillar/lib/python3.8/site-packages/keras/saving/hdf5_format.py", line 835, in load_weights_from_hdf5_group
    raise ValueError(
ValueError: Weight count mismatch for layer #2 (named cnn/block1/conv2d0 in the current model, cnn/block1/conv2d0 in the save file). Layer expects 1 weight(s). Received 2 saved weight(s)
解决办法:
加载模型
pillar_net.load_weights(os.path.join(MODEL_ROOT, "model.h5"))
改为
pillar_net.load_weights(os.path.join(MODEL_ROOT, "model.h5"),by_name=True , skip_mismatch=True)

Q:module 'cffi' has no attribute 'FFI'

  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/numba/core/typing/context.py", line 158, in refresh
    self.load_additional_registries()
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/numba/core/typing/context.py", line 701, in load_additional_registries
    from . import (
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/numba/core/typing/cffi_utils.py", line 19, in <module>
    ffi = cffi.FFI()
AttributeError: module 'cffi' has no attribute 'FFI'
错误原因:
pip显示已安装cffi,但是在Anaconda环境中找不到

解决办法:
在Anaconda中安装cffi
conda install cffi

如果安装没有权限
conda install -c local cffi

Q:module.__version__版本检查错误

  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/matplotlib/__init__.py", line 201, in _check_versions
    if LooseVersion(module.__version__) < minver:
AttributeError: module 'kiwisolver' has no attribute '__version__'
错误原因:
在matplotlib版本检查过程中,由于kiwisolver没有 `__version__` 这个属性而报错

解决办法:
注释版本检查的代码

在这里插入图片描述
在这里插入图片描述
其他类似的问题,操作一致

Traceback (most recent call last):
...
...
...
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/torch/utils/tensorboard/__init__.py", line 4, in <module>
    LooseVersion = distutils.version.LooseVersion
AttributeError: module 'distutils' has no attribute 'version'
Traceback (most recent call last):
...
...
...
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/matplotlib/__init__.py", line 209, in _check_versions
    if parse_version(module.__version__) < parse_version(minver):
AttributeError: module 'dateutil' has no attribute '__version__'

Q:imp包被弃用

./pytorch/train.py:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
错误原因:
DeprecationWarning:已弃用imp模块,改用importlib;有关其他用途,请参阅该模块的文档

解决办法:
由于imp包暂未使用,注释掉即可
# import imp

Q:ModuleNotFoundError: No module named 'torchplus.tools'

Traceback (most recent call last):
  File "./pytorch/train.py", line 21, in <module>
    from second.pytorch.builder import (box_coder_builder, input_reader_builder, 
 ...
 ...
 ...
"/mnt/d/MyDocuments/cache/second.pytorch/second/../second/pytorch/core/box_torch_ops.py", line 10, in <module>
    from torchplus.tools import torch_to_np_dtype
ModuleNotFoundError: No module named 'torchplus.tools'
错误原因:
torchplus版本太低

解决办法:
卸载并安装新版本
pip uninstall torchplus
pip install torchplus

Q:ImportError: cannot import name 'BeautifulSoup' from 'bs4'

Traceback (most recent call last):
  File "./pytorch/train.py", line 14, in <module>
    import torchplus
....
...
...
File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/notion/utils.py", line 4, in <module>
    from bs4 import BeautifulSoup
ImportError: cannot import name 'BeautifulSoup' from 'bs4' (/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/bs4/__init__.py)
错误原因:
beautifulsoup4版本太低

解决办法:
卸载并安装新版本
pip uninstall beautifulsoup4
pip install beautifulsoup4

Q:ImportError: 'pyctlib.watch.debugger' cannot be used without dependency 'line_profiler'.

Traceback (most recent call last):
  File "./pytorch/train.py", line 14, in <module>
    import torchplus
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/torchplus/__init__.py", line 33, in <module>
    from .tensor import *
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/torchplus/tensor.py", line 33, in <module>
    from pyctlib.visual.debugger import profile
  File "/home/yoyo/miniconda3/envs/ppillar-torch/lib/python3.8/site-packages/pyctlib/visual/debugger.py", line 18, in <module>
    raise ImportError("'pyctlib.watch.debugger' cannot be used without dependency 'line_profiler'. ")
ImportError: 'pyctlib.watch.debugger' cannot be used without dependency 'line_profiler'.
错误原因:
缺少line-profiler包

解决办法:
安装line-profiler
pip install line-profiler

猜你喜欢

转载自blog.csdn.net/m0_37605642/article/details/128987547
今日推荐