3D 点云特征描述与提取是点云信息处理中的最基础也是最关键的部分,点云的识别、分割、重采样、配准、曲面重建等大部分算法,都十分依赖特征描述与提取的结果。
从尺度上来划分,一般分为局部特征描述和全局特征描述。例如局部的法线等几何形状特征的描述,全局的拓扑特征描述,都属于3D点云特征描述与提取范畴。在 PCL 中,目前已有很多基本的特征描述子与提取算法。
特征点¶
理想情况下,在使用同一种度量规则情况下,相同或相似的表面上的点的特征值应该非常相似,不同表面上的点的特征描述子有明显的差异。通过以下几个条件的变化仍能够获取获取相同或相似的局部表面特征,则说明该特征表示方式比较优秀:
- 刚性变换(rigid transformations):即数据中的3D旋转和3D平移不应影响结果特征向量F的估计
- 多种采样密度(varying sampling density):原则上,在一个局部表面或多或少采样密度的应具有相同的特征向量
- 噪声(noise):在数据中存在轻微噪声的情况下,由特征点描述的特征向量必须相同或非常相近
通常,PCL功能使用近似的方法通过快速的kd-tree查询来计算查询点的最近邻居,我们感兴趣的查询有两种:
- k搜索:确定查询点的k个(用户给定参数)邻居
- 半径搜索:确定半径为r的球面内查询点的所有邻居。
如何传递输入参数¶
由于PCL中几乎所有类都继承自基本pcl::PCLBase
类,因此pcl::Feature
类以两种不同的方式接受输入数据:
- 通过
setInputCloud(PointCloudConstPtr&)
给出的整个点云数据集, 任何试图在给定输入云中的每个点上进行特征估计。 - 通过
setInputCloud(PointCloudConstPtr&)
和setIndices(IndicesConstPtr&)
(可选)给出的点云数据集的子集, 所有特征估计方式都将用来尝试估计输入云中每个在indices索引列表中点的特征。默认情况下,如果未给出索引集,则将考虑云中的所有点。
另外,可以通过额外的调用setSearchSurface(PointCloudConstPtr&)
来指定要搜索使用的点邻居集。此调用是可选的,如果未提供搜索范围的情况下,默认将使用输入点云数据集。 由于始终需要setInputCloud()
,因此可以使用<setInputCloud(),setIndices(),setSearchSurface()
>创建多达四个组合。假设我们有两个点云,P={p1,p2,…pn}和Q={q1,q2,…,qn},下图显示了所有四种情况:
-
setIndices() = false, setSearchSurface() = false
毫无疑问,这是PCL中最常用的情况,用户仅输入单个PointCloud数据集,并期望在云中的所有点处估计某个特征。
由于我们不希望根据是否给出一组indices(索引)和/或search surface(搜索表面)来维护不同的实现副本,因此每当
indices = false
时,PCL就会创建一组内部索引(作为std::vector <int>
)基本上指向整个数据集(索引= 1..N,其中N是云中的点数)。在上图中,这对应于最左边的情况。首先,我们估计p_1的最近邻居,然后估计p_2的最近邻居,依此类推,直到用尽P中的所有点。
-
setIndices() = true, setSearchSurface() = false
如前所述,特征估计方法将仅计算在给定indices(索引) 的vecter中具有索引的那些点的特征;
在上图中,这对应于第二种情况。在此,我们假设p_2的索引不是给定索引vector的一部分,因此不会在p2处估计任何邻居或特征。
-
setIndices() = false, setSearchSurface() = true
与第一种情况一样,将对输入的所有点进行特征估计,但是
setSearchSurface()
中给定的基础相邻表面将用于获取输入的最近邻居点,而不是输入云本身;在上图中,这对应于第三种情况。如果Q={q1,q2}是作为输入云,把P设置为Q的search surface搜索面,则将从P计算q_1和q_2的邻居。
-
setIndices() = true, setSearchSurface() = true
这是最罕见的情况,同时给出了indices索引和search surface搜索表面。在这种情况下,将使用
setSearchSurface
中提供的搜索表面信息来计算<input, indices>
中的子集的特征。最后,在上图中,这对应于最右边的情况。在此,我们假设q_2的索引不属于为Q指定的索引向量,因此在q2处不会估计出任何邻居或特征。
使用setSearchSurface
的最有用的情况是:当我们有一个非常密集的输入数据集,但我们不想估计其中所有点的特征,而是要估计使用pcl_keypoints
中的方法发现的某些关键点,或者在点云的降采样版本上(例如,使用pcl::VoxelGrid <T>
过滤器获得的降采样点云)。在这种情况下,我们通过setInputCloud
传递降采样/关键点输入,并将原始数据作为setSearchSurface
传递,这样可以大幅提高效率
#include<pcl/point_types.h>
#include<pcl/features/normal_3d.h>
#include<pcl/io/pcd_io.h>
#include<pcl/visualization/pcl_visualizer.h>
int main()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PCDReader reader;
reader.read("table_scene_lms400_downsampled.pcd", *cloud);
//创建法向量估算类,传递输入数据集
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
// 创建一个空的kd树
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
// 定义输出数据集
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals(new pcl::PointCloud<pcl::Normal>());
// 使用一个半径为3cm的球体中的所有邻居点
ne.setRadiusSearch(0.03);
// 计算特征
ne.compute(*cloud_normals);
// 创建可视化对象
pcl::visualization::PCLVisualizer viewer("PCL Viewer");
// 设置可视化颜色,一般针对点云设置
pcl::visualization::PointCloudColorHandlerGenericField<pcl::PointXYZ> fildColor(cloud, "z");//按照z字段进行渲染
viewer.setBackgroundColor(0.0, 0.0, 0);
viewer.addPointCloud<pcl::PointXYZ>(cloud, fildColor, "cloud");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "cloud");
// 参数int level=5 表示每n个点绘制一个法向量
// 参数float scale=0.1 表示法向量长度缩放为0.1倍
viewer.addPointCloudNormals<pcl::PointXYZ, pcl::Normal>(cloud, cloud_normals, 5, 0.1, "normals");
viewer.addCoordinateSystem(1.0);
while (!viewer.wasStopped()) {
viewer.spinOnce();
}
return 0;
}
估计部分点云法向量
#include<pcl/point_types.h>
#include<pcl/features/normal_3d.h>
#include<pcl/io/pcd_io.h>
#include<pcl/visualization/pcl_visualizer.h>
int main()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr full_cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PCDReader reader;
reader.read("table_scene_lms400_downsampled.pcd", *full_cloud);
// 估计点云所有的法向量
#if 0
//创建法向量估算类,传递输入数据集
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
// 创建一个空的kd树
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
// 定义输出数据集
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals(new pcl::PointCloud<pcl::Normal>());
// 使用一个半径为3cm的球体中的所有邻居点
ne.setRadiusSearch(0.03);
// 计算特征
ne.compute(*cloud_normals);
#endif
加载完整的点云数据
//pcl::PointCloud<pcl::PointXYZ>::Ptr full_cloud(new pcl::PointCloud<pcl::PointXYZ>);
//pcl::io::loadPCDFile<pcl::PointXYZ>("your_point_cloud.pcd", *full_cloud);
// 创建法线估计对象
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(full_cloud);
// 定义搜索半径(用于估计每个点的法向量)
float radius = 0.03;
ne.setRadiusSearch(radius);
// 指定部分点云的索引列表
std::vector<int> indices(5000); // 示例索引列表
for (int i = 0; i < 5000; i++) indices.emplace_back(i);
// 创建部分点云对象
pcl::PointCloud<pcl::PointXYZ>::Ptr partial_cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::copyPointCloud(*full_cloud, indices, *partial_cloud);
// 设置部分点云索引
pcl::IndicesPtr indices_ptr(new std::vector<int>(indices));
ne.setIndices(indices_ptr);
// 估计法向量
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
ne.compute(*normals);
// 可视化显示部分点云和法向量
pcl::visualization::PCLVisualizer viewer("Partial Point Cloud with Normals");
viewer.setBackgroundColor(0.0, 0.0, 0.0);
viewer.addPointCloud<pcl::PointXYZ>(partial_cloud, "cloud");
viewer.addPointCloudNormals<pcl::PointXYZ, pcl::Normal>(partial_cloud, normals, 10, 0.02, "normals");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 0.0, 1.0, 0.0, "normals");
viewer.spin();
return 0;
}
估计一个点云的表面法线
表面法线是几何体表面的重要属性,在很多领域都有大量应用,例如:在进行光照渲染时产生符合可视习惯的效果时需要表面法线信息才能正常进行,对于一个已知的几何体表面,根据垂直于点表面的矢量,因此推断表面某一点的法线方向通常比较简单。然而由于我们获取的点云数据集在真实物体的表面表现为一组定点样本这样就会有两种解决方法:
- 使用曲面重建技术从取的点云数据集中得到采样点对应的曲面,然后从曲面模型中计算表面法线;
- 直接从点云数据集中近似推断表面法线
已知一个点云数据集在其中的每个点处直接近似计算表面法线。
尽管有许多不同的法线估计方法,本教程中着重介绍的是其中最简单的一个,表述如下,确定表面一点法线的问题近似于估计表面的一个相切面法线的问题,因此转换过来以后就变成一个最小二乘法平面拟合估计问题注意;更多信息包含最小二乘法问题的数学方程式,请见[RusuDissertation]。因此估计表面法线的解决方案就变成了分析一个协方差矩阵的特征矢量和特征值(或者 PCA一主成分分析),这个协方差矩阵从查询点的近邻元素中创建。更具体地说,对于每一个点 P;,对应的协方差矩阵 C如下:
#include<pcl/point_types.h>
#include<pcl/features/normal_3d.h>
#include<pcl/io/pcd_io.h>
#include<pcl/visualization/pcl_visualizer.h>
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::PCDReader reader;
reader.read("table_scene_lms400_downsampled.pcd", *cloud_downsampled);
reader.read("table_scene_lms400.pcd", *cloud);
// 输入数据集中的所有点估算一组表面法线,但将使用另一个数据集(原始点云)估计其最近的邻居
// 创建法向量估算类,将降采样后的数据作为输入点云
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud_downsampled);
// 传入降采样之前的原始数据作为search surface
ne.setSearchSurface(cloud);
// 创建一个空的kdtree,将值传递给法向量估算对象
// 这个tree对象将会在ne内部根据输入的数据集进行填充(这里设置没有其他的search surface)
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
// 定义输出数据集
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals(new pcl::PointCloud<pcl::Normal>);
// 使用一个半径为3cm的球体中的所有邻居点
ne.setRadiusSearch(0.03);
// 计算特征
ne.compute(*cloud_normals);
// 可视化显示部分点云和法向量
pcl::visualization::PCLVisualizer viewer("Partial Point Cloud with Normals");
viewer.setBackgroundColor(0.0, 0.0, 0.0);
pcl::visualization::PointCloudColorHandlerGenericField<pcl::PointXYZ> fildColor(cloud, "z");//按照z字段进行渲染
viewer.addPointCloud<pcl::PointXYZ>(cloud_downsampled, fildColor,"cloud");
viewer.addPointCloudNormals<pcl::PointXYZ, pcl::Normal>(cloud_downsampled, cloud_normals, 10, 0.02, "normals");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 0.0, 1.0, 0.0, "normals");
viewer.spin();
return 0;
}