【3D点云】分割算法总结(持续汇总)


前言(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, 3
    S = npoint                            # 1024
    fps_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_norm
    if 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.device
    B, 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] = farthest
        centroid = xyz[batch_indices, farthest, :].view(B, 1, 3)
        dist = torch.sum((xyz - centroid) ** 2, -1)
        mask = dist < distance
        distance[mask] = dist[mask]
        farthest = torch.max(distance, -1)[1]
    return centroids

def index_points(points, idx):
    device = points.device
    B = 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 radius
        nsample: max sample number in local region
        xyz: all points, [B, N, 3]
        new_xyz: query points, [B, S, 3]
    Return:
        group_idx: grouped points index, [B, S, nsample]
    """
    device = xyz.device
    B, N, C = xyz.shape               # 8, 4096, 3
    _, S, _ = new_xyz.shape                   # 1024
    group_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] = N
    group_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 == N
    group_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))      # 这里是(1232) (3232) (3264)
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.shape

        if 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 / norm
            interpolated_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_points

        new_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)))                             # 再映射回256return 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相比,透视图体素化可以捕获互补信息,这在对象距离远、采样稀疏的情况下尤其有用。

三、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个用来检测,最终的比较结果为:
在这里插入图片描述

四、 PointGroup (实例分割 CVPR 2020)

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

1.摘要

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

2.架构

与2D图像不同,3D场景不存在视野遮挡问题,3D中分散的目标是通常由空隙空间自然隔开。下图概述了我们方法的架构,它具有三个主要组件,即骨干网点聚类部分和ScoreNet
在这里插入图片描述
首先,我们使用主干网络来提取每个点的特征F,

骨干网络:可选择多种网络。在我们的实现中,我们对这些点进行体素化,并构建具有子流形稀疏卷积(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 = xyz
    N,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 * attention
    new_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 externally
        if the value of the constructor was not set (in case you change your
        mind about wanting the projection)
    """
    # 雷达参数
    fov_up = self.proj_fov_up / 180.0 * np.pi               # 视野的up值,固定参数:3/180*pi = 0.0523
    fov_down = self.proj_fov_down / 180.0 * np.pi  # 视野的 down 值:-25/180*pi = -0.43
    fov = abs(fov_down) + abs(fov_up)                          # 整体视野范围     0.488

    # 得到所有点的深度
    depth = np.linalg.norm(self.points, 2, axis=1)            # (124668) 个点的2范数

    # get scan components
    scan_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                                                             # 归一化的角度*2048
    proj_y *= self.proj_H                                                             # 归一化的角度*64

    # round and clamp for use as index
    proj_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 order

    proj_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  
    # 重构的图像从左上角(00)到右下角(632048),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)* -1
   proj_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 indexes
          unproj_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 scan
        pred_np = unproj_argmax.cpu().numpy()
        pred_np = pred_np.reshape((-1)).astype(np.int32)

        # map to original label
        pred_np = to_orig_fn(pred_np)            # 从[0-19]映射回原来类别

        # save scan
        path = 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 prediction
        pred_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 = nclasses
    print("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 do
        something REALLY smart to handle unaligned number of points in memory
    '''
    # get device
    if proj_range.is_cuda:
      device = torch.device("cuda")
    else:
      device = torch.device("cpu")

    # sizes of projection scan
    H, W = proj_range.shape                           # 64, 2048

    # number of points
    P = unproj_range.shape                            # 124668

    # check if size of kernel is odd and complain
    if (self.search % 2 == 0):
      raise ValueError("Nearest neighbor kernel must be odd number")

    # calculate padding
    pad = 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 points
    idx_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.search
    unproj_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 points
    center = int(((self.search * self.search) - 1) / 2)                                  # 24
    unproj_unfold_k_rang[:, center, :] = unproj_range                        # ( 1, 49, 124668 ) : 49中的第24为depth(124668)

    # now compare range
    k2_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 more
    inv_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 weighing
    k2_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 argmax
    proj_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 pixel
    knn_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 items
    if 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 cutoff
    knn_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 again
    knn_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

在这里插入图片描述

十三、 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) -12224)
    # 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(224192,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(384192return 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..]
# 1.设置超参数:实例半径0.04、平均实例数300、阈值0.05、各类别点云的平均数量(一个场景中)
radius ,mean_active ,npoint_thr= self.grouping_cfg.radius/mean_active/npoint_thr   
class_numpoint_mean = torch.tensor( self.grouping_cfg.class_numpoint_mean, dtype=torch.float32)       
  # 13*[1823.,7457.,6189.,7424.,,,39796.] 每一类别的点云中位数,属于先验

# 2.遍历13个类别
for class_id in range(self.semantic_classes):
    if class_id in self.grouping_cfg.ignore_classes:                  # 忽略类别01
       continue
    scores = semantic_scores[:, class_id].contiguous()

    # 3.筛掉分数低于0.2的类别,以及其中点云数少于100的类别
    object_idxs = (scores > self.grouping_cfg.score_thr).nonzero().view(-1)     # > 0.2  --> index:(234432)
    if object_idxs.size(0) < self.test_cfg.min_npoint:                          # < 100,点云数少于100略过
       continue
    batch_idxs_ = batch_idxs[object_idxs]                             # (234432 *[batch 0])
    batch_offsets_ = self.get_batch_offsets(batch_idxs_, batch_size)  # [ 0, 234432]
    idx, start_len = ballquery_batch_p(coords_ + pt_offsets_, batch_idxs_, batch_offsets_,
                      radius, mean_active)   # (66365386): [10618,10632,...130336,230608] , (234432, 2)

九、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 rgb
        t.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_min
    uniq_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 = coord

feat = 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_list

if 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, nsample
        if 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] points
            n_o, count = [o[0].item() // self.stride], o[0].item() // self.stride      # 11839//4 = 2959
            for i in range(1, o.shape[0]):
                count += (o[i].item() - o[i-1].item()) // self.stride
                n_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=816,来提取邻域特征并做池化
            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_o
        else:
            x = self.relu(self.bn(self.linear(x)))  # (n, c)
        return [p, x, o]

PointTransformerBlock 主要模块为 transformer2

class PointTransformerBlock(nn.Module):
    expansion = 1

    def __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 = x
        x = self.relu(self.bn1(self.linear1(x)))  # mlp
        x = self.relu(self.bn2(self.transformer2([p, x, o])))  # - seems like trans/convolute - bn - relu
        x = self.bn3(self.linear3(x))  # linear (+bn)
        x += identity
        x = 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 // 1
        self.out_planes = out_planes
        self.share_planes = share_planes
        self.nsample = nsample
        self.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_planes
        x = ((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 x


class PointTransformerBlock(nn.Module):
    expansion = 1

    def __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 = x
        x = self.relu(self.bn1(self.linear1(x)))  # mlp
        x = self.relu(self.bn2(self.transformer2([p, x, o])))  # - seems like trans/convolute - bn - relu
        x = self.bn3(self.linear3(x))  # linear (+bn)
        x += identity
        x = 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 back
            collect_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-loop
        nsample = self.nsample[i] - 1  
        neighbor_idx = neighbor_idx[..., 1:].contiguous()
        m = neighbor_idx.shape[0]

        
        # 得到m=200089个点的邻域nsample=35 的label
        neighbor_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 = -dist
                   dist = 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        # 1
                   exp = torch.exp(dist)

                   if invalid_mask is not None:
                       valid_mask = 1 - invalid_mask
                       exp = exp * valid_mask

                   pos = 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.伪代码

在这里插入图片描述
在这里插入图片描述

#、常用点云分割数据集

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45752541/article/details/126330335