随笔 - 868  文章 - 1  评论 - 61 

RandLA-Net: Efficient Semantic Segmentation of Large-Scale Point Clouds翻译和解读

RandLA-Net: 大场景下点云的有效语义分割
本文是2020.5月份刊出的文章,发在2020CVPR

作者来自于牛津大学,中山大学,国防科大(https://www.shenlanxueyuan.com/open/course/53)。

论文解读的参考之一https://cloud.tencent.com/developer/article/1694704

论文解读的参考之二(代码分析)https://blog.csdn.net/wqwqqwqw1231/article/details/106208592/

论文解读的参考之三(代码分析):https://blog.csdn.net/qq_43058685/article/details/105089579

论文:https://arxiv.org/pdf/1911.11236.pdf

代码:https://github.com/QingyongHu/RandLA-Net

作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
先上效果图:
2

亮点:

  • Local Feature Aggregation Module

Introduction

实现高效、准确的大场景三维点云语义分割是当前三维场景理解、环境智能感知的关键问题之一。然而,由于深度传感器直接获取的原始点云通常是非规则化 (irregular)、非结构化 (unstructure)并且无序 (orderless)的,目前广泛使用的卷积神经网络并不能直接应用于这类数据。

【针对目前点云分割存在速度慢、显存需求大的问题,该文提出以一种高效率学习的方法。从论文的结果来看,该文不仅在计算时间和计算资源上大幅缩减,分割效果也是达到甚至超过了SOTA。

采样

大规模点云处理的一个挑战在于如何快速且有效地进行采样,从而加速应用所需的时间和计算资源。针对这个问题,本文的一个贡献在于比对了现有方法的效率,结论是尽管最远点采样是最流行的作法,但是对于LiDAR数据,每一帧上万个点需要处理,随机采样是最适合的,速度快并且performance也不错。但是随机采样可能会丢失重要的点,所以作者提出Local Feature Aggregation。https://www.cnblogs.com/xiaoaoran/p/12342144.html

Motivation

自从2017年能够直接在非规则点云上进行处理的PointNet [1] 被提出以来,越来越多的研究者开始尝试提出能够直接处理非规则点云的网络结构,出现了许多诸如PointNet++ [2], PointCNN [3], PointConv [4] 等一系列具有代表性的工作。尽管这些方法在三维目标识别和语义分割等任务上都取得了很好的效果,但大多数方法依然还局限于在非常小(small-scale)的点云上(e.g., PointNet, PointNet++, Pointconv等一系列方法在处理S3DIS数据集时都需要先将点云切成一个个1m×1m的小点云块, 然后在每个点云块中采样得到4096个点输入网络)。这种预处理方式虽然说方便了后续的网络训练和测试,但同时也存在着一定的问题。举例来说,将整个场景切成非常小的点云块是否会损失整体的几何结构用一个个小点云块训练出来的网络是否能够有效地学习到空间中的几何结构

 

 

作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

带着这样的疑问,我们对PointNet在S3DIS数据集Area 5上的分割结果进行了可视化。如上图highlight的区域所示,PointNet错误地将一张桌子的左半部分识别为桌子,而将右半部分识别为椅子。造成这样明显不一致结果的原因是什么呢?可以看到,这张桌子在预处理切块(左图)的时候就已经被切分成几个小的点云块,而后再分别不相关地地输入到网络中。也就是说,在点云目标几何结构已经被切块所破坏的前提下,网络是难以有效地学习到桌子的整体几何结构的

既然切块太小会导致整几何结构被破坏,那我能不能把块切大一点?这样不就可以在一定程度上更好地保留原始点云的信息了吗?

图 2. PointNet和PointNet++在S3DIS Area5的对比实验结果。S3DIS中的数据分别被切割为1m×1m到5m×5m的点云块,然后再输入到网络中进行训练和测试。

对此,我们也进一步设计了对比实验,把切块的尺寸从最初的1m×1m增加到5m×5m(每个block中的点数也相应地从4096增加至102400),得到的实验结果如上图所示,可以看到:

  • PointNet的mIoU结果出现了比较明显性能上的下降。我们分析这主要是由于在PointNet框架中,每个点的特征是由shared MLP提取的per-point feature(没有学习点与点之间的交互,即没有local geometry特征)以及global max-pooling提取的global feature组成(点数越大,max-pooling丢掉的信息越多,10万个点的时候,其实大多数点的特征都会被丢掉)。当输入点云的规模越来越大时,通过简单的global max-pooling得到的全局特征能发挥的作用就越来越小,进而导致分割性能随着block size增大而持续地下降
  • PointNet++的分割性能随着block_size的增大有了一定提升,这是符合我们预期的(因为它注意到了点云局部集合特征,i.e., local geometry)。然而,从右边的时间变化曲线我们也可以进一步看到,网络inference的时间也随着block_size增大而出现了显著的增长,从最开始的每3s/百万点增加到需要接近100s/百万点(降采样策略FPS造成的)。

上述实验结果表明:简单地增大block_size也并不能有效地解决这个问题。通过进一步分析我们发现,阻碍当前大多数方法直接处理大场景点云的原因主要有以下三点:

  • 网络的降采样策略。现有的大多数算法采用的降采样策略要么计算代价比较昂贵,要么内存占用大。比如说,目前广泛采用的最远点采样(farthest-point sampling)需要花费超过200秒的时间来将100万个点组成的点云降采样到原始规模的10%。
  • 许多方法的特征学习模块依赖于计算代价高的kernelisation或graph construction。
  • 现有大多数方法在提取特征时感受野(receptive fields)比较有限,难以高效准确地学习到大场景点云中复杂的几何结构信息

当然,最近也有一些工作已经开始尝试去直接处理大规模点云。比如说SPG用超图(super graph)和超点(superpoints)来表征大场景点云,FCPN和PCT等方法结合了voxel和point的优势来处理大规模点云。尽管这些方法也达到了不错的分割效果,但大多数方法的预处理计算量太大或内存占用高,难以在实际应用中部署。

本文的目标是设计一种轻量级,计算效率高(computationally-efficient)、内存占用少(memory-efficient)的网络结构,并且能够直接处理大规模3D点云,而不需要诸如voxelization/block partition/graph construction等预处理/后处理操作。然而,这个任务非常具有挑战性,因为这种网络结构需要:

  • 一种内存和计算效率高的采样方法,以实现对大规模点云持续地降采样,确保网络能够适应当前GPU内存及计算能力的限制;
  • 一种有效的局部特征学习模块,通过逐步增加每个点的感受野的方式来学习和感知复杂的几何空间结构。

基于这样的目标,我们提出了一种基于简单高效的随机降采样和局部特征聚合的网络结构(RandLA-Net)。该方法不仅在诸如Semantic3D和SemanticKITTI等大场景点云分割数据集上取得了非常好的效果,并且具有非常高的效率(e.g. 比基于图的方法SPG快了接近200倍)。 本文的主要贡献包括以下三点:

  • 我们对现有的降采样方法进行了分析和比较,认为随机降采样是一种适合大规模点云高效学习的方法
  • 我们提出一种有效的局部特征聚合模块,通过逐步增加每个点的感受野来更好地学习和保留大场景点云中复杂的几何结构
  • RandLA-Net在多个大场景点云的数据集上都展现出了非常好的效果以及非常优异的内存效率以及计算效率

我们研究了大场景下三维点云的有效语义分割问题。依赖成本昂贵的采样技术或计算量大的预处理/后处理步骤,大多数现有的方法只能在小尺度点云上训练和操作。本文中,我们引入了一种高效的轻量级的网络神经架构,直接推断大规模场景下每点点云的语义。我们的方法的关键是使用随机点抽样而不是更复杂点选择方法。尽管计算和存储效率高,随机抽样可能偶然地放弃关键特性。为了克服这个问题,我们引入了一种新的局部特征聚合模块,以逐步增加每个三维点的感受野,从而有效地保留几何细节。广泛的实验表明,我们的RandLA-Net可以处理100万个点,比现有的方法有高达200倍的速度。此外,我们的RandLA-Net在两个大规模基准语义Semantic3D 和KITTI上明显优于最先进的语义分割方法。

大场景下有小的3维点云语义分割对于实时智能系统,例如自动驾驶和增强现实来说是一件基础和必不可少的能力。挑战之一是深度传感器获取的原始点云通常是不规则采样,具有非结构化和无序的特点。尽管深度卷神经积网络在结构化的2维计算机视觉任务上有优秀的表现,但它们不能直接被用于这种非结构化数据。

    最近,在直接处理3维点云方面,PointNet网络做了开创性的工作,它是一种很有前景的方法。它使用共享的多层感知机学习每一个点的特征,它计算效率高,但未能捕捉到每个点的更广泛的上下文信息

【上下文信息------做图像的,上下文特征是很常见的,其实上下文大概去理解就是图像中的每一个像素点不可能是孤立的,一个像素一定和周围像素是有一定的关系的,大量像素的互相联系才产生了图像中的各种物体,所以上下文特征就指像素以及周边像素的某种联系。

具体到图像语义分割,一般论文会说我们的XXX算法充分结合了上下文信息,意思也就是在判断某一个位置上的像素属于哪种类别的时候,不仅考察到该像素的灰度值,还充分考虑和它临近的像素。

为了学习更加丰富的局部结构特征,许多专用的神经网络模块随后被迅速引入。这些模块大体上分为:
1)邻域特征池化[44, 32, 21, 70, 69] 。
2)图形信息传递[57, 48, 55, 56, 5, 22, 34] 。
3)基于核的卷积[49, 20, 60, 29, 23, 24, 54, 38]。
4)基于注意力的聚合[61, 68, 66, 42] 。
 虽然这些方法在对象识别和语义分割方面取得了令人印象深刻的结果,但几乎所有这些方法都局限于小场景的三维点云(例如4k点或1×1m的块),如果没有块划分等预处理步骤,就不能直接扩展到较大的点云(例如数百万个点和最多200×200米的块)。这种限制的原因有三个方面。
1)这些网络常用的点采样方法要么需要极大的算力,要么内存效率低下。例如,广泛使用的最远点采样[44]需要花费超过200秒采样100万个点中的10%。
2)大多数现有的局部特征学习者通常依赖于算力昂贵的核化或图形构造,从而无法处理大量的点。
3)对于通常由数百个对象组成的大规模点云,现有的局部特征学习者要么无法捕获复杂的结构特征,要么由于其有限大小的感受野而效率低下。
  最近的一些工作出现了直接处理大规模点云的任务。 在应用神经网络学习每个超点语义之前,SPG[26]将大点云作为超图进行预处理。 FCPN[45]和PCT[7]都结合体素化和点级别的网络来处理大规模点云。 虽然它们具有良好的分割精度,但预处理和体素化步骤都需要消耗大的算力,无法部署在实时应用中。
  在本文中,我们的目标是设计一个内存和计算效率高的神经网络结构,它能够直接处理大规模的三维点云,而不需要任何前/后处理步骤,如体素化,块划分或图构建。 然而,这项任务是非常具有挑战性的,因为它需要:
1)一种内存和计算效率高的采样方法,逐步降低大规模点云,以适应当前GPU的限制;
2)一种有效的局部特征学习者,以逐步增加感受野的大小,以保留复杂的原始几何结构。 为此,我们首先系统地证明了随机抽样是深层神经网络有效处理大规模点云的关键手段。 然而,随机抽样可能丢弃关键信息,特别是对于具有稀疏点的对象。 为了应对随机抽样的潜在有影响,我们提出了一种新的、高效的局部特征聚合模块,用于在越来越小的点集上捕获复杂的局部结构。
   在现有的采样方法中,最远点采样和逆密度采样是小尺度点云[44,60,33,70,15]中最常用的采样方法。 由于点采样是这些网络中的一个基本步骤,我们在第3.2节中研究了不同方法的相对优点,看到常用的采样方法限制了向大规模点云处理的过度,并成为实时处理的一个重要瓶颈。 然而,我们确定随机抽样是迄今为止最适合大规模点云处理的组件,因为它快速,并且大规模点云场景下仍然有效。随机抽样并不是没有成本的,因为突出的点特征可能会被偶然地丢弃,并且它不能在现有网络中直接使用而不招致性能下降。 为了克服这一问题,我们在3.3节中设计了一个新的局部特征聚合模块,它能够通过逐步增加神经网络每层中的感受野大小来有效地学习复杂的局部结构。特别是,对于每个三维点,我们首先引入一个局部空间编码(Locse)单元来显式地保留局部几何结构。 其次,我们利用注意池来自动保持有用的局部特性。 第三,我们将多个LOCSE单元和注意池叠加为一个扩张的剩余块,大大增加了每个点的有效感受野。 请注意,所有这些神经网络成分都是作为共享MLP实现的,因此具有显著的内存和计算效率。
   总的来说,基于简单随机抽样和有效的局部特征聚合器的原理,我们的高效神经结构,名为randla-net,不仅比现有的大尺度点云方法快200倍,而且在Semantic3d[17]和KITTI[3]基准上都超过了最先进的语义分割方法。 图1显示了我们的方法的定性结果。 我们的主要贡献是:
  • 我们分析和比较了现有的抽样方法,确定随机抽样是在大规模点云上进行有效学习的最合适的组成部分。
  • 我们提出了一个有效的局部特征聚合模块,通过逐步增加每个点的感受野保留复杂的局部结构。
  • 我们在基准数据集上展示了显著的内存和计算增益,并在多个大规模基准上超越了最先进的语义分割方法。
 2.相关工作
为了从三维点云中提取特征,传统的方法通常依赖于手工制作的特征[11,47,25,18]。 最近的基于学习的方法[16,43,37]主要包括基于投影的、基于体素的和基于点的方案,这里概述了这些方案。
 (1)基于投影和体素的网络。 为了利用2d CNN的成功,许多工作[30,8,63,27]将项目/平坦的3D点云投影到2d图像上,以解决对象检测的任务。 然而,几何细节可能会在投影过程中丢失。 或者,点云可以被体素化成三维网格,然后在[14,28,10,39,9]中应用强大的三维CNN。 虽然它们在语义分割和对象检测方面取得了领先的结果,但它们的主要局限性是计算量大,特别是在处理大规模点云时。

(2)基于点的网络。受PointNet/ pointnet++[43,44]的启发,许多最近的作品引入了复杂的神经模块来学习每个点的局部特征。
这些模块一般可分为:

1)邻近特征池[32、21、70、69],

2)图信息传递[57、48、55、56、5、22、34、31],

3)基于核的卷积[49、20、60、29、23、24、54、38],

4)基于注意力的聚合[61,68、66、42]。

虽然这些网络在小点云上显示了良好的效果,它们中的大多数都不能直接扩展到大型场景中。它们的计算和内存成本都很高。相比有了它们,我们提出的RandLA-Net在三种方法:

1)只依赖于样本内部的随机抽样网络,因此需要更少的内存和计算;

2)提出的局部特征聚合器可以通过显式考虑局部空间关系和点特征来获得连续较大的感受域,从而更有效、更健壮地学习复杂的局部模式;

3)整个网络只有共享的MLPs,不需要依赖任何昂贵的操作,比如graph构造和核化,因此对大规模点云非常有效。

 (3)大型点云学习。 SPG[26]将大点云预处理为超点图,以学习每个超点语义。 最近的fcpn[45]和pct[7]应用基于体素和基于点的网络来处理大量的点云。 然而,图划分和体素化在计算上都是昂贵的。 相比之下,我们的randla-net是端到端的可训练的,不需要额外的预处理/后处理步骤。
3. RandLA-Net
3.1. Overview
如下图所示,对于一个覆盖数百米范围、由百万量级的点组成的大场景点云而言,如果希望将其直接输入到深度神经网络中进行处理,那么持续有效地对点云进行逐步地降采样,同时尽可能地保留有用的几何结构信息是非常有必要的。
如图2所示,给定一个大尺度的点云,有数百万个点跨越数百米,要用深度神经网络处理它,不可避免地需要在每个神经网络层中逐步有效地降采样这些点,而不丢失有用的点特征。 在我们的Randla-net中,我们建议使用简单而快速的随机抽样(RS)方法来大大降低点密度,同时应用精心设计的局部特征聚合模块来保留突出的特征。 这使得整个网络能够在效率和有效性之间实现极好的权衡
【但Random Sampling的问题在于,本身大量的点就集中在离LiDAR近的区域,远处的区域稀疏。通过sampling,由于是random的,比例是一样的,那么远处的就更稀疏了,没准那次就丢掉了边缘的点。而FPS则是能够最好的覆盖整个区域,所以相比random sampling,FPS确实更适合语义分割这个问题。但本文追求的是快啊,那么如何才能弥补RS这个缺点呢?那就是增大每个点的感受野,使得在sampling过后,保留下来的点有足够大的感受野,能够包含丢掉的点的信息,即使是在远处点很稀疏的情况下。来自:https://blog.csdn.net/wqwqqwqw1231/article/details/105604389

图2. 在Randla-net的每一层中,大规模点云被显著的下采样,但能够保留精确语义分割所需的特征。

 3.2.寻求有效抽样
为了寻找到一种高效的降采样方法。我们首先对现有的的降采样方法进行研究:主要可以分为Heuristic Sampling以及Learning-based Sampling两大类:
现有的点抽样方法[44,33,15,12,1,60]可以大致分为启发式和基于学习的方法。 然而,仍然没有适合大规模点云的标准采样策略。因此,我们分析和比较了它们的相对优点和复杂度,如下。
(1)启发式采样
作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • Farthest Point Sampling (FPS):顾名思义,也就是每次采样的时候都选择离之前采样得到的 k-1个点距离最远的点。FPS能够比较好地保证采样后的点具有较好的覆盖率,因而在点云分割领域被广泛地使用(e.g., PointNet++, PointCNN, PointConv, PointWeb)。然而,FPS的计算复杂度是 [公式] ,计算量与输入点云的点数呈平方相关。这表明从FPS可能不适合用来处理大规模点云。举例来说,当输入一个具有百万量级点的大场景点云时,使用FPS将其降采样到原始规模的10%需要多达200秒。
  • Inverse Density Importance Sampling (IDIS): 这个也比较好理解,简而言之就是根据每个点的密度来对其重新进行排序,尽可能地保留密度比较低的地方的点。IDIS [5] 的计算复杂度近似为 [公式] (取决于如何计算每个点的密度)。相比于FPS, IDIS显然更加高效,但IDIS对噪点(outliers)也更加敏感
  • Random Sampling (RS): 随机降采样均匀地从输入的 N 个点中选择 K 个点,每个点具有相同的被选中的概率。RS的计算复杂度为 [公式] , 其计算量与输入点云的总点数无关,只与降采样后的点数 K 有关,也即常数时间复杂度。因而具有非常高的效率以及良好的可扩展性。 与FPS和IDIS相比,RS仅需0.004s即可完成与FPS相同的降采样任务。其最明显的缺点是由于采样是随机的,所以可能会丢掉比较重要的关键点。
  • 最远点采样(FPS):为了从具有n个点的大规模点云P中采样k个点,FPS返回度量空的重新排序,使得每个PK都是距离前k−1个点的最远点。 FPS广泛应用于小点集的语义分割[44,33,60]。 虽然它对整个点集有很好的覆盖范围,但它的计算复杂度是o(N的平方)。 对于大规模点云(n∼10的6次方),在单个GPU上处理FPS需要最多200秒。 这表明FPS不适合大规模点云。
  •  逆密度重要性采样(IDIS):从n个点采样k个点,IDIS根据每个点的密度重新排序所有n个点,然后[15]选择密度最大的前k个点。 其计算复杂度约为o(N)。 根据经验,处理10的6次方个点需要10秒。 与FPS相比,IDIS更有效,但对异常值更敏感。 然而,在实时系统中使用仍然太慢。
  •  随机抽样(Rs):随机抽样从原始n个点中均匀地选择k个点。 它的计算复杂度为o(1),它与输入点的总数无关,即它是恒定的时间,因此具有内在的可伸缩性。 与FPS和IDIS相比,随机采样具有最高的计算效率,而不考虑输入点云的规模。 处理10的6次方个点只需0.004s。
 2)基于学习的采样
  • Generator-based Sampling (GS):与传统降采样方法不一样,这类方法通过学习生成一个子集来近似表征原始的点云。GS [6,7] 是一种task-oriented, data-driven的learnable的降采样方法,但问题在于inference阶段需要将生成的子集与原始点云进行匹配,这一步依赖于FPS matching,进而引入了更多额外的计算。使用GS将百万量级点的大场景点云降采样到原始规模的10%需要多达1200秒。
  • Continuous Relaxation based Sampling (CRS): CRS [8,9] 使用reparameterization trick来将non-differentiable的降采样操作松弛(relax)到连续域使得端到端训练变成可能。CRS采样后得到的每个采样点其实都是整个点云的一个加权和(weighted sum)。具体来说,对于一个大场景的输入点云(size: N×3),CRS通过学习得到一个采样矩阵 (size: K×N) (最终会非常稀疏), 最后采样矩阵左乘输入点云即可实现降采样。然而,当N是一个非常大的值时(e.g. 10^6), 这种方式学习到的采样矩阵会带来非常大的内存消耗。举例来说,使用CRS将百万量级点的大场景点云降采样到原始规模的10%需要多达300GB的GPU内存。
  • Policy Gradient based Sampling (PGS): PGS [10] 将降采样操作表示为一个马尔科夫决策过程,旨在学习到一种有效的降采样策略。该方法序贯地对每一个点学习到一个概率来决定是否保留。然而,当输入是大场景点云时,整个网络有着极大的搜索空间(exploration space)。举例来说,完成与上述采样方法相同的任务的搜索空间是 [公式]。通过进一步地实验我们发现,将PGS应用到大型点云时,网络非常难以收敛。

again:

 
  • 基于生成器的采样(GS):GS[12]学习生成小点集来近似表示原始的大点集。 然而,FPS通常用于在推理阶段将生成的子集与原始集合匹配,从而产生额外的计算。 在我们的实验中,采样100万个点的10%需要1200秒。
  •  基于连续松弛的采样(CRS):CRS方法[1,66]使用重新参数化技巧将采样操作放宽到连续域进行端到端训练。 特别是,每个采样点都是基于对整点云的加权和来学习的。 当用一次矩阵乘法同时采样所有新点时,它会产生一个大的权重矩阵,从而导致负担不起的内存成本。 例如,估计需要超过300GB的内存占用来采样100万个点中的10%。

 图3. 本文所提出的局部特征聚合模块。 顶部面板显示了提取特征的局部空间编码块,以及基于局部上下文和几何的加权最重要邻域特征的注意池机制。 底部面板显示了如何将这些组件中的两个连接在一起,在残余块中增加感受野大小。【这个图就是为了能够弥补RS所提出来的局部特征聚合提取的方案,关键词:多层叠加】

  • 基于策略梯度的抽样(PGS):PGS将抽样操作描述为马尔可夫决策过程[62]。 它依次学习概率分布来采样点。 然而,当点云较大时,由于勘探空间极大,所学概率具有较高的方差。 例如,对100万个点中的10%进行采样,勘探空间为 ,不太可能学习有效的采样策略。 我们经验性地发现,如果PGS用于大点云,网络很难收敛。

对于大场景点云,FPS, IDIS和GS的计算代价都比较高, CRS对GPU内存的要求太高,而PGS难以学到一个有效的采样策略(sampling policy)。相比之下,随机采样具有以下两个优点:1)计算效率高, 因为是常数计算复杂度, 与输入点数无关 2)内存开销少,采样过程并不需要额外的内存消耗。因此,对于大场景点云作为输入的情况,我们何不尝试下随机降采样呢?

但新的问题又来了:随机地对点云进行降采样势必会导致有用的信息被丢失,如何克服这个问题?



作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  

总的来说,FPS、IDIS和GS的计算成本太高,无法应用于大规模点云。 CRS方法内存占用过多,PGS很难学习。 相反,随机抽样具有以下两个优点:

1)它具有显著的计算效率,因为它与输入点的总数无关。

2)它不需要额外的内存来计算。

因此,我们安全地得出结论,与所有现有的替代方案相比,随机抽样是处理大规模点云的最合适的方法。 然而,随机抽样可能会导致许多有用的点特征被删除。 为了克服这一问题,我们提出了一个强大的局部特征聚合模块,如下一节所示。

3.3.局部特征聚合

为了缓解这个问题,我们进一步提出了与随机采样互补的局部特征聚合模块(Local feature aggregation)。 如图所示,该模块主要包括三个子模块:1)局部空间编码(LocSE), 2) attentive pooling, 3)扩张残差块(dilated residual block)。
该部分能并行处理点,包含三个units:1) local spatial encoding (LocSE), 2) attentive pooling, and 3) dilated residual block.

作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如图3所示,我们的局部特征聚合模块并行应用于每个3D点,它由三个神经单元组成:

1)局部空间编码(Locse)

2)注意池化

3)扩张残余块

(1)局部空间编码

对于每个采样点,在原所有点云中找其KNN个临近点(欧氏距离);再将该点和临近点以及他们之间的差concatenate并用MLP学习得到相对位置(公式1)每个采样点得到相对位置后,将其和该点的特征concate,得到该点的一系列最终的增强采样点,组成一个向量。

此模块用于显式地对输入的点云的三维坐标信息进行编码。不同于直接将各个点的三维坐标作为一个普通的通道特征输入到网络中,LocSE模块旨在显式地去编码三维点云的空间几何形状信息,从而使得网络能够从各个点的相对位置以及距离信息中更好地学习到空间的几何结构。具体来说分为以下步骤:

  • 首先,我们用 [公式] 最近邻搜索算法为每一个点 [公式] 找到欧氏空间中最近的[公式]个邻域点
  • 对于 [公式][公式]个最近邻点 [公式] , 我们显式地对点的相对位置进行编码,将中心点的三维坐标 [公式] , 邻域点的三维坐标 [公式] , 相对坐标 [公式] 以及欧式距离 [公式] 连接(concatenation)到一起。如下所示:[公式]

公式(1)

  • 最后我们将邻域点[公式] 对应的点特征 [公式] 与编码后的相对点位置 [公式] 连接到一起,得到新的点特征 [公式]


作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

给定一个点云P和每个点特征(例如,原始的 RGB,或中间学到的特征),这个局部空间编码单元显式嵌入所有相邻点的x-y-z坐标,使对应的点特征总是意识到它们的相对空间位置。这允许局部空间编码单元显式地观察局部几何样式从而最终有利于整个网络有效地学习复杂的局部结构。特别地,本单元包括以下步骤:

  • 找到邻近的点。 对于第i点,它的相邻点首先由(KNN)算法收集以提高效率。 该KNN算法是基于逐点欧氏距离的。
  • 相对点位置编码。 对于距离中心点pi的最近k个最近点

我们显式地编码相对点位置如下:

 其中pi和是点的x-y-z位置,⊕是级联操作,|| . ||计算相邻点和中心点之间的欧几里德距离。 似乎是从冗余点位置编码的。 有趣的是,这往往有助于网络学习局部特性,并在实践中获得良好的性能。

  • 点特征增强。 对于每个邻域点,编码的相对点位置与其相应的点特征连接,获得 的增广特征向量。 最终,locse单元的输出是一组新的相邻特征 ,它显式地编码中心点pi的局部几何结构。 然而,在[36]中位置用于学习点的得分,而我们的LocSE显式编码要增加的相对位置相邻的点特征。

(2)注意力池化

用attention的方式取代max/mean pooling进行特征融合

此模块用于将上述单元输出的邻域点特征集聚合到一起。现有的大多数算法通常采用启发式的max/mean/sum pooling来hard integrate邻域点特征集,这样做有可能导致许多有用的信息被丢失。不同于此,我们希望通过attention mechanism来自动学习和聚合邻域点特征集中有用的信息。具体来说,对于一个邻域特征点集合 [公式] ,我们首先设计一个共享函数 [公式] 来为每一个点学习一个单独的attention score,其中:[公式] [公式] 是共享MLP的可学习参数。然后,我们将学习到的attention score视作一个能够自动选择重要特征的soft mask,最终得到的特征是这些邻域特征点集的加权求和,如下所示:[公式]


作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

该神经单元用于聚合邻近点特征集。 现有的工作通常[44,33]使用max/mean池化来硬集成相邻的特性,导致大多数信息丢失。 相反,我们求助于强大的注意力机制来自动学习重要的局部特征。 特别是,在[65]的启发下,我们注意力池化单元包括以下步骤。

  • 计算注意力分数。 给定一组局部特征

     

     

    ,我们设计了一个共享函数g()来学习每个特征的唯一注意力分数。 基本上,函数g()由一个共享的MLP和Softmax组成。 它的正式定义如下:

 

 其中w是共享MLP的可学习权重。

  • 加权求和。 学习的注意力分数可以看作是一个软掩码,它自动选择重要的特征。 在形式上,这些特征被加权归纳如下:

 

 总之,给定输入点云p,对于第一点pi,我们的locse和注意池单元学会聚合其k个最近点的几何模式和特征,并最终生成信息丰富的特征向量

(3)扩张的残块
考虑到输入的点云会被持续大幅度的降采样,因此显著地增加每个点的感受野是非常有必要的。换句话来说也就是,我们希望即便RandLA-Net随机地丢弃某些点的特征,输入点云的整体的几何细节也能够被保留下来。基于这样一个想法,我们将多个LocSE,Attentive Pooling以及skip connection连接在一起组成扩张残差块(Dilated Residual Block)。下图进一步说明了扩展残差块的作用,可以看到: 红色的点在第一次LocSE/Attentive Pooling操作后的有效感受野是与之相邻的 [公式] 个相邻点,然后在第二次聚合以后最多能够将感受野扩展到 [公式] 个邻域点。相比于直接增大K最近搜索中的K值而言,这是一种更加廉价高效的方式来增大每个点的感受野以及促进邻域点之间的feature propogation。通过后面的ablation实验,我们的扩张残差块最终使用两组LocSE和attentive pooling单元,以平衡最终的分割性能以及计算效率。
 
由于大规模点云将被大幅度地降采样,因此希望显著增加每个点的感受野,使得输入点云的几何细节更有可能被保留,即使有些点被丢弃。 如图3所示,在成功的RESNET[19]和有效的扩展网络[13]的启发下,我们使用跳跃连接将多个LOCSE和注意池单元叠加作为扩展的剩余块。
 为了进一步说明我们膨胀的剩余块的能力,图4显示,红色3D点在第一个LOCSE/注意池操作之后观察到k个相邻点,然后能够从最多k的平方个相邻点接收信息。 即,它有两个跳跃的邻域(?)。 这是一种通过特征传播扩展感受野和扩展有效邻域的廉价方法。 理论上,我们叠加的单位越多,这个块就越强大,因为它的范围越来越大。 然而,更多的单元不可避免地会牺牲整体计算效率。 此外,整个网络很可能被过度拟合。 在我们的Randla-net中,我们简单地将两组LOCSE和注意池叠加为标准剩余块,在效率和有效性之间取得了令人满意的平衡。
 

 

 图4. 放大的残余块的插图,它显著增加了每个点的感受野(虚线圆),彩色点代表聚集的特征。L:局部空间编码,a:注意力池化。

 总的来说,我们的局部特征聚合模块旨在通过显式考虑相邻的几何形状和显著增加的感受野来有效地保持复杂的局部结构。 此外,该模块仅由前馈MLPs组成,因此计算效率高。

3.4.Implementation

我们通过叠加多个局部特征聚合模块和随机采样层来实现randla-net。 详细的体系结构见附录。 我们使用带有默认参数的adam优化器。 初始学习率设定为0.01,每个世代后下降5%。 最邻近点k的数目设置为16。为了并行训练我们的Randla-net,我们从每个点云中采样固定数量的点(∼10的5次方)作为输入。 在测试过程中,整个原始点云被输入到我们的网络中,以推断每个点的语义,而不需要预/后处理,如几何或块划分。 所有实验都是在NVIDIA RTX2080ti GPU上进行的。

4.实验

4.1.本节中随机抽样的效率,

我们对现有抽样策略的效率进行了实证评估,主要从计算时间和GPU内存消耗两个方面来考量包括FPS、IDIS、RS、GS、CRS和PGS,这些方法在第3.2节中已经讨论过,我们进行了以下4组实验

  • 组1. 给定一个小型点云(∼10的3次方点),我们使用每一种抽样方法来逐步向下抽样。【仿照PointNet++的主体框架,我们持续地对点云进行降采样,总共五次降采样,每次采样仅保留原始点云中25%的点】具体来说,点云是向下采样的通过五个步骤,每个步骤只保留25%的点,即四倍抽取比。这意味着只留下~(1/4)5(次方)×10^3个点(?)。该向下采样策略仿真程序在pointnet++[44]中使用。

图5. 不同采样方法的时间和内存消耗。虚线表示估计值(由于有限的GPU内存)。

  • 2/3/4组。点云总数增加,向大规模发展,即:10的4次方,10的5次方分和10的6次方分。我们使用相同的5个抽样步骤(同组1)

分析

图5比较了不同采样方法下处理不同规模点云总时间和内存的消耗量。可以看出:

1)对于小规模的点云~10^3, 上述采样方法在计算时间和内存消耗的差距并不明显, 总体来说都是可接受的

2)对于大规模点云~10^6, FPS/IDIS/GS所需要的计算时间显著增加, 而CRS需要占用大量的GPU内存(图b虚线)。相比之下,随机抽样总体上具有更好的时间和存储效率。这个结果清楚地证明了大多数现有深度学习网络[44,33,60,36,70,66]只能这样处理和优化规模点云,主要是因为他们依赖耗时耗算力的抽样方法。受这个现象启发,我们使用了有效的随机抽样策略RandLA-Net。

4.2. Efficiency of RandLA-Net

在本节中,我们系统地评估RandLA-Net用于现实世界大规模点云上语义分割的整体效率。特别是,我们评估RandLA-Net上SemanticKITTI[3]数据集上的表现,评估我们的网络在08序列(序列8:一共4071帧)上的总耗时。我们也在相同的数据集上评估了近期代表作品的时间消耗[43, 44, 33, 26, 54]。公平起见,我们在每一帧中将相同数量的点(81920)输入到baseline以及我们的RandLA-Net中。此外,我们还评估RandLA-Net和baseline的内存消耗。特别是,我们不仅报告每个网络的参数总数,还要测量每个网络在单次传递中可以作为输入的最大3D点数量,以推断逐点语义。注意,所有实验都是在同样的机器一个AMD 3700X @3.6GHz的CPU和一块NVIDIA GPU RTX2080Ti显卡上。

表1. 不同方法在处理SemanticKITTI数据集的序列8的总时间、模型参数和最多可处理点数的对比

分析。表1定量地显示了不同方法总时间和内存消耗。它可以可以看出,

1)SPG[26]网络参数最少,但处理时间最长点云,这是由于几何空间划分和超图构建等步骤的计算代价较高;

2) PointNet + +[44]和PointCNN[33]在计算上也很耗时主要是由于FPS的采样操作在处理大规模点云时比较耗时;

3) PointNet[43]和KPConv[54]无法一次性处理大规模数量的点云(例如10的6次方点),主要原因是没有降采样操作(PointNet)或者模型较为复杂。

4)得益于简单的随机抽样加上高效的MLP局部特征聚合模块,我们的RandLA-Net耗时最少(~23帧/秒),并且能够一次性推断高达10^6数量的点云的逐点语义信息。

4.3. Semantic Segmentation on Benchmarks

在这一节中,我们在三个大型公共数据集户外Semantic3D[17]和SemanticKITTI[3],以及室内S3DIS[2]评估RandLA-Net的语义分割能力。
(1)对Semantic3D的评价。Semantic3D数据集[17]包括15个用于训练的点云和15个用于在线测试的点云。每个点云有包含多达1亿个点,覆盖在真实的三维空间中,可达160×240×30米。原始的三维点共有8类,包含三维坐标,RGB信息和强度。我们只使用3D坐标和颜色信息来训练和测试我们的RandLANet。所有类别的平均交并比(mIoU)和整体准确度(OA)被用作标准度量。为了公平比较,我们只包括最近发表的强的基准测试的结果[4,52,53,46,69,56,26]以及目前最先进的方法KPConv[54]。

表2. 不同方法用于Semantic3D (reduced-8)[17]的定量结果。只比较了最近发表的方法。于2020年3月31日访问。

表2给出了不同方法的定量结果。RandLA-Net明显优于所有现有的方法采用mIoU和OA。值得注意的是,RandLANet在8个类中的6个类上也取得了卓越的性能,除了低植被和扫描艺术。

表3. 不同方法对SemanticKITTI[3]的定量结果。只比较最近发表的方法和所有的分数均从在线单次扫描评估轨迹中获得。于2020年3月31日访问。

图6. RandLA-Net对SemanticKITTI[3]验证集的定性结果。红色圆圈表示失败的案例

(2)对SemanticKITTI的评价。SemanticKITTI [3]由属于21个序列的43552个密集注释的激光雷达扫描组成。每次扫描都是一个大规模的点云在三维空间中,空间跨度为约105点,长度为160×160×20米。正式来说,序列00 ~ 07和和序列09 ~ 10(19130帧)作为训练集,序列08(4071帧)作为验证集,序列11 ~ 21(20351帧)用于在线测试。原始的3D点只有坐标信息,没有颜色信息。10个类别的平均交并比得分被用作度量标准。

表3显示了我们的RandLANet与两类最近的方法的定量比较,即1)基于点的方法[43,26,494,44,51]和2)基于投影的方法方法[58,59,3,40],图6显示了RandLA-Net在验证分割上的一些定性结果。它可以可以看到,我们的RandLA-Net远超过所有基于点的方法[43,26,49,44,51]。我们也优于所有基于投影的方法[58,59,3,40],但这种优势不是很明显,主要是因为RangeNet++[40]实现对小对象分类有更好的结果,如交通标志。然而,我们的RandLA-Net比RangeNet++[40]少40倍的网络参数,而且计算效率更高,因为它不需要算力较高的前/后投影的步骤。

(3)对S3DIS的评价。S3DIS数据集[2]由6个大区域的271个房间组成。每一个点云是一个中型的单人房间(∼20×15×5米)密集的三维点。为了评估我们的RandLA-Net的语义分割,我们使用标准的6-fold交叉验证。IoU (mIoU)的均值等级精度(mAcc)和总体精度(OA)总共13个类进行比较。如表4所示,我们的RandLA-Net达到了平均水平或者比最先进的方法性能更好。请注意大多数基准[44,33,70,69,57,6]趋向于使用复杂但昂贵的操作或取样优化小块点云(如1×1米),相对较小的房间被分成小块。相比之下,RandLA-Net将整个房间作为输入,并且能够在一次传递中有效地推断每点语义。

表4. 不同方法在S3DIS数据集上的定量的结果[2](6-fold交叉验证)。只有最近出版的方法被包括在内。

4.4. 消融研究(Ablation study)
由于充分研究了随机抽样的影响第4.1节,我们对我们的本地特性聚合模块进行了以下消融研究。所有消融网络对序列00 ~ 07和09 ~ 10进行训练,并在数据集[3]的序列08上测试。(1)去除局部空间编码(LocSE)。这个单元使每个3D点可以显式地观察其局部几何。删除locSE后,我们直接输入local将特性点到后续的注意力池中。

(2 ~ 4)用max/mean/sum替换注意池。注意池单元学习自动结合所有的局部点特征。相比之下,广泛使用的max/mean/sum 池化倾向于硬选择或结合特征,因此它们的性能可能不是最优的。

(5)对膨胀残块进行简化。膨胀的残余块堆积了多个LocSE单元和注意池,大大地膨胀了接收野每个3d点。通过简化这个块,我们只使用了一个LocSE单元和注意池,也就是说,在我们的原始RandLA-Net我们不连接多个块。表5比较了所有消融网络的mIoU得分。由此,我们可以看出:

1)受到影响最大的移除空间嵌入链和注意池模块。图4中突出显示了如何使用两个链接的区块从一个更广泛的邻域来传播信息,也就是大约K的平方个点而不是k个点。对于随机抽样尤其重要,因为随机抽样不能保证保留一组特定的点。

2)删除的局部空间编码单元对性能的影响次之,说明该模块对于有效学习局部和相对几何上下文是必要的。

3)去除注意力模块,不能有效保留有用的特征,这会降低性能。从这个消融研究,我们可以看到提出的神经单元如何的进行互补,以达到我们的最高水平性能

注解:

  • 从(1)中可以看到,去掉LocSE,也就是单纯使用Attention的方式并不是很有效。
  • 从(2)-(4)可以看出Attention的效果也是有的,比单纯的pooling好
  • 从(5)可以看出扩展感受野的必要性

表5所示. 基于我们的完整的RandLA-Net,所有消融网络的平均IoU分数

5. 结论
在本文中,通过使用轻量级网络架构,我们证明了高效和有效地分割大规模点云是可能的。与此形成鲜明对比的是对于大多数依赖于算力计算代价高的抽样策略的方法,我们在我们的系统中使用随机抽样框架大大减少了内存占用计算成本。局部特性聚合模块可以有效地保存来自邻域的有用特征(持续地增大每个点的有效感受野,以确保大多数有效信息不会因为随机采样而丢失)。大量的实验显示了我们的方法的高效率和最先进的表现。利用最新的工作[64]把我们的工作扩展到用于研究大规模点云上端到端的3D实例分割,以及用于点云的实时动态处理上面将是一件很有趣的事情。

最后总结一下,我们提出了一种针对大规模三维点云场景的轻量级、高效点云语义分割算法,与当前的大多数基于FPS等计算代价高的采样策略的算法不同,本文尝试使用简单高效的随机采样来显著地减少计算量以及内存消耗,并且引入了局部特征聚合模块持续地增大每个点有效的感受野,以确保大多数有效的信息不会因为随机采样而丢失。在Semantic3D,S3DIS以及SemanticKITTI等多个数据集上的大量实验证明了我们的方法的有效性。下一步可以尝试将我们的工作延申到大场景三维点云实例分割以及实时动态点云处理。

最后放一下我们的demo:

demo(见作者知乎专栏)

最后的话

  • 对于三维点云语义分割任务而言,与其在被切割的点云上提出非常复杂的算法来提升性能,不如直接尝试在大场景点云上进行处理,这样更加有实际意义。
  • 三维点云分割网络的scalability也是实际应用中一个比较重要的点。i.e., 理想情况下train好的网络应该可以用于inference任意点数的输入点云,因为每个时刻采集到的点云的点数不一定是相同的。这也是RandLA-Net没有使用全局特征的原因,i.e. 确保学到的参数是agnostic to number of points.
  • 顺便打一波广告,对于刚刚进入三维点云处理领域的同学,有一份最新的综述论文(Deep Learning for 3D Point Clouds: A Survey)可供参考,内含大量主流的点云目标分类,三维目标检测,三位场景分割算法的最新研究进展及总结。

欢迎大家关注我们的更多新工作:



作者:Qingyong Hu
链接:https://zhuanlan.zhihu.com/p/105433460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

鸣谢:本研究部分由中国留学基金委奖学金资助。

郭玉兰是由国家自然科学基金资助。
国家自然科学基金(第61972435号)广东省(2019A1515011271),深圳科技及创新委员会

 

我的思考【https://blog.csdn.net/wqwqqwqw1231/article/details/105604389

我看的语义分割这方面的论文不多,目前大多针对点云处理的backbone的方法都会做语义分割的实验,基本都是使用FPS。所以本文使用RS能做到SOTA的效果证明,只要特征能提取的有效,则FPS不是必须的。

Dilated Residual Block其实和PointNet++中的SA相比有以下不同:
1)LocSE中使用的kNN,这也就保证了稀疏的地方仍然能够找到临近点提取特征,而SA中用的是一定半径内的球形邻域,这就在稀疏的地方就不是很好使了。
2)在Ablation Studies证明了的Attention操作是优于pooling的,这个操作将临近点之间的特征联系起来,而不是单单max pooling了。
3)Dilated Residual Block的串联结构有效的增大了感受野,SA中并联的MSG结构则提取multi-scale的特征。

我认为可以试一下做一下将RS改为FPS的实验,看看最终效果涨不涨,来证明FPS是否确实是在效果上可以被RS替代。

 

 

 

 

附录
A.抽样评价的细节。
我们在4.1节中提供了不同抽样方法的实施细节。从拥有N个点(或者点特征)的大规模点云P中抽样K个点(或者点特征):
1. 最远点抽样(FPS):我们遵循pointnet++[44]提供的实现,它也是广泛应用于[33,60,36,6,70]。特别是FPS作为一个运行在GPU上的运算符进行实现。
2. 逆密度重要抽样(IDIS):给定点pi,通过计算点pi和它的邻域t个点之间的距离之和去近似计算它的密度ρ。正式:

式中,表示邻域点的集中第j个点的坐标。参数t被设置为16。所有的点根据所有点的逆密度1/ρ排序。最后,选择最前面的K个点。

3.随机抽样(RS):我们使用python numpy包实现随机抽样。具体来说,我们首先使用numpy函数umpy.random.choice()生成K个索引。然后我们利用这些点的索引收集相应的空间坐标和每个点的特征。
4. 基于生成器的采样(GS):遵循[12]提供的代码去实现。我们先训练ProgressiveNet[12],目的是根据他们和任务的相关性把原始点云转变为有序点集。之后前K个点被保留,剩下的点被丢弃。
5. 连续松弛采样(CRS)
通过自主gumbel-softmax抽样[1][66]去实现。给定一个点特征集p∈RN×(d+3),每个点包含三维坐标和点特征,我们首先通过MLP层参数化的分数函数估计一个概率分数向量s∈RN,即s = softmax(MLP(P)),它学习a分类分布。然后,伴随着Gumbel噪声g∈RN从Gumbel(0;1)分布。计算每个采样点特征向量y∈Rd+3:

其中s(i)和g(i)分别表示向量s和g中的第i个元素,P (i)表示矩阵P中第i行输入向量。是退火温度。当它趋近于0的时候,方程5趋于离散分布,并依据概率p(y = p(i)) = s(i)对P中的每一行向量进行采样。

6. 基于策略梯度的采样:给定一个带有三维坐标的点特征集p∈RN×(d+3)和每个点特征,我们首先通过MLP函数为每个点特征预测一个分数s,即s =softmax (MLP (P)) +  ξ  ξ是0均值高斯噪声,带有随机方差Σ。在这之后,我们根据前K个得分值对P中的K个向量采样。抽样每个点/向量可以被看作是一个独立的行为,它们的一个序列形成了一个马尔可夫序列决策过程(MDP),有以下政策函数:

ai是点集P中是否采样第i个向量的二值决策,θ为多层感知机的网络参数。因此,为了合理地改进采样策略,使采样过程具有一定的可扩展,我们应用加强算法[50]作为梯度估计。分割精度R被当作整个采样过程中的奖励值J = R。使用下面的估计梯度进行优化:

其中M为批大小,bc和b(P (i))为缓解政策梯度高方差问题的两个控制变量[41]。 

图7. 我们的RandLA-Net的详细架构。(N;D)分别表示点数和特征维数。FC:全连通层,LFA:局部特征聚集,RS:随机抽样,MLP:共享多层感知器,US:上采样,DP:随机丢弃

 

B.网络架构细节
图7显示了RandLANet的详细架构,整个网络就是由局部特征聚合模块堆叠而成的。

最后,我们将随机采样以及局部特征聚合模块组合到一起,基于标准的encoder-decoder结构组建了RandLA-Net。网络的详细结构如下图所示,可以看到,输入的点云在RandLA-Net中持续地进行降采样以节约计算资源及内存开销。此外,RandLA-Net中的所有模块都由简单高效的feed-forward MLP组成,因此具有非常高的计算效率。最后,在解码器中的上采样阶段,不同于广泛采用的三线性插值(trilinear interpolation),我们选择了更加高效的最近邻插值(nearest interpolation),进一步提升了算法的效率】

网络遵循广泛使用具有跳跃连接的编码器-解码器架构。输入点云首先喂到一个共享的MLP层,以提取每个点的特征。四个编码和解码层然后用于学习每个点的特性。最后,三层全连接并使用dropout层对语义标签进行预测每一个点。各部分详情如下:

网络输入:输入为大规模点云,尺寸为N×din时,批维度会降低(为简单起见),其中N为点的个数,din为每个输入点的特征维数。对于这两个S3DIS[2]和Semantic3D[17]数据集,每个点为由其三维坐标和颜色信息表示(即x-y-z-R-G-B),而SemanticKITTI的每个点[3]数据集仅由3D坐标表示。
编码层:我们使用了四个编码层网络的大小逐步减小点云的规模和增加每个点的特征维度。每个编码层由一个局部特征聚合模块(3.3节)和一个随机抽样组成操作(3.2节)。点云以四倍抽取率向下采样。特别是,每一层之后只有25%的点特征被保留后,即(N->N/4->N/16->N/64->N/256)。同时,每一层的点特征维数逐渐增大以保存更多的信息,即(8->32->128->256->512)

解码层:上述编码层之后,使用了4层解码层。对于每一层解码层,我们首先使用KNN算法为每一个查询点找到一个最近的邻域点,点特征集通过最近邻插值向上采样。接下来,上采样的特征图与编码层产生的中间特征图通过跳跃连接被合并,然后将共享的MLP应用于刚刚合并的特征图

最终语义预测:每个点的最终语义标签是通过三个共享的全连接层获得的(N, 64)->(N, 32)->(N, nclass)和随机丢弃层。丢弃率为0.5。

网络输出:RandLA-Net的输出为所有点的预测语义,大小为N×nclass,其中nclass是类的数量。

C. 在LocSE网络上的消融研究

在3.3节中,我们对相对点位置进行编码由下式可知:

在此框架中,我们进一步研究了不同空间信息的影响。特别是,我们进行
以下是更多关于LocSE的消融实验:
•1)只对点pi的坐标进行编码。
•2)只对相邻点pik的坐标进行编码。
•3)对点pi的坐标和它的邻域点pik进行编码。
•4)对点pi的坐标和它的邻域点pik,以及欧氏距离IIpi−pik||进行编码。
•5)对点pi和相邻点pik的坐标,以及pi和相邻点pik的相对位置pi - pik进行编码。
表6比较了所有消融网络在SemanticKITTI数据集上的交并比得分。我们可以看到

1)明确编码所有空间信息有最好的mIoU交并比表现。

2)相对位置pi - pik在这一成分中起重要作用因为相对点的位置使网络能够注意到局部几何特征。

3)仅对点位置pi或pik进行编码不太可能改进性能,因为相对位置没有显式编码。

表6. 不同空间信息编码下RandLa-Net的交并比结果

D. 对扩张的残余块进一步消融研究
在我们的RandLA-Net中,我们堆叠了两个LocSE和专注池化单元作为标准的扩大剩余块,使感受野逐渐增大。为了进一步评估聚集单元的数量对影响整个网络的影响,我们做了以下两组实验。
•1)我们简化了膨胀的剩余块,只使用一个LocSE单元和注意池。
•2)我们增加了一个本地单元和注意池,也就是说,有三个聚合单元链接在一起。

表7. 在残余块中使用不同数量的组合单元RandLA-Net的交并比得分

表7显示了在SemanticKITTI[3]数据集上验证分割的不同消融网络平均交并比得分。可以看出:

1)中只有一个聚集单元由于感受野的限制,扩张的残块导致分割性能显著下降。

2)每个块的三个聚合单元没有提高按预期的准确性。这是因为感受野的显著增加和大量的可训练参数往往会过度拟合。

E.注意力得分可视化
为了更好地理解助理解注意池化,使习得注意力得分可视化。然而,由于注意池操作在一个相对较小的局部点集上(即K=16),在如此小的局部区域内很难识别有意义的点集形状。所以,我们将方程2中定义的学习注意力权重矩阵W可视化。如图8所示,注意权重在第一个编码层中有较大的值,则在随后的层中逐渐变得光滑且稳定。
这说明注意池在开始时倾向于选择突出的或关键的特征。点云被显著地向下采样之后,注意池层倾向于保留这些点特征中的大多数特征。

图8。学习到的注意力矩阵在不同层的可视化。从左上至右下:16×16注意力矩阵,64×64注意力矩阵,128×128注意力矩阵,256×256注意力矩阵。黄色代表高的注意力得分。

F.关于Semantic3D的附加结果
更多的RandLA-Net对Semantic3D[17]数据集分割(reducated -8)的定性结果如图9所示。
G. 关于SemanticKITTI的附加结果

图10显示了我们的RandLANet对SemanticKITTI[3]验证集的更多定性结果。红色的盒子展示了失败的案例。可以看出,属于其他车辆的点很容易被误分类为car,主要是由于没有颜色的部分点云很难从两个相似的类中区分出来。此外,我们的方法趋向于在自行车、摩托车、自行车手和摩托车手类别上失败,由于数据集中极度的不平衡点分布。例如,植被的点云比摩托车手点云南的多7000倍。

图9。RandLA-Net对Semantic3D的reduced-8分割的定性结果。从左到右:全RGB彩色点云,全点云的预测语义标签,彩色点云的详细视图,预测语义标签的详细视图。请注意,测试集的语义标签不是公开的

H. S3DIS附加结果
我们在8中报告了RandLA-Net在S3DIS[2]数据集上详细的六倍交叉验证结果。图11显示了我们的方法有更多的定性结果。

I.视频说明

我们提供一个视频来显示RandLA-Net在室内和室外数据集上定性的结果,可以在https://www.youtube.com/watch?v=Ar3eY_lwzMk&t=9s. 查看

图10. RandLA-Net对SemanticKITTI[3]验证分割的定性结果。红色框显示失败案例.

表8所示。不同方法在S3DIS[2]的定量结果(6-fold交叉验证)。总体精度(OA, %),平均类别精(mAcc, %)、平均交并比(mIoU, %)和每个类别的交并比(%)。

 

 

图11。在S3DIS中,我们的RandLA-Net对S3DIS区域1-6的完整点云的语义分割结果。左:全RGB输入云;中间:预测标签;右:真实标签。



 (4) SemanticKITTI部分的代码阅读:

git clone --depth=1 https://github.com/QingyongHu/RandLA-Net && cd RandLA-Net

代码克隆。

conda create -n randlanet python=3.5
source activate randlanet
pip install -r helper_requirements.txt
sh compile_op.sh

conda建立深度学习的环境:

#利用conda建立一个新环境,同时指定该坏境中python的版本
conda create -n myenv python=3.5
-n应该是new的意思
source activate randlanet

 激活、关闭环境

source activate envname、source deactivate 已经过时。

pip install -r helper_requirements.txt
安装helper_requirements.txt内容中的所有包
sh compile_op.sh

 sh是一个shell,运行sh a.sh,表示我使用sh来解释这个脚本。

表示在本地编译C++代码。

二、项目结构

整个项目文件非常简洁。

文件作用
helper_tf_util.py 封装了一些卷积池化操作代码
helper_tool.py 有训练时各个数据集所用到的一些参数信息,还有一些预处理数据时的一些模块。
main_*.py 训练对应数据的主文件
RandLANet.py 定义网络的主题结构
tester_*.py 测试对应数据的文件,该文件在main_*.py中被调用
utils 改文件夹里面有对数据集预处理的模块以及KNN模块。

 表来自:https://blog.csdn.net/qq_43058685/article/details/105089579

SemanticKITTI数据集的一些情况:

 

 

 我们提供了用于学习和推理的体素网格,您必须下载它才能获得SemanticKITTI体素数据(700MB)。此存档包含训练(所有文件)和测试数据(仅bin文件)。请参阅开发工具包以了解如何读取二进制文件。

data_prepare_semantickitti.py文件的运行:

helper_tool.py文件里面:

from open3d import linux as open3d

改成:

import open3d

 

import cpp_wrappers.cpp_subsampling.grid_subsampling as cpp_subsampling
import nearest_neighbors.lib.python.nearest_neighbors as nearest_neighbors

改成:

import utils.cpp_wrappers.cpp_subsampling.grid_subsampling as cpp_subsampling
import utils.nearest_neighbors.lib.python.nearest_neighbors as nearest_neighbors

main_SemanticKITTI.py第15行的路径需要修改一下,改成文件夹sequences_0.06的路径。

self.dataset_path=''

 

如果还是不能运行,就在RandLA-Net文件夹下打开终端,运行:

sh compile_op.sh

 sematic-kitti.yaml文件显示了semantic数据集的基本情况:

# This file is covered by the LICENSE file in the root of this project.
labels: 
  0 : "unlabeled"    #未标记
  1 : "outlier" #噪声点
  10: "car"      #小汽车
  11: "bicycle"   #自行车
  13: "bus"     #公共汽车
  15: "motorcycle"  #摩托车
  16: "on-rails"    #在轨电车
  18: "truck"    #卡车
  20: "other-vehicle"   #其它机动车辆
  30: "person"    #行人
  31: "bicyclist"    #骑自行车的人
  32: "motorcyclist"   #骑摩托车的人
  40: "road"    #道路
  44: "parking"   #停车区域
  48: "sidewalk"   #人行道
  49: "other-ground"   #其它路面
  50: "building"   #建筑物
  51: "fence"    #篱笆
  52: "other-structure"   #其它结构物
  60: "lane-marking"   #车道标线
  70: "vegetation"  #植被
  71: "trunk"   #树干
  72: "terrain"   #远离城镇、有田野、树林和农场的地区
  80: "pole"   #杆状物
  81: "traffic-sign"   #交通标志
  99: "other-object"   #其它物体
  252: "moving-car"   #移动的小汽车
  253: "moving-bicyclist"   #移动的骑车人
  254: "moving-person"   #移动的人
  255: "moving-motorcyclist"   #移动的摩托车骑行者
  256: "moving-on-rails"   #在轨道上移动的物体
  257: "moving-bus"   #移动的公共汽车
  258: "moving-truck"   #移动的卡车
  259: "moving-other-vehicle"   其它移动的激动车辆
color_map: # bgr   颜色图,指的是每个类别的物体的着色
  0 : [0, 0, 0]
  1 : [0, 0, 255]
  10: [245, 150, 100]
  11: [245, 230, 100]
  13: [250, 80, 100]
  15: [150, 60, 30]
  16: [255, 0, 0]
  18: [180, 30, 80]
  20: [255, 0, 0]
  30: [30, 30, 255]
  31: [200, 40, 255]
  32: [90, 30, 150]
  40: [255, 0, 255]
  44: [255, 150, 255]
  48: [75, 0, 75]
  49: [75, 0, 175]
  50: [0, 200, 255]
  51: [50, 120, 255]
  52: [0, 150, 255]
  60: [170, 255, 150]
  70: [0, 175, 0]
  71: [0, 60, 135]
  72: [80, 240, 150]
  80: [150, 240, 255]
  81: [0, 0, 255]
  99: [255, 255, 50]
  252: [245, 150, 100]
  256: [255, 0, 0]
  253: [200, 40, 255]
  254: [30, 30, 255]
  255: [90, 30, 150]
  257: [250, 80, 100]
  258: [180, 30, 80]
  259: [255, 0, 0]
content: # as a ratio with the total number of points   各个类别的物体的占比
  0: 0.018889854628292943
  1: 0.0002937197336781505
  10: 0.040818519255974316
  11: 0.00016609538710764618
  13: 2.7879693665067774e-05
  15: 0.00039838616015114444
  16: 0.0
  18: 0.0020633612104619787
  20: 0.0016218197275284021
  30: 0.00017698551338515307
  31: 1.1065903904919655e-08
  32: 5.532951952459828e-09
  40: 0.1987493871255525
  44: 0.014717169549888214
  48: 0.14392298360372
  49: 0.0039048553037472045
  50: 0.1326861944777486
  51: 0.0723592229456223
  52: 0.002395131480328884
  60: 4.7084144280367186e-05
  70: 0.26681502148037506
  71: 0.006035012012626033
  72: 0.07814222006271769
  80: 0.002855498193863172
  81: 0.0006155958086189918
  99: 0.009923127583046915
  252: 0.001789309418528068
  253: 0.00012709999297008662
  254: 0.00016059776092534436
  255: 3.745553104802113e-05
  256: 0.0
  257: 0.00011351574470342043
  258: 0.00010157861367183268
  259: 4.3840131989471124e-05
# classes that are indistinguishable from single scan or inconsistent in
# ground truth are mapped to their closest equivalent

#通过一帧扫描中无法区分的类或与真实类别不一致的类映射到它们最接近的等价类
learning_map: #映射图 0 : 0 # "unlabeled" 1 : 0 # "outlier" mapped to "unlabeled" ----------异常点映射到无标签类----------------mapped 10: 1 # "car" 11: 2 # "bicycle" 13: 5 # "bus" mapped to "other-vehicle" ------公共汽车映射到其它机动车辆--------------------mapped 15: 3 # "motorcycle" 16: 5 # "on-rails" mapped to "other-vehicle" ---有轨电车映射到其它机动车辆------------------mapped 18: 4 # "truck" 20: 5 # "other-vehicle" 30: 6 # "person" 31: 7 # "bicyclist" 32: 8 # "motorcyclist" 40: 9 # "road" 44: 10 # "parking" 48: 11 # "sidewalk" 49: 12 # "other-ground" 50: 13 # "building" 51: 14 # "fence" 52: 0 # "other-structure" mapped to "unlabeled" ----其它结构物映射到无标签类别--------------mapped 60: 9 # "lane-marking" to "road" -------车道标线映射到道路--------------------------mapped 70: 15 # "vegetation" 71: 16 # "trunk" 72: 17 # "terrain" 80: 18 # "pole" 81: 19 # "traffic-sign" 99: 0 # "other-object" to "unlabeled" -----其它物体映射到无标签类别-----------------------mapped 252: 1 # "moving-car" to "car" -----移动的小汽车映射到小汽车类-------------------------------mapped 253: 7 # "moving-bicyclist" to "bicyclist" ----移动的自行车骑行者映射到自行车骑行者--------------------mapped 254: 6 # "moving-person" to "person" -----移动的人映射到人-------------------------mapped 255: 8 # "moving-motorcyclist" to "motorcyclist" ---移动的摩托车骑行者映射到摩托车骑行者---------------mapped 256: 5 # "moving-on-rails" mapped to "other-vehicle" ---电车轨道上移动的物体映射到其它车辆-----------mapped 257: 5 # "moving-bus" mapped to "other-vehicle" ----移动的公共汽车映射到其它车辆---------------mapped 258: 4 # "moving-truck" to "truck" ----移动的卡车映射到卡车----------------------------mapped 259: 5 # "moving-other"-vehicle to "other-vehicle" ----其它移动车辆映射到其它车辆------------mapped learning_map_inv: # inverse of previous map 0: 0 # "unlabeled", and others ignored 1: 10 # "car" 2: 11 # "bicycle" 3: 15 # "motorcycle" 4: 18 # "truck" 5: 20 # "other-vehicle" 6: 30 # "person" 7: 31 # "bicyclist" 8: 32 # "motorcyclist" 9: 40 # "road" 10: 44 # "parking" 11: 48 # "sidewalk" 12: 49 # "other-ground" 13: 50 # "building" 14: 51 # "fence" 15: 70 # "vegetation" 16: 71 # "trunk" 17: 72 # "terrain" 18: 80 # "pole" 19: 81 # "traffic-sign" learning_ignore: # Ignore classes 0: True # "unlabeled", and others ignored 1: False # "car" 2: False # "bicycle" 3: False # "motorcycle" 4: False # "truck" 5: False # "other-vehicle" 6: False # "person" 7: False # "bicyclist" 8: False # "motorcyclist" 9: False # "road" 10: False # "parking" 11: False # "sidewalk" 12: False # "other-ground" 13: False # "building" 14: False # "fence" 15: False # "vegetation" 16: False # "trunk" 17: False # "terrain" 18: False # "pole" 19: False # "traffic-sign" split: # sequence numbers 训练、验证与测试的分割情况 train: - 00 - 01 - 02 - 03 - 04 - 05 - 06 - 07 - 09 - 10 valid: - 08 test: - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21

 

 

 

 

 

dataset_path = '/data/semantic_kitti/dataset/sequences'
output_path = '/data/semantic_kitti/dataset/sequences' + '_' + str(grid_size)

改成:

dataset_path = 'sequences的全路径'
output_path = '真实路径'

 sequences--00包含004541个扫描场景。

对000000.bin可视化:

利用代码中的变量points和labels将点云坐标文件000000.bin和对应标签文件000000.label存到一起并可视化:

pointsandlabels=np.hstack((points,labels.reshape(labels.shape[0],1)))
filename=r'/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/00/000000.txt'
np.savetxt(filename, pointsandlabels, fmt="%.8f,%.8f,%.8f,%d", delimiter=',')

得到文件(共124668个点,约12万左右个点):

利用下面这两句可视化000081.bin:

scan_id='000001.bin'
points = DP.load_pc_kitti(join(pc_path, scan_id))
labels = DP.load_label_kitti(join(label_path, str(scan_id[:-4]) + '.label'), remap_lut)

 

scan_id='000000.bin'
points = DP.load_pc_kitti(join(pc_path, scan_id))
labels = DP.load_label_kitti(join(label_path, str(scan_id[:-4]) + '.label'), remap_lut)
pointsandlabels=np.hstack((points,labels.reshape(labels.shape[0],1)))
filename=r'/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/00/000000.xyz'
np.savetxt(filename, pointsandlabels, fmt="%.8f %.8f %.8f %d", delimiter=',')

结果:

 

 

可视化004540.bin:

 

 

 

 这些文件的的点云尺寸都大约一致,应该是实际场景中切割出来后再经过坐标转换的点云。点云场景长约160m,宽约107m,高约13~25m。

 

 

 x的坐标的范围是大约-70m~80m,y的坐标范围约为-50~50m, z的坐标范围大约为-15~5m.

对点云进行降采样:

sub_points, sub_labels = DP.grid_sub_sampling(points, labels=labels, grid_size=grid_size)

 以sequences00里面的000018.xyz为例,降采样的效果如下图:

 

# utils/data_prepare-semantickitti.py

# line 42-50
points = DP.load_pc_kitti(join(pc_path, scan_id))
labels = DP.load_label_kitti(join(label_path, str(scan_id[:-4]) + '.label'), remap_lut)
sub_points, sub_labels = DP.grid_sub_sampling(points, labels=labels, grid_size=grid_size)
search_tree = KDTree(sub_points)
KDTree_save = join(KDTree_path_out, str(scan_id[:-4]) + '.pkl')
np.save(join(pc_path_out, scan_id)[:-4], sub_points)
np.save(join(label_path_out, scan_id)[:-4], sub_labels)
with open(KDTree_save, 'wb') as f:
    pickle.dump(search_tree, f)

可以看到,上述预处理,是把point和对应label做了grid sampling,并且生成了一个kdtree保存下来。

  • main_SemanticKITTI.py文件:
  1 from helper_tool import DataProcessing as DP
  2 from helper_tool import ConfigSemanticKITTI as cfg
  3 from helper_tool import Plot
  4 from os.path import join
  5 from RandLANet import Network
  6 from tester_SemanticKITTI import ModelTester
  7 import tensorflow as tf
  8 import numpy as np
  9 import os, argparse, pickle
 10 #import _pickle as cPickle
 11 class SemanticKITTI:
 12     def __init__(self, test_id):
 13         self.name = 'SemanticKITTI'
 14         self.dataset_path = '/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_0.06'
 15         self.label_to_names = {0: 'unlabeled',
 16                                1: 'car',
 17                                2: 'bicycle',
 18                                3: 'motorcycle',
 19                                4: 'truck',
 20                                5: 'other-vehicle',
 21                                6: 'person',
 22                                7: 'bicyclist',
 23                                8: 'motorcyclist',
 24                                9: 'road',
 25                                10: 'parking',
 26                                11: 'sidewalk',
 27                                12: 'other-ground',
 28                                13: 'building',
 29                                14: 'fence',
 30                                15: 'vegetation',
 31                                16: 'trunk',
 32                                17: 'terrain',
 33                                18: 'pole',
 34                                19: 'traffic-sign'}
 35         self.num_classes = len(self.label_to_names)
 36         self.label_values = np.sort([k for k, v in self.label_to_names.items()])
 37         self.label_to_idx = {l: i for i, l in enumerate(self.label_values)}
 38         self.ignored_labels = np.sort([0])
 39 
 40         self.val_split = '08'
 41 
 42         self.seq_list = np.sort(os.listdir(self.dataset_path))
 43         self.test_scan_number = str(test_id)
 44         self.train_list, self.val_list, self.test_list = DP.get_file_list(self.dataset_path,
 45                                                                           self.test_scan_number)
 46         self.train_list = DP.shuffle_list(self.train_list)
 47         self.val_list = DP.shuffle_list(self.val_list)
 48 
 49         self.possibility = []
 50         self.min_possibility = []
 51 
 52     # Generate the input data flow
 53     def get_batch_gen(self, split):
 54         if split == 'training':
 55             num_per_epoch = int(len(self.train_list) / cfg.batch_size) * cfg.batch_size
 56             path_list = self.train_list
 57         elif split == 'validation':
 58             num_per_epoch = int(len(self.val_list) / cfg.val_batch_size) * cfg.val_batch_size
 59             cfg.val_steps = int(len(self.val_list) / cfg.batch_size)
 60             path_list = self.val_list
 61         elif split == 'test':
 62             num_per_epoch = int(len(self.test_list) / cfg.val_batch_size) * cfg.val_batch_size * 4
 63             path_list = self.test_list
 64             for test_file_name in path_list:
 65                 points = np.load(test_file_name)
 66                 self.possibility += [np.random.rand(points.shape[0]) * 1e-3]
 67                 self.min_possibility += [float(np.min(self.possibility[-1]))]
 68 
 69         def spatially_regular_gen():
 70             # Generator loop
 71             for i in range(num_per_epoch):
 72                 if split != 'test':
 73                     cloud_ind = i
 74                     pc_path = path_list[cloud_ind]
 75                     pc, tree, labels = self.get_data(pc_path)
 76                     # crop a small point cloud
 77                     pick_idx = np.random.choice(len(pc), 1)
 78                     selected_pc, selected_labels, selected_idx = self.crop_pc(pc, labels, tree, pick_idx)
 79                 else:
 80                     cloud_ind = int(np.argmin(self.min_possibility))
 81                     pick_idx = np.argmin(self.possibility[cloud_ind])
 82                     pc_path = path_list[cloud_ind]
 83                     pc, tree, labels = self.get_data(pc_path)
 84                     selected_pc, selected_labels, selected_idx = self.crop_pc(pc, labels, tree, pick_idx)
 85 
 86                     # update the possibility of the selected pc
 87                     dists = np.sum(np.square((selected_pc - pc[pick_idx]).astype(np.float32)), axis=1)
 88                     delta = np.square(1 - dists / np.max(dists))
 89                     self.possibility[cloud_ind][selected_idx] += delta
 90                     self.min_possibility[cloud_ind] = np.min(self.possibility[cloud_ind])
 91 
 92                 if True:
 93                     yield (selected_pc.astype(np.float32),
 94                            selected_labels.astype(np.int32),
 95                            selected_idx.astype(np.int32),
 96                            np.array([cloud_ind], dtype=np.int32))
 97 
 98         gen_func_a = spatially_regular_gen()
 99         gen_func_a.__next__()
100         gen_func = spatially_regular_gen
101         gen_types = (tf.float32, tf.int32, tf.int32, tf.int32)
102         gen_shapes = ([None, 3], [None], [None], [None])
103 
104         return gen_func, gen_types, gen_shapes
105 
106     def get_data(self, file_path):
107         seq_id = file_path.split('/')[-3]
108         frame_id = file_path.split('/')[-1][:-4]
109         kd_tree_path = join(self.dataset_path, seq_id, 'KDTree', frame_id + '.pkl')
110         # Read pkl with search tree
111         with open(kd_tree_path, 'rb') as f:
112             search_tree = pickle.load(f)
113         points = np.array(search_tree.data, copy=False)
114         # Load labels
115         if int(seq_id) >= 11:
116             labels = np.zeros(np.shape(points)[0], dtype=np.uint8)
117         else:
118             label_path = join(self.dataset_path, seq_id, 'labels', frame_id + '.npy')
119             labels = np.squeeze(np.load(label_path))
120         return points, search_tree, labels
121 
122     @staticmethod
123     def crop_pc(points, labels, search_tree, pick_idx):
124         # crop a fixed size point cloud for training
125         center_point = points[pick_idx, :].reshape(1, -1)
126         select_idx = search_tree.query(center_point, k=cfg.num_points)[1][0]
127         select_idx = DP.shuffle_idx(select_idx)
128         select_points = points[select_idx]
129         select_labels = labels[select_idx]
130         return select_points, select_labels, select_idx
131 
132     @staticmethod
133     def get_tf_mapping2():
134 
135         def tf_map(batch_pc, batch_label, batch_pc_idx, batch_cloud_idx):
136             features = batch_pc
137             input_points = []
138             input_neighbors = []
139             input_pools = []
140             input_up_samples = []
141 
142             for i in range(cfg.num_layers):
143                 neighbour_idx = tf.py_func(DP.knn_search, [batch_pc, batch_pc, cfg.k_n], tf.int32)
144                 sub_points = batch_pc[:, :tf.shape(batch_pc)[1] // cfg.sub_sampling_ratio[i], :]
145                 pool_i = neighbour_idx[:, :tf.shape(batch_pc)[1] // cfg.sub_sampling_ratio[i], :]
146                 up_i = tf.py_func(DP.knn_search, [sub_points, batch_pc, 1], tf.int32)
147                 input_points.append(batch_pc)
148                 input_neighbors.append(neighbour_idx)
149                 input_pools.append(pool_i)
150                 input_up_samples.append(up_i)
151                 batch_pc = sub_points
152 
153             input_list = input_points + input_neighbors + input_pools + input_up_samples
154             input_list += [features, batch_label, batch_pc_idx, batch_cloud_idx]
155 
156             return input_list
157 
158         return tf_map
159 
160     def init_input_pipeline(self):
161         print('Initiating input pipelines')
162         cfg.ignored_label_inds = [self.label_to_idx[ign_label] for ign_label in self.ignored_labels]
163         gen_function, gen_types, gen_shapes = self.get_batch_gen('training')
164         gen_function_val, _, _ = self.get_batch_gen('validation')
165         gen_function_test, _, _ = self.get_batch_gen('test')
166 
167         self.train_data = tf.data.Dataset.from_generator(gen_function, gen_types, gen_shapes)
168         self.val_data = tf.data.Dataset.from_generator(gen_function_val, gen_types, gen_shapes)
169         self.test_data = tf.data.Dataset.from_generator(gen_function_test, gen_types, gen_shapes)
170 
171         self.batch_train_data = self.train_data.batch(cfg.batch_size)
172         self.batch_val_data = self.val_data.batch(cfg.val_batch_size)
173         self.batch_test_data = self.test_data.batch(cfg.val_batch_size)
174 
175         map_func = self.get_tf_mapping2()
176 
177         self.batch_train_data = self.batch_train_data.map(map_func=map_func)
178         self.batch_val_data = self.batch_val_data.map(map_func=map_func)
179         self.batch_test_data = self.batch_test_data.map(map_func=map_func)
180 
181         self.batch_train_data = self.batch_train_data.prefetch(cfg.batch_size)
182         self.batch_val_data = self.batch_val_data.prefetch(cfg.val_batch_size)
183         self.batch_test_data = self.batch_test_data.prefetch(cfg.val_batch_size)
184 
185         iter = tf.data.Iterator.from_structure(self.batch_train_data.output_types, self.batch_train_data.output_shapes)
186         self.flat_inputs = iter.get_next()
187         self.train_init_op = iter.make_initializer(self.batch_train_data)
188         self.val_init_op = iter.make_initializer(self.batch_val_data)
189         self.test_init_op = iter.make_initializer(self.batch_test_data)
190 
191 
192 if __name__ == '__main__':
193     parser = argparse.ArgumentParser()
194     parser.add_argument('--gpu', type=int, default=0, help='the number of GPUs to use [default: 0]')
195     parser.add_argument('--mode', type=str, default='train', help='options: train, test, vis')
196     parser.add_argument('--test_area', type=str, default='14', help='options: 08, 11,12,13,14,15,16,17,18,19,20,21')
197     parser.add_argument('--model_path', type=str, default='None', help='pretrained model path')
198     FLAGS = parser.parse_args()
199 
200     os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
201     os.environ['CUDA_VISIBLE_DEVICES'] = str(FLAGS.gpu)
202     os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
203     Mode = FLAGS.mode
204 
205     test_area = FLAGS.test_area
206     dataset = SemanticKITTI(test_area)
207     dataset.init_input_pipeline()
208 
209     if Mode == 'train':
210         model = Network(dataset, cfg)
211         model.train(dataset)
212     elif Mode == 'test':
213         cfg.saving = False
214         model = Network(dataset, cfg)
215         if FLAGS.model_path is not 'None':
216             chosen_snap = FLAGS.model_path
217         else:
218             chosen_snapshot = -1
219             logs = np.sort([os.path.join('results', f) for f in os.listdir('results') if f.startswith('Log')])
220             chosen_folder = logs[-1]
221             snap_path = join(chosen_folder, 'snapshots')
222             snap_steps = [int(f[:-5].split('-')[-1]) for f in os.listdir(snap_path) if f[-5:] == '.meta']
223             chosen_step = np.sort(snap_steps)[-1]
224             chosen_snap = os.path.join(snap_path, 'snap-{:d}'.format(chosen_step))
225         tester = ModelTester(model, dataset, restore_snap=chosen_snap)
226         tester.test(model, dataset)
227     else:
228         ##################
229         # Visualize data #
230         ##################
231 
232         with tf.Session() as sess:
233             sess.run(tf.global_variables_initializer())
234             sess.run(dataset.train_init_op)
235             while True:
236                 flat_inputs = sess.run(dataset.flat_inputs)
237                 pc_xyz = flat_inputs[0]
238                 sub_pc_xyz = flat_inputs[1]
239                 labels = flat_inputs[17]
240                 Plot.draw_pc_sem_ins(pc_xyz[0, :, :], labels[0, :])
241                 Plot.draw_pc_sem_ins(sub_pc_xyz[0, :, :], labels[0, 0:np.shape(sub_pc_xyz)[1]])

 在98行之前加上以下两句代码:

        gen_func_a = spatially_regular_gen()
        gen_func_a.__next__()

可以调试运行69行的这个函数:def spatially_regular_gen():

第75行是取保存的降采样数点云数据,返回点云、标签和序列化数据kdtree数据。

对第75行进行可视化:

 75                     pc, tree, labels = self.get_data(pc_path)

可视化的代码:

pointsandlabels=np.hstack((pc,labels.reshape(labels.shape[0],1)))
filename=r'/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/02/000477.xyz'
np.savetxt(filename, pointsandlabels, fmt="%.8f,%.8f,%.8f,%d", delimiter=',')

 

 

 

 

 

上述可以出从预处理的数据得到训练数据的过程:

  1. line 72:通过get_data这个函数从文件中读取预处理保存好的point,kdtree,label
  2. line 123~130:通过crop_pc这个函数,从point中任意指定一个初始点,寻找cfg.num_points(4096*11=45056个)临近点作为crop后的点,也就是网络的输入。然后打乱这些点的顺序
  3. line 141-150:计算输入点中每个点的k(16个)临近点,计算下采样的点(由于点的顺序已经打乱,所以从前面取就行),计算下采样的点在上一层点的序号pool_idx,计算恢复过程中要使用的序号up_idx
  4. line 152-153:将上述信息合起来作为网络的输入

RandLA-Net

由于RandLA-Net的结构比较简单,就不画图表示了,直接看代码吧。

前向计算

# RandLANet.py

# line 103-125
def inference(self, inputs, is_training):
     d_out = self.config.d_out
     feature = inputs['features']
     feature = tf.layers.dense(feature, 8, activation=None, name='fc0')
     feature = tf.nn.leaky_relu(tf.layers.batch_normalization(feature, -1, 0.99, 1e-6, training=is_training))
     feature = tf.expand_dims(feature, axis=2)

     # ###########################Encoder############################
     f_encoder_list = []
     for i in range(self.config.num_layers):
         f_encoder_i = self.dilated_res_block(feature, inputs['xyz'][i], inputs['neigh_idx'][i], d_out[i],
                                              'Encoder_layer_' + str(i), is_training)
         f_sampled_i = self.random_sample(f_encoder_i, inputs['sub_idx'][i])
         feature = f_sampled_i
         if i == 0:
             f_encoder_list.append(f_encoder_i)
         f_encoder_list.append(f_sampled_i)
     # ###########################Encoder############################

     feature = helper_tf_util.conv2d(f_encoder_list[-1], f_encoder_list[-1].get_shape()[3].value, [1, 1],
                                     'decoder_0',
                                     [1, 1], 'VALID', True, is_training)

     # ###########################Decoder############################
     f_decoder_list = []
     for j in range(self.config.num_layers):
         f_interp_i = self.nearest_interpolation(feature, inputs['interp_idx'][-j - 1])
         f_decoder_i = helper_tf_util.conv2d_transpose(tf.concat([f_encoder_list[-j - 2], f_interp_i], axis=3),
                                                       f_encoder_list[-j - 2].get_shape()[-1].value, [1, 1],
                                                       'Decoder_layer_' + str(j), [1, 1], 'VALID', bn=True,
                                                       is_training=is_training)
         feature = f_decoder_i
         f_decoder_list.append(f_decoder_i)
     # ###########################Decoder############################

     f_layer_fc1 = helper_tf_util.conv2d(f_decoder_list[-1], 64, [1, 1], 'fc1', [1, 1], 'VALID', True, is_training)
     f_layer_fc2 = helper_tf_util.conv2d(f_layer_fc1, 32, [1, 1], 'fc2', [1, 1], 'VALID', True, is_training)
     f_layer_drop = helper_tf_util.dropout(f_layer_fc2, keep_prob=0.5, is_training=is_training, scope='dp1')
     f_layer_fc3 = helper_tf_util.conv2d(f_layer_drop, self.config.num_classes, [1, 1], 'fc', [1, 1], 'VALID', False,
                                         is_training, activation_fn=None)
     f_out = tf.squeeze(f_layer_fc3, [2])
     return f_out

inference这个函数就是前向计算的函数,从中可以看到,RandLA-Net有以下结构:

  1. 将特征升维到8
  2. encoder:由4个(dilated_res_block+random_sample)构成,形成特征金字塔
  3. 将金字塔尖的特征再次计算以下
  4. decoder:由4个(nearest_interpolation+conv2d_transpose)构成,恢复到point-wise的特征
  5. 由point-wise经过一些MLP,得到f_out

 

 

/utils/data_prepare_semantickitti.py第54行:

proj_inds = np.squeeze(search_tree.query(points, return_distance=False))

意思是:对构建的采样子集建立k_d搜索树,然后查询原点集中每个点的k个最邻近子集点,默认K=1.查询范围是采样点

函数:query(self, X, k=1, return_distance=True, dualtree=False, breadth_first=False)

函数作用:在(k_d搜索)树中查询k个最近邻。

参数:X:数组,是查询参考点,或者说中心点。

k: 要返回的最邻近点的数量,默认是1。

返回的索引是采样点集的索引值,因为最小索引值是0,最大索引值是88108(以sequences08中的000000.bin点云文件为例),而采样点个数正是88109个。所以,应该是针对初始点云中的每一个点在降采样点云中寻找最邻近点。

对sequences00~10(训练集,08除外)进行随机采样,并:

  • 保存随机采样的点云坐标和标签[到文件夹[velodyne]  [labels]]
  • 对随机采样点云构建k_d树,并保存[到文件夹[KDTree]]

其中sequences08(验证集)用来验证,并:

  • 保存随机采样的点云坐标和标签[到文件夹[velodyne]  [labels]]
  • 对随机采样点云构建k_d树,并保存[到文件夹KDTree]
  • 保存初始点云(00xxxx.bin)从采样点中找到的k个邻近点索引[到文件夹proj]

 对测试集sequences11~21:

  • 保存随机采样的点云坐标[到文件夹velodyne],不保存对应标签。
  • 对随机采样点云构建k_d树,并保存[到文件夹KDTree]
  • 保存初始点云(00xxxx.bin)从采样点中找到的k个邻近点索引[到文件夹proj]

 对测试集的测试结果进行可视化,可视化代码如下:

'''
点云和标签另存代码:
pointsandlabels=np.hstack((pc,labels.reshape(labels.shape[0],1)))
filename=r'/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/02/000477.xyz'
np.savetxt(filename, pointsandlabels, fmt="%.8f,%.8f,%.8f,%d", delimiter=',')


读取.npy文件:
import numpy as np
points_path='/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences/11/velodyne/000000.bin'
pointcloud=np.load(points_path) #main_SemanticKITTI.py L123
print('hello')

下面的代码的作用是另存测试点云和标签,以便可视化,看测试结果
'''



#The following code are from data_prepare_semantickitti.py file


import pickle, yaml, os, sys
import numpy as np
from os.path import join, exists, dirname, abspath
from sklearn.neighbors import KDTree
import random
BASE_DIR = dirname(abspath(__file__))
ROOT_DIR = dirname(BASE_DIR)
sys.path.append(BASE_DIR)
sys.path.append(ROOT_DIR)
from helper_tool import DataProcessing as DP

data_config = os.path.join(BASE_DIR, 'semantic-kitti.yaml')
DATA = yaml.safe_load(open(data_config, 'r'))
remap_dict = DATA["learning_map"]
max_key = max(remap_dict.keys())
remap_lut = np.zeros((max_key + 100), dtype=np.int32)
remap_lut[list(remap_dict.keys())] = list(remap_dict.values())

grid_size = 0.06
dataset_path = '/media/dell/D/qcc/RandLA-Net/test/sequences'
output_path = '/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences' + '_' + str(grid_size)
#seq_list = np.sort(os.listdir(dataset_path))
seq_list_point='14' #原始点云所在文件夹
seq_list='14_20epoch' #预测标签所在文件夹
pointcloud_path='/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences/14/velodyne'
label_path=''.join(['/media/dell/D/qcc/RandLA-Net/test/sequences/',seq_list,'/predictions'])
#for seq_id in seq_list:
    #print('sequence' + seq_id + ' start')
    #seq_path = join(dataset_path, seq_id)
    #seq_path_out = join(output_path, seq_id)
    #pc_path = join(seq_path, 'predictions')
    #pc_path_out = join(seq_path_out, 'velodyne')
    #KDTree_path_out = join(seq_path_out, 'KDTree')
    #os.makedirs(seq_path_out) if not exists(seq_path_out) else None
    #os.makedirs(pc_path_out) if not exists(pc_path_out) else None
    #os.makedirs(KDTree_path_out) if not exists(KDTree_path_out) else None

    #if int(seq_id) <= 21:
        #label_path = join(seq_path, 'labels')
        #label_path_out = join(seq_path_out, 'labels')
        #os.makedirs(label_path_out) if not exists(label_path_out) else None
        #scan_list = np.sort(os.listdir(pc_path))
        #for scan_id in scan_list:
            #print(scan_id)
#scan_list = np.sort(os.listdir(pointcloud_path))
scan_list = os.listdir(pointcloud_path)
scanrandom_list=random.sample(scan_list,10)
for scan_id in scanrandom_list:
    #print(scan_id)
    pointcloud=join(pointcloud_path,scan_id)
    lname=''.join([scan_id.split('.')[0],'.label'])
    labe_path = join(label_path,lname)
    points = DP.load_pc_kitti(pointcloud)
    labels = DP.load_label_kitti(labe_path, remap_lut)
    pointsandlabels=np.hstack((points,labels.reshape(labels.shape[0],1)))
    filename=''.join(['/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/',seq_list,'/',scan_id.split('.')[0],'.xyz']) #另存的点云全路径
    np.savetxt(filename, pointsandlabels, fmt="%.8f,%.8f,%.8f,%d", delimiter=',')
    print(''.join([scan_id,'  save points and corresponding labels finished!']))

下面是测试集14的预测结果(使用作者的训练模型):

 

 下面是测试集14的原始点云和预测标签另存结果(随机选取10个文件进行另存):

 另存的000561.xyz文本文件及可视化如下:

 

 

 不同的标签分开存成不成的文件:

'''
save pointclouds and labels into different files
according to labels, so as to visulize different
kinds of point clouds.
'''
import os
from os.path import join
import numpy as np
path = '/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/14/000561.xyz' #path是打开的文件的全路径''
b = os.path.splitext(path)#把打开的文件的全路径分割成文件名和后缀名
#c = [b[0], '_trace', '.xyz']#文件名加上其他字符变成想要的文件名,相当于在原来的路径下修改一下文件名
#d = ''.join(c)#要写入的文件的名字,
d='/media/dell/D/qcc/RandLA-Net/data/semantic_kitti/dataset/sequences_saveastxtfile_Qin/14/000561/'

labels=[]
indexes=[]

with open(path, 'r', encoding='utf-8') as f:
    for line in f:
        s1 = line.strip()  # 把每一行行末的换行符去掉
        label = (s1.strip().split(' ')[-1])  # 以空格分隔数据,并倒着去每一行的第4,5,6个数据(Y,X,H)
        labels.append(label)
label_array=np.array(labels)

labels_numbers=np.unique(label_array)
for j in labels_numbers:
    sign='0'
    #label_id='18'
    file_name=''.join([d,'Label_',j,'.xyz'])
    with open(file_name, 'w+') as f1:
        with open(path, 'r', encoding='utf-8') as f:
            for line in f:
                s1 = line.strip()#把每一行行末的换行符去掉
                label = (s1.strip().split(' ')[-1])#以空格分隔数据,并倒着去每一行的第4,5,6个数据(Y,X,H)
                if label==j:
                    sign='1'
                    f1.writelines(line)
    if sign=='1':
        print(''.join(['file- ',j,'.txt',' -finished!']))

 

posted on 2020-11-04 19:25  一杯明月  阅读(232)  评论(0编辑  收藏