文章目录

  • 前言(Related work)
  • 一、PointNet++(分类+分割2018)
    • 1.关键代码
      • 1.点云采样
      • 2.卷积下采样(升维)
      • 3.上采样:self.fp4(l3_xyz, l4_xyz, l3_points, l4_points)
  • 二、MVF(动态体素融合2019)
    • 1.动态体素
    • 2.特征融合网络结构
    • 3.损失函数
  • 三、Point RCNN(检测 CVPR2019)
  • 四、深度估计的雷达成像(检测 CVPR2019)
  • 五、RandLA-Net(分割 2019)
    • 一、简介
    • 二、取样
    • 三、局部特征聚合
    • 四、补充与实验
  • 六、3DBoNet(检测+实例分割 NeurIPS2019)
    • 1.核心思想
    • 2.主要算法
  • 七、 PointGroup (实例分割 CVPR 2020)
    • 1.摘要
    • 2.架构
    • 4. Clustering Algorithm
    • 5.ScoreNet
  • 八、Point Transformer(分割 2020)
    • 1. Point Transformer Layer
    • 2.网络框架
  • 九、HAIS(pointgroup扩展:ICCV2021 )
    • 2.架构
  • 十、SqueezeSeg V3(分割 2021oral)
    • 一、投影公式
    • 二、SAC卷积
    • 三、损失函数
    • 四、重点代码
  • 十一、LiDAR Panoptic Segmentation(全景分割 2021)
  • 十二、IA-SSD目标检测(CVPR 2022)
    • 1.摘要
    • 2、相关工作(综述)
      • 1.基于Voxel方法
      • 2.基于Point方法
      • 3.Point-Voxel方法
    • 3.创新点
      • 1.实例感知下采样策略
      • 2.上下文实例质心感知
    • 4.实验细节
  • 十三、 SoftGroup (CVPR 2022)
    • 1.摘要
    • 2.设计理念
    • 3. Method
    • 4.Top-Down Refinement
    • 5 .代码解读
  • 十四、ContrastBoundary(CVPR2022)
    • 1、背景
    • 2、核心思想
    • 主要代码
      • 1.数据预处理
      • 2、model architecture
  • 十五、RepSurf(CVPR2022 Oral)
    • 1.点云提取方法
    • 2.伪代码
  • #、常用点云分割数据集
  • 总结

前言(Related work)

总结几种不同方法:

3D点云的深度学习
点云表示是3D场景理解的常用数据格式。为了处理点云,早期的方法[2,3,36,37]根据点的统计属性提取手工制作的特征。最近的深度学习方法学习从点中提取特征。基于PointNet的方法[pointnet]提出通过共享的多层感知器(MLP)处理点,然后从对称函数(例如最大池化)中聚合区域和全局特征。卷积方法被积极探索用于点云处理。连续卷积方法[23、40、44、45]学习与局部点的空间分布相关的内核。离散卷积方法[5,8,13,19,25,34]学习从点量化获得的规则网格的内核。 Transformers[18, 50]和基于图的方法[38, 39, 43]也被提出来解决点云的数据不规则性。

基于Proposal的实例分割
基于proposal的方法考虑了一种自上而下的策略,该策略生成区域proposal,然后在每个proposal中分割目标。现有的基于proposal的3D点云方法很大程度上受到Mask-R CNN用于2D图像的成功的影响。为了处理点云的数据不规则性,Li等人[47]提出了GSPN,它采用综合分析策略来生成高目标3D proposal,并由基于区域的PointNet进行细化。Hou等人[12]提出了3DSIS,它结合了多视图RGB输入和3D几何来预测边界框和实例mask。Yang等人[46]提出了3D-BoNet,它直接输出一组边界框,无需生成anchor和非极大值抑制,然后通过逐点二元分类器对目标进行分割。Liu等人[22]提出GICN将每个目标的实例中心近似为高斯分布,对其进行采样以获得目标候选,然后生成相应的边界框和实例mask。

基于分组的实例分割
基于分组的方法依赖于自下而上的pipeline,该pipeline产生逐点预测(例如语义图、几何位移或潜在特征),然后将点分组到实例中。Wang等人[41]提出SGPN来为所有点构建特征相似性矩阵,然后将具有相似特征的点分组为实例。Pham等人[29]提出了JSIS3D,它通过多值条件随机场模型合并语义和实例标签,并联合优化标签以获得目标实例。Lahoud等人[17]提出了MTML来学习特征和方向嵌入,然后在特征嵌入上执行mean-shift聚类以生成object segments,这些object segments根据它们的方向特征一致性来评分。Han等人[9]介绍了OccuSeg,它执行由目标占用信号引导的基于图形的聚类,以获得更准确的分割输出。Zhang等人[48]考虑了一种概率方法,将每个点表示为三变量正态分布,然后进行聚类步骤以获得目标实例。Jiang等人[15]提出了点群算法来分割原始点集和偏移点集上的目标,该算法简单而有效,可将具有相同标签的邻近点进行分组,并逐步扩展该组。Chen等人[4]扩展了PointGroup并提出了HAIS,它进一步吸收实例的周围片段,然后基于实例内预测来细化实例。Liang等人[20]SSTNet从预先计算的超级点构建树网络,然后遍历树并分割节点以获得目标实例。

常见的基于proposal和基于分组的方法各有优缺点。基于proposal的方法独立处理每个目标proposal,不受其他实例的干扰。基于分组的方法无需生成proposal即可处理整个场景,从而实现快速推理。然而,基于proposal的方法难以生成高质量的proposal,因为该点仅存在于目标表面上。基于分组的方法高度依赖于语义分割,使得语义预测中的错误传播到实例预测中


提示:以下是本篇文章正文内容

一、PointNet++(分类+分割2018)

VoxelNet:(苹果2017)
VoxelNet: End-to-End Learning for Point Cloud Based 3D Object Detection

PointPillar:(2019)

  1. 点云到伪图像的转换
  2. 2D backbone 网络学习高层次表征
  3. 检测头进行 3D Box 的检测和回归


Set abstraction 包括 sampling,grouping 和PointNet三部分:

1)sampling:对输入点云进行采样,只保留部分点进入下一层网络。采样数一般是输入点云总数量的一半,

采样算法是Farthest point sampliing (FPS),以保证采样点均匀分布在整个点云集上 。

2)grouping:为每个采样点寻找半径r(r=0.2)范围内的固定k(k=32)个邻域点,所有点坐标都是归一化后的。

3)PointNet:对这些点用PointNet(MLP)提取特征并max pooling 聚合为采样点坐标。

1.关键代码

1.点云采样

1.new_xyz, new_points = sample_and_group(self.npoint, self.radius, self.nsample, xyz, points)

输入: 1024 0.1 32 (8,4096,3) (8,4096,9) -> 输出: ( 8,1024,3 ) ( 8,1024,32,3+9 )

def sample_and_group(npoint, radius, nsample, xyz, points, returnfps=False):"""Input:npoint 1024:radius 0.1:nsample 32:xyz: input points position data, [B, N, 3]  (8,4096,3)points: input points data, [B, N, D]   (8,4096,9)Return:new_xyz: sampled points position data, [B, npoint, nsample, 3]new_points: sampled points data, [B, npoint, nsample, 3+D]"""B, N, C = xyz.shape           # 8, 4096, 3S = npoint                            # 1024fps_idx = farthest_point_sample(xyz, npoint)        # [B=8, 1024, C=1]  最远点采样new_xyz = index_points(xyz, fps_idx)                        # ( 8,1024,3 )idx = query_ball_point(radius, nsample, xyz, new_xyz)        # ( 8,1024,32 )  采样点附近选32个点grouped_xyz = index_points(xyz, idx) # [B, npoint, nsample, C]         (8,4096,3) -- > ( 8,1024,32, 3 ) grouped_xyz_norm = grouped_xyz - new_xyz.view(B, S, 1, C)               # ( 8,1024,32, 3 ) 归一化后if points is not None:grouped_points = index_points(points, idx)             # ( 8,1024,32, 9 ) new_points = torch.cat([grouped_xyz_norm, grouped_points], dim=-1) # [B, npoint, nsample, C+D]      ( 8,1024,32,12 ) else:new_points = grouped_xyz_normif returnfps:return new_xyz, new_points, grouped_xyz, fps_idx      # 跳过else:return new_xyz, new_points             # ( 8,1024,3 )   ( 8,1024,32,3+9 )

1.最远点采样

输入是(bs,4096,3)的点云,输出为( 8,1024 )的索引

def farthest_point_sample(xyz, npoint):device = xyz.deviceB, N, C = xyz.shape                                        # ( 8,4096,3 )centroids = torch.zeros(B, npoint, dtype=torch.long).to(device)              # ( 8,1024 ) *[0]distance = torch.ones(B, N).to(device) * 1e10                                                    # ( 8,4096 ) *[100000]farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device)              # (8)batch_indices = torch.arange(B, dtype=torch.long).to(device)                # [ 0,1,2,3,4,5,6,7 ]for i in range(npoint):centroids[:, i] = farthestcentroid = xyz[batch_indices, farthest, :].view(B, 1, 3)dist = torch.sum((xyz - centroid) ** 2, -1)mask = dist < distancedistance[mask] = dist[mask]farthest = torch.max(distance, -1)[1]return centroidsdef index_points(points, idx):device = points.deviceB = points.shape[0]view_shape = list(idx.shape)                    # [ 8,1024 ]view_shape[1:] = [1] * (len(view_shape) - 1)           # [ 8, 1 ]repeat_shape = list(idx.shape)                                     # [8,1024]repeat_shape[0] = 1                                                          # [1,1024]batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape)             # (8,1024) new_points = points[batch_indices, idx, :]return new_points

2.采样点附近,选最近32个点
xyz是原始点云(4096),new_xyz是采样后点云(1024)

def query_ball_point(radius, nsample, xyz, new_xyz):"""Input:radius: local region radiusnsample: max sample number in local regionxyz: all points, [B, N, 3]new_xyz: query points, [B, S, 3]Return:group_idx: grouped points index, [B, S, nsample]"""device = xyz.deviceB, N, C = xyz.shape               # 8, 4096, 3_, S, _ = new_xyz.shape                   # 1024group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1])           # (8,1024,4096):[0,1,2,...4095]sqrdists = square_distance(new_xyz, xyz)                                                                                                        #  ( 8,1024,4096 )group_idx[sqrdists > radius ** 2] = Ngroup_idx = group_idx.sort(dim=-1)[0][:, :, :nsample]                                                                                # ( 8,1024,32 ) 从小到大排序group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample])mask = group_idx == Ngroup_idx[mask] = group_first[mask]return group_idx                                                                                 # ( 8,1024,32 )

2.卷积下采样(升维)

#----------------------------------1.先定义网路------------------------------------------
self.mlp_convs = nn.ModuleList()
self.mlp_bns = nn.ModuleList()
self.mlp_convs.append(nn.Conv2d(last_channel, out_channel, 1))      # 这里是(12,32) (32,32) (32,64)
self.mlp_bns.append(nn.BatchNorm2d(out_channel))#----------------------------------2. forward------------------------------------------
new_points = new_points.permute(0, 3, 2, 1) # [B, C+D, nsample,npoint]            ( 8,12,32,1024 )
for i, conv in enumerate(self.mlp_convs):bn = self.mlp_bns[i]new_points =  F.relu(bn(conv(new_points)))                # ( 8,64,32,1024 )new_points = torch.max(new_points, 2)[0]                       # ( 8, 64, 1024 )
new_xyz = new_xyz.permute(0, 2, 1)
return new_xyz, new_points                    # (8,3,1024) (8,64,1024)

以上就是如下函数的全部内容:

 l1_xyz, l1_points = self.sa1(l0_xyz, l0_points)

随后是:

l2_xyz, l2_points = self.sa2(l1_xyz, l1_points)                    #  (8,3,256) (8,128,256)
l3_xyz, l3_points = self.sa3(l2_xyz, l2_points)                    # (8,3,64)   (8,256,64)
l4_xyz, l4_points = self.sa4(l3_xyz, l3_points)                    #  (8,3,16)   (8,512,16)l3_points = self.fp4(l3_xyz, l4_xyz, l3_points, l4_points)    # ( 8,256,64 )
l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points)    # ( 8,256, 256)
l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points)    # (8,128,1024)
l0_points = self.fp1(l0_xyz, l1_xyz, None, l1_points)             # (8,128,4096)x = self.drop1(F.relu(self.bn1(self.conv1(l0_points))))
x = self.conv2(x)                                                                              # (8,13,4096)
x = F.log_softmax(x, dim=1)
x = x.permute(0, 2, 1)                                                                     # (8,4096,13)
return x, l4_points

其中, self.sa2同样用到:
new_xyz, new_points = sample_and_group(self.npoint, self.radius, self.nsample, xyz, points)
其中超参数改为:self.npoint= 256(上一步是1024),self.radius =0.2。其余不变;

self.sa3中:self.npoint= 64,self.radius =0.4。其余不变;
self.sa4中:self.npoint= 16,self.radius =0.8。其余不变;

self.conv1 = nn.Conv1d(128, 128, 1)
self.bn1 = nn.BatchNorm1d(128)
self.drop1 = nn.Dropout(0.5)
self.conv2 = nn.Conv1d(128, num_classes, 1)

3.上采样:self.fp4(l3_xyz, l4_xyz, l3_points, l4_points)

    def forward(self, xyz1, xyz2, points1, points2):"""Input:xyz1: input points position data, [B, C, N]xyz2: sampled input points position data, [B, C, S]points1: input points data, [B, D, N]points2: input points data, [B, D, S]Return:new_points: upsampled points data, [B, D', N]"""xyz1 = xyz1.permute(0, 2, 1)xyz2 = xyz2.permute(0, 2, 1)points2 = points2.permute(0, 2, 1)B, N, C = xyz1.shape_, S, _ = xyz2.shapeif S == 1:interpolated_points = points2.repeat(1, N, 1)else:dists = square_distance(xyz1, xyz2)           # 求2个矩阵距离,函数同上 (8,64,3)(8,16,3) -->  (8,64,16)dists, idx = dists.sort(dim=-1)             # (8,64,16)dists, idx = dists[:, :, :3], idx[:, :, :3]  # [B, N, 3]  取最近的前三个点: (8,64,3)dist_recip = 1.0 / (dists + 1e-8)               # (8,64,3)norm = torch.sum(dist_recip, dim=2, keepdim=True)              # (8,64,1)weight = dist_recip / norminterpolated_points = torch.sum(index_points(points2, idx) * weight.view(B, N, 3, 1), dim=2)   # 16个点,找到对应最近(前三名)的64个点,求和:(8,64,512)if points1 is not None:points1 = points1.permute(0, 2, 1)new_points = torch.cat([points1, interpolated_points], dim=-1)   # cat(8,64,256),(8,64,512) --> (8,64,768)else:new_points = interpolated_pointsnew_points = new_points.permute(0, 2, 1)for i, conv in enumerate(self.mlp_convs):bn = self.mlp_bns[i]new_points = F.relu(bn(conv(new_points)))                             # 再映射回256维return new_points                                                                                    # ( 8,256,64 )

二、MVF(动态体素融合2019)

论文:End-to-End Multi-View Fusion for 3D Object Detection in Lidar Point Clouds
链接:https://arxiv.org/abs/1910.06528v2

Multi-View Fusion (MVF):两个创新:动态体素 和 特征融合网络结构

1.动态体素

Voxelization and Feature Encoding 体素和特征编码

hard体素化:给定点云P = {p1;::;pN},该过程将N个点分配给大小为 KTF 的缓冲区,其中K为体素的最大数量,T为一个体素的最大的点的数量,F为特征维数。在分组阶段:基于空间的坐标将点{Pi}分配到体素{Vj}.由于一个体素可能被分配了比它的固定点容量T所允许的更多的点,采样阶段子样本从每个体素中抽取固定的T个点。相似的,如果点云产生的体素大于固定体素容量K,则对体素进行降采样。另一方面,当点(体素)比固定容量T (V)少时,缓冲区中未使用的条目将被填充为零。我们称这个过程为硬体素化。

dynamic 体素化 (DV):DV保持了分组阶段的不变,但是,它没有将点采样到固定数量的固定容量体素中,而是保留了点与体素之间的完整映射。因此,体素的数量和每个体素的点的数量都是动态的,这取决于特定的映射函数。这消除了对固定大小缓冲区的需要,并消除了随机点和体素dropout。

2.特征融合网络结构

融合来自不同观点的信息激光雷达点云:鸟瞰视图透视视图。鸟瞰图是基于笛卡尔坐标系统定义的,在该系统中,物体保持其规范的三维形状信息,并自然可分离。当前的大多数硬体素化的三维物体探测器就是在这种情况下工作的。然而,它的缺点是点云在较长的范围内变得高度稀疏。另一方面,透视视图可以表示LiDAR距离图像密集,并能在球面坐标系中对场景进行相应的平铺。透视图的缺点是对象的形状不是距离不变的,而且在一个杂乱的场景中对象之间可能会大量重叠。因此,最好利用两种观点的互补信息

到目前为止,我们认为每个体素在鸟瞰时都是一个长方体的体积。在这里,我们建议将传统的体素扩展为一个更通用的概念,在我们的例子中,在透视图中包含一个3D截锥体。给定点云f(xi;yi;zi) 定义在笛卡尔坐标系中,其球面坐标表示计算为:

1)提出的MVF首先通过一个全连接(FC)层将每个点嵌入到一个高维特征空间中,该层用于不同的视图(将两个视图的局部坐标和点强度连接起来,然后通过一个全连接(FC)层嵌入到一个128D特征空间中。)

2)然后分别在鸟瞰图和透视图中应用动态体素化,建立点与体素之间的双向映射Fv(Pi) 和 Fp(Vj)

3)接下来,在每个视图中,它使用一个额外的FC层来学习与视图相关的特性,它通过参考Fv(Pi)来最大池来聚合体素信息(FC层:学习64维视图相关的特性)

4)在体素方向的特征图上,它使用一个卷积塔在扩大的接受域内进一步处理上下文信息,同时仍然保持相同的空间分辨率。(卷积塔,就是常用卷积下采样+反卷积。输入、输出都64维)

5)它融合了来自三个不同来源的特征:鸟瞰点对应的笛卡尔体素,透视点对应的球面体素;。

3.损失函数

ground truth 和 anchor box 为:{Xg, Yg, Zg, Lg, Wg, θg},{Xa, Ya, Za, La, Wa, θa}。回归差值表示如下:

anchor的对角线为da ^2 = la^2 + wa^2, 总的回归损失为

评估模型的标准平均精度(AP)指标为7自由度(DOF) 3D box和5自由度BEV box,使用相交超过联合(IoU)阈值,车辆0.7,行人0.5(数据集官网建议)。
实验设置:设置立体像素大小0.32m和检测范围(-74:88)沿着这X轴和Y轴两个类。

waymo开放数据集和KITTI数据集上:视图融合生成了更准确的远程遮挡对象检测。即与BEV相比,透视图体素化可以捕获互补信息,这在对象距离远、采样稀疏的情况下尤其有用。

三、Point RCNN(检测 CVPR2019)

论文:PointRCNN: 3D Object Proposal Generation and Detection from Point Cloud

Two-stage detector (Faster R-CNN!)
● Stage-1: proposal generation

Stage-II

result:

四、深度估计的雷达成像(检测 CVPR2019)

题目:Pseudo-LiDAR from Visual Depth Estimation

五、RandLA-Net(分割 2019)

Randla-net: Efficient semantic segmentation of large-scale point clouds. arXiv
preprint arXiv:1911.11236 (2019)

一、简介

在大规模的3D点云语义分析中,现有的技术主要是依赖于复杂的取样技术以及包含有繁重计算的预处理和后处理,而 RandLA-Net 是一种 高效而且轻量级 的技术,用在大型的点云中,关键是用 随机点取样 来代替其它复杂的取样技术,由于随机取样可能会带来关键信息的丢失,所以为了防止丢失又引入了局部特征聚合这一关键技术,能够兼顾高效和数据量。

二、取样

点云数据量庞大,需要选取一部分点进行计算,这样在不影响判断的情况下简化数据。现有的取样方式主要是分为两类:

①启发式采样

a-最远点采样(Farthest Point Sampling)
这种采样方式,给我最直观的感受就是一个反向的dijkstra算法,这个算法并不难,首先选择一个初始点a,之后初始化一个距离数组,记录剩下点到这个初试点的距离,选择里面最远的点加入集合,假设加入的点为b,那么现在集合里面有点a和b,之后计算b到所有点的距离,如果这个距离小于距离数组中记录的值,就更新为到b的值,全部更新完之后,将距离最大的点加入集合,重复操作知道采样的数目达到要求。
可见这个距离数组记录的实际上是剩下所有点到集合的最短距离,每次加入集合的点都是最远的点,所以叫最远点采样。这种采样方式在小范围的点云中应用比较广泛,但是如果放在大范围的点云中,缺点也很直观,基本就是一个暴力的运算,这个算法的时间复杂度可以达到o(N2),所以点一旦多了起来,耗时会特别大。所以在大范围的点云中,并不能采用这种方式

b-反密度重要性采样(Inverse Density Importance Sampling)
这种采样方式和名字一样,就是根据密度进行选择,而选择的方式是选择密度低的点。对于这种采样方式,相比于FPS时间复杂度的降低是很明显的,但是由于需要计算密度,对噪音比较敏感。此外尽管时间复杂度已经有了一定的改善,但是对于实时系统而言,仍然是达不到标准。

在论文最后的appendices部分中补充了这里密度计算的方法,给出一个点,密度并不是像物理上那样计算,而是利用距离,这里的密度其实是一个距离和,计算这个点周围的最近的t个点的距离之和作为密度,然后选择点的时候,根据密度的倒数来进行选点,也就是选择密度小的点。

c-随机采样(Random Sampling)
这种采样方式是这片论文所采用的方式,随机采样公平地从所有点中选择一定数目的点,由于是等概率的随机选择,所以时间复杂度是O(1),其计算量与输入点云的总数并没有关系,只与要采样的点的数目有关,在实时性和扩展性上都表现不错,尽管在数据量上还是有一点限制,但是时间复杂度的性能已经优于FPS和IDIS太多。

②基于学习的采样

a-基于生成器的采样(Generator-based Sampling)
与传统的采样方式不同,这种采样方式通过学习生成一个子集来近似表征原始的点云,相当于训练了一个替身,但是缺点也很致命,这种方式在匹配子集的时候需要使用FPS,前面也提到了,FPS的时间复杂度特别大,所以相当于是使用了一个特别费时间的工具去完成一个任务,所以引入了更复杂的过程,时间复杂度也上升了。

b-基于连续松弛的采样(Continuous Relaxation based Sampling)
这种采样方式是用大量的矩阵计算,得到的每个采样点实际上是整个点云的一个加权和。这个方式出发点是好的,但是采用了矩阵去计算,反而导致开销变大了。

c-基于策略梯度的采样(Policy Gradient based Sampling)
本身属于一种马尔可夫决策过程,采用概率分布去进行采样,但是由于采用了排列组合去产生搜索空间,所以当用于大型点云的时候,网络十分难收敛。

总结一下论文中提到的六种采样方式,FPS/IDIS/GS这三种方式在用于大型点云时时间复杂度都太大,CRS需要额外的存储空间,PGS在大型点云的情况下难以收敛。但是正相反,随机采样一方面时间复杂度有着绝对优势,另一方面也不需要额外的存储空间。因此选择随机采样作为算法的一个关键。

三、局部特征聚合

从名字就可以看出来,聚合的是局部的特征,用来防止采用随机采样而将重要数据丢失。局部特征聚合的总图示如下:
局部特征聚合主要是三个部分:局部空间编码、注意力池化和扩张残块。下面记录一下三个部分:

①局部空间编码

这部分的主要目的特征扩充,首先输入的数据是包含各种特征的向量,输出的结果是扩充后的特征向量。
局部空间编码主要是三步:

a 寻找临近点。给定N个点,对每个点使用一次KNN算法,找出欧式距离最近的K个点。

b 相对位置编码。这一部分最好结合图片去理解,根据上面的图示,绿色部分是局部空间编码,其中选中的点利用KNN算法变成了K个点,每个点有3+d个属性,其中3代表三维空间的位置坐标,d代表特征属性,将三维坐标取出来,就是K个三维向量,这些向量做下面的操作:

结合上面的图,这个公式的意思就是将中心点的三维坐标、当前点的三维坐标、相对坐标、欧式距离给连接起来,之后利用MLP对维度进行调整,调整成长d的向量。

c 点特征增强: 将前面扩充的矩阵和原输入矩阵拼接,最后结果是一个k×2d的矩阵。

②注意力池化
经过局部特征编码,我们将一个点变成了一组扩展细节之后的向量(代表着周围一定范围的信息)。随后需要将其整合为一个特征向量。注意力池化主要分为两个步骤:计算注意力值加权求和。下面分别记录一下两个步骤。

a-计算注意力值
这一部分主要是根据局部特征编码得到的矩阵,计算得到一个新矩阵。论文里面的原话是说需要设计一个共用的函数g(),利用这个函数来学习一个特征向量对应的注意力值,其中在计算过程中需要用到一个共享MLP,所以s_ik的计算应该是下面的式子:
其中W是 共享MLP 的可学习权重。原话为:“Shared MLP 是点云处理网络中的一种说法,强调对点云中的每一个点都采取相同的操作。其本质上与普通MLP没什么不同,其在网络中的作用即为MLP的作用:特征转换、特征提取”。

b-加权求和
这一步主要还是利用前面学习的注意力值来加权求和,注意力值可以看做一个可以自动筛选重要信息的soft mask,将周围点信息进行筛选,得到的就是精简之后的特征向量。特征向量应该按照下面的式子进行计算:

局部信息编码 是扩充信息的过程,将一个点的信息变成了一个范围的点的信息,再经过 注意力池化,将范围的点的信息再次整个为一个向量,也就是用一个点来代表一个范围,从而实现了对范围信息的整合。经过这两个步骤,一开始N个长度为3+d的向量显示变成了N×K×2d的向量组,之后经过注意力池化,变成N个1×d‘的向量,这些向量包含着一开始N个点周围的信息。

③扩张残块
在RandLA-Net中选择使用两轮的局部信息编码和注意力池化。

由于大的点云将大幅向下采样,因此需要显著增加每个点的感知域,这样即使一些点被删除,输入点云的几何细节也更有可能被保留。一般来说采用的轮(一次编码一次池化)数越多,最终得到的点能代表的范围信息就越大,但是轮数过多会牺牲一定的计算效率,而且容易导致过拟合,所以在RandLa-Net中使用两轮就可以了,这样就可以实现效率和效果的平衡。

四、补充与实验

整个RandLA-Net的结构如下,从大的层面上分分成四轮的编码解码、输入、最终语义分割以及网络的输出。这里面的一层实际上对应的是一个箭头,而不是一个方框,方框是经过一层的处理之后数据的规模。解码层,对于每一层,都使用KNN算法来找出每个点的最临近点,使用最临近插值来放大数据。之后将放大后的特征地图与原来的编码后的地图进行拼接。解码完成后就可以进行最后的语义分析(三层的全连接+一层dropout)。

实验采用SemanticKITTL数据集,包含有43552个带有注解的LIDAR扫描数据,分为21个序列,其中10个序列用来训练,1个用来核验,10个用来检测,最终的比较结果为:

六、3DBoNet(检测+实例分割 NeurIPS2019)

论文链接:https://arxiv.org/abs/1906.01140
代码链接:https://github.com/Yang7879/3D-BoNet (tensorFlow)

1.核心思想

利用逐点多层感知机(MLP),直接预测3D边框,同时预测边框内各个点的二分标志,即判断它是属于物体还是属于背景。因此有 两个组成部分,一是3D边框预测网络,二是框内点分类网络。3D-BoNet不仅是单阶段、无锚点、端到端的系统,而且相比于传统思路效率大幅度提高,因为它不需要非极大值抑制(NMS)、特征采样、聚类、投票等后处理操作

2.主要算法

传统方法缺点:实例分割在此之前主要有两类,第一类是基于候选目标框的算法(Proposal-based methods),这类方法通常先生成大量的候选框,然后依赖于两阶段的训练和昂贵的非极大值抑制等操作来对密集的候选框进行选择,缺点自然是运算量特别大。第二类是无候选目标框的算法(Proposal-free methods),这类方法的核心思想是为每个点学习一个分类,然后再通过聚类方法来将属于同一个实例的点聚集到一起,缺点最终聚类到一起的实例目标性比较差,而且后处理步骤的时间成本也比较高。

改进思路:首先不使用候选框,但为了提高目标性,不能放弃边界框,所以可以直接预测边界框,设计了一种边界框关联的方法,把预测和真值关联在一起,完成关联,后续的损失值设计等步骤就好做了。最后再对回归的边界框内的点做一个二分类,把背景点去掉,就可以得到实例分割的结果了。

流程:首先提取全局特征,直接使用这个全局特征来预测边界框。与此同时,提取点的特征,待边界框回归结束以后,某些点就会落入对应的框中,这样就可以进行二值分类,分割出实例目标。

训练时,边界框的关联(真值与预测值)是本文重点,可以分为以下几步:

1分配预测框给最近的真值框
2 计算顶点之间的欧式距离、交并比、交叉熵
3计算损失函数
4反向估计

完成了边界框的回归,剩下的最后一步就是背景点的剔除了,具体流程图下图所示。即根据点特征和边界框的得分,对框内的点进行二分类.


ScanNet 上实验结果:

消融实验:

七、 PointGroup (实例分割 CVPR 2020)

论文:PointGroup: Dual-Set Point Grouping for 3D Instance Segmentation

1.摘要

这是一种新的端到端自下而上的架构,特别专注于通过探索目标之间的空隙空间来更好地对点进行分组。1.设计了一个双分支网络来提取点特征并 预测语义标签偏移量,以将每个点移向其各自的实例质心。2.(基于双坐标集的点聚类方法)聚类组件利用原始点坐标集和偏移偏移点坐标集,利用它们的互补优势。3.制定了ScoreNet来评估候选实例,然后使用非最大抑制(NMS)来删除重复项。

2.架构

与2D图像不同,3D场景不存在视野遮挡问题,3D中分散的目标是通常由空隙空间自然隔开。下图概述了我们方法的架构,它具有三个主要组件,即骨干网点聚类部分和ScoreNet

骨干网络:可选择多种网络。在我们的实现中,我们对这些点进行体素化,并构建具有子流形稀疏卷积(SSC)和稀疏卷积(SC)的U-Net,然后从体素中恢复点以获得逐点特征。 U-Net很好地提取了上下文和几何信息,为后续处理提供了判别性的逐点特征F。

获得语义标签后,我们开始根据目标之间的空隙空间将点分组为实例聚类。在点聚类部分图(b),我们引入了一种聚类方法,如果它们具有相同的语义标签,则将彼此接近的点分组到同一个聚类中。然而,直接基于点坐标集P = { pi } 进行聚类可能无法将3D空间中彼此靠近的同类别目标分开并错误分组,例如并排悬挂的两张图片墙。因此,我们使用学习到的偏移量 oi 将点 i 向其各自的实例质心移动,并获得移动后的坐标qi = pi + oi ∈ R3。对于属于同一目标实例的点,与pi 不同,偏移坐标qi 在同一质心周围杂乱无章。因此,通过基于偏移坐标集Q = { qi } 的聚类,我们可以更好地分离附近的目标,即使它们具有相同的语义标签。

但是,对于靠近目标边界的点,预测的偏移量可能不准确。因此,我们的聚类算法采用dual点坐标集,即原始坐标P和移动坐标Q。我们将聚类结果C 表示为Cp =和Cq的并集,分别是基于P 和Q 发现的聚类。这里
最后,我们构建ScoreNet(图2©)来处理 proposal 的点聚类C = Cp ∪ Cq ,并为每个聚类proposal 生成一个分数。然后将NMS应用于这些具有分数的proposal,以生成最终实例预测。

3.损失函数
Semantic Segmentation Branch 采用交叉熵函数
Offset Prediction Branch 偏移分支对F进行编码以产生N个偏移向量O,L1损失:

其中m = { m1 , … , mN } 是二进制mask。如果点i在实例上,则mi = 1 ,否则为 0 。^ci 是点i所属实例的质心,即

g(i)是将点i 映射到其对应的ground-truth实例的索引。
偏移向量的方向损失: 我们不是基于几个子采样种子点的投票来回归边界框,而是预测每个点的偏移向量以围绕一个共同的实例质心收集实例点,以便更好地将相关点聚集到同一个实例中。此外,我们观察到从点到它们的实例质心的距离通常具有较小的值(0到1m)。考虑到不同类别的不同目标大小,我们发现网络很难回归精确的偏移量,特别是对于大尺寸目标的边界点,因为这些点距离实例质心相对较远。为了解决这个问题,我们制定了方向损失来约束预测偏移向量的方向。将损失定义为负余弦相似度的一种方法,即

4. Clustering Algorithm


5.ScoreNet



八、Point Transformer(分割 2020)

三个变体:

  1. Point Transformer最早能追溯到的德国团队2020年11月的文章(简称 PT1),其local global attention效果并不好。在Global Feature生成的基础上,又设计了一个对Local feature 深层提取的网络SortNet感觉是多次一举的举措(至少提升不大)。且Local feature 最后因为他构造的SortNet对local feature(仅使用Top-K 数据代表特征,不具有说服力),没有多层次的对特征进行提取。

  2. 2020年12月16日 (注: 笔者生日)由清华大学团队提出的Point Cloud Transformer (以下简称PCT)横空出世, 紧随其后隔天香港中文大学和牛津大学团队推出的Point Transformer (以下简称PT2)也挂到了Arxiv上.
    PCT用的是global attention,是用了四层的Attention Feature组合形成(体感上有点像DGCNN)效果上稍差一些,有点主要在于Offset-Attention的部分。Point Transformer 用的是local vector attention,可以节省计算量。从整体上看像一个更深的PointNet++,效果也比较好。但是其实这三个point transformer都使用到了pointnet里的SetAbstraction(SA)

1. Point Transformer Layer

注意力公式:

位置编码:

2.网络框架

Point Transformer Block
我们构建了一个以Point Transformer层为核心的残差Point Transformer块,如图4(a)所示。transformer块集成了self-attention层,可以降维和加速处理的线性投影以及残差连接。输入是一组具有相关3D坐标 p 的特征向量 x。Point Transformer块便于这些局部特征向量之间的信息交换,为所有数据点产生新的特征向量作为其输出。信息聚合既适应特征向量的内容,也适应它们在3D中的布局。

Point Transformer是网络中主要的特征聚合操作。作者不使用卷积进行预处理或者辅助分支:网络完全基于Point Transformer层、逐点变换和池化。

Backbone structure.
用于语义分割和分类的Point Transformer网络中的特征编码器具有五个对点集进行下采样的阶段。每个阶段的下采样速率是[1,4,4,4,4],因此每个阶段后点集中点的数量为[N,N/4,N/16,N/64,N/256],其中N是输入点数。注意,级数和下采样速率可以根据应用而变化,例如构建用于快速处理的轻量主干。连续的阶段由转换模块连接:向下转换用于特征编码,向上转换用于特征解码。
Transition down.
功能在于减少点的数量。将输入的点集表示为P1 ,输出点集为P2 。在P1 中执行最远点采样,来获得分布良好的子集P2 ⊂ P1。使用P1的kNN图(k=16)将特征向量从P1 汇集到P2。每一个输入特征都经过一个线性变换,随后是batch归一化和ReLU,接着是将P2在P1的 k 个邻居最大池化到P2 的每个点。Transition down模块如图4(b)所示。
Transition up.
对于密集的预测任务,例如语义分割,作者采用了一种U-net设计,其中上述编码器与对称解码器耦合。解码器中的连续级由Transition up模块连接,主要功能是将来自下采样的输入点集P2的特征映射到其超集P1 ⊃ P2 。为此,每个输入点都要经过一个线性图层处理,然后进行批量归一化和ReLU,再通过三线性插值将P2 特征映射到更高分辨率的点集P上。来自前一解码器级的这些内插特征通过跳跃连接与来自相应编码器级的特征相结合。Transition up模块的结构如图4©所示。
Output head.
对于语义分割,最终的解码器阶段为输入点集中的每个点生成一个特征向量。再应用MLP将这个特征映射到最终的逻辑。对于分类,我们对逐点特征执行全局平均汇集,以获得整个点集的全局特征向量。这个全局特征通过一个MLP得到全局分类逻辑。

九、HAIS(pointgroup扩展:ICCV2021 )

论文:Hierarchical Aggregation for 3D Instance Segmentation
论文地址:https://openaccess.thecvf.com/content/ICCV2021/papers/Chen_Hierarchical_Aggregation_for_3D_Instance_Segmentation_ICCV_2021_paper.pdf
代码:https://github.com/hustvl/HAIS

1.摘要
提出了一个简洁的基于聚类的框架HAIS,它充分利用了点和点集的空间关系。考虑到基于聚类的方法可能导致过度分割或分割不足,我们引入层次聚合来逐步生成实例proposals,即用于初步聚类点到集合的点聚合和用于从集合生成完整实例的集合聚合。一旦获得完整的3D实例,就采用实例内预测的子网络进行噪声点过滤和掩码质量评分。HAIS速度快(Titan X上每帧仅410ms),不需要非极大值抑制。它在ScanNet v2 benchmark中排名达到最高的69.9% A P_50

输入点云、ground truth实例mask和3D实例预测结果,不带和带分层聚合。如红色圈出的关键区域所示,对于大尺寸和零散点云的目标,预测容易被过度分割。所提出的分层聚合将不完整的实例与片段结合起来形成完整的实例预测。
然而,直接将一个点云聚类成多个实例是非常困难的,原因如下:
(1)一个点云通常包含大量的点;
(2)一个点云中的实例数量对于不同的3D场景有很大的变化;
(3)实例规模差异显着;
(4)每个点都有一个非常弱的特征,即3D坐标和颜色。点和实例身份之间的语义差距是巨大的。因此,过度分割或分割不足是常见的问题并且容易存在。

HAIS遵循基于聚类的范式,但与现有的基于聚类的方法在两个方面有所不同。首先,大多数基于聚类的方法需要复杂且耗时的聚类过程,但我们的HAIS采用更简洁的管道并保持高效率。其次,以前的方法通常根据点级嵌入对点进行分组,没有实例级校正。HAIS引入了集合聚合和实例内预测,以在目标级别细化实例

对于输入点云,我们的方法首先采用具有子流形稀疏卷积的类3D UNet结构进行逐点特征学习。然后,我们使用点的空间约束来执行固定带宽的点聚合。基于点聚合结果,进行具有动态带宽的集合聚合,形成实例proposals。实例内预测是为异常值过滤和掩码质量评分而设计的。

2.架构

  1. Point-wise Prediction Network
    这里跟pointgroup相同,使用子流形稀疏卷积的UNET从点云中提取特征,分别输出语义标签与偏移量。其中偏移分支的损失函数为:实例中心定义为该
  2. .Point Aggregation
    在3D空间中,同一实例的点本质上是彼此相邻的。利用这种空间约束进行聚类是很直观的。因此,基于语义标签和中心偏移向量,我们使用基本且紧凑的聚类方法来获得初步实例。首先,如图3(b)所示,根据逐点中心移动向量Δx,将每个点x_i^orig 向其实例中心移动,使同一实例的点在空间上彼此更接近。偏移坐标计算为

图 3.分层聚合的插图。不同颜色的点属于不同的类别。黑点属于背景。 (a):分布在真实 3D 空间中的点。 (b):将中心偏移向量应用于每个点后,属于同一实例的点在 3D 空间中更接近。©:点聚合。基于 固定的空间聚类带宽 将点聚合成集合:将每个前景点视为一个节点。对于每一对节点,如果它们具有相同的语义标签并且它们的空间距离小于固定的空间聚类带宽r_point ,则在这两个节点之间创建一条边。在遍历所有节点对并建立边之后,整个点云被分成多个独立的集合 (d):设置聚合。主实例以动态的聚类带宽吸收周围的分片,形成完整的实例。

  1. Set Aggregation
    与ground truth实例的大小相比,点聚合生成的实例预测数量要多得多,而且尺寸较小。这是因为中心偏移矢量并不完全准确。点聚合不能保证一个实例中的所有点都分组在一起。如图3(d)所示,大多数具有准确中心偏移向量的点可以聚集在一起以形成不完整的实例预测。我们称这些实例为“主实例”。但是中心偏移向量预测较差的少数点从大多数点中分裂出来,形成了小尺寸的碎片实例,我们称之为“碎片”。碎片的大小太小,不能被视为完整的实例,但可能是主实例的缺失部分。考虑到分片数量众多,直接过滤掉具有硬阈值的碎片是不合适的。直观地说,我们可以在集合级别聚合主要实例和片段以生成完整的实例预测。

满足以下两个条件,我们认为片段 m 是主实例 n 的一部分。首先,在所有与片段 m 具有相同语义标签的主实例中,主实例 n 是几何中心最接近片段 m 的那个。其次,对于分片m和主实例n,它们的几何中心之间的距离应该小于rset,即动态聚类带宽定义为

  1. Intra-instance Prediction Network
    层聚合可能会错误地吸收属于其他实例的碎片,从而产生不准确的实例预测。因此,我们提出了用于进一步细化实例的实例内预测网络.首先,裁剪实例点云块作为输入,并使用3D子流形稀疏卷积网络来提取实例内的特征。实例特征提取,mask分支预测二进制mask以区分实例前景和背景。对于每个预测的实例,我们选择最匹配的GT(ground truth)作为mask监督。预测实例和GT之间的重叠部分被分配正标签,其他部分被分配负标签。低质量实例(低IoU与GT)包含很少的实例级信息,对于优化mask分支毫无价值。因此,仅将IoU高于0.5的实例用作训练样本,而忽略其他实例。对于mask预测,损失表示为,

    其中N_ins 表示实例数,N_i 表示实例i的点数。

除了mask预测之外,还需要实例确定性分数来对实例进行排名。我们利用mask来获得更好的评分实例,如图5所示。首先,mask用于过滤背景点的特征,这将是评分的噪声。剩余的前景特征被发送到具有sigmoid层的MLP中以预测实例确定性分数。其次,我们将预测mask和GT mask之间的IoU视为mask质量,并使用它们来监督实例确定性。对于分数预测,损失表示为,
5. 实验结果

十、SqueezeSeg V3(分割 2021oral)

论文:SqueezeSegV3: Spatially-Adaptive Convolution for Efficient Point-Cloud Segmentation
代码:https://github.com/chenfengxu714/SqueezeSegV3.

1.概述球形投影 三维点云得到一个二维激光雷达图像,并使用卷积进行处理。整体框架为:

2.解决问题与创新

提出了空间自适应卷积(SAC),根据输入图像对不同的位置采用不同的滤波器。由于投影后的2D图像的特征分布在不同的图像位置有很大变化:使用标准卷积来处理会有误差,因为卷积滤波器接收到只在图像的特定区域活跃的局部特征。

下面左图显示了COCO2017CIFAR10中所有图像中红色通道的像素分布情况。右边显示了投影后的2D雷达图像上X坐标上像素的分布。

一、投影公式


(x,y,z)是三维坐标,(p,q)是角坐标,(h,w)是所需的投影地图的高度和宽度,实验中为(64,2048);f=f_up+ f_down是激光雷达传感器的垂直视场,实验中为【-5,1.5】,r=x、y、z的平方和开根号,是每个点的范围。对于投影到(p、q)的每个点,我们使用它对(x、y、z、r)和强度的测量作为特征,并沿着通道维度堆叠它们。

二、SAC卷积

原始卷积:

其中Y∈R(O×S×S)为输出张量,X∈R(I×S×S)为输入张量,W∈R(O×I×K×K)为卷积权值。O、I、S、K分别为输出通道大小、输入通道大小、图像大小和权值的核大小。ˆi=i− K/2,ˆj=j− K/2。σ(·)是一个非线性激活函数。

SAC卷积:被设计为具有空间自适应和内容感知的。根据输入,它调整其滤波器来处理图像的不同部分。

W(·)∈R(O×I×S×S×K×K)是原始输入X_0的函数。它是空间自适应的,因为W取决于位置(p,q)。它是内容感知的,因为W是原始输入x0的函数。以这种一般形式计算W是非常昂贵的,因为W包含太多的元素来计算。为了降低计算成本,我们将W分解为标准卷积权值和空间自适应注意图的乘积,代码如下。

  def forward(self, input):xyz, new_xyz, feature = input          # 输入为3维坐标和32维特征,new_xyz = xyzN,C,H,W = feature.size()new_feature = F.unfold(feature, kernel_size = 3, padding = 1).view(N, -1, H, W)     # feature:(1, 32, 64, 2048) --> ( 1, 288, 64, 2048 ) 特征重复或扩展attention = F.sigmoid(self.attention_x(new_xyz))                # 7*7conv: (1, 3, 64, 2048) --> ( 1, 288, 64, 2048 )new_feature = new_feature * attentionnew_feature = self.position_mlp_2(new_feature)               #  2*CBR: ( 1, 288, 64, 2048 ) -->  ( 1, 32, 64, 2048 )fuse_feature = new_feature + feature                                        #  ( 1, 32, 64, 2048 )return xyz, new_xyz, fuse_feature                    # ( 1, 3, 64, 2048 )  ( 1, 3, 64, 2048 )  ( 1, 3, 64, 2048 )

文中还设计了几种SAC卷积变体,代表不同的精度和计算量:

三、损失函数

引入一个多层交叉熵损失来训练所提出的网络,在训练过程中,从阶段1到阶段5,我们在每个阶段的输出中添加一个预测层。对于每个输出,我们分别将GT映射降采样为1x、2x、4x、8x和8x,并使用它们来训练阶段1的输出到阶段5。损失函数可以描述为

四、重点代码

1.球面投影
函数 do_range_projection,在文件 src/common/laserscan.py 中

def do_range_projection(self):""" Project a pointcloud into a spherical projection image.projection.Function takes no arguments because it can be also called externallyif the value of the constructor was not set (in case you change yourmind about wanting the projection)"""# 雷达参数fov_up = self.proj_fov_up / 180.0 * np.pi               # 视野的up值,固定参数:3/180*pi = 0.0523fov_down = self.proj_fov_down / 180.0 * np.pi  # 视野的 down 值:-25/180*pi = -0.43fov = abs(fov_down) + abs(fov_up)                          # 整体视野范围     0.488# 得到所有点的深度depth = np.linalg.norm(self.points, 2, axis=1)            # (124668) 个点的2范数# get scan componentsscan_x = self.points[:, 0]scan_y = self.points[:, 1]scan_z = self.points[:, 2]# 得到所有点的角度yaw = -np.arctan2(scan_y, scan_x)                                  # 偏移角 (124668) pitch = np.arcsin(scan_z / depth)                                     # 仰角   (124668) # 得到图像坐标系的映射proj_x = 0.5 * (yaw / np.pi + 1.0)                                       #角度归一化proj_y = 1.0 - (pitch + abs(fov_down)) / fov                 # 角度归一化# 使用角度分辨率,缩放到图像尺寸proj_x *= self.proj_W                                                             # 归一化的角度*2048proj_y *= self.proj_H                                                             # 归一化的角度*64# round and clamp for use as indexproj_x = np.floor(proj_x)proj_x = np.minimum(self.proj_W - 1, proj_x)proj_x = np.maximum(0, proj_x).astype(np.int32)   # in [0,W-1]self.proj_x = np.copy(proj_x)  # store a copy in orig orderproj_y = np.floor(proj_y)proj_y = np.minimum(self.proj_H - 1, proj_y)proj_y = np.maximum(0, proj_y).astype(np.int32)   # in [0,H-1]self.proj_y = np.copy(proj_y)  # stope a copy in original order# 投影前的点云深度(npoints,1)self.unproj_range = np.copy(depth)# 根据点云 depth 做降序排列 indices = np.arange(depth.shape[0])      # [0,1,2,3,4...124668]order = np.argsort(depth)[::-1]                  # (124668)*index :  点云按照由远到近排序depth = depth[order]indices = indices[order]points = self.points[order]remission = self.remissions[order]proj_y = proj_y[order]proj_x = proj_x[order]# assing to images  # 重构的图像从左上角(0,0)到右下角(63,2048),depth值由大到小。# 没有depth值的地方填充-1。若坐标重复(偏移角与仰角接近),则近的点会替代远的self.proj_range[proj_y, proj_x] = depth                               # ( 64, 2048 )self.proj_xyz[proj_y, proj_x] = points                                   # ( 64, 2048, 3 )self.proj_remission[proj_y, proj_x] = remission              # ( 64, 2048 )self.proj_idx[proj_y, proj_x] = indices                                  # ( 64, 2048 )self.proj_mask = (self.proj_idx > 0).astype(np.int32)    # ( 64, 2048 )

2.数据预处理
在迭代过程中,一个输入点云经过预处理,会产生8个变量:
src/tasks/semantic/dataset/kitti/parser.py

scan = LaserScan(project=True,H=self.sensor_img_H,W=self.sensor_img_W,fov_up=self.sensor_fov_up,fov_down=self.sensor_fov_down)# 打开点云文件
scan.open_scan(scan_file)
if self.gt:scan.open_label(label_file)
# 将标签映射到【0~19】 (also for projection)
scan.sem_label = self.map(scan.sem_label, self.learning_map)
scan.proj_sem_label = self.map(scan.proj_sem_label, self.learning_map)# 按照张量维度,初始化8个变量
unproj_n_points = scan.points.shape[0]                                                             # 124668
unproj_xyz = torch.full((self.max_points, 3), -1.0, dtype=torch.float)     # 15000
unproj_xyz[:unproj_n_points] = torch.from_numpy(scan.points)
unproj_range = torch.full([self.max_points], -1.0, dtype=torch.float)
unproj_range[:unproj_n_points] = torch.from_numpy(scan.unproj_range)
unproj_remissions = torch.full([self.max_points], -1.0, dtype=torch.float)
unproj_remissions[:unproj_n_points] = torch.from_numpy(scan.remissions)
if self.gt:unproj_labels = torch.full([self.max_points], -1.0, dtype=torch.int32)unproj_labels[:unproj_n_points] = torch.from_numpy(scan.sem_label)else:unproj_labels = []# 得到点和标签(利用上一步的球面投影)
proj_range = torch.from_numpy(scan.proj_range).clone()                      # ( 64, 2048 )
proj_xyz = torch.from_numpy(scan.proj_xyz).clone()                                # ( 64, 2048, 3 )
proj_remission = torch.from_numpy(scan.proj_remission).clone()    # ( 64, 2048 )
proj_mask = torch.from_numpy(scan.proj_mask)                                      # ( 64, 2048 )
if self.gt:proj_labels = torch.from_numpy(scan.proj_sem_label).clone()proj_labels = proj_labels * proj_mask
else:proj_labels = []proj_x = torch.full([self.max_points], -1, dtype=torch.long)                 # (15000)* -1proj_x[:unproj_n_points] = torch.from_numpy(scan.proj_x)proj_y = torch.full([self.max_points], -1, dtype=torch.long)proj_y[:unproj_n_points] = torch.from_numpy(scan.proj_y)proj = torch.cat([proj_range.unsqueeze(0).clone(),proj_xyz.clone().permute(2,0,1),proj_remission.unsqueeze(0).clone()])                                       # 深度、坐标、强度拼接成5维 (5,64,2048)proj = (proj - self.sensor_img_means[:, None, None]) / self.sensor_img_stds[:, None, None]       # 归一化proj = proj * proj_mask.float()# get name and sequence
path_norm = os.path.normpath(scan_file)
path_split = path_norm.split(os.sep)
path_seq = path_split[-3]
path_name = path_split[-1].replace(".bin", ".label")return proj, proj_mask, proj_labels, unproj_labels, path_seq, path_name, \proj_x, proj_y, proj_range, unproj_range, proj_xyz, unproj_xyz, proj_remission, unproj_remissions, unproj_n_points

3.主函数

for i, (proj_in, proj_mask, _, _, path_seq, path_name, p_x, p_y, proj_range, unproj_range, _, _, _, _, npoints) in enumerate(loader):proj_output, _, _, _, _ = self.model(proj_in, proj_mask)     # ( 1, 5, 64, 2048 ) --> (1, 20, 64, 2048)proj_argmax = proj_output[0].argmax(dim=0)                       # ( 64, 2048 )if self.post:# knn后处理,可以提升检测结果的精度unproj_argmax = self.post(proj_range,  unproj_range,  proj_argmax, p_x,  p_y)else:# put in original pointcloud using indexesunproj_argmax = proj_argmax[p_y, p_x]if torch.cuda.is_available():torch.cuda.synchronize()print("Infered seq", path_seq, "scan", path_name,"in", time.time() - end, "sec")end = time.time()# save scan# get the first scan in batch and project scanpred_np = unproj_argmax.cpu().numpy()pred_np = pred_np.reshape((-1)).astype(np.int32)# map to original labelpred_np = to_orig_fn(pred_np)            # 从[0-19]映射回原来类别# save scanpath = os.path.join(self.logdir, "sequences",path_seq, "predictions", path_name)           # sample_output/sequences/00/predictions/000000.label'pred_np.tofile(path)depth = (cv2.normalize(proj_in[0][0].cpu().numpy(), None, alpha=0, beta=1,norm_type=cv2.NORM_MINMAX,dtype=cv2.CV_32F) * 255.0).astype(np.uint8)           # ( 64,2048 )print(depth.shape, proj_mask.shape,proj_argmax.shape)out_img = cv2.applyColorMap(depth, Trainer.get_mpl_colormap('viridis')) * proj_mask[0].cpu().numpy()[..., None]# make label predictionpred_color = self.parser.to_color((proj_argmax.cpu().numpy() * proj_mask[0].cpu().numpy()).astype(np.int32))      # ( 64,2048,3 )out_img = np.concatenate([out_img, pred_color], axis=0)          # (128,2048,3)print(path)cv2.imwrite(path[:-6]+'.png',out_img)

4.KNN后处理
原理:将预测结果与周围最近的7个点预测值放在一起,进行投票。类别数最多的class代表盖点的类别。

class KNN(nn.Module):def __init__(self, params, nclasses):super().__init__()print("*"*80)print("Cleaning point-clouds with kNN post-processing")self.knn = params["knn"]self.search = params["search"]self.sigma = params["sigma"]self.cutoff = params["cutoff"]self.nclasses = nclassesprint("kNN parameters:")print("knn:", self.knn)print("search:", self.search)print("sigma:", self.sigma)print("cutoff:", self.cutoff)print("nclasses:", self.nclasses)print("*"*80)def forward(self, proj_range, unproj_range, proj_argmax, px, py):''' Warning! Only works for un-batched pointclouds.If they come batched we need to iterate over the batch dimension or dosomething REALLY smart to handle unaligned number of points in memory'''# get deviceif proj_range.is_cuda:device = torch.device("cuda")else:device = torch.device("cpu")# sizes of projection scanH, W = proj_range.shape                           # 64, 2048# number of pointsP = unproj_range.shape                            # 124668# check if size of kernel is odd and complainif (self.search % 2 == 0):raise ValueError("Nearest neighbor kernel must be odd number")# calculate paddingpad = int((self.search - 1) / 2)                   # 3# unfold neighborhood to get nearest neighbors for each pixel (range image)proj_unfold_k_rang = F.unfold(proj_range[None, None, ...],kernel_size=(self.search, self.search),padding=(pad, pad))                      # ( 1, 49, 64, 2048 )--># ( 1, 49, 131072)# index with px, py to get ALL the pcld pointsidx_list = py * W + px              # (124668):  64*2048 =131072个图像点中的索引unproj_unfold_k_rang = proj_unfold_k_rang[:, :, idx_list]         # ( 1, 49, 124668 )# WARNING, THIS IS A HACK# Make non valid (<0) range points extremely big so that there is no screwing# up the nn self.searchunproj_unfold_k_rang[unproj_unfold_k_rang < 0] = float("inf")         # depth: ( 1, 49, 124668 )  其中(781331)* inf# now the matrix is unfolded TOTALLY, replace the middle points with the actual range pointscenter = int(((self.search * self.search) - 1) / 2)                                  # 24unproj_unfold_k_rang[:, center, :] = unproj_range                        # ( 1, 49, 124668 ) : 49中的第24为depth(124668)# now compare rangek2_distances = torch.abs(unproj_unfold_k_rang - unproj_range)          # ( 1, 49, 124668 )# make a kernel to weigh the ranges according to distance in (x,y)# I make this 1 - kernel because I want distances that are close in (x,y)# to matter moreinv_gauss_k = (1 - get_gaussian_kernel(self.search, self.sigma, 1)).view(1, -1, 1)inv_gauss_k = inv_gauss_k.to(device).type(proj_range.type())                     # (1,49,1): 生成 7*7高斯核,最外侧为1,中心为0.84# apply weighingk2_distances = k2_distances * inv_gauss_k                          # ( 1, 49, 124668 )# find nearest neighbors_, knn_idx = k2_distances.topk(self.knn, dim=1, largest=False, sorted=False)                   # ( 1, 7, 124668 )# do the same unfolding with the argmaxproj_unfold_1_argmax = F.unfold(proj_argmax[None, None, ...].float(),kernel_size=(self.search, self.search),padding=(pad, pad)).long()                                            # (1,64,2048) --> (1,49,64,2048) --> (1,49, 131072)  每个点的预测类别   unproj_unfold_1_argmax = proj_unfold_1_argmax[:, :, idx_list]                # ( 1, 49, 124668 )  每个点(及其周围49个点) 的预测类别   # get the top k predictions from the knn at each pixelknn_argmax = torch.gather(input=unproj_unfold_1_argmax, dim=1, index=knn_idx)             # ( 1, 7, 124668 ) : 7*7=49的范围内,找到depth距离最近的7个点# fake an invalid argmax of classes + 1 for all cutoff itemsif self.cutoff > 0:knn_distances = torch.gather(input=k2_distances, dim=1, index=knn_idx)   # ( 1, 7, 124668 ):排名前7的点到中心点的depth距离knn_invalid_idx = knn_distances > self.cutoff                                       # ( 1, 7, 124668 )knn_argmax[knn_invalid_idx] = self.nclasses                                       # 距离大于1的,类别设置为20# now vote# argmax onehot has an extra class for objects after cutoffknn_argmax_onehot = torch.zeros((1, self.nclasses + 1, P[0]), device=device).type(proj_range.type())            # ( 1, 21, 124668 )ones = torch.ones_like(knn_argmax).type(proj_range.type())                          # ( 1, 7, 124668 )knn_argmax_onehot = knn_argmax_onehot.scatter_add_(1, knn_argmax, ones)        # ( 1, 21, 124668 )  将7个点的类别,分别分配到对应的21个类别当中# now vote (as a sum over the onehot shit)  (don't let it choose unlabeled OR invalid)knn_argmax_out = knn_argmax_onehot[:, 1:-1].argmax(dim=1) + 1               # ( 1, 124668 )# reshape againknn_argmax_out = knn_argmax_out.view(P)return knn_argmax_out                   # (124668)

十一、LiDAR Panoptic Segmentation(全景分割 2021)

A Benchmark for LiDAR-based Panoptic Segmentation based on KITTI, arXiv:2003.02371

十二、IA-SSD目标检测(CVPR 2022)

题目:End-to-End Multi-View Fusion for 3D Object Detection in LiDAR Point Clouds
论文:https://arxiv.org/abs/1910.06528v2

1.摘要

对于detector(即预测7自由度的三维box框,包括三维位置、三维尺寸、方向和类别标签)来说,前景点本质上比背景点更重要。基于此,论文提出了一种高效的单级基于point的3D目标检测器,称为IA-SSD:利用两种可学习的, 面向任务、实例感知 的下采样策略来分层选择属于感兴趣对象的前景点。此外,还引入了上下文质心感知模块,以进一步估计精确的实例中心。为了提高效率,论文按照 纯编码器 架构构建了IA-SSD。

由于三维点云的非结构化和无序性质,早期的工作通常首先将原始点云转换为中间规则表示,包括将三维点云投影到鸟瞰视图或正面视图的二维图像,或转换为密集的三维体素(3D-2D投影或体素化引入了量化误差),基于点的pipeline,许多重要的前景点在最终的边界框回归步骤之前被丢弃。
在kitti数据集上:

2、相关工作(综述)

1.基于Voxel方法

为了处理非结构化三维点云,基于体素的检测器通常首先将不规则点云转换为规则体素网格,这允许利用成熟的卷积网络架构。早期的工作,对输入点云进行密集体素化,然后利用卷积神经网络学习特定的几何模式。然而,效率是这些方法的主要限制之一,因为计算和内存成本随着输入分辨率呈立方体增长。为此,Yan等人[49]通过利用3D子流形稀疏卷积[9],提出了一种称为SECOND的高效架构。通过减少对空体素的计算,计算和存储效率显著提高。此外,提出了PointPillars,以进一步将体素简化为pillars (即仅在平面中进行体素化)。

现有的方法大致可分为单级检测器[7,11,54,55,57,58]和两级检测器[4,36–39,53]。尽管简单有效,但由于空间分辨率降低和结构信息不足,尤其是对于具有稀疏点的小对象,它们通常无法实现令人满意的检测性能。为此,SA-SSD通过引入辅助网络来利用结构信息。Ye等人[54]介绍了一种混合体素网络(HVNet),用于集中和投影多尺度特征图,以获得更好的性能。郑等人[58]提出了置信IoU感知(CIA-SSD )网络来提取空间语义特征,用于目标检测。相比之下,两级检测器可以获得更好的性能,但计算/存储成本较高。Shi等人[39]提出了一种两级检测器,即Part-A2,它由Part-aware和聚合模块组成。Deng等人[5]通过引入完全卷积网络来扩展PV-RCNN[36],以进一步利用原始点云的体积表示并同时进行细化。

总的来说,基于体素的方法可以实现良好的检测性能和良好的效率。然而,体素化不可避免地引入量化损耗。为了补偿预处理阶段的结构失真,需要在[20,25,27,28,35]中引入复杂的模块设计,这反过来会大大降低最终检测效率。此外,考虑到复杂的几何结构和各种不同的对象,在实践中确定最佳分辨率并不容易。

2.基于Point方法

与基于体素的方法不同,基于点的方法[30,38,52]直接从非结构化点云学习几何,进一步为感兴趣的对象生成特定proposal。考虑到3D点云的无序性,这些方法通常采用PointNet[31]及其变体[22、32、33、45、47],使用对称函数聚合独立的逐点特征。Shi等人[38]提出了PointRCNN,一种用于3D对象检测的两阶段3D区域proposal框架。该方法首先从分割的前景点生成对象建议,然后利用语义特征和局部空间线索回归高质量的三维边界框。Qi等人[30]介绍了VoteNet,这是一种基于深度Hough投票的单级point 3D检测器,用于预测实例质心。受2D图像中单级检测器[21]的启发,Yang等人[52]提出了一种3D单级检测(3DSSD)框架,而关键是融合采样策略,包括特征和欧几里德空间上的最远点采样。PointGNN[40]是一个将图形神经网络推广到3D对象检测的框架。

基于点的方法直接在原始点云上操作,无需任何额外的预处理步骤(如体素化),因此通常直观直观。然而,基于点的方法的主要瓶颈是学习能力不足和效率有限。

3.Point-Voxel方法

为了克服基于点的方法(即不规则和稀疏的数据访问、较差的内存局部性[23])和基于体素方法(如量化损失)的缺点,已经开始使用几种方法[3、16、36、37、53]从3D点云学习点-体素联合表示。PV-RCNN【36]及其后续工作[37]从体素抽象网络中提取逐点特征,以细化从三维体素主干生成的proposal。

HVPR[29]是一种单级3D探测器,通过引入高效内存模块以增强基于点的功能,从而在准确性和效率之间提供更好的折衷。Qian等人[34]提出了一种轻量级区域聚合细化网络(BANet)通过局部邻域图构造,产生更精确的box边界预测。

3.创新点

梗概: 首先将输入的激光雷达点云送入网络以提取逐点特征,然后进行拟议的实例感知下采样,以逐步降低计算成本,同时保留信息丰富的前景点。学习的潜在特征进一步输入到上下文质心感知模块,以生成实例建议并回归最终边界框。

1.实例感知下采样策略

计算效率前景点的保留 之间实现理想的权衡。为此,论文首先进行了一项实证研究,以定量评估不同的抽样方法,并遵循常用的编码体系结构(即具有4个编码层的PointNet++[32]),评估了随机点采样[14]、基于欧几里德距离的FPS(D-FPS)[32]和特征距离(Feat FPS)[52]等方法。

实验显示,在多次随机下采样操作后,实例召回率显著下降,表明大量前景点已被删除。D-FPS和Feat FPS在早期阶段都实现了相对较好的实例召回率,但在最后一个编码层也无法保留足够的前景点。因此,精确检测感兴趣的目标仍然是一项挑战,特别是对于行人和骑自行车的人等小目标,在这些小目标中只剩下极有限的前景点。

为了尽可能多地保留前景点,论文利用每个点的潜在语义,因为随着分层聚合在每个层中运行,学习的点特征可能包含更丰富的语义信息。根据这一思想,论文通过将前景语义先验合并到网络训练管道中,提出了以下两种面向任务的采样方法:

Class-aware Sampling

该采样策略旨在学习每个点的语义,从而实现选择性下采样。为了实现这一点,论文引入了额外的分支来利用潜在特征中丰富的语义。通过将两个MLP层附加到编码层,以进一步估计每个点的语义类别:

C表示类别数,si表示one-hot标签,si^表示预测Logit。在推理过程中,具有前k个前景分数的点被保留,并被视为馈送到下一编码层的代表点(为保留更多的前景点,实现了较高的实例召回率)。

Centroid-aware Sampling

考虑到实例中心估计是最终目标检测的关键,进一步提出了一种质心感知下采样策略,为更接近实例质心的点赋予更高的权重。将实例i的soft point mask定义如下:

其中f∗, b∗, l∗, r∗, u∗, d∗ 分别表示点到边界框的6个surface(前、后、左、右、上和下)的距离。在这种情况下,靠近长方体质心的点可能具有更高的mask score(最大值为1),而位于surface上的点的mask分数为0。在训练期间,soft point mask将用于根据空间位置为边界框内的点指定不同的权重,因此将几何先验隐含地纳入网络训练。

将soft point mask与前景点的损失项相乘,以便为中心附近的点分配更高的概率。注意,在推理过程中不再需要box,如果模型训练充分,只需在下采样后保留得分最高的前k个点。

2.上下文实例质心感知

上下文质心预测

受2D图像中上下文预测成功的启发[6,51],论文试图利用边界框周围的上下文线索(质心预测)。遵循[30]明确预测偏移量∆ c到实例中心,并添加了正则化项以最小化质心预测的不确定性,质心预测损失公式如下:

基于质心的实例聚合

对于移位代表(质心)点,进一步利用PointNet++模块学习每个实例的潜在表示。将相邻点转换为局部规范坐标系,然后通过共享MLP和对称函数聚合点特征。

Proposal Generation Head

将聚集的质心点特征输入到提案生成头中,预测具有带有类别的bounding box。论文将proposal编码为具有位置、规模和方向的多维表示。最后,所有proposal都通过具有特定IoU阈值的3D-NMS后处理进行过滤。

4.实验细节

为了提高效率,论文基于单级编码器体系结构构建了IA-SSD。SA层[32]用于提取逐点特征,并使用具有递增半径组的多尺度分组([0.2,0.8]、[0.8,1.6]、[1.6,4.8])来稳定地提取局部几何特征。考虑到早期层中包含的有限语义,在前两个编码层中采用D-FPS,然后是所提出的实例感知下采样。256个代表点特征被馈送到上下文质心预测模块中,然后是三个MLP层(256→256→3) 以预测实例质心。最后,添加分类和回归层(三个MLP层)以输出语义标签和相应的边界框。

十三、 SoftGroup (CVPR 2022)

1.摘要

本文提出了一种被称为SoftGroup的三维实例分割方法,通过 自下而上的soft grouping自上而下的细化来 完成。SoftGroup 允许每个点与多个类别相关联,以减轻语义预测错误带来的问题,并通过学习将其归类为背景来抑制false positive实例。

2.设计理念

最先进的方法如 HAIR、point group、superpoint tree 将3D实例分割视为自下而上的pipeline。他们学习逐点语义标签和中心偏移向量,然后将具有较小几何距离的相同标签的点分组到实例中。这些分组算法是在hard语义预测上执行的,其中一个点与单个类相关联。在许多情况下,目标是局部模糊的,输出的语义预测显示不同部分的不同类别,因此使用hard语义预测进行实例分组会导致两个问题:1.预测实例与ground-truth值之间的低重叠度 2.来自错误语义区域的额外false-positive实例

3. Method

SoftGroup的整体架构如图所示,分为两个阶段。在自下而上分组阶段,逐点预测网络将点云作为输入并生成逐点语义标签和偏移向量。软分组模块(第3.2节)处理这些输出以产生初步的实例proposal。在自上而下的细化阶段,基于proposal,从主干中提取相应的特征,并用于预测类、实例mask和mask分数作为最终结果。

Point-wise Prediction Network阶段包括体素化网格U-Net主干获得点特征、语义分支偏移分支组成。与softgroup、HAIR相似,后面主要讲soft grouping部分:
软分组模块接收语义分数和偏移向量作为输入并生成实例proposal。首先,偏移向量用于将点移向相应的实例中心。为了使用语义分数进行分组,我们定义了一个分数阈值τ来确定一个点属于哪些语义类,允许该点与多个类相关联。给定语义分数S ∈ R (N × Nclass) ,我们遍历N_class 类,并且在每个类索引处,我们对整个场景的一个点子集进行切片,该子集的分数(相对于类索引)高于阈值τ。按照**[softgroup]**对每个点子集进行分组。由于每个子集中的所有点都属于同一类,因此我们只需遍历子集中的所有点,并创建几何距离小于分组bandwidth b的点之间的链接即可获得实例proposal。对于每次迭代,对整个扫描的一个点子集进行分组,确保快速推理。整体实例proposal是来自所有子集的proposal的联合。

我们注意到现有的基于proposal的方法通常将边界框视为目标proposals,然后在每个proposal中执行分割。直观地说,与实例高度重叠的边界框的中心应该靠近目标中心。但是,在3D点云中生成高质量的边界框proposals具有挑战性,因为该点仅存在于目标表面上。相反,SoftGroup依赖于更准确的point-level proposal,并且自然地继承了点云的分散属性。

由于来自分组的实例proposals的质量高度依赖于语义分割的质量,我们定量分析了τ对语义预测的召回率和精度的影响,最终设置为0.2,精度接近50%,导致前景和背景点之间的比率,以确保阶段是平衡的。

4.Top-Down Refinement

自上向下的细化阶段从自底向上的分组阶段对实例proposal进行分类和细化。特征提取器层处理每个proposals以提取其相应的主干特征。在预测分类分数、实例mask和后续分支的mask分数之前,提取的特征被输入到一个微小的U-Net网络(具有少量层的U-Net类型的网络)





带* 分别是分类、分割和mask评分目标。K 是proposal的总数,1 { . } 表示proposal是否为正样本。整体损失为:

实验结果

5 .代码解读

  1. self.forward_backbone提取特征,得到每个点类别(13)、偏移(3)、特征(32维)

    def forward_4_parts(self, x, input_map): 场景切成4部分,依次提取特征:
    for i in range(4):out = self.input_conv(x.feature[i])   # 子流型稀疏卷积,升维:eat(55914, 6),indice(55914, 4) --> feat(55914, 32)out = self.unet(out)           # (55914,32) -->(55914,32),具体代码见下行out = self.output_layer(out)   # BN(32)、Relu
    
output = self.blocks(input)
# 1.self.blocks包含两个相同的block,构成为 横等映射、BN、RElu、自流型稀疏卷积(前后输入维度不变)
# SubMConv3d(128, 128, kernel_size=[3, 3, 3], stride=[1, 1, 1], padding=[1, 1, 1], dilation=[1, 1, 1], output_padding=[0, 0, 0], bias=False)identity = spconv.SparseConvTensor(output.features, out.indices, out.spatial_shape,out.batch_size)
if len(self.nPlanes) > 1:output_decoder = self.conv(output)        # 2.self.conv 为稀疏卷积,每次循环会降维: (55914,32)->(19384,64)->(5477,96)->(344,160) -(12,224)# sparseConv3d(128, 160, kernel_size=[2, 2, 2], stride=[2, 2, 2], padding=[0, 0, 0], dilation=[1, 1, 1], output_padding=[0, 0, 0], bias=False)output_decoder = self.u(output_decoder)   # 会跳到上一行 self.blocks 处output_decoder = self.deconv(output_decoder)# 3.反稀疏卷积 self.deconv = spconv.SparseSequential(norm_fn(224), nn.ReLU(),spconv.SparseInverseConv3d(224,192,kernel_size=2,bias=False, indice_key='spconv{}'.format(indice_key_id)))out_feats = torch.cat((identity.features, output_decoder.features), dim=1)output = output.replace_feature(out_feats)output = self.blocks_tail(output)# 4.self.blocks_tail分为两部分:横等映射部分为 Custom1x1Subm3d(384, 192,k=1,s=1)是矩阵乘法;conv-branch部分为子流型稀疏卷积SubMConv(384,192)
return output

随后对32维特征进行分类和回归:

semantic_scores = self.semantic_linear(output_feats)       # (344097,13)
pt_offsets = self.offset_linear(output_feats)              #  (344097,3)
semantic_preds = semantic_scores.max(1)[1]                 # (344097, 13) --> (344097)
  1. self.forward_grouping
proposals_idx, proposals_offset = self.forward_grouping(semantic_scores, pt_offsets, batch_idxs, coords_float, self.grouping_cfg)
# semantic_scores(n=344097,13) pt_offsets(n,3)coords(n,3) batch_idxs(n)*[0,0,0..]







十四、ContrastBoundary(CVPR2022)

Contrastive Boundary Learning for Point Cloud Segmentation
代码:https://github.com/LiyaoTang/contrastBoundary

1、背景

大多数以前的3D分割方法通常忽略了场景边界的分割。尽管一些方法考虑了边界,但它们仍然缺乏明确和全面的研究来分析边界区域的分割性能。它们在整体分割性能上的表现也不尽如人意。特别是,当前流行的分割指标缺乏对边界性能的具体评估,这就使得现有方法无法更好的展示边界分割的质量。为了更清楚地了解边界的性能,本文中作者分别计算了边界区域和内部(非边界)区域的mIoU。通过比较不同区域类型的表现以及整体表现,直接揭示边界区域的不理想表现。

此外,为了更全面地描述边界上的性能,作者考虑了GT中边界与模型分割结果中的边界之间的对齐。提出了一种新颖的对比边界学习(Contrastive Boundary Learning,CBL)框架,以更好地将模型预测的边界与真实数据的边界对齐。如图1所示,CBL优化了边界区域点的特征表示模型,增强了跨场景边界的特征识别。此外,为了使模型更好地了解多个语义尺度的边界区域,作者设计了一种子场景边界挖掘策略,该策略利用子采样过程来发现每个子采样点云中的边界点。具体来说,CBL在不同的子采样阶段进行操作,并促进3D分割方法来学习更好的边界区域周围的特征表示。

本文主要贡献如下:

(1) 探索了当前3D点云分割中存在的边界问题,并使用考虑边界区域的指标对其进行量化。结果表明,目前的方法在边界区域的准确性比它们的整体性能差得多。

(2) 提出了一种新颖的对比边界学习(CBL)框架,它通过对比场景边界上的点特征来改进特征表示。因此,它提高了边界区域周围的分割性能,从而提高了整体性能。

(3) 通过全面的实验证明CBL可以在边界区域以及所有baseline的整体性能上带来显着且一致的改进。这些实验结果进一步证明了CBL对提高边界分割性能是有效的,准确的边界分割对于鲁棒的3D分割很重要。

2、核心思想

A.边界分割:

由于当前大部分工作都集中在改进一般指标,例如mean intersection over union(mIoU)、overall accuracy(OA)和mean average precision(mAP),点云分割中的边界质量通常被忽略。与最近的边界相关工作仅给出边界的定性结果不同,作者引入了一系列用于量化边界分割质量的指标,包括mIoU@boundary、mIoU@inner和来自2D实例分割任务的边界IoU(B-IoU)分数。

主要代码

1.数据预处理

train_transform = t.Compose([t.RandomScale([0.9, 1.1]),  # 缩放t.ChromaticAutoContrast(),  # 增强 rgb对比度t.ChromaticTranslation(),   # 在 rgb上添加随机噪声t.ChromaticJitter(),        # 扰动 jitter on rgbt.HueSaturationTranslation(),   # aug on hue & saturation])if transform:coord, feat, label = transform(coord, feat, label)
if voxel_size:# 按0.04体素下采样, 36w点 -> 27294个点coord_min = np.min(coord, 0)coord -= coord_minuniq_idx = voxelize(coord, voxel_size)coord, feat, label = coord[uniq_idx], feat[uniq_idx], label[uniq_idx]      init_idx = label.shape[0] // 2
coord_init = coord[init_idx]           # [0.58, 2.41, 2.33]
if shuffle_index:shuf_idx = np.arange(coord.shape[0])np.random.shuffle(shuf_idx)coord, feat, label = coord[shuf_idx], feat[shuf_idx], label[shuf_idx]
xyz = coordfeat = torch.FloatTensor(feat) / 255.

2、model architecture

模型由encodedecodehead三部分组成

pxo = inputs['points'], inputs['features'], inputs['offset']
p0, x0, o0 = pxo[:3]  # (n=473580, 3), (n, c), (b) - c = in_feature_dims
x0 = torch.cat((p0, x0), 1)# ---------------------1.encoder:点云下采样,增加维度-------------------------------
p1, x1, o1 = self.enc1([p0, x0, o0])       # (473580,3)(473580,32) [47358,94716,142074...473580]
p2, x2, o2 = self.enc2([p1, x1, o1])       # (118390,3)(118390,64) [11839,23678,35517... 118390]
p3, x3, o3 = self.enc3([p2, x2, o2])       # (29590,3) (29590,128) [2959,...              29590]
p4, x4, o4 = self.enc4([p3, x3, o3])       # (7390,3) (7390,256)   [739,...                7390]
p5, x5, o5 = self.enc5([p4, x4, o4])       # (1840,3) (1840,512)   [184,...                1840]
down_list = [{'p_out': p1, 'f_out': x1, 'offset': o1},  # (n, 3), (n, base_fdims), (b) - default base_fdims = 32{'p_out': p2, 'f_out': x2, 'offset': o2},  # n_1{'p_out': p3, 'f_out': x3, 'offset': o3},  # n_2{'p_out': p4, 'f_out': x4, 'offset': o4},  # n_3{'p_out': p5, 'f_out': x5, 'offset': o5},  # n_4 - fdims = 512]stage_list['down'] = down_list# --------------------2.decoder:双线性插值,重建上一层特征-----------------------
x5 = self.dec5[1:]([p5, self.dec5[0]([p5, x5, o5]), o5])[1]  # no upsample - concat with per-cloud mean: mlp[ x, mlp[mean(x)] ] (1840,512)
x4 = self.dec4[1:]([p4, self.dec4[0]([p4, x4, o4], [p5, x5, o5]), o4])[1]        # (7390,256)
x3 = self.dec3[1:]([p3, self.dec3[0]([p3, x3, o3], [p4, x4, o4]), o3])[1]        # (29590,128)
x2 = self.dec2[1:]([p2, self.dec2[0]([p2, x2, o2], [p3, x3, o3]), o2])[1]        # (118390,64)
x1 = self.dec1[1:]([p1, self.dec1[0]([p1, x1, o1], [p2, x2, o2]), o1])[1]        # (473580,32)
up_list = [{'p_out': p1, 'f_out': x1, 'offset': o1},  # n_0 = n, fdims = 32{'p_out': p2, 'f_out': x2, 'offset': o2},  # n_1{'p_out': p3, 'f_out': x3, 'offset': o3},  # n_2{'p_out': p4, 'f_out': x4, 'offset': o4},  # n_3{'p_out': p5, 'f_out': x5, 'offset': o5},  # n_4 - fdims = 512 (extracted through dec5 = mlps)]stage_list['up'] = up_listif self.head is not None:x, stage_list = self.head(stage_list)return x, stage_list

01.encoder 分为TransitionDownPointTransformerBlock两部分

class TransitionDown(nn.Module):def __init__(self, in_planes, out_planes, stride=1, nsample=16):super().__init__()self.stride, self.nsample = stride, nsampleif stride != 1:self.linear = nn.Linear(3+in_planes, out_planes, bias=False)self.pool = nn.MaxPool1d(nsample)else:self.linear = nn.Linear(in_planes, out_planes, bias=False)self.bn = nn.BatchNorm1d(out_planes)self.relu = nn.ReLU(inplace=True)def forward(self, pxo):p, x, o = pxo               # (n=113890, 3), (n, c=64), (b) :[11839,23678,...118390]if self.stride != 1:        # 第一个encode 不做下采样,为False# calc the reduced size of points for the batch - n = [BxN], with each b clouds, i-th cloud containing N = b[i] pointsn_o, count = [o[0].item() // self.stride], o[0].item() // self.stride      # 11839//4 = 2959for i in range(1, o.shape[0]):count += (o[i].item() - o[i-1].item()) // self.striden_o.append(count)n_o = torch.cuda.IntTensor(n_o)1.最远点采样,得到4倍下采样点idx = pointops.furthestsampling(p, o, n_o)  n_p = p[idx.long(), :]  # (m, 3)2.self.nsample=8或16,来提取邻域特征并做池化x = pointops.queryandgroup(self.nsample, p, n_p, x, None, o, n_o, use_xyz=True)  # (m, 3+c, nsample)x = self.relu(self.bn(self.linear(x).transpose(1, 2).contiguous()))  # (m, c, nsample)x = self.pool(x).squeeze(-1)  # (m, c)p, o = n_p, n_oelse:x = self.relu(self.bn(self.linear(x)))  # (n, c)return [p, x, o]

PointTransformerBlock 主要模块为 transformer2

class PointTransformerBlock(nn.Module):expansion = 1def __init__(self, in_planes, planes, share_planes=8, nsample=16):super(PointTransformerBlock, self).__init__()self.linear1 = nn.Linear(in_planes, planes, bias=False)self.bn1 = nn.BatchNorm1d(planes)self.transformer2 = PointTransformerLayer(planes, planes, share_planes, nsample)self.bn2 = nn.BatchNorm1d(planes)self.linear3 = nn.Linear(planes, planes * self.expansion, bias=False)self.bn3 = nn.BatchNorm1d(planes * self.expansion)self.relu = nn.ReLU(inplace=True)def forward(self, pxo):p, x, o = pxo  # (n, 3), (n, c), (b)identity = xx = self.relu(self.bn1(self.linear1(x)))  # mlpx = self.relu(self.bn2(self.transformer2([p, x, o])))  # - seems like trans/convolute - bn - relux = self.bn3(self.linear3(x))  # linear (+bn)x += identityx = self.relu(x)  # relu(x+shortcut)return [p, x, o]class PointTransformerLayer(nn.Module):def __init__(self, in_planes, out_planes, share_planes=8, nsample=16):super().__init__()self.mid_planes = mid_planes = out_planes // 1self.out_planes = out_planesself.share_planes = share_planesself.nsample = nsampleself.linear_q = nn.Linear(in_planes, mid_planes)self.linear_k = nn.Linear(in_planes, mid_planes)self.linear_v = nn.Linear(in_planes, out_planes)self.linear_p = nn.Sequential(nn.Linear(3, 3), nn.BatchNorm1d(3), nn.ReLU(inplace=True), nn.Linear(3, out_planes))self.linear_w = nn.Sequential(nn.BatchNorm1d(mid_planes), nn.ReLU(inplace=True),nn.Linear(mid_planes, mid_planes // share_planes),nn.BatchNorm1d(mid_planes // share_planes), nn.ReLU(inplace=True),nn.Linear(out_planes // share_planes, out_planes // share_planes))self.softmax = nn.Softmax(dim=1)def forward(self, pxo) -> torch.Tensor:p, x, o = pxo  # (n, 3), (n=1840, c=512), (b)1.特征映射到 qkv 三个维度 x_q, x_k, x_v = self.linear_q(x), self.linear_k(x), self.linear_v(x)  # (n=1840, c=512)2.在k=16/8 邻域内采样特征x_k = pointops.queryandgroup(self.nsample, p, p, x_k, None, o, o, use_xyz=True)  # (n=16, n=1840, 3+512=515)x_v = pointops.queryandgroup(self.nsample, p, p, x_v, None, o, o, use_xyz=False)  # (n=16, n=1840, c=512)p_r, x_k = x_k[:, :, 0:3], x_k[:, :, 3:]3.self.linear_p主要用来将坐标(3维)升维(例如128维),linear_w 则是降维。随后与特征拼接for i, layer in enumerate(self.linear_p): p_r = layer(p_r.transpose(1, 2).contiguous()).transpose(1, 2).contiguous() if i == 1 else layer(p_r)    # (n, nsample, c)w = x_k - x_q.unsqueeze(1) + p_r.view(p_r.shape[0], p_r.shape[1], self.out_planes // self.mid_planes, self.mid_planes).sum(2)  # (n=1840, nsample=16, c=512)for i, layer in enumerate(self.linear_w): w = layer(w.transpose(1, 2).contiguous()).transpose(1, 2).contiguous() if i % 3 == 0 else layer(w)w = self.softmax(w)  # (n=1840, nsample=16, c=64)n, nsample, c = x_v.shape; s = self.share_planesx = ((x_v + p_r).view(n, nsample, s, c // s) * w.unsqueeze(2)).sum(1).view(n, c)  # v * A (1840,512)return x
  1. decoderTransitionUpPointTransformerBlock 组成。 PointTransformerBlock主要结构为 transformer2,同enc
class TransitionUp(nn.Module):def __init__(self, in_planes, out_planes=None):super().__init__()if out_planes is None:self.linear1 = nn.Sequential(nn.Linear(2*in_planes, in_planes), nn.BatchNorm1d(in_planes), nn.ReLU(inplace=True))self.linear2 = nn.Sequential(nn.Linear(in_planes, in_planes), nn.ReLU(inplace=True))else:self.linear1 = nn.Sequential(nn.Linear(out_planes, out_planes), nn.BatchNorm1d(out_planes), nn.ReLU(inplace=True))self.linear2 = nn.Sequential(nn.Linear(in_planes, out_planes), nn.BatchNorm1d(out_planes), nn.ReLU(inplace=True))def forward(self, pxo1, pxo2=None):p1, x1, o1 = pxo1; p2, x2, o2 = pxo2   # unet左侧p4(7390,3)(7390,256)  unet右侧x5(1840,3) (1840,512)x = self.linear1(x1) + pointops.interpolation(p2, p1, self.linear2(x2), o2, o1)   # linear2(512,256)return xclass PointTransformerBlock(nn.Module):expansion = 1def __init__(self, in_planes, planes, share_planes=8, nsample=16):super(PointTransformerBlock, self).__init__()self.linear1 = nn.Linear(in_planes, planes, bias=False)self.bn1 = nn.BatchNorm1d(planes)self.transformer2 = PointTransformerLayer(planes, planes, share_planes, nsample)self.bn2 = nn.BatchNorm1d(planes)self.linear3 = nn.Linear(planes, planes * self.expansion, bias=False)self.bn3 = nn.BatchNorm1d(planes * self.expansion)self.relu = nn.ReLU(inplace=True)def forward(self, pxo):p, x, o = pxo  # (n, 3), (n, c), (b)identity = xx = self.relu(self.bn1(self.linear1(x)))  # mlpx = self.relu(self.bn2(self.transformer2([p, x, o])))  # - seems like trans/convolute - bn - relux = self.bn3(self.linear3(x))  # linear (+bn)x += identityx = self.relu(x)  # relu(x+shortcut)return [p, x, o]

03.head 包括4层MLP,用来对5层不同维度特征图统一将维到32,并拼接为160维

class MultiHead(nn.Module):def forward(self, stage_list):collect_list = []for (n, i), func in zip(self.ni_list, self.infer_list):rst = func(stage_list[n][i], 'f_out')  # process to desired fdim     (473580,32) (118390,64)->(118390,32)stage_list[n][i][self.ftype] = rst  # store backcollect_list.append(self.upsample(n, i, stage_list))  # (n, c) - potentially 上采样  5*( 473580,32)x = self.comb_ops(collect_list, 1)  # torch.cat -> (473580,160)x = self.cls(x)                     # (473580,160) => (473580,13)return x, stage_list

损失函数:

self.xen = nn.CrossEntropyLoss(ignore_index=config.ignore_label)
loss_list = [self.xen(output, target)]
if self.contrast_head is not None:loss_list += self.contrast_head(output, target, stage_list)def point_contrast(self, n, i, stage_list, target):p, features, o = fetch_pxo(n, i, stage_list, self.ftype)if self.project is not None:features = self.project[f'{n}{i}'](features)nsample = self.nsample[i]labels = get_subscene_label(n, i, stage_list, target, self.nstride, self.config.num_classes)  # 得到类别的onehot编码  (m=200089,cls=13)neighbor_idx, _ = pointops.knnquery(nsample, p, p, o, o) # (m=200089, nsample=36)# exclude self-loopnsample = self.nsample[i] - 1  neighbor_idx = neighbor_idx[..., 1:].contiguous()m = neighbor_idx.shape[0]# 得到m=200089个点的邻域nsample=35 的labelneighbor_label = labels[neighbor_idx.view(-1).long(), :].view(m, nsample, labels.shape[1]) # (m=200089, nsample=35, ncls=13)neighbor_feature = features[neighbor_idx.view(-1).long(), :].view(m, nsample, features.shape[1])# 得到该点标签,与其nsample=35 邻域标签相同点的位置索引posmask = self.posmask_cnt(labels, neighbor_label)  # (m, nsample) :bool型# 筛选出 与其邻域标签相同数量最多的点point_mask = torch.sum(posmask.int(), -1)  # (m=200089)point_mask = torch.logical_and(0 < point_mask, point_mask < nsample)# 求m个点特征,与其35邻域特征之间的L2距离dist = self.dist_func(features, neighbor_feature)    # (m=23180,35)# 将配对的 posmask作为正样本,dist 作为负样本,计算损失loss = self.contrast_func(dist, posmask)  # (m)def contrast_softnn(self, dist, posmask, invalid_mask=None):dist = -distdist = dist - torch.max(dist, -1, keepdim=True)[0]  # NOTE: max return both (max value, index)if self.temperature is not None:dist = dist / self.temperature        # 1exp = torch.exp(dist)if invalid_mask is not None:valid_mask = 1 - invalid_maskexp = exp * valid_maskpos = torch.sum(exp * posmask, axis=-1)  # (m)neg = torch.sum(exp, axis=-1)  # (m)loss = -torch.log(pos / neg + _eps)return loss

十五、RepSurf(CVPR2022 Oral)

题目:Surface Representation for Point Clouds,波士顿东北大学联合腾讯优图
代码地址: https://github.com/hancyran/RepSurf
论文地址: http://arxiv.org/abs/2205.05740

0.摘要
提出了 RepSurf(representative surface),一种新颖的点云表示,显式的描述了非常局部的点云结构

RepSurf 包含两种变体,Triangular RepSurf (轻量)和 Umbrella RepSurf,其灵感来自计算机图形学中的三角形网格和伞形曲率。我们在表面重建后通过预定义的几何先验计算 RepSurf 的表征。RepSurf 可以成为绝大多数点云模型的即插即用模块,这要归功于它与无规则点集的自由协作。

在只有0.008M参数数量、0.04G FLOPs 和 1.12ms推理时间的增的情况下,我们的方法在分类数据集 ModelNet40 上达到 94.7% (+0.5%),在 ScanObjectNN 上达到 84.6% (+1.8%) ;而在分割任务的 S3DIS 6-fold 上达到74.3%(+0.8%) mIoU,在ScanNet 上达到70.0% (+1.6%) mIoU 。检测在 ScanNetV2 上达到71.2% (+2.1%) mAP25、54.8% (+2.0%) mAP50 和在 SUN RGB-D数据集上64.9% (+1.9%) mAP25、47.7% (+ 2.5%) mAP50的性能。

1.点云提取方法

受到泰勒级数的启发。泰勒级数用导数表示局部曲线。为了简化它,我们只考虑到二阶导数。因此,我们可以通过其对应的切线粗略地表示局部曲线,或者我们称之为 3D 点云中的“surface”。

2.伪代码


#、常用点云分割数据集

代码如下(示例):


总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

【3D点云】算法总结(持续汇总)相关推荐

  1. 使用python实现3D点云算法模型

    你想问的是如何使用 Python 实现 3D 点云算法模型吗?如果是,我可以给你一些指引. 首先,你需要准备好 3D 点云数据.这可以通过扫描物体或环境来生成,也可以从已有的数据集中获取.然后,你可以 ...

  2. 面向自动驾驶领域的3D点云目标检测方法汇总!(单模态+多模态/数据+代码)

    背景介绍 3D检测用于获取物体在三维空间中的位置和类别信息,主要基于点云.双目.单目和多模态数据等方式.其中,点云数据由于具有较为丰富的几何信息,相比于其它单模态数据更为稳定,基于激光雷达点云数据的3 ...

  3. 激光雷达和3D点云算法

    文章目录 1.1 激光雷达硬件平台 1.2 激光雷达原理 1.3 三维激光系统研发难点 1.4 点云应用方向 1.5 点云分类,点云分割,点云特征提取(pointnet++) 1.6 点云补全(PF- ...

  4. ECCV 2020 3D点云 Point Cloud 文章汇总

    一.点云文章资源 近年来,对于点云处理的研究越来越火热.Github上面有一个工程,汇总了从2017年以来各大会议上点云论文,awesome-point-cloud-analysis ,本文作者之前整 ...

  5. 老子云打造3D技术云服务平台,加速三维互联网变革进程

    高新科技发展日新月异,已悄然改变我们的生活,而三维技术在互联网的迅猛发展,让"视界"立起来的梦想照进现实. 十三届全国人大四次会议表决通过关于"十四五"规划和2 ...

  6. 3D点云补全算法汇总及最新进展

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 Part 1  前言 在探讨3D 点云补全专题前,先介绍三个概念: 概念一:partial obser ...

  7. 汇总|基于3D点云的深度学习方法

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达本文转自|计算机视觉工坊 前言 三维数据通常可以用不同的格式表示,包 ...

  8. 基于激光雷达点云的3D目标检测算法—端到端多视图融合

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者丨Rubicon007@知乎 来源丨https://zhuanlan.zhihu.com/p/44 ...

  9. 史上最全综述:3D目标检测算法汇总!

    来源:自动驾驶之心 本文约16000字,建议阅读10+分钟 本文将演示如何通过阈值调优来提高模型的性能.本文的结构安排如下:首先,第2节中介绍了3D目标检测问题的定义.数据集和评价指标.然后,我们回顾 ...

最新文章

  1. 编写程序判断等腰、等边或者普通三角形
  2. 失败在大学生活中的三种功能
  3. 在线音乐网站Pandora申请IPO融1亿美元
  4. 既生Flash,又何生EEPROM?
  5. Lambda表达式Java教程
  6. Python之数据重塑——【stack()方法和unstack()方法、pivot()方法】
  7. JZs3c2440裸板程序GPIO操作总结
  8. 身体出现十个信号当心短命
  9. 素数筛选法--hdu4548美素数
  10. namenode和datanode的功能分别是什么_海德堡印刷机电路板分别是什么功能
  11. tcp/ip协议详解
  12. AI量化交易(二)——Tushare财经数据框架
  13. ABP框架----写一个WebAPI
  14. Ubuntu扩展磁盘内存
  15. input、textarea等输入框输入中文时,拼音在输入框内会触发input事件的问题
  16. 查表程序c8051汇编语言,汇编程序 查表求平方的实现
  17. 计算机名更改后重启名称又变回来了,修改主机DNS重启后又恢复,奇怪了.
  18. 深度学习数据标注工具
  19. 3dmax2017骨骼蒙皮
  20. H5新特性:canvas学习1

热门文章

  1. 学习笔记三十:IO流(二)
  2. 详解剖析Shell中的正则表达式
  3. k的倍数(k小于等于12)
  4. 如何对付团队中的“害群之马”
  5. Java实现 蓝桥杯 算法训练 数字游戏
  6. mx450计算机专业的够用吗,新一代MX450独显轻薄本已经开卖,有几个点需要注意...
  7. 计算机二级Python真题(二)
  8. 深入理解梅尔刻度、梅尔滤波器组和梅尔时频谱图
  9. “过了腊八就是年” 中国各地举办民俗活动共庆腊八节
  10. linux firefox 段错误,中标麒麟v10在arm64下360浏览器(chromium)访问崩溃(段错误)