点云采样方法及算法实现

最近学习点云重建三维网格,看了写帖子,这是几个帖子的综合,最后有引用的链接。

1 点云采样方法

点云采样的方法有很多种,常见的有均匀采样、几何采样、随机采样、格点采样等。下面介绍一些常见的采样方法。

1.1 格点采样

格点采样,也称格子采样cell sampling、网格采样grid sampling,就是把三维空间用格点离散化,然后在每个格点里采样一个点。具体方法如下:

1. 创建格点:如图1中间所示,计算点云的包围盒,然后把包围盒离散成小格子。格子的长宽高可以用户设定,也可以通过设定包围盒三个方向的格点数来求得。

2. 每个小格子包含了若干个点,取离格子中心点最近的点为采样点,如图1右所示。

图1 格点采样示意图

格点采样的特点:

  1. 效率非常高
  2. 采样点分布比较均匀,但是均匀性没有均匀采样高
  3. 可以通过格点的尺寸控制点间距
  4. 不能精确控制采样点个数

1.2 均匀采样

均匀采样的方法有很多,并且有一定的方法来评估采样的均匀性。这里介绍一种简单的均匀采样方法,最远点采样。具体方法如下:

输入点云记为C,采样点集记为S,S初始化为空集。

1. 随机采样一个种子点Seed,放入S。如图2所示。

2. 每次采样一个点,放入S。采样的方法是,在集合C-S里,找一点距离集合S距离最远的点。其中点到集合的距离为,这点到集合里所有点距最小的距离。如图2所示,采样点S的数量分别为2,4,10,20,100.

图2 均匀采样示意图

最远点采样的特点:

  1. 采样点分布均匀
  2. 算法时间复杂度有些高,因为每次采样一个点,都要计算集合到集合之间的距离。可以采用分治的方法来提高效率。
  3. 采样点一般先分布在边界附近,这个性质在有些地方是有用的,比如图元检测里面的点采样

1.3 几何采样

几何采样,在点云曲率越大的地方,采样点个数越多。下面介绍一种简单的几何采样方法,具体方法如下:

输入是一个点云,目标采样数S,采样均匀性U

  1. 点云曲率计算比较耗时,这里我们采用了一个简单方法,来近似达到曲率的效果:给每个点计算K邻域,然后计算点到邻域点的法线夹角值。曲率越大的地方,这个夹角值就越大。
  2. 设置一个角度阈值,比如5度。点的邻域夹角值大于这个阈值的点,被放入几何特征区域G。这样点云就分成了两部分,几何特征区域G和其它区域。
  3. 均匀采样几何特征区域G和其它区域,采样数分别为S * (1 - U),S * U。

下图是一个均匀采样和几何采样的比较图,这个采样方法的特点:

  1. 几何特征越明显的区域,采样点个数分布越多
  2. 计算效率高
  3. 采样点局部分布是均匀的
  4. 稳定性高:通过几何特征区域的划分,使得采样结果抗噪性更强

2 pcl点云采样算法库

2.1 pcl点云算法库简介

点云数据的处理可以采用获得广泛应用的Point Cloud Library (点云库,PCL库)。PCL库是一个最初发布于2013年的开源C++库,目前最新的版本是2020年3月19日发布的1.10.1。它实现了大量点云相关的通用算法和高效的数据管理。支持多种操作系统平台,可在Windows、Linux、Android、Mac OS X、部分嵌入式实时系统上运行。如果说OpenCV是2D信息获取与处理的技术结晶,那么PCL在3D信息获取与处理上,就与OpenCV具有同等地位,PCL是BSD授权方式,可以免费进行商业和学术应用。

PCL包含多个模块,如滤波(filter)、特征提取(features)、关键点(keypoint)、配准(registration)、k-d树(k-d tree)、八叉树(octree)、分割(segmentation)、抽样一致(Random sample consensus)、表面重建(surface)、识别(recognition)、输入输出(io),visualization(可视化)等模块。其中滤波模块常用来进行点云采样。

2.2 pcl常用采样算法及使用

(1)下采样Downsampling

一般下采样是通过构造一个三维体素栅格,然后在每个体素内用体素内的所有点的重心近似显示体素中的其他点,这样体素内所有点就用一个重心点来表示,进行下采样的来达到滤波的效果,这样就大大的减少了数据量,特别是在配准,曲面重建等工作之前作为预处理,可以很好的提高程序的运行速度。其实就是栅格滤波,这种滤波方式在项目中是经常使用的一种方式,能够有效的减少数据量,提高计算效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char** argv)
{
    // 创建点云对象
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PointCloud<pcl::PointXYZ>::Ptr filteredCloud(new pcl::PointCloud<pcl::PointXYZ>);

    // 读取PCD文件
    if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
    {
        return -1;
    }

    // 创建滤波对象
    pcl::VoxelGrid<pcl::PointXYZ> filter;
    filter.setInputCloud(cloud);
    // 设置体素栅格的大小为 1x1x1cm
    filter.setLeafSize(0.01f, 0.01f, 0.01f);
    filter.filter(*filteredCloud);
}

(2)均匀采样

这个类基本上是相同的,但它输出的点云索引是选择的关键点在计算描述子的常见方式。均匀采样的方法在数据噪声处理中也是经常使用的一种采样方法,能够有效的降低噪声。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pcl/io/pcd_io.h>
#include <pcl/keypoints/uniform_sampling.h>

int
main(int argc, char** argv)
{
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PointCloud<pcl::PointXYZ>::Ptr filteredCloud(new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
    {
        return -1;
    }
    // Uniform sampling object.
    pcl::UniformSampling<pcl::PointXYZ> filter;
    filter.setInputCloud(cloud);
    filter.setRadiusSearch(0.01f);
    // We need an additional object to store the indices of surviving points.
    pcl::PointCloud<int> keypointIndices;

    filter.compute(keypointIndices);
    pcl::copyPointCloud(*cloud, keypointIndices.points, *filteredCloud);
}

(3)增采样

采样是一种表面重建方法,当你有比你想象的要少的点云数据时,增采样可以帮你恢复原有的表面(S),通过内插你目前拥有的点云数据,这是一个复杂的猜想假设的过程。所以构建的结果不会百分之一百准确,但有时它是一种可选择的方案。所以,在你的点云云进行下采样时,一定要保存一份原始数据!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <pcl/io/pcd_io.h>
#include <pcl/surface/mls.h>

int main(int argc,char** argv)
{
// 新建点云存储对象
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr filteredCloud(new pcl::PointCloud<pcl::PointXYZ>);

    // 读取文件
    if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
    {
        return -1;
    }
    // 滤波对象
    pcl::MovingLeastSquares<pcl::PointXYZ, pcl::PointXYZ> filter;
    filter.setInputCloud(cloud);
    //建立搜索对象
    pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree;
    filter.setSearchMethod(kdtree);
    //设置搜索邻域的半径为3cm
    filter.setSearchRadius(0.03);
    // Upsampling 采样的方法有 DISTINCT_CLOUD, RANDOM_UNIFORM_DENSITY
    filter.setUpsamplingMethod(pcl::MovingLeastSquares<pcl::PointXYZ, pcl::PointXYZ>::SAMPLE_LOCAL_PLANE);
    // 采样的半径是
    filter.setUpsamplingRadius(0.03);
    // 采样步数的大小
    filter.setUpsamplingStepSize(0.02);

    filter.process(*filteredCloud);
}

(4)表面重建

通常,传感器的测量是不准确的,和由此产生的点云也是存在的测量误差,比如离群点,孔等表面,可以用一个算法重建表面,遍历所有的点云和插值数据,试图重建原来的表面。比如增采样,PCL使用MLS算法和类。执行这一步是很重要的,因为由此产生的点云的法线将更准确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <pcl/io/pcd_io.h>
#include <pcl/surface/mls.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/visualization/cloud_viewer.h>
#include <boost/thread/thread.hpp>
int
main(int argc, char** argv)
{
 
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);    
    pcl::PointCloud<pcl::PointNormal>::Ptr smoothedCloud(new pcl::PointCloud<pcl::PointNormal>);

    if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) != 0)
    {
        return -1;
    }

    // Smoothing object (we choose what point types we want as input and output).
    pcl::MovingLeastSquares<pcl::PointXYZ, pcl::PointNormal> filter;
    filter.setInputCloud(cloud);
    // Use all neighbors in a radius of 3cm.
    filter.setSearchRadius(0.03);
    // If true, the surface and normal are approximated using a polynomial estimation
    // (if false, only a tangent one).
    filter.setPolynomialFit(true);
    // We can tell the algorithm to also compute smoothed normals (optional).
    filter.setComputeNormals(true);
    // kd-tree object for performing searches.
    pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree;
    filter.setSearchMethod(kdtree);

    filter.process(*smoothedCloud);


  boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(new pcl::visualization::PCLVisualizer("smooth"));
viewer->addPointCloud<pcl::PointNormal>(smoothedCloud,"smoothed");

while(!viewer->wasStopped())
  {
 viewer->spinOnce(100);
boost::this_thread::sleep(boost::posix_time::microseconds(1000000));
 }
}

参考资料

https://zhuanlan.zhihu.com/p/86044055

https://zhuanlan.zhihu.com/p/99207501

https://zhuanlan.zhihu.com/p/124725398

https://www.openmp.org//