PCL中3D点云特征描述与提取(一)_点云特征提取有什么-程序员宅基地

技术标签: PCL  PCL入门教程  


  3D点云特征描述与提取是点云信息处理中最基础也是最关键的一部分,点云的识别、分割、重采样、配准、曲面重建等处理的大部分算法,都严重依赖特征描述与提取的结果。从尺度上来分,一般分为局部特征描述和全局特征描述,例如局部的法线等几何形状特征的描述,全局的拓扑特征描述,都属于3D点云特征描述与提取范畴。在PCL中,目前已有很多基本的特征描述子与提取算法,相信在PCL的快速发展下,将来会集成和添加更多特征描述子和提取算法。

  本章首先对涉及的部分点云特征描述与提取的概念进行简介,由于特征描述子和提取算法的多样性,和实例相关的概念在后面结合实例也进行了详细介绍;其次对PCL的特征描述与提取相关模块及类进行简单介绍;最后通过应用实例来展示如何对PCL中特征描述与提取相关模块进行灵活运用,例如法线估计、各种点特征描述子的提取方法等。

1 特征描述与提取的概念及相关算法

1.1 3D形状内容描述子

  利用描述子建立曲面间的对应点在3D物体识别领域有广泛应用。采用一个向量描述曲面上指定点及其邻域的形状特征,通过匹配向量的值来建立不同曲面间点的对应关系,此向量即为指定点的描述子。3D形状内容描述子构造简单,辨别力强,且对噪声不敏感。其构造方法为:在以指定点p为中心的球形支撑域内,沿径向、方向角和俯仰角3个坐标方向划分成网格,统计落入网格内的点数,构造向量V。V的每个元素与支撑域内的一个网格对应,元素的值为对应网格中的点数,向量V即为点p的描述子。3D shape context网格划分如下图所示。详细内容请参考[Andrea Frome, Daniel Huber, Ravi Kolluri and Thomas Bulow, Jitendra Malik: Recognizing Objects in Range Data Using Regional Point Descriptors, In proceedings of the 8th European Conference on Computer Vision (ECCV), Prague, May 11-14, 2004]
在这里插入图片描述

3D shape context 网格划分

1.2 旋转图像(Spin Images)

  旋转图像最早是Johnson提出的特征描述子,主要用于3D场景中的曲面匹配和模型识别。如下面第一幅图所示,在模型表面上,存在顶点 p p p 和其法向量 n n n 定义的二维基,以及切平面 P P P,假设模型上任意顶点 x x x ,现定义 α \alpha α x x x 在平面 P P P 上投影点与 p p p点的距离,规定其值取大于零的实数, β \beta β x x x 与其在平面 P P P 上投影点之间的距离,按照向上或向下规定其有正负之分,点 p p p 的旋转图像则为,将除 p p p 点外其他模型上的顶点在 P P P 上的投影( α i \alpha _{i} αi β i \beta _{i} βi),其中 i i i 表示顶点的一维索引,将( α i \alpha _{i} αi β i \beta _{i} βi)统计得到二维直方图即为点 p p p 的旋转图像,图像的坐标由 α \alpha α β \beta β 而定,强度为( α \alpha α β \beta β)落在同一统计区间的点的统计个数。下面第二幅图所示模型表面三个点的旋转图像可帮助大家理解。
在这里插入图片描述

旋转图像生成示意图

在这里插入图片描述

模型表面三个点的旋转图像

1.3 PCL中特征描述与提取模块及类

  PCL中pcl_features库提供了特征描述与提取相关的基本数据结构与算法,目前PCL内部的特征提取算法包含基础和最新的点云或曲面模型相关的描述子实现,包括法线估计、多种基于近邻的局部描述算子、基于视角的全局描述算子等等,其依于commonsearchkdtreeoctreerange_image模块。类和函数的接口说明受篇幅所限,请感兴趣的读者自行查阅相关资料,或者查看官网

2 点云特征描述与提取入门级实例解析

2.1 PCL中描述三维特征相关基础

  本小节介绍点云库(PCL)中的三维特征描述子工作原理,以及在pcl::feature模块中类的通用调用习惯。

2.1.1 理论基础

  在原始表示形式下,点的定义是用笛卡尔坐标系坐标 x x x y y y z z z 相对于一个给定的原点来简单表示的三维映射系统的概念,假定坐标系的原点不随着时间而改变,这里有两个点 p 1 p_{1} p1 p 2 p_{2} p2 ,分别在时间 t 1 t_{1} t1 t 2 t_{2} t2 捕获,有着相同的坐标。对这两个点做比较其实是属于不适定问题(ill-posed problem),因为虽然相对于一些距离测度(如:欧几里得度量)它们是相等的,但是它们取样于完全不同的表面,因此当把它们和邻近的其他环境中的点放在一起时,它们表达着完全不同的信息,这是因为在 t 1 t_{1} t1 t 2 t_{2} t2 之间局部环境有可能发生改变。一些获取设备也许能够提供取样点的额外数据,例如强度或表面反射率等,甚至颜色,然而那并不能完全解决问题,单从两个点之间来对比仍然是不适定问题。由于各种不同需求需要进行对比以便能够区分曲面空间的分布情况,应用软件要求更好的特征度量方式,因此作为一个单一实体的三维点概念和笛卡尔坐标系被淘汰了,出现了一个新的概念取而代之:局部描述子(local descriptor)。文献中对这一概念的描述有许多不同的命名,如:形状描述子(shape descriptor)或几何特征(geometric features),本文中剩余部分都统称之为点特征表示(point feature representations)。通过包括周围的邻域,特征描述子能够表征采样表面的几何性质,它有助于解决不适定的对比问题。如下图所示,理想情况下,相同或相似表面上的点的特征值将非常形似(相对特定度量准则),而不同表面上的点的特征描述子将有明显差异。下面几个条件,通过能否获得相同的局部表面特征值,可以判定点特征表示方式的优劣。

   ∙ \bullet 刚体变换(rigid transformations)—— 即三维旋转和三维平移变化不会影响特征向量F估计,即特征向量具有平移旋转不变性。
   ∙ \bullet 改变采样密度(varying sampling density)—— 原则上,一个局部表面小块的采样密度无论是大还是小,都应该有相同的特征向量值,即特征向量具有抗密度干扰性。
   ∙ \bullet 噪音(noise)—— 数据中有轻微噪音的情况下,点特征表示在它的特征向量中必须保持相同或者投其相似的值,即特征向量对点云噪声具有鲁棒性。

在这里插入图片描述

点特征描述子示意图

  通常,PCL中特征向量利用快速kd tree查询,使用近似法来计算查询点的最近邻元素,有两种常用的查询类型。
  (1)决定一个查询点的k邻域元素(k为用户已给参数)(也称为k-搜索)
  (2)在半径r的围内,确定一个查询点的所有邻元素(也称为半径-搜索)

2.1.2 输入点云调用习惯

  因为所有点云库中的类都继承来自基类pcl::PCLBasepcl::Feature类接受以下两种不同方式的输入数据。
  (1)一个完整的点云数据集,由setInputCloud (PointCloudConstPtr &) 给出——此函数必须设置,这样后续特征算子才能正常计算,任何可以进行特征描述子估计的类,为给定的输入点云中的每个点估计一个特征向量。
  (2)点云数据集的一个子集,由setInputCloud (PointCloudConstPtr &)setIndices (IndicesConstPtr &) 给出——后面的setIndices 函数为可选设置。如果传入IndicesConstPtr 参数,则任何可以进行特征估计的类将为给定输入点云中的索引对应的点估计一个特征,默认情况下,如果没有给出一组索引,点云中的所有点参与计算。

  此外,通过一个附加调用程序,可以明确指定搜索时使用的点邻域集合setSearchSurface (PointCloudConstPtr &),这个调用是可选的,当搜索点邻域集合未给出时,则输入点云数据为默认的搜索空间。因为总是需要 setInputCloud (),所以我们可以使用<setInputCloud(), setIndices(), setSearchSurface()>来创建四个组合。假如我们有两个点云, P = { p 1 , p 2 , . . . , p n } P = \left \{ p_{1}, p_{2},..., p_{n}\right \} P={ p1,p2,...,pn} Q = { q 1 , q 2 , . . . , q n } Q = \left \{ q_{1}, q_{2},..., q_{n}\right \} Q={ q1,q2,...,qn},如下图所示,则表示了所以四种情况:从左到右边依次为,未指定索引和搜索点云集合、只指定了索引、只指定了搜索点云集合、指定了索引和搜索点云集合。

点云输入组合

点云输入组合

   ∙ \bullet setIndices() = false, setSearchSurface() = false —— 毫无疑问这是点云库中最常用的情况,用户只需要输入一个单一的点云数据集,并且为点云中的所有点估计一特征向量。不论一组索引和(或)搜索点云是否给定,都不希望保存不同的实现副本,无论何时,即使indices = false,PCL都会创建一组内部索引(为 std::vector<int>),这个索引集是指向整个数据集的(indices=1..NN是点云中点的数目)。上述与图中最左边的情况对应,首先,我们估计了 p 1 p_{1} p1的最近邻元素,然后是 p 2 p_{2} p2的最近邻元素,以此类推,直到我们估计完 P P P中的所有点。

   ∙ \bullet setIndices() = true, setSearchSurface() = false —— 如前面所提到的,特征估计方法只计算已给索引的点的特征。对应上图的第二种情况,这里,我们假设 p 2 p_{2} p2的索引不在已给的索引向量中,因此在 p 2 p_{2} p2点处,没有估计邻元素或者特征向量。

   ∙ \bullet setIndices() = false, setSearchSurface() = true —— 如第一种情况,对所有已给点进行特征向量估计,但是,在setSearchSurface()中给出的采样面点云将用来为输入点获取最近邻元素,而不是输入点云本身,上述对应图中第三种情况。如果 Q = { q 1 , q 2 } Q = \left \{ q_{1}, q_{2}\right \} Q={ q1,q2}作为输入,是不同于 P P P的另一个给出的点云, P P P Q Q Q的搜索表面,那么将从 P P P中计算两个点 q 1 q_{1} q1 q 2 q_{2} q2的近邻。

   ∙ \bullet setIndices() = true, setSearchSurface() = true —— 这种组合可能是最少见的情况,索引和搜索点云都给定。这种情况下,将使用setSearchSurface()中给出的搜索点云,只对<input, indices>中的子集进行特征向量估计。上述对应图中最后(最右端)一种情况,这里,我们假设 q 2 q_{2} q2的索引没有在 Q Q Q的已给索引向量中,因此在 q 2 q_{2} q2点处,没有估计其邻元素或者特征。

  在使用 setSearchSurface()时,最有用的案例是:当有一个非常密集的输入点云数据集时,我们不想对它里面的所有点都进行特征估计,而是希望在找到的一些关键点处(使用pcl_keypoints中的方法进行估计),或者在点云的下采样版本中(如:使用pcl::VoxelGrid<T>过滤而获得的)进行特征估计。这种情况下,我们通过setInputCloud()来把下采样后的点云/ 关键点传递给特征估计算法,而把原始数据通过setSearchSurface()设置为搜索集合,从而提高程序的运行效率。

2.2 估计一个点云的表面法线

  表面法线是几何体表面的重要属性,在很多领域都有大量应用,例如:在进行光照渲染时产生符合可视习惯的效果时需要表面法线信息才能正常进行,对于一个已知的几何体表面,根据垂直于点表面的矢量,因此推断表面某一点的法线方向通常比较简单。然而,由于我们获取的点云数据集在真实物体的表面表现为一组定点样本,这样就会有两种解决方法。
  (1)使用曲面重建技术,从获取的点云数据集中得到采样点对应的曲面,然后从曲面模型中计算表面法线。
  (2)直接从点云数据集中近似推断表面法线。

  本小节将针对后一种情况进行讲解,已知一个点云数据集,在其中的每个点处直接近似计算表面法线。

2.2.1 理论基础

  尽管有许多不同的法线估计方法,本教程中着重讲解的是其中最简单的一个,表述如下:确定表面一点法线的问题近似于估计表面的一个相切面法线的问题,因此转换过来以后就变成一个最小二乘法平面拟合估计问题。
  注意:更多信息,包含最小二乘法问题的数学方程式,可以查看相关文章

  因此估计表面法线的解决方案就变成了分析一个协方差矩阵的特征矢量和特征值(或者PCA——主成分分析),这个协方差矩阵从查询点的近邻元素中创建。更具体地说,对于每一个点 P i P_{i} Pi,对应的协方差矩阵 C C C如下:
C = 1 K ∑ i = 1 K ( P i − P ˉ ) ( P i − P ˉ ) T , C ⋅ V ⃗ j = λ j ⋅ V ⃗ j , j ϵ { 0 , 1 , 2 } C = \frac{1}{K}\sum_{i=1}^{K}\left ( P_{i} - \bar{P}\right )\left ( P_{i} - \bar{P} \right )^{T},C\cdot \vec{V}_{j}=\lambda _{j}\cdot \vec{V}_{j}, j\epsilon \left \{ 0,1,2 \right \} C=K1i=1K(PiPˉ)(PiPˉ)T,CV j=λjV j,jϵ{ 0,1,2}
此处, K K K 是点 P i P_{i} Pi 邻近点的数目, P ˉ \bar{P} Pˉ 表示最近邻元素的三维质心, λ j \lambda _{j} λj 是协方差矩阵的第 j j j 个特征值, V ⃗ j \vec{V}_{j} V j 是第 j j j 个特征向量。

  在PCL内估计一点集对应的协方差矩阵,可以使用以下函数调用实现:

//定义每个表面小块的3x3协方差矩阵的存储对象
Eigen::Matrix3f covariance_matrix;
//定义一个表面小块的质心坐标16字节对齐存储对象
Eigen::Vector4f xyz_centroid;
//估计质心坐标
compute3DCentroid(cloud, xyz_centroid);
//计算3x3协方差矩阵
computeCovarianceMatrix(cloud, xyz_centroid, covariance_matrix);

  通常,没有数学方法能解决法线的正负向问题,如上所示,通过主成分分析法(PCA)来计算它的方向也具有二义性,无法对整个点云数据集的法线方向进行一致性定向。下图(1)中显示出对一个更大数据集的两部分产生的影响,此数据集来自于厨房环境的一部分,很明显估计的法线方向并非完全一致,图(2)部分展现了其对应扩展的高斯图像(EGI),也称为法线球体(normal sphere),它描述了点云中所有法线的方向。由于数据集是2.5维,其只从一个单一的视角获得,因此法线应该仅呈现出一半球体的扩展高斯图像(EGI)。然而,由于定向的不一致性,它们遍布整个球体,如下图所示。

在这里插入图片描述

          (1)估计厨房环境的表面法线                (2)法线球体

  如果实际知道视点 V p V_{p} Vp,那么这个问题的解决是非常简单的。对所有法线 V ⃗ j \vec{V}_{j} V j 定向只需要使它们一致朝向视点方向,满足下面的方程式:
V ⃗ j ⋅ ( V p − P i ) > 0 \vec{V}_{j}\cdot \left ( V_{p}-P_{i} \right )>0 V j(VpPi)>0
  下图展示了上图中的数据集的所有法线被一致定向到视点后的结果:

在这里插入图片描述

将所有法线一致定向到后视点的结果

  在PCL中对一个已知点的法线进行手动重新定向,可以使用如下代码:

flipNormalTowardsViewpoint (const PointT &point, float vp_x, float vp_y, float vp_z, Eigen::Vector4f &normal)

  注意:如果数据集是从多个捕获视点中配准后集成的,那么上述法线的一致性定向方法就不适用了。需要使用更复杂的算法,可以查看文章

2.2.2 选择合适的尺度

  如之前介绍的,在估计一个点的表面法线时,我们需要从周围支持这个点的邻近点着手(也称作k邻域)。最近邻估计问题的具体内容又提出了另一个问题“合适的尺度”:已知一个取样点云数据集,k的正确取值是多少(k通过pcl::Feature::setKSearch给出)或者确定一个点r为半径的圆内的最近邻元素集时使用的半径r该取什么值(r通过pcl::Feature::setRadiusSearch给出)。这个问题非常重要,并且在一个点特征算子的自动估计时(例如用户没有给定阈值)是一个限制因素。为了更好地说明这个问题,以下图示表现了选择更小尺度(如:r值或k取相对小)与选择更大尺度(如:r值或k值比较大)时的两种不同效果,下面两幅图分别为近视图和远视图,两图中左边部分展示选择了一个合理的比例因子,估计的表面法线近似垂直于两个平面,即使在互相垂直的边缘部分,可明显看到边缘。如果这个尺度取得太大(右边部分),这样邻近点集将更大范围地覆盖邻近表面的点,估计的点特征表现就会扭曲失真,在两个平面边缘处出现旋转表面法线,以及模糊不清的边界,这样就隐藏了一些细节信息。

在这里插入图片描述

表面法线估计近视图

在这里插入图片描述

表面法线估计远视图

  无法深入探究更多讨论,现在可粗略假设,以应用程序所需的细节需求为参考,选择确定点的邻域所用的尺度。简言之,如果杯子手柄和圆柱体部分之间边缘的曲率是重要的,那么需要足够小的尺度来捕获这些细节信息,而在其他不需要细节信息的应用中可选择大的尺度。

2.2.3 法线估计实例详解

  首先创建一个工作空间normal_estimation,然后再在工作空间创建一个文件夹src用于存放源代码:

mkdir -p normal_estimation/src

  接着,在normal_estimation/src路径下,创建一个文件并命名为normal_estimation.cpp,拷贝如下代码:

#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>

int main ()
{
    
    /* 加载点云 */
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::io::loadPCDFile ("../pcd/table_scene_lms400.pcd", *cloud);

    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;   // 创建法线估计对象
    ne.setInputCloud (cloud);                               // 把原始点云数据传递给法线估计对象

    //基于给出的输入数据集,kdtree将被建立
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());   // 创建一个空的kdtree对象
    ne.setSearchMethod (tree);      // 把kdtree对象传递给法线估计对象

    pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);         // 创建输出数据集对象
    
    ne.setRadiusSearch (0.03);      // 使用半径在查询点周围3厘米范围内的所有近邻元素
    
    ne.compute (*cloud_normals);    // 计算法线,并将结果存储到cloud_normals
    
    /* 法线可视化 */
    pcl::visualization::PCLVisualizer viewer("PCL Viewer");
    viewer.setBackgroundColor (0.0, 0.0, 0.0);
    // viewer.addPointCloud(cloud);
    viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud, cloud_normals);

    while (!viewer.wasStopped ())
    {
    
        viewer.spinOnce ();
    }

    return 0;
}

  法线估计类NormalEstimation的实际计算调用内部程序执行以下操作。

  (1)对点 P P P 中的每个点,得到 p p p 点的最近邻元素
  (2)计算 p p p 点的表面法线 n n n
  (3)检查 n n n 的方向是否一致指向视点,如果不是则翻转。

  视点坐标默认为(0,0,0),可以使用以下代码进行更换:

setViewPoint (float vpx, float vpy, float vpz)

  计算单个点的法线,使用:

computePointNormal(const pcl::PointCloud<PointInT> &cloud, const std::vector<int> &indices, Eigen::Vector4f &plane_parameters, float &curvature)

  此处,cloud是包含点的输入点云,indices是点的k-最近邻元素集索引,plane_parameterscurvature是法线估计的输出,plane_parameters前三个坐标中, 以(nx,ny,nz)来表示法线。输出表面曲率curvature通过协方差矩阵的特征值之间的运算估计得到。

【编译和运行程序】
  在工作空间根目录normal_estimation下,编写CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(normal_estimation)

find_package(PCL 1.2 REQUIRED)

include_directories(${
    PCL_INCLUDE_DIRS})
link_directories(${
    PCL_LIBRARY_DIRS})
add_definitions(${
    PCL_DEFINITIONS})	

add_executable (${
    PROJECT_NAME}_node src/normal_estimation.cpp)
target_link_libraries (${
    PROJECT_NAME}_node ${
    PCL_LIBRARIES})

  在工作空间根目录normal_estimation下创建一个build文件夹,用于存放编译过程中产生的文件,然后执行编译:

mkdir build
cd build
cmake ..
make

  此时,会在build文件夹下生成一个可执行文件normal_estimation_node,运行该可执行文件:

./normal_estimation_node

  运行上述命令后,可以在3D可视化窗口中看到如下效果:

在这里插入图片描述

法线估计前原始点云

在这里插入图片描述

法线估计结果可视化

  上述代码片段估计了输入数据集中所有点的一组曲面法线,可以对上述代码稍微修改,从而只为输入数据集中的一部分点估计一组曲面法线。修改之后的代码如下所示:

#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
#include <boost/make_shared.hpp>

int main ()
{
    
    /* 加载点云 */
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_sub (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::io::loadPCDFile ("../pcd/table_scene_lms400.pcd", *cloud);

    std::vector<int> indices (std::floor (cloud->size () / 2)); // 创建原始输入点云的一个子集索引,取前50%
    for (std::size_t i = 0; i < indices.size (); ++i)
    {
    
        indices[i] = i;
        cloud_sub->push_back(cloud->at(i));
    }

    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;   // 创建法线估计对象
    ne.setInputCloud (cloud);                               // 把原始点云数据传递给法线估计对象

    pcl::IndicesPtr indicesptr (new std::vector<int> (indices));
    ne.setIndices (indicesptr);     // 把子集的索引传递给法线估计对象

    //基于给出的输入数据集,kdtree将被建立
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());   // 创建一个空的kdtree对象
    ne.setSearchMethod (tree);      // 把kdtree对象传递给法线估计对象

    pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);         // 创建输出数据集对象
    
    ne.setRadiusSearch (0.03);      // 使用半径在查询点周围3厘米范围内的所有近邻元素
    
    ne.compute (*cloud_normals);    // 计算法线,并将结果存储到cloud_normals

    /* 法线可视化 */
    pcl::visualization::PCLVisualizer viewer("PCL Viewer");
    viewer.setBackgroundColor (0.0, 0.0, 0.0);
    // viewer.addPointCloud(cloud);
    viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud_sub, cloud_normals);

    while (!viewer.wasStopped ())
    {
    
        viewer.spinOnce ();
    }

    return 0;
}

  上述代码中创建原始输入点云的一个子集索引,取前50%,然后利用 setIndices 函数把子集的索引传递给法线估计对象,这样求解出来的就是这一组子集所对应的曲面法线,结果如下所示:

在这里插入图片描述

法线估计结果可视化(求解子集所对应的曲面法线)

  最后,下面的代码片段将为输入数据集中的所有点估计一组曲面法线,但将使用另一个数据集估计它们的最近邻:

#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
#include <pcl/filters/voxel_grid.h>
#include <boost/make_shared.hpp>

int main ()
{
    
    /* 加载点云 */
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_downsampled (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::io::loadPCDFile ("../pcd/table_scene_lms400.pcd", *cloud);

    pcl::VoxelGrid<pcl::PointXYZ> sor;      // 创建滤波对象
    sor.setInputCloud (cloud);              // 给滤波对象设置需要过滤的点云
    sor.setLeafSize (0.01f, 0.01f, 0.01f);  // 设置滤波时创建的体素大小为1cm立方体
    sor.filter (*cloud_downsampled);        // 执行滤波处理,存储输出到cloud_downsampled

    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;   // 创建法线估计对象
    ne.setInputCloud (cloud_downsampled);                   // 把下采样后的点云数据传递给法线估计对象

    // Pass the original data (before downsampling) as the search surface
    ne.setSearchSurface (cloud);            // 传递原始数据(下采样之前的点云数据)作为搜索点云

    //基于给出的输入数据集,kdtree将被建立
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());   // 创建一个空的kdtree对象
    ne.setSearchMethod (tree);      // 把kdtree对象传递给法线估计对象

    pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);         // 创建输出数据集对象
    
    ne.setRadiusSearch (0.03);      // 使用半径在查询点周围3厘米范围内的所有近邻元素
    
    ne.compute (*cloud_normals);    // 计算法线,并将结果存储到cloud_normals
    
    /* 法线可视化 */
    pcl::visualization::PCLVisualizer viewer("PCL Viewer");
    viewer.setBackgroundColor (0.0, 0.0, 0.0);
    // viewer.addPointCloud(cloud);
    viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud_downsampled, cloud_normals);

    while (!viewer.wasStopped ())
    {
    
        viewer.spinOnce ();
    }

    return 0;
}

  在上述代码中,利用 setInputCloud 函数把下采样后的点云数据传递给法线估计对象,然后利用 setSearchSurface 函数传递原始数据(下采样之前的点云数据)作为搜索点云,最终可视化结果如下所示:
在这里插入图片描述

法线估计结果可视化(指定搜索时使用的点邻域集合)

2.2.4 使用OpenMP加速法线估计

  对于对运算速度有要求的用户,PCL点云库提供了一个表面法线的附加实现程序,它使用多核/多线程开发规范,利用OpenMP来提高计算速度。它的类命名为pcl::NormalEstimationOMP,并且它的应用程序接口(API)100%兼容单线程pcl::NormalEstimation,这使它适合作为一个可选提速方法。在8核系统中,可以轻松提速6-8倍。

2.3 使用积分图进行法线估计

  本小节我们将学习如何使用积分图(integral images)计算一个有序点云的法线,注意该方法只适用于有序点云。

  首先创建一个工作空间normal_estimation_using_integral_images,然后再在工作空间创建一个文件夹src用于存放源代码:

mkdir -p normal_estimation_using_integral_images/src

  接着,在normal_estimation_using_integral_images/src路径下,创建一个文件并命名为normal_estimation_using_integral_images.cpp,拷贝如下代码:

#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/visualization/cloud_viewer.h>

int main ()
{
    
    /* 加载点云 */
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::io::loadPCDFile ("../pcd/table_scene_mug_stereo_textured.pcd", *cloud);

    /* 估计法线 */
    pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal>);
    pcl::IntegralImageNormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
    ne.setNormalEstimationMethod (ne.AVERAGE_3D_GRADIENT);
    ne.setMaxDepthChangeFactor(0.02f);
    ne.setNormalSmoothingSize(10.0f);
    ne.setInputCloud(cloud);
    ne.compute(*normals);

    /* 法线可视化 */
    pcl::visualization::PCLVisualizer viewer("PCL Viewer");
    viewer.setBackgroundColor (0.0, 0.0, 0.0);
    viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud, normals);
    while (!viewer.wasStopped ())
    {
    
        viewer.spinOnce ();
    }
    return 0;
}

【解释说明】
  上述代码的第一部分,我们从文件中加载了一个点云存储在点云对象,以备后续作为法线估计对象的输入:

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile ("../pcd/table_scene_mug_stereo_textured.pcd", *cloud);

  在第二部分中,定义了存储估计法线的点类型指针,并为创建了一个积分图法线估计的对象ne设置对象计算时需要的参数,例如估计方法、点云等:

pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal>);
pcl::IntegralImageNormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setNormalEstimationMethod (ne.AVERAGE_3D_GRADIENT);	// 设置估计方法
ne.setMaxDepthChangeFactor(0.02f);						// 最大深度变化系数
ne.setNormalSmoothingSize(10.0f);						// 优化法线方向时考虑邻域大小
ne.setInputCloud(cloud);								// 输入点云,必须为有序点云
ne.compute(*normals);									// 执行法线估计,存储结果到 normals

  以下是可使用的法线估计方法:

enum NormalEstimationMethod
{
    
	COVARIANCE_MATRIX,
	AVERAGE_3D_GRADIENT,
	AVERAGE_DEPTH_CHANGE
}

  COVARIANCE_MATRIX模式从具体某个点的局部邻域的协方差矩阵创建9个积分图,来计算这个点的法线。AVERAGE_3D_GRADIENT模式创建了6个积分图来计算水平和垂直方向平滑后的三维梯度,并使用两个梯度间的向量积计算法线。AVERAGE_DEPTH_CHANGE模式只创建了一个单一的积分图,并从平均深度变化计算法线。

【编译和运行程序】
  在工作空间根目录normal_estimation_using_integral_images下,编写CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 2.8 FATAL_ERROR)

project(normal_estimation_using_integral_images)

find_package(PCL 1.2 REQUIRED)
include_directories(${
    PCL_INCLUDE_DIRS})
link_directories(${
    PCL_LIBRARY_DIRS})

add_definitions(${
    PCL_DEFINITIONS})	
add_executable (${
    PROJECT_NAME}_node src/normal_estimation_using_integral_images.cpp)
target_link_libraries (${
    PROJECT_NAME}_node ${
    PCL_LIBRARIES})

  在工作空间根目录normal_estimation_using_integral_images下创建一个build文件夹,用于存放编译过程中产生的文件,然后执行编译:

mkdir build
cd build
cmake ..
make

  此时,会在build文件夹下生成一个可执行文件normal_estimation_using_integral_images_node,运行该可执行文件:

./normal_estimation_using_integral_images_node

  运行上述命令后,可以在3D可视化窗口中看到如下效果:

在这里插入图片描述

积分图法线估计示例结果

  从上图可以看出,法线方向基本一致朝向视点,视图视点朝向场景中的桌面,桌面上的杯子处出现平行于桌面的法线,而桌面上的点集的法线都垂直于桌面并指向点云本身获取时的视点,利用此方法进行法线估计只适用于有序点云,对于无序点云就只能采用其他方法了。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_29923461/article/details/120655062

智能推荐

彻底关闭Win10自动更新(Win10企业版或专业版)_waasmedicsvc start-程序员宅基地

文章浏览阅读2k次。彻底关闭Win10自动更新(Win10企业版或专业版)关闭Windows Updata服务右键此电脑->管理->服务和应用程序->服务Windows Update-> 常规->启动类型->禁用 恢复->第一次失败->无操作 常规->如果服务状态正在运行->点击停止关闭Windows更新计划任务任务计划程序->任务计划程序库->Microsoft->Windows->WindowsUpdate->Sch_waasmedicsvc start

计算机网络基础知识大汇总-程序员宅基地

文章浏览阅读2.7k次,点赞2次,收藏9次。一、什么是TCP/IP网络和协议1. TCP/IP是一类协议系统,它是一套支持网络通信的协议集合。网络是计算机或类似计算机的设备之间通过常用的传输介质进行通信的集合。2. 网络协议就是一套通用规则,用来帮助定义复杂数据传输的过程。数据传输从一台计算机上的应用程序开始,通过计算机网络硬件,经过传输介质到正确目的地,然后上传到目的地计算机网络硬件,最后到达负责接收的应用程序。3. TCP/IP协议定义了网络通信过程,更重要的是,定义了数据单元的格式和内容,以便接收计算机能够正确解释接收到的消_计算机网络基础知识

nacos集群raft选举算法_什么集群用到了raft-程序员宅基地

文章浏览阅读2.1k次。选举算法Nacos集群采用raft算法来实现,它是相对zookeeper的选举算法较为简单的一种。选举算法的核心在RaftCore 中,包括数据的处理和数据同步raft算法演示地址:http://thesecretlivesofdata.com/raft/在Raft中,节点有三种角色:Leader:负责接收客户端的请求Candidate:用于选举Leader的一种角色Follower..._什么集群用到了raft

[量化学院]借助talib使用技术分析指标来炒股_talib atr-程序员宅基地

文章浏览阅读6.7k次,点赞2次,收藏44次。什么是技术分析所谓股票的技术分析,是相对于基本面分析而言的。基本分析法着重于对一般经济情况以及各个公司的经营管理状况、行业动态等因素进行分析,以此来研究股票的价值,衡量股价的高低。而技术分析则是透过图表或技术指标的记录,研究市场过去及现在的行为反应,以推测未来价格的变动趋势。其依据的技术指标的主要内容是由股价、成交量或涨跌指数等数据计算而得的,技术分析只关心证券市场本身的变化,而不考虑会对其产生..._talib atr

Java异常回滚_java yichanghuigun-程序员宅基地

文章浏览阅读656次。Java异常回滚欢迎使用Markdown编辑器所以修改点欢迎使用Markdown编辑器![外部循环内部逻辑](https://img-blog.csdnimg.cn/20201021103913881.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNjY4OTk5,size_16,color_FFFFFF,t_70#pic_cent_java yichanghuigun

【机器学习论文翻译】去信任的机器学习合约:在以太坊区块链上评估和交换机器学习模型1_机器学习论文及翻译-程序员宅基地

文章浏览阅读1k次。一、前言区块链是最近特别火热的一门技术,特别是比特币、挖矿概念的兴起,更是推动了区块链的发展。目前基本上确定的未来的研究方向是AI + 区块链,所以从现在开始,做个小白,不断开始滚雪球。如果你有什么问题,希望跟我能够一起交流,除了通过博客交流外,欢迎你加入我的QQ群,一起交流有关于机器学习、深度学习、计算机视觉有关内容。目前我处于广泛涉猎阶段,希望我们能够一起沟通。下图是我的群二维码:..._机器学习论文及翻译

随便推点

java游戏超级玛丽以及游戏改进(视频请看b站尚学堂)_java马里奥-程序员宅基地

文章浏览阅读2k次,点赞22次,收藏41次。一个简单的马里奥游戏项目_java马里奥

【路径规划】基于模糊控制的路径规划算法附matlab仿真实现_路径规划matlab仿真程序-程序员宅基地

文章浏览阅读379次。基于模糊控制的路径规划算法是一种利用模糊逻辑和模糊规则来设计无人机或其他机器人的路径规划方法。以下是一个基本的框架:问题定义:明确路径规划问题的目标和约束条件。例如,确定起始位置和目标位置,考虑避障要求、动态环境等。环境感知:通过传感器获取环境信息,并将其转化为可理解的数据形式。这可能涉及到使用视觉传感器、激光雷达、超声波等设备。建立模糊规则库:根据问题定义和感知信息,设计一组模糊规则,将输入变量(如距离、障碍物密度等)映射到输出变量(如移动方向、速度等)。_路径规划matlab仿真程序

打印时选 平铺 原始大小_如何使用平铺打印制作自己的海报-程序员宅基地

文章浏览阅读1.3k次。打印时选 平铺 原始大小If the boring wall posters at Spencers aren’t tickling your fancy and you’d rather have something more customized to your tastes, you can actually make and print your own posters using a m..._启用平铺打印是什么意思

Linux下查看软件安装路径_linux 找到安装包的位置-程序员宅基地

文章浏览阅读2.4w次,点赞16次,收藏67次。对于刚接触LINUX的新手来说,Linux中查看某 个软件的安装路径(地址)有时显得非常重要。比如某个文件的快速启动项被删除,或者你要建立快速启动项,或者想删除、 添加安装文件等等,很多地方都要用到查看文件安装路径的命令。一、查看文件安装路径:由于初次大部分软件的安装都是系统自动安装的,所以先说查看文件安装的所有路径(地址)。这里以Subversion为例。比如说我安装了SVN,但是不知道文件都安装在哪些地方、放在哪些文件夹里,可 以用下面的命令查看所有的文件路径在终端输入:wh..._linux 找到安装包的位置

coco数据集标注图转为二值图python(附代码)-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏13次。coco数据集大概有8w张以上的图片,而且每幅图都有精确的边缘mask标注。后面后分享一个labelme标注的json或xml格式转二值图的源码(以备以后使用)而我现在在研究显著性目标检测,需要的是边缘mask的二值图像。搜了很久,并没有人做过这种工作,只能得到如下的掩膜图而我需要的图像为二值图,如下说下 我的过程 并附上代码:首先,coco数据集将所有的8w多张图片标..._coco数据集二值掩膜

【cmake 基础小知识】如何在CMake项目中使用add_dependencies命令管理目标依赖关系_cmake add_dependencies-程序员宅基地

文章浏览阅读941次,点赞19次,收藏16次。在现代软件开发过程中,构建系统扮演着至关重要的角色。CMake,作为一个跨平台的构建系统,允许开发者定义和管理复杂的项目构建过程。本章节旨在介绍CMake的基础概念,重点讲解如何在CMake中建立目标之间的依赖关系,确保构建顺序的正确性。_cmake add_dependencies