道同志合网

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

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

文章目录

  • 前言
  • 一、云算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)
    • 一、法总简介
    • 二、结持取样
    • 三、续汇局部特征聚合
    • 四、云算补充与实验
  • 六、法总SqueezeSeg V3(分割 2021oral)
    • 一、结持投影公式
    • 二、续汇SAC卷积
    • 三、云算损失函数
    • 四、法总重点代码
  • 七、结持LiDAR Panoptic Segmentation(全景分割 2021)
  • 八、IA-SSD目标检测(2022 CVPR)
    • 1.摘要
    • 2、相关工作(综述)
      • 1.基于Voxel方法
      • 2.基于Point方法
      • 3.Point-Voxel方法
    • 3.创新点
      • 1.实例感知下采样策略
      • 2.上下文实例质心感知
    • 4.实验细节
  • 九、RepSurf(CVPR2022 Oral)
    • 1.点云提取方法
    • 2.伪代码
  • #、常用点云分割数据集
  • 总结

前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、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 centroidsdef 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))      # 这里是(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.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)))                             # 再映射回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个用来检测,最终的比较结果为:
在这里插入图片描述

六、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      # 重构的图像从左上角(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]                                                             # 124668unproj_xyz = torch.full((self.max_points, 3), -1.0, dtype=torch.float)     # 15000unproj_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_maskelse:   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 sequencepath_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

未经允许不得转载:道同志合网 » 【3D点云】算法总结(持续汇总)