特征
一、概述
点特征是根据该点周围可用的信息来描述几何图案,在点周围选择的数据空间通常称为k邻域。两个最广泛使用的几何点特征是下表面的估计曲率和点法线,使用其k个最近点提供的信息来表征一个点。
为了有效地确定k,通常使用空间分解技术(例如八叉树或kD树)将输入数据集分成较小的块,然后在该空间中执行最近点搜索。 可以选择在p点附近的k个固定点,或者以点为中心的半径r的球体内的所有点。
计算点p处的表面法线和曲率变化的最简单方法是在k邻域内进行特征分解(即计算特征向量和特征值)。对应于最小特征值的特征向量将在点p处近似表面法线n,而表面曲率变化将从特征值估算为
二、特征模块在PCL中是如何工作的
通过setInputCloud(PointCloudConstPtr&)
给出整个点云的数据集,评估点云中每个点的特征;
通过setIndices(IndicesConstPtr&)
给定索引列表,具有索引的点才进行特征评估。 默认为false,不指定点,当setIndices()=true
时只对索引点进行评估;
通过setSearchSurface(PointCloudConstPtr&)
来指定要使用的点邻域集。指定要使用邻域点的表面;
当我们有一个非常密集的输入数据集时,只想要估计使用pcl_keypoints
方法发现的某些关键点,可以通过setInputCloud()
进行关键点输入,并将原始数据作为setSearchSurface()
传递。
代码将为输入数据集中的所有点估计一组表面法线。
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
{
//定义一个点集
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
//读取或创建点云数据
//创建法线估计类,把输入的点云数据传递给它
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud (cloud);//给定点云数据集
//创建一个空的kdtree,并将其传递给法线估计类
// Its content will be filled inside the object, based on the given input dataset (as no other search surface is given).
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>);
// Use all neighbors in a sphere of radius 3cm
ne.setRadiusSearch (0.03);
//计算发现特征
ne.compute (*cloud_normals);
// cloud_normals->points.size () should have the same size as the input cloud->points.size ()
}
以下为输入点云中的部分点的一组表面法线
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);//定义一组点云
... read, pass in or create a point cloud ...
// 创建一组要使用的索引。 索引的大小为输入点云中前10%的点
std::vector<int> indices (std::floor (cloud->points.size () / 10));
for (std::size_t i = 0; i < indices.size (); ++i) indices[i] = i;
// 创建普通的估计类,并将输入数据集传递给它
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud (cloud);
// 传递索引
pcl::shared_ptr<std::vector<int> > indicesptr (new std::vector<int> (indices));
ne.setIndices (indicesptr);
//创建一个空的kdtree表示形式,并将其传递给正常的估计对象
//它的内容将根据给定的输入数据集填充到对象内部(因为未提供其他搜索表面)。
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
ne.setSearchMethod (tree);
// Output datasets
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);
// Use all neighbors in a sphere of radius 3cm
ne.setRadiusSearch (0.03);
// Compute the features
ne.compute (*cloud_normals);
// cloud_normals->points.size () should have the same size as the input indicesptr->size ()
}
以下为输入数据集中的所有点估计一组表面法线,但将使用另一个数据集估计每个点最近的邻居。输入是表面的降采样版本
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
//下采样版本的数据集
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_downsampled (new pcl::PointCloud<pcl::PointXYZ>);
... read, pass in or create a point cloud ...
... create a downsampled version of it ...
// Create the normal estimation class, and pass the input dataset to it
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
//下采样的数据集作为输入
ne.setInputCloud (cloud_downsampled);
// Pass the original data (before downsampling) as the search surface
//初始的点云数据集作为搜索表面
ne.setSearchSurface (cloud);
// Create an empty kdtree representation, and pass it to the normal estimation object.
// Its content will be filled inside the object, based on the given surface dataset.
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
ne.setSearchMethod (tree);
// Output datasets
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);
// Use all neighbors in a sphere of radius 3cm
ne.setRadiusSearch (0.03);
// Compute the features
ne.compute (*cloud_normals);
// cloud_normals->points.size () should have the same size as the input cloud_downsampled->points.size ()
}
三、估计表面法线
1.理论知识
曲面上某点的法线的求解近似于曲面上某平面切线的法线的求解,因此这变成最小二乘平面拟合问题。
估计表面法线的方法简化为,求点邻域内点集的协方差矩阵的特征向量和特征值(或进行PCA 主成分分析)。对于每个点,我们将协方差矩阵的元素为:
代码为
// Placeholder for the 3x3 covariance matrix at each surface patch
Eigen::Matrix3f covariance_matrix;
// 16-bytes aligned placeholder for the XYZ centroid of a surface patch
Eigen::Vector4f xyz_centroid;
// Estimate the XYZ centroid
compute3DCentroid (cloud, xyz_centroid);
// Compute the 3x3 covariance matrix
computeCovarianceMatrix (cloud, xyz_centroid, covariance_matrix);
为了使得求得的法线方向一致,设置所有的法线都朝向视点
视点默认为(0,0,0),可以更改
setViewPoint (float vpx, float vpy, float vpz);
2.范围选择
给定采样点云数据集,通过k(pcl :: Feature :: setKSearch
)或r(pcl :: Feature:: setRadiusSearch
)确定点邻域
k如果太大,那邻域的集合覆盖了来自相邻曲面的较大点,则估计的点要素表示会失真,在两个平面边缘处旋转的曲面法线会被涂抹。
四、使用积分图像进行法线估计
使用积分图像计算有组织的点云的法线
积分图像的定义见:link
创建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 ("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.5);
viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud, normals);//把点云和法线加在一起
while (!viewer.wasStopped ())
{
viewer.spinOnce ();//界面更新一次
}
return 0;
}
法线估计方法
COVARIANCE_MATRIX
模式创建9个积分图像,以根据其局部邻域的协方差矩阵为特定点计算法线。 AVERAGE_3D_GRADIENT
模式创建6个积分图像以计算水平和垂直3D渐变的平滑版本,并使用这两个渐变之间的叉积计算法线。
AVERAGE_DEPTH_CHANGE
模式仅创建单个积分图像,并根据平均深度变化来计算法线
五、点特征直方图
1.理论基础
表面法线和曲率的估计尽管快速方便,但是它们无法捕获更多细节,因为它们仅用几个值来表示点的k邻域的几何形状。 为了更好的描述细节特征,引入点特征直方图。PFH通过计算查询点与k邻域点的表面法线的关系,对点的k邻域几何特性进行描述, 考虑到了估计法线方向之间的所有相互作用,尽可能地捕获了采样的表面变化。
对于查询点Pq,与其距离小于r的所有k个相邻点Pki,计算两两之间的关系得到直方图
对于点Ps和Ps,它们的法线为ns,nt,为了描述他们法线的转换关系,定义一个坐标系uvw,u与ns同向,v垂直于ns与两点连线所在的表面
两点的关系可以通过三个角度和欧几里得距离d来表示,三个角度表示两个法线的坐标变换
计算两点间的PFH的四个参数,使用
computePairFeatures (const Eigen::Vector4f &p1, const Eigen::Vector4f &n1,//第一点
const Eigen::Vector4f &p2, const Eigen::Vector4f &n2,//第二点
float &f1, float &f2, float &f3, float &f4);//输出四个特征值
计算直方图的时候,每个特征值的范围划分为n个区间,统计落在某个区间内的点的个数。在扫描局部点密度影响特征时,d值是可以忽略的,所以一般只统计三个特征值
2.估计PFH特征
将三个特征值的范围分为五个区间,计算直方图后得到一个5^3字节的特征向量,储存为```ocl::PFHSignature125``类型的数
代码为
#include <pcl/point_types.h>//点类型头文件
#include <pcl/features/pfh.h>//pfh特征估计类头文件
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal> ());
... read, pass in or create a point cloud with normals ...
... (note: you can create a single PointCloud<PointNormal> if you want) ...
//创建PFH估计对象pfh,并将输入点云数据集cloud和法线normals传递给它
pcl::PFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::PFHSignature125> pfh;
pfh.setInputCloud (cloud);
pfh.setInputNormals (normals);//如果点云是类型为PointNormal,则执行pfh.setInputNormals (cloud);
//创建一个空的kd树表示法,并把它传递给PFH估计对象。
//基于已给的输入数据集,建立kdtree.
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
//pcl::KdTreeFLANN<pcl::PointXYZ>::Ptr tree (new pcl::KdTreeFLANN<pcl::PointXYZ> ()); -- older call for PCL 1.5-
pfh.setSearchMethod (tree);
// Output datasets
pcl::PointCloud<pcl::PFHSignature125>::Ptr pfhs (new pcl::PointCloud<pcl::PFHSignature125> ());
// Use all neighbors in a sphere of radius 5cm
//注意:此处使用的半径必须要大于估计表面法线时使用的半径!!!
pfh.setRadiusSearch (0.05);
// 以上属于输入计算数据,设置计算方法,以下进行计算,输入到pfhs里
pfh.compute (*pfhs);
// pfhs->points.size () should have the same size as the input cloud->points.size ()*
}
从k邻域计算单个PFH特征
computePointPFHSignature (const pcl::PointCloud<PointInT> &cloud,//输入点云
const pcl::PointCloud<PointNT> &normals,//包含法线的点云
const std::vector<int> &indices,//查询点的k邻域元素集
int nr_split,//直方图区间数
Eigen::VectorXf &pfh_histogram);//保存输入直方图
检查法线是不是not a number 或无限
for (int i = 0; i < normals->points.size(); i++)
{
if (!pcl::isFinite<pcl::Normal>(normals->points[i]))
{
PCL_WARN("normals[%d] is not finite\n", i);
}
}
六、快速点特征直方图
1.理论基础
为了提高计算速度,将计算的复杂度从O(nk^2)降低到O(nk)。
第一步,简单点特征直方图计算(SPFH),计算Pq点与它的k邻域点的PFH(不同于两两之间都要计算);
第二步,对于k邻域的点再分别进行SPFH,得到
因此,对于给定的查询点Pq,该算法首先通过在自身及其邻居之间创建对来估计其SPFH值(用红线表示)。 对数据集中的所有点重复此操作,然后使用其邻居的SPFH值对Pq的SPFH值进行重新加权,从而创建的FPFH。 黑色线表示由于额外的加权方案而导致的额外FPFH连接。 如图所示,某些值对将被计数两次(在图中用粗线标记)。
2.PFH与FPFH
1.FPFH未完全互连其所有邻域点,会缺少一些可能有助于确定查询点周围的几何图形的值;
2.PFH在查询点周围对精确确定的表面进行建模,而FPFH的范围在r半径球面之外(最多到2r处);
3.由于采用了重新加权方案,FPFH合并了SPFH值,并重新捕获了一些相邻点;
4.FPFH的整体复杂性大大降低,因此可以在实时应用中使用;
5.通过对值进行去相关,可以简化生成的直方图,即只需创建d个单独的特征直方图(每个特征维一个),然后将它们连接在一起。
3.代码
FPFH的直方图,每个特征值划分11个区间,三个特征值,生成一个33字节的特征向量,储存在pcl::FPFHSignature33
的数据类型
#include <pcl/point_types.h>
#include <pcl/features/fpfh.h>
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal> ());
... read, pass in or create a point cloud with normals ...
... (note: you can create a single PointCloud<PointNormal> if you want) ...
//创建一个FPFH的估计类,把输入数据和它的法向量传递给他
pcl::FPFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::FPFHSignature33> fpfh;
fpfh.setInputCloud (cloud);
fpfh.setInputNormals (normals);
// alternatively, if cloud is of tpe PointNormal, do fpfh.setInputNormals (cloud);
// Create an empty kdtree representation, and pass it to the FPFH estimation object.
// Its content will be filled inside the object, based on the given input dataset (as no other search surface is given).
pcl::search::KdTree<PointXYZ>::Ptr tree (new pcl::search::KdTree<PointXYZ>);
fpfh.setSearchMethod (tree);
// Output datasets
pcl::PointCloud<pcl::FPFHSignature33>::Ptr fpfhs (new pcl::PointCloud<pcl::FPFHSignature33> ());
// Use all neighbors in a sphere of radius 5cm
// IMPORTANT: the radius used here has to be larger than the radius used to estimate the surface normals!!!
fpfh.setRadiusSearch (0.05);
// Compute the features
fpfh.compute (*fpfhs);
// fpfhs->points.size () should have the same size as the input cloud->points.size ()*
}
FPFH估计计算的步骤
第一步:
1.得到p的邻域元素
2.计算每一对p, p_k的三个角度参数值(p_k是:p的邻元素)
3.把所有结果统计输出到一个SPFH直方图
第二步:
1.得到p的最近邻元素
2.使用p的每一个SPFH和一个权重计算式,来计算最终p的FPFH
七、视点特征直方图(VFH)
1.理论基础
视点特征直方图起源于FPFH,运用在点云聚类识别和六自由度位姿估计,利用FPFH的强大的识别力,在保留尺度不变的情况下增加视点变量。VFH把FPFH估计扩展到整个点云对象,并计算每个点上估计的视点和法线之间的统计数据。为了实现这点,把视点方向与估计的法线特征角混合起来。
第一个特征分量:通过统计视点方向与每个法线之间角度的直方图来计算视点相关的特征分量。 注意,并不是指与每个法线的视角,因为法线的视角在尺度变换下具有可变性,而是指中心视点方向与每个法线之间的夹角。 第二个特征分量是快速点特征直方图中的三个角度,但是是在中心视点方向与每个法线之间进行测量。
以下为视点相关的分量,和FPFH描述表面形状的分量
2.估计VFH特征
对扩展的FPFH分量来说,在VFH里使用45个区间进行统计,而对于视点分量要使用128个区间进行统计,这样VFH就由一共308个浮点数组成阵列。利用pcl::VFHSignature308
的点类型来存储。PFH/FPFH描述子和VFH之间的主要区别是:对于一个已知的点云数据集,只一个单一的VFH描述子,而合成的PFH/FPFH特征的数目和点云中的点数目相同。
#include <pcl/point_types.h>
#include <pcl/features/vfh.h>
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal> ());
... read, pass in or create a point cloud with normals ...
... (note: you can create a single PointCloud<PointNormal> if you want) ...
//创建VFH估计对象vfh,并把输入数据集cloud和法线normal传递给它
pcl::VFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::VFHSignature308> vfh;
vfh.setInputCloud (cloud);
vfh.setInputNormals (normals);
// alternatively, if cloud is of type PointNormal, do vfh.setInputNormals (cloud);
// Create an empty kdtree representation, and pass it to the FPFH estimation object.
// Its content will be filled inside the object, based on the given input dataset (as no other search surface is given).
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
vfh.setSearchMethod (tree);
// Output datasets
pcl::PointCloud<pcl::VFHSignature308>::Ptr vfhs (new pcl::PointCloud<pcl::VFHSignature308> ());
// Compute the features
vfh.compute (*vfhs);
// vfhs->points.size () should be of size 1*
}
3.可视化直方图
lib_pcl visualization
包含一个特殊的PCLHistogramVisualization
类,pcl_viewer
也使用这个类来自动将VFH描述符显示为浮动值的直方图.