1、点云的拓扑邻域
在三维空间数据处理的领域中,点云的邻域概念显得尤为关键,它不仅链接了点云数据之间的拓扑结构,而且在构建点云间的拓扑关系时起到了桥梁的作用。这种关系的建立,使得我们能够以一种高效、迅速的方式管理庞大的三维点云数据集。想象一下,成千上万的点,每一个都携带着位置、颜色、反射率等信息,它们通过邻域的连接,形成了一个复杂而有序的网络。这不仅为点云数据的精简提供了可能,使得我们可以去除冗余的点而不损失关键信息,还为点云分割提供了依据,使得我们可以根据不同的特征将点云分成有意义的子集。此外,点云识别、点云配准以及点云曲面重建等后续处理工作,都依赖于这种拓扑关系的精确构建。通过邻域的分析,我们可以识别出点云中的物体,将它们与数据库中的模型进行匹配,甚至可以将来自不同视角的点云数据进行精确对齐。最终,这些处理工作共同为三维重建、虚拟现实、机器人导航等应用提供了坚实的基础,推动了这些领域技术的发展和创新。
1.1 、点云的邻域类型
邻域是数学和拓扑学中的概念,在三维点云数据中,表示有一定空间位置分布的离散点集且包含了采样点云数据的一个集合。假如一个物体的点云数据用V表示,结构若干空间换分之后,A为V中的一部分点云数据,P为A的一个点云数据点,则称A为点云P的邻域,用集合关系表示为如下:
对于点云数据集P,p为P中的一个点云,点云p的邻域可以用索引集合Ni来表示,通过对下标i的操作,就能访问到与点云p有关系的点云数据。点云数据空间分部决定了邻域的类型,在三维点云数据处理中常见的有K邻域、Voronoi邻域和BSP邻域,其中K邻域在很多点云处理算法较为常用。三种邻域类型的图示如图1。由于K领域较常见,下面仅对K领域进行简要的讲解。
1.1.1、K领域
K邻域就是表示目标点云p,与K个欧式距离最近点云组成的一个点云数据集,邻域内的点云数据满足下面的关系式子:
式中:为排列值从小到大,。K邻域的点云索引集合为:
K邻域另外一种以半径R为球的定义,目标点云p为球心,在半径R内的球内搜索满足条件的点云组成K邻域,具体按照自己的需求来选择数据点K个,另外还按半径R距离的方式构建K邻域。
1.2、 建立点云拓扑关系的常有方法
为了构建点云数据的拓扑关系,一种直接的方法是计算与目标点云数据最近的点之间的绝对距离,从而确定拓扑链接的依据。然而,这种方法在处理大规模的点云数据集时,效率显得非常低下,这并不利于对数据进行整体的处理和分析。为了提高处理效率,空间索引技术在建立点云数据的拓扑关系中得到了广泛的应用。空间索引技术通过构建特定的数据结构,可以快速定位和检索点云数据中的点,从而大大提高了数据处理的速度和效率。一些著名的空间索引结构包括空间栅栏法、KDB树、R树、K-D树、四叉树、八叉树以及BSP树等。这些空间索引结构各有其特点和适用场景,在三维点云数据处理领域,栅栏法、K-D树(KDtree树)和八叉树(Octree树)尤其受到青睐,因为它们在处理大规模三维数据时,能够提供更为高效和准确的数据组织和检索方式。在PCL库中提供的有K-D树(KDtree树)和八叉树(Octree树)两种方法,本节和下节将对这两个方法进行讲解和代码示例。
2、KD树(KDtree)
2.1、 KDtree基本理论
K-D树是一种用于构建K维空间中数据集之间访问关系的数据结构,KDTree就是二叉查找树(Binary Search Tree,BST)的变种。它在三维点云的空间划分和最近邻域搜索方面尤为便捷。在离散点云数据集中,每个独立的点都由K-D树的非空叶子节点表示,从而构建起离散点集之间的拓扑关系。接下来,我将简要介绍K-D树在空间划分和最近邻域搜索。
2.1.1 、KDtree空间划分
K-D树的空间划分是基于数据集的维度进行的。树的叶子节点存储K维的数据点,而非叶子节点则代表对应维度的划分线。在进行空间划分时,我们首先让划分线平行于K维中的某一维参考轴,将空间中的数据点分为两部分:一部分大于该维度上的参考值,另一部分小于该维度上的参考值。然后,根据这两部分的不同属性,决定(K-1)维空间的划分方向,并继续划分,直至空间被划分至一维。之后,返回到K维,重复上述操作,直到满足特定条件为止。上述提到的参考值,通常是该维度下所有数据点的平均值或接近平均值的数值。
在三维点云数据的空间划分中,K-D树首先使用X轴来确定划分面。具体操作如下:计算点云数据中所有X坐标的平均值,然后通过这个均值点作一个平行于YOZ平面的分割面,将点云分布的空间分为两部分。接着,在K-D树的下一层,即通过YOZ平面分割得到的子空间中,计算所有Y坐标的平均值,通过这个均值点作一个平行于XOZ平面的分割面,再次将点云分布的空间分割为子空间。最后,在K-D树的再下一层,即通过XOZ平面分割得到的子空间中,计算所有Z坐标的平均值,通过这个均值点作一个平行于XOY平面的分割面,将点云分布的空间再次分割为子空间。递归执行这三个步骤,直至完成整个空间的划分。
三维K-D树则是分别用X-Y-Z-X轴为参考轴,根据约束的条件确定分割面,将分布在三维空间的数据划分到K-D树的叶子节点中,三维示意图见图2。
2.1.2、KDtree空间划分示例
下面以二维的KD树例,进行空间划分说明。假设有如下由七个二维数据组成的二维数据集:
X = { ( 3 , 7 ) , ( 2 , 6 ) , ( 0 , 5 ) , ( 1 , 8 ) , ( 7 , 5 ) , ( 5 , 4 ) , ( 6 , 7 ) }
第1次划分:按照第1维特征(x轴特征)进行划分,选取( 3 , 7 ) 为根节点将整个点集划分为左右两部分(蓝色线为分界线),结果如下:
第2次划分:按照第2维特征(y轴特征)进行划分,分别以( 2 , 6 )和( 7 , 5 )为根节点将左右两平面划分为上下两个平面(绿色线为分界线) ,结果如下:
最终的KD-Tree结构,黄色线上的点是叶子节点 ,完成划分。
2.1.3、KDtree的最近邻域搜索
在三维点云数据处理中,最常用K-D树最近邻域搜索算法,其目标是根据给定目标点云找到树中最近的K邻域,由于K-D树左子树小于根节点,右子树大于根节点的特点,能够很快的排除一些不必要访问的空间,搜索速度很快。
2.1.4、KDtree邻域搜索示例
继续上面的二维KD-Tree例子。假设待查询点为( 5 , 5.5 ),如下:
二分查找,与根节点的第1维特征进行比较,由于5 > 3 ,因此进入右子树(红色部分)。计算与根节点的距离d i s t = 2.5 作为当前最短距离。
二分查找,与右半平面分割线上节点的第2维特征进行比较,由于5.5 > 5 ,因此进入该节点的右子树(橙色部分)。计算与( 7 , 5 ) 的距离d i s t ≈ 2.0616 小于当前最短距离,因此更新当前最短距离。
二分查找,计算与叶子节点( 6 , 7 ) 的距离d i s t ≈ 1.8028 ,小于当前最短距离,因此更新当前最短距离。由于搜索到了叶子节点,接下来开始回溯 。
以待查询点为圆心,当前最短距离为半径作圆,与( 7 , 5 ) 所在的分割线相交,因此对节点( 7 , 5 ) 的另一子树进行搜索。
计算与叶子节点( 5 , 4 ) 在距离d i s t = 1.5,小于当前最短距离,因此更新当前最短距离,继续进行回溯。由于以待查询点为圆心,当前最短距离为半径的圆与其他分割线无交点,因此认为节点( 5 , 4 ) 即为待查询点的最近邻,最短距离为1.5,搜索完毕。
注意:K-D树在数据维度较小时(如小于50),搜索效率很快,当维度大于100时,搜索效率会随着维度的增加而下降。一般而言数据的规模满足N>>2的D次方,搜索速度高效。
2.2、PCL库中的KDtree
PCL库中实现的KDtree方法在两个模块都有,分别是kdtree和search模块,但是功能都是一样的。下面仅对Kdtree模块中的进行说明,另外的一个模块后序用到时在展开,基本功能都差不多。
PCL库的KDtree是基于第三库FLANN封装实现的,有pcl::KdTree<PointT>和pcl::KdTreeFLANN<PointT>两个模板类实现,其中pcl::KdTree<PointT>基础类,pcl::KdTreeFLANN<PointT>是pcl::KdTree<PointT>的子类,两个类实现了R半径距离和K个最近点的K领域搜索方法。主要的函数方法有如下表内容。
主要函数 | 简要说明 |
virtual void setInputCloud (const PointCloudConstPtr &cloud, const IndicesConstPtr &indices = IndicesConstPtr ()) | 设置需要构建KDtree树的点云,参数:点云和对应点云的索引(默认可以不用输入)。 |
virtual int nearestKSearch (const PointT &p_q, int k, std::vector<int> &k_indices, std::vector<float> &k_sqr_distances) const = 0; | K个领域搜索函数接口,参数:参考点、搜索的K个数、K个的点对应的索引和K个点与参考点之间的距离平方(从小到大排列)。 |
virtual int radiusSearch (const PointT &p_q, double radius, std::vector<int> &k_indices, std::vector<float> &k_sqr_distances, unsigned int max_nn = 0) const = 0; | R半径搜索函数接口,参数:参考点、半径r、r半径内的点对应的索引和r半径内点与参考点之间的距离平方(从小到大排列)。 |
template <typename PointTDiff> inline int nearestKSearchT (const PointTDiff &point, int k, std::vector<int> &k_indices, std::vector<float> &k_sqr_distances) const | K个领域搜索函数接口,参数:模版点类型、搜索的K个数、K个的点对应的索引和K个点与参考点之间的距离平方(从小到大排列) |
template <typename PointTDiff> inline int radiusSearchT (const PointTDiff &point, double radius, std::vector<int> &k_indices, std::vector<float> &k_sqr_distances, unsigned int max_nn = 0) const | R半径搜索函数接口,参数:模版点类型、半径r、r半径内的点对应的索引和r半径内点与参考点之间的距离平方(从小到大排列)。 |
inline void setPointRepresentation (const PointRepresentationConstPtr &point_representation) | 设置N维数据构建KDtree树,参数:N维的点数据。 |
2.3、PCL库中的KDtree的代码示例
新建文件 PCLKDtreemain.cpp,内容如下,示例代码中进行R半径距离和K个最近点的K领域搜索示例,和结果可视化,具体内容参考代码。
/*****************************************************************//**
* \file PCLKDtreemain.cpp
* \brief
*
* \author YZS
* \date December 2024
*********************************************************************/
#include<iostream>
#include <vector>
#include <ctime>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/io/auto_io.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/search/kdtree.h>
#include <pcl/visualization/pcl_visualizer.h>
using namespace std;
void PCLKDtreeUse()
{
// 随机种子初始化
srand(time(NULL));
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 生成点云数据1000个
cloud->width = 3000;
cloud->height = 1;
cloud->points.resize(cloud->width * cloud->height);
for (size_t i = 0; i < cloud->points.size(); ++i) {
cloud->points[i].x = 1024.0f * rand() / (RAND_MAX + 1.0f);
cloud->points[i].y = 1024.0f * rand() / (RAND_MAX + 1.0f);
cloud->points[i].z = 1024.0f * rand() / (RAND_MAX + 1.0f);
}
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
//pcl::search::KdTree<pcl::PointXYZ> kdtree;
//设置搜索的空间,点云数据cloud
kdtree.setInputCloud(cloud);
// 生成一个查询点
pcl::PointXYZ searchPoint;
searchPoint.x = 1024.0f * rand() / (RAND_MAX + 1.0f);
searchPoint.y = 1024.0f * rand() / (RAND_MAX + 1.0f);
searchPoint.z = 1024.0f * rand() / (RAND_MAX + 1.0f);
// 方式1:K个最近邻域搜索
int K = 100;//表示搜索100个临近点
std::vector<int>KNSearch(K);// 保存搜索到的临近点的索引
std::vector<float> KNSquaredDistance(K);//保存对应临近点的距离的平方
std::cout << "K nearest neighbor search at (" << searchPoint.x
<< " " << searchPoint.y
<< " " << searchPoint.z
<< ") with K=" << K << std::endl;
//保存K个领域的点云数据
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudK(new pcl::PointCloud<pcl::PointXYZ>);
if (kdtree.nearestKSearch(searchPoint, K, KNSearch, KNSquaredDistance) > 0) {
for (size_t i = 0; i < KNSearch.size(); ++i)
{
std::cout << " " << cloud->points[KNSearch[i]].x
<< " " << cloud->points[KNSearch[i]].y
<< " " << cloud->points[KNSearch[i]].z
<< " (距离平方: " << KNSquaredDistance[i] << ")" << std::endl;
pcl::PointXYZ pt;
pt.getVector3fMap() = cloud->points[KNSearch[i]].getVector3fMap();
cloudK->points.emplace_back(pt);
}
}
// 方式2:R半径邻域搜索
std::vector<int> radiusSearch;
std::vector<float>radiusSquaredDistance;
// R半径值
float radius = 300.0f;
std::cout << "Neighbors within radius search at (" << searchPoint.x
<< " " << searchPoint.y
<< " " << searchPoint.z
<< ") with radius=" << radius << std::endl;
//保存R半径领域的点云数据
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudR(new pcl::PointCloud<pcl::PointXYZ>);
if (kdtree.radiusSearch(searchPoint, radius, radiusSearch, radiusSquaredDistance) > 0) {
for (size_t i = 0; i < radiusSearch.size(); ++i) {
std::cout << " " << cloud->points[radiusSearch[i]].x
<< " " << cloud->points[radiusSearch[i]].y
<< " " << cloud->points[radiusSearch[i]].z
<< " (距离平方:: " << radiusSquaredDistance[i] << ")" << std::endl;
pcl::PointXYZ pt;
pt.getVector3fMap() = cloud->points[radiusSearch[i]].getVector3fMap();
cloudR->points.emplace_back(pt);
}
}
//K个邻域的结果可视化--双窗口
// PCLVisualizer对象
pcl::visualization::PCLVisualizer viewer("DoubleVIS");
//创建左右窗口的ID v1和v2
int v1(0);
int v2(1);
//设置V1窗口尺寸和背景颜色
viewer.createViewPort(0.0, 0.0, 0.5, 1, v1);
viewer.setBackgroundColor(0, 0, 0, v1);
//设置V2窗口尺寸和背景颜色
viewer.createViewPort(0.5, 0.0, 1, 1, v2);
viewer.setBackgroundColor(0.1, 0.1, 0.1, v2);
//设置cloud1的渲染颜色,点云的ID和指定可视化窗口v1
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloud1_color(cloud, 255, 255, 255);
viewer.addPointCloud(cloud, cloud1_color, "cloud1", v1);
//设置cloud2的渲染颜色,点云的ID和指定可视化窗口v2
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloud2_color(cloud, 250, 255, 255);
viewer.addPointCloud(cloud, cloud2_color, "cloud2", v2);
//添加cloudK到可视化窗口v1中
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloudk_color(cloudK, 255, 0, 0);
viewer.addPointCloud(cloudK, cloudk_color, "cloudk", v1);
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "cloudk");
//添加cloudR到可视化窗口v2中
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloudr_color(cloudK, 0, 255, 0);
viewer.addPointCloud(cloudR, cloudr_color, "cloudr", v2);
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "cloudr");
// 可视化循环主体
while (!viewer.wasStopped())
{
viewer.spinOnce();
}
}
int main(int argc,char *argv[])
{
PCLKDtreeUse();
std::cout<<"Hello PCL!"<<std::endl;
std::system("pause");
return 0;
}
可视化 结果:
至此完成第七节PCL库中点云数据拓扑关系之K-D树简单学习,下一节我们将进入《PCL库中点云数据拓扑关系之OCtree》的学习。