一、参考资料
原始论文:PointPillars: Fast Encoders for Object Detection from Point Clouds
pointpillars-fast-encoders-for-object
PointPillars - gitbook_docs
使用 NVIDIA CUDA-Pointpillars 检测点云中的对象
3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现
模型部署入门教程(三):PyTorch 转 ONNX 详解
PointPillar代码解析-OpenPCDet
pointpillars deployment 学习
模型部署——pointpillars转一个onnx
二、相关介绍
1. 3D目标检测
在自动驾驶领域,基于Lidar的3D目标检测,就是从3D点云数据中定位到目标框和类别。具体地,输入是点云 X ∈ R N × c X∈R^{N×c} X∈RN×c (一般 c=4 ),输出是 n 个检测框bboxes, 以第 i 个检测框bbox为例, 它包括位姿信息 ( x i , y i , z i , w i , l i , h i , θ i ) (x_i,y_i,z_i,w_i,l_i,h_i,θ_i) (xi,yi,zi,wi,li,hi,θi) 和 类别信息 ( l a b e l i , s c o r e i ) (label_i,score_i) (labeli,scorei)。
基于Lidar的3D目标检测模型有:Point-based,Voxel-based,Point-Voxel-based,Multi-view-based。
1.1 Point-based
基于 Point-based 的模型,直接对点云进行处理,可以减少位置信息的损失,但同时也带来了巨大的计算资源消耗,使其很难做到实时性。
经典模型:
1.2 Voxel-based
基于 Voxel-base 的模型,相较于 Point-base 的模型在推理速度上有所提升,但是由于模型中使用了三维卷积的 backbone,所以也仍然很难做到实时性。
相较于其他的模型,PointPillars 在推理速度方面有着明显的优势(遥遥领先),同时又能保持着不错的准确性。
经典模型:
- PointPillars(CVPR19);
- CenterPoint(CVPR21)。
1.3 Point-Voxel-based
经典模型:
1.4 Multi-view-based
经典模型:
- PIXOR(CVPR18)。
2. voxelization(体素化)
2.1 简介
在实际使用过程中,我们希望模型又快又准,所以为了权衡速度和精度,VoxelNet 提出使用 voxelization(体素化)方法来处理点云。
点云是三维空间中的物体表示,因此一个自然的思路是将空间在长宽高三个方向划分格子,每个格子称为 voxel(体素)。将其转换为 3 维数组的形式,再使用 3D 卷积和 2D 卷积对网络进行处理,如下图所示:
2.2 体素化存在的问题
体素化也会带来一些问题,例如不可避免的造成一些信息丢失,对体素参数较为敏感,以及转换成 3 维数组后提取特征时通常需要用到 3D卷积,而3D卷积是一个相当耗时的操作。所以,如果体素化的粒度设置过大时将导致丢失较多信息,如果体素化的粒度设置过小将导致计算时间几何增加。
2.3 改进体素化
PointPillars 在 VoxelNet 中的 voxel 基础上提出了一种改进版本的点云表征方法 pillar,可以将点云转换成伪图像的形式,进而通过 2D 卷积实现目标检测。相较于 VoxelNet 将点云转换成 voxel 形式然后使用相当耗时的 3D卷积来处理特征,PointPillars 这种使用 2D卷积的网络在推理速度上有很大的优势。
什么是 pillar?原文中的描述是“ a pillar is a voxel with unlimited spatial extent in the z direction ”,其实很简单,将空间的 x,y 轴两个方向上划分格子,然后再将每个格子在 z 轴上拉伸,使其可以覆盖整个z 轴空间,就可以得到一个 pillar,且空间中的每个点都可以划分到某个 pillar 中。
3. 点云配准
4. 相机标定
5. 体素特征编码层(VFE)
VFE(Voxel Feature Encode),体素特征编码层,其实是简化版的pointnet。
三、PointPillar相关介绍
【模型加速】PointPillars模型TensorRT加速实验(1)
【模型加速】PointPillars模型TensorRT加速实验(2)
【模型加速】PointPillars模型TensorRT加速实验(3)
1. 问题引入(背景)
3D卷积太昂贵了,不适合边缘设备部署。
VoxelNet 直接采用体素3D卷积,SECOND采用稀疏卷积,pointpillars采用pillar方式转换成2D卷积以加深网络,从而提高效率与精度。
PointPillar延续了VoxelNet和SECOND的思路,VoxelNet的方法是直接用体素做3D卷积,SECOND用了稀疏卷积,而PointPillar使用了pillar的方式,直接转成2D卷积进行加速。
2. PointPillar创新点
PointPillars 是一个既简单又实用的模型,在保持较高精度的同时又有很高的推理速度,同时部署也很友好,是一个十分常用的3D目标检测模型。
- 提出了一种新的点云编码器和新网络pointpillar,实现对3D目标检测网络的端到端训练;
- 将三维点云处理为二维伪图像,用传统CNN对伪图像进行特征提取,推理速度显著提升,是其他方法(含3维卷积)的2-4倍。
3. PointPillar网络结构
整个算法逻辑包含3个部分:数据预处理,神经网络,后处理。其中神经网络部分,原论文将其结构描述为3个部分:
- PFN(Pillar Feature Net):将输入的点云转换为稀疏的伪图像的特征形式。
- Backbone(2D CNN):使用 2D 的 CNN 处理伪图像特征,得到高维度的特征。
- Detection Head(SSD):检测和回归 3D 边界框。
在实际部署的时候,结构拆分和论文中的稍微有些出入。主要是分成PFN,MFN和RPN。其中MFN是用来将PFN提取的Pillar级的点云深度特征进一步转化为伪点云图像。RPN是Backbone,而检测头的部分功能被包含在后处理的逻辑里面。
4. PFN模块
4.1 PFN结构
PFN(Pillar Feature Network)结构如下图所示。
PFN模块的工作原理:
- 首先将一个样本的点云空间划分成(在 X 轴方向上点云空间的范围 / pillar size,在 Y 轴方向上点云空间的范围 / pillar size)pillar 网格,样本中的点会被包含在各个 pillar 中,没有点的 pillar 则视为空 pillar。
- 假设样本中包含的非空 pillar 数量为 P,同时限制每个 pillar 中的点的最大数量为 N,如果一个 pillar 中点的数量不及 N,则用 0 补全,若超过 N,则从 pillar 内的点中采样 N 个点。然后对 pillar 中的每个点进行编码,每个点的表示包括:点云坐标,点云(反射)强度,每个Pillar包含的实际点云数量,pillar 的几何中心,点与 pillar 几何中心的相对位置。将每个点表示的长度记为 D,这样一个点云样本就可以用一个(P,N,D)的张量来表示。
- 得到点云的 pillar 表示的张量后,我们对其进行特征提取。通过使用简化版的 PointNet 中的 SA 模块来处理每个 pillar,即先对每个 pillar 中的点使用多层 MLP 来使得每个点的维度从 D 变成 C,这样张量变成了(P,N,C),然后对每个 pillar 中的点使用 Max Pooling,得到每个 pillar 的特征向量,也使得张量中的 N 的维度消失,得到了(P,C)维度的特征图。
- 最后将(P,C)的特征根据 pillar 的位置展开成伪图像特征,将 P 展开为(H,W)。这样就得到了类似图像的(C,H,W)形式的特征表示。
shape变化:(P,N,D) -> (P,N,C) -> (P,C) -> (C,H,W)。
4.2 PFN的输入
PFN有8个输入:
pillar_x
:包含Pillar化后的点云x坐标,shape为(1,1,P,100);pillar_y
:包含Pillar化后的点云y坐标,shape为(1,1,P,100);pillar_z
:包含Pillar化后的点云z坐标, shape为(1,1,P,100);pillar_i
:包含Pillar化后的点云强度值,shape为(1,1,P,100);num_points
:保存每个Pillar包含的实际点云数量,shape为(1,P);x_sub_shaped
:保存Pillar的中心x坐标,shape为(1,1,P,100);y_sub_shaped
:保存Pillar的中心y坐标,shape为(1,1,P,100);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.3 PFN的输出
PFN的输出shape为(1,64,pillar_num,1),pillar_num表示非空pillar的数量。因为不同点云帧的点云数量是变化的,非空Pillar的数量自然也是不同的,在考虑将PFN导出为ONNX模型时,需要采用dynamic shape。
从PFN的8个输入可知,num_points
表示每个Pillar包含的实际点云数量是dynamic的。
5. MFN模块
MFN(Middle Feature Extractor Network)是用来将PFN提取的Pillar级的点云深度特征进一步转化为伪点云图像。
5.1 MFN的输入
MFN有2个输入,且都是dynamic shape:
voxel_features
:voxel_features是PFN的输出,经过PFN后每个非空pillar表示为一个64维的深度特征,其shape为(1,64,P,1),P表示pillar的数量,是dynamic shape;coords
:pillar在x-y网格中的坐标,shape为(P,4),P表示pillar的数量,是dynamic shape;
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 的几何中心,点与 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}
X∈RN×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
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. 下载预训练模型
4. 下载KITTI数据集
- KITTI数据集论文: Are we ready for autonomous driving? the kitti vision benchmark suite [CVPR 2012] 和 Vision meets robotics: The kitti dataset [IJRR 2013]
- KITTI数据集下载(下载前需要登录): point cloud(velodyne, 29GB), images(image_2, 12 GB), calibration files(calib, 16 MB)和labels(label_2, 5 MB)。数据velodyne, calib 和 label_2的读取详见
utils/io.py
。
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官网需要翻墙,且访问速度慢。网上有很多百度网盘链接,推荐用网盘下载。
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 AP@0.70, 0.70, 0.70:
bbox AP:0.00, 0.00, 0.00
Car AP@0.70, 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
六、相关项目
MMDeploy + MMDetection3D + PointPillars
打通部署全流程 | 快速部署 3D 目标检测模型 PointPillars
OpenVINO + PointPillars
Optimization of PointPillars by using Intel® Distribution of OpenVINO™ Toolkit
七、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