3D cloud point detection
data
点云数据的维度是(N * 4),N是point数量,每帧点云的N可能是变化的,4是xyz和反射率
dataset中bbox一般用7个数表示(x,y,z,w,l,h,theta)
pointNet
pointNet是用于点云分类或分割任务的网络。
点云与图像最大的区别是图像是结构化的数据,而点云的点是非结构化的无序的数据点,换言之点云的点的顺序是可以随意变换的。因此本文的思想是,找到一种对称函数,对点云顺序不敏感,无论点云顺序如何,经过对称函数映射后结果趋向统一,而不能影响最终的结果。
无序输入的对称函数 (MLP)
为了使模型对输入排列保持不变,存在三种策略:1)将输入排序为规范顺序; 2)将输入视为一个序列来训练RNN,但通过各种排列来扩充训练数据; 3)使用一个简单的对称函数来聚合来自每个点的信息。这里,一个对称函数将 n 个向量作为输入,并输出一个对输入顺序不变的新向量。
虽然排序听起来像是一个简单的解决方案,但在高维空间中实际上不存在稳定的排序。因此,排序并不能完全解决排序问题,并且随着排序问题的持续存在,网络很难学习从输入到输出的一致映射。
使用 RNN 的想法将点集视为顺序信号,并希望通过使用随机排列的序列训练 RNN,RNN 将变得对输入顺序保持不变。然而,在“OrderMatters”中,作者已经表明顺序确实很重要,不能完全省略。虽然 RNN 对小长度(几十个)序列的输入排序具有相对较好的鲁棒性,但很难扩展到数千个输入元素,这是点集的常见大小。根据经验,我们还表明基于 RNN 的模型的性能不如我们提出的方法。
我们的想法是在点集的元素上,用对称函数近似一般函数:f ({x 1 , . . . , x n }) ≈ g(h(x 1 ), . . . , h(x n )) g是对称函数。根据经验,我们的基本模块非常简单:通过多层感知器网络MPL近似 h(x),通过组合一个单变量函数h(x)的和一个最大池化函数近似 g(x) 。通过实验发现这很有效。通过一个 组h(x) ,我们可以学习到多个f(x)来捕捉集合的不同特征。虽然我们的关键模块看起来很简单,但它具有有趣的属性,并且可以在一些不同的应用程序中实现强大的性能。
注意上面的MLP方向,如上图第一个MLP,3 → 64,这个相当于三个点的全连接,仅关联点内不同的数值!
如果是分类任务,可以直接拿上图的1024特征进行分类了!
联合校准网络(T-net)
在后续的改进算法中,这个结构被证明不太重要,此处省略介绍
分割任务
分割需要二维特征,因此分割把1024特征复制N份以后,再拼接上原来的N * 64维特征(左边的MLP输出),变为N * 1088的维度,这样该特征中既包含了每个点的独特的特征,也包含了全局信息。
完整结构如下,重点看如下的分割分支,经过两次MLP后获得m * n的特征图,用于分割任务,n就是输入点数,然后就可以奇回归得到每个点所属的类别了。
参考易懂视频: pointNet讲解参考
VoxelNet
利用了稀疏点云的结构特性,直接在稀疏的3D点上进行操作,并通过高效的并行处理体素网格来获得性能的提升。
将点云数据量化到一个个Voxel里,常见的有VoxelNet 和SECOND
voxel Stacked Voxel Feature Encoding
输入 T * N * 7 , T:非空体素数量,N每个体素内的点数(大于N的随机抽样N个,小于N的补零),7是点云点的维度,四个数加相对偏移
最后输出 T * C,然后映射到空间位置,获得稀疏的向量 C D H W
结构,多个VFE(下图)提取特征,最后经过一个全连接,然后映射C D H W
具体操作是这样的,首先将点云数据通过一个全连接层,得到每个点对应的特征(Point-wise Feature),得到的特征利用Element-wise maxpool得到局部聚合特征(Locally Aggregated Feature),局部聚合特征和每个点的特征拼接起来,作为下一个VFE层的输入。代码如下(其中FCN为全连接层):
# Voxel Feature Encoding layer
class VFE(nn.Module):
def __init__(self,cin,cout):
super(VFE, self).__init__()
assert cout % 2 == 0
self.units = cout // 2
self.fcn = FCN(cin,self.units)
def forward(self, x, mask):
# point-wise feauture
pwf = self.fcn(x)
#locally aggregated feature
laf = torch.max(pwf,1)[0].unsqueeze(1).repeat(1,cfg.T,1)
# point-wise concat feature
pwcf = torch.cat((pwf,laf),dim=2)
# apply mask
mask = mask.unsqueeze(2).repeat(1, 1, self.units * 2)
pwcf = pwcf * mask.float()
return pwcf
经过计算后,需要将原来空点的特征置0(体素内点数不够补零的情况),这样就不会影响后续计算,也即上述代码中的mask参数的作用。
从上图和上述代码可以看到VFE的输入是T * N * Cin,输出是T * N * Cout,VFE重复多次,第一次的时候,Cin=7,最后一层时,经过全连接,最终的维度变为,T * C,这样就可以根据体素位置映射为C D H W
VFE设计分析:上图中voxel中有三个点(3*7),经过全连接后变成3 * C,这里是点内特征融合(仍然对应上图的三种颜色),经过一个池化,repeat,拼接后,这样就相当于进行了一次点之间的特征融合。
Convolutional Middle Layers
经过FLN之后,输出变为128×10×400×352(128代表特征维度,后面是voxel的个数)。
用ConvMD 代表一个M维的卷积层。三维的时候,k=(k,k,k)卷积核是3维的
经过Conv3D(128,64,3,(2,1,1),(1,1,1)),Conv3D(64,64,3,(1,1,1),(0,1,1)),Conv3D(64,64,3,(2,1,1),(1,1,1))之后,输出变为64×2×400×352,经过reshape,变为128×400×352。所以输入RPN的feature map的大小就是128×400×352.
RPN
RPN网路的输入是通过 Convolutional Middle Layers出来的特征图,3个全卷积的block,每次都是下采样,如下图所示,将后两个上采样,然后与第一个block拼接,将得到的拼接特征用于目标回归。此处RPN类似FPN的作用。
SECOND
SECOND用到了稀疏卷积
卷积本质上是矩阵运算,稀疏卷积就是稀疏的矩阵运算,如何对稀疏的矩阵进行高效的运算,一种方法就是把稀疏的矩阵中有效的数字提取出来,把稀疏的大矩阵变成稠密的小矩阵再进行运算,大小之间如何转换,方法是哈希映射。
稀疏卷积,讲的真的不错:稀疏卷积通俗理解
pointPillars
PointPillars的最大贡献是在VoxelNet中Voxel的基础上提出了一种改进版本的点云表征方法Pillar,可以将点云转换成伪图像,进而通过2D卷积实现目标检测。PointPillars整个网络结构分为三个部分:最大的创新点是PFN部分
- Pillar Feature Net:将输入的点云转换为稀疏的Pseudo image
- Backbone:处理Pseudo image得到高层的特征
- Detection Head:检测和回归3D框
Point Feature Net
输入的尺度是如何变化的
P * N * D(30000 x 20 x 9)->P * N * C(30000 x 20 x 64)-> P * C(30000 * 64)->H * W * C(512 * 512 * 64)
其中P代表选取的Pillar数量,N是每个Pillar存储的最大点云数量,D是点云的属性,P和N都是超参数,需要根据激光雷达的线束和Pillar中点云数量设置,论文中使用取P=30000,N=20。如果某个Pillar中的点云数量大于20个,则多余的丢弃,若少于20个,则用0 padding补充。
最后一步转换P * C(30000 * 64)->H * W * C(512 * 512 * 64),也叫point scatter,把pillar平铺到bev图上。具体细节再看!
Stacked Pillars是如何变成Learned Features的?
上述的转换过程非常简单,可以表述为PND(30000 x 20 x 9)->PNC(30000 x 20 x 64)-> P*C(30000 * 64),对应的处理流程可参考代码如下,先是经过一个Linear+BN+ReLU,然后通过MaxPooling操作将每个Pillar中最大响应的点云提取出来。上述中的Linear+BN+ReLU可以堆叠,论文使用了最简单的方法
Learned Features 是如何变化为Pseudo Images? point scatter
是通过Scatter运算实现的。在PointPillars网络结构图中从Point cloud构造Stacked Pillars的过程中,记录了每个Pillar对应的x,y坐标,也就是上图中的Pillar Index,其维度是P*2,P是Pillar的数量,2是x和y对应的坐标。在Learned Features构造Pseudo Images的时候根据Pillar Index,将Pillar填充到对应的Pseudo Images上。
这一步的方法比较简单,就是通过每个点的Pillar索引值将上一步生成的(C,P)张量转换回其原始的Pillar坐标用来创建大小为(C,H,W)的伪图像。这里需要解释一下伪图像的高度H和宽度W是怎么来的:在第一步对点云进行Pillar划分的时候会设置XY平面上点云坐标的范围和每个Pillar的大小,假设X轴的范围是[0,69.12],Y轴的范围是[-39.68,39.68],每个Pillar的大小是``0.16x0.16,那么以X轴表示宽,Y轴表示高,一个Pillar表示一个像素的话,那么这个伪图像的宽W = (69.12 - 0) / 0.16 = 432,高H = (39.68 -(-39.68)) / 0.16 = 496。如果有落到同一个pillar的多个点怎么办?均值吗?需再研究
点云属性为啥是9
centerpoint
centerpoint是基于heatmap的3d检测算法,前面的backbone可以使用voxelNet或pointpillar等,heat map后连接检测head,分别回归目标中心点、长宽高、角度(只有水平方向上的yaw角)、以及速度。稍微有点特殊之处主要有两方面
- 一个是在普通的heat map head上,又添加了一个二阶段检测结构
- 回归目标的速度,用来做目标跟踪
二阶段
二阶段金回归目标的置信度和3d box(中心点和长宽高,待确认),二阶段的实现方式比较交单,一阶段有了每个目标框的具体位置,二阶段根据目标框的边框中心点位置(如上图,每个框的四个点),到backbone的最后一层的对应位置提取相应位置的特征,四个点的位置是精细化的,backbone最后一层特征是特征点格式,因此使用了双线性插值的方法,取出特征送入二阶段网络。
二阶段具体实现如下,每帧点云图像取N个目标进行二阶段回归,N固定。拿到特征后,进行了一次reshape(代码有注释),目标数N reshape到batchsize里,这样做的好处有二,第一实现较简单,第二所有目标(不同帧的目标以及相同帧的不同目标)共享参数,减小了参数量。
def forward(self, batch_dict, training=True):
"""
:param input_data: input dict
:return:
"""
batch_dict['batch_size'] = len(batch_dict['rois'])
if training:
targets_dict = self.assign_targets(batch_dict)
batch_dict['rois'] = targets_dict['rois']
batch_dict['roi_labels'] = targets_dict['roi_labels']
batch_dict['roi_features'] = targets_dict['roi_features']
batch_dict['roi_scores'] = targets_dict['roi_scores']
# RoI aware pooling
if self.add_box_param:
batch_dict['roi_features'] = torch.cat([batch_dict['roi_features'], batch_dict['rois'], batch_dict['roi_scores'].unsqueeze(-1)], dim=-1)
# reshape,将目标数N reshape到batchsize里,这样后续实现比较简单,另外所有目标共享参数,减小参数量
pooled_features = batch_dict['roi_features'].reshape(-1, 1,
batch_dict['roi_features'].shape[-1]).contiguous() # (BxN, 1, C)
batch_size_rcnn = pooled_features.shape[0]
pooled_features = pooled_features.permute(0, 2, 1).contiguous() # (BxN, C, 1)
shared_features = self.shared_fc_layer(pooled_features.view(batch_size_rcnn, -1, 1))
rcnn_cls = self.cls_layers(shared_features).transpose(1, 2).contiguous().squeeze(dim=1) # (B, 1 or 2)
rcnn_reg = self.reg_layers(shared_features).transpose(1, 2).contiguous().squeeze(dim=1) # (B, C)
centerPoint的跟踪任务
head回归目标在x轴和y轴的方向上的速度,用来辅助跟踪任务。
- 如何回归速度,训练的时候,不仅输入当前帧的点云,还有前一帧的点云以及他们之间的时间差,学习两帧点云的差异,加上时间差,这样就可以学习目标的平均速度
- 有了速度如何跟踪,推理的时候,同样输入上一帧点云和时间差,这样网络输出了每个目标的速度、位置等,根据目标的位置、速度以及时间差,可以反推目标在上帧点云的位置,然后再和上一帧的目标进行匹配(贪心算法),完整跟踪。
point transformer
point transformer和上面介绍的pointNet属于一个流派,基于点对点云进行处理,这种结构仅能用于分类和分割任务,不能用于目标检测。
自注意力机制适合处理点云的原因:
- 排列不变性:3D点云是无序的,即点的顺序不应影响最终的处理结果。自注意力机制天然具有排列不变性,因为它通过对所有点对的关系进行建模来处理输入,而不依赖于任何特定的输入顺序。
- 捕捉全局上下文:点云数据通常覆盖了3D空间中的对象或场景,理解这些数据需要捕捉点之间复杂的空间关系。自注意力机制能够有效地捕捉这些关系,因为它为每对点赋予一个注意力权重,这反映了它们之间的相对重要性。
Point Transformer Layer
点云中每个点都和周围最近的N个点进行attention,这里的self attention和之前了解的不太一样?
Χ(i)是与x_i临近的N个点的集合,也就是要与临近的N个点进行特征融合,上式中的几个函数主要是mlp或linear,如下图所示。
整体结构
结构如下图,可以进行分类或分割,上图分割,下图分类。
分割模型,整体类似于unet的对称结构,先上采样,然后进行下采样,与cnn不同,这里上下采样所采用的结构分别是transition up和transition down。
point transformer v2
也是点云分割网络
lidar-rcnn
二阶段检测网络,可以后接在任何检测网络后。以pointNet作为主干网络。它的主干部分由5个mlp和一个max-pooling组成,不论是参数量还是计算量都非常小。
类似于Faster RCNN的ROI Pooling,我们直接去原始点云上抠取proposal内的点云xyz,并将其旋转平移至proposal坐标系(图2),之后将它们输入进PointNet,由分类和回归两个loss来监督网络的训练。值得一提的是一个3D框是有7个自由度的(框中心xyz、长宽高、角度),所以我们的回归目标也是7个变量。我们二阶段网络的输入是原始点云,并不依赖于一阶段所训练的特征,所以它可以很灵活地适用于各种3D检测器上。
代码调试
model输入
输入子点云和pre_bbox,实际上pre_bbox在网络中没有使用,上面的模型图可以看出实际只有1个输入
子点云pts(256 12 1024)
pre_bbox(256, 7)
model输出
分类logits(256, 2),如果是多分类,2替换为类别加1,以及centers(3个数)、sizes(3个数)、heading(角度,1个数)
datasets
datasets输入:proposal、gt(与proposalIOU最大或最近)、点云全图
点云crop是在data_processor数据预处理脚本中进行的,调用extract_points(),默认外扩三米。
crop获得子点云后,还会添加上frame_idx(时序)信息,如果没有时序,无需进行该处理。
该脚本还会为每个一阶段预测proposal匹配一个gt_box,通过iou匹配,生成正负样本标签。
datasets里会根据IOU再进行relabel,生成二阶段的类比标签,例如如果只有一类,标签只有0 1,表示正负样本。
datesets点云处理(process_pcd):还会根据proposal坐标,把点云坐标平移到proposal中心为圆点,只平移x/y坐标。还会根据proposal角度旋转点云,这样二阶段只需要预测角度的差值(gt和一阶段proposal角度的差值)即可。此外还会计算点云点到proposal六个平面的距离,拼接到点云后面,组成12维数据。