PCL库初学指南
pcl库初学心得
简介:本指南仅为刚接触pcl库对各个模块粗浅的理解,由于pcl库的库函数丰富,提供的接口众多,函数的调用形式也较为复杂。本指南并不意在解释每种函数和算法的基本原理,为了 尽早上手点云库实战,我把点云的比较重要的模块选择出来,粗略的了解了下算法以及重要的函数,在此记录以便以后仿照调用,也对第一次上传git留下纪念。
2022疫情寝室无聊至极……
编译
cd <模块>/<案例>
mkdir build&&cd build
cmake ..
make
运行
./<target>
pcl_viewer ***.pcd
pcl_viewer -multiview <1.pcd> <2.pcd> <3.pcd>
base
base文件夹提供了对点云的最基本的处理,包括pcd文件的写入 ,以及矩阵的变换(以实现旋转和平移).
重要函数及操作
pcl::PointCloud<pcl::PointXYZ> cloud;
cloud.width = 5;
cloud.height = 1;
cloud.is_dense = false;
cloud.points.resize(cloud.width * cloud.height);
pcl::io::savePCDFileASCII("test_pcd.pcd", cloud);
pcl::io::loadPLYFile(argv[filenames[0]], *source_cloud)
pcl::PointCloud<pcl::PointXYZ>::Ptr transformed_cloud(new pcl::PointCloud<pcl::PointXYZ>())
pcl::transformPointCloud(*source_cloud, *transformed_cloud, transform_2);
可视化
pcl::visualization::PCLVisualizer viewer("Matrix transformation example");
// Define R,G,B colors for the point cloud
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> source_cloud_color_handler(source_cloud, 255, 255, 255);
// We add the point cloud to the viewer and pass the color handler
viewer.addPointCloud(source_cloud, source_cloud_color_handler, "original_cloud");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> transformed_cloud_color_handler(transformed_cloud, 230, 20, 20); // Red
viewer.addPointCloud(transformed_cloud, transformed_cloud_color_handler, "transformed_cloud");
viewer.addCoordinateSystem(1.0, "cloud", 0);
viewer.setBackgroundColor(0.05, 0.05, 0.05, 0); // Setting background to a dark grey
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "original_cloud");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "transformed_cloud");
//viewer.setPosition(800, 400); // Setting visualiser window position
while (!viewer.wasStopped())
{ // Display the visualiser until 'q' key is pressed
viewer.spinOnce();
}
return 0;
kdtree
通过3D相机(雷达、激光扫描、立体相机)获取到的点云,一般数据量较大,分布不均匀,数据主要表征了目标物表面的大量点的集合,这些离散的点如果希望实现基于邻域关系的快速查找比对功能,就必须对这些离散的点之间建立拓扑关系。
kdtree是指选定几个特定维度,比如说x-y-z-x-y-z…然后按每个维度的数据的中位数作为当前根节点,将大于小于的分别二叉分割到左子树、右子树。然后,在左右两个子树上都重复此过程,直到要分区的最后一棵树仅由一个元素组成。k-d tree对于范围搜索和最近邻居搜索的适用性强。
kdtree搜索可以k邻居搜索,也可指定半径搜索。
重要函数及操作
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
kdtree.setInputCloud (cloud);
kdtree.nearestKSearch (searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance)
kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance)
octree
我们可以想象一个立方体,我们最少可以切成多少个相同等分的小立方体?答案就是8个。
每个八叉树节点有八个子节点或没有子节点。根节点描述了一个包围所有点的3维包容盒子。这样就可以对点数据集进行空间分区,下采样和搜索操作。
pcl_octree实现提供了有效的最近邻居搜索(邻域搜索)API,例如“ 体素(Voxel)邻居搜索”,“ K最近邻居搜索”和“半径搜索邻居”。叶子节点类也提供其他功能,例如空间“占用率”和“每个体素(Voxel)的点密度”检查;序列化和反序列化功能可将八叉树结构有效地编码为二进制格式;
重要函数及操作
pcl::octree::OctreePointCloudSearch<pcl::PointXYZ> octree(resolution); //初始化Octree
octree.setInputCloud(cloud); //设置输入点云 这两句是最关键的建立PointCloud和octree之间的联系
octree.addPointsFromInputCloud(); //构建octree
octree.nearestKSearch(searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance)
octree.radiusSearch(searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance)
octree.getPointIndicesFromNewVoxels(newPointIdxVector);
随机性采样
RANSAC算法假定我们要查看的所有数据均由内部值和异常值组成。可以用带有一组特定参数值的模型来解释离群值,而离群值在任何情况下都不适合该模型。
利用RANSAC可以实现点云分割,目前 PCL 中支持的几何模型分割有 空间平面、直线、二维或三维圆、圆球、锥体等 。 RANSAC的另一应用就是点云的配准对的剔除。
RANSAC从样本中随机抽选出一个样本子集,使用最小方差估计算法对这个子集计算模型参数,然后计算所有样本与该模型的偏差,再使用一个预先设定好的阈值与偏差比较,当偏差小于阈值时,该样本点属于模型内样本点.否则为外样本点。
重要函数及操作
//创建随机采样一致性对象
pcl::SampleConsensusModelSphere<pcl::PointXYZ>::Ptr
model_s(new pcl::SampleConsensusModelSphere<pcl::PointXYZ> (cloud)); //针对球模型的对象
pcl::SampleConsensusModelPlane<pcl::PointXYZ>::Ptr
model_p (new pcl::SampleConsensusModelPlane<pcl::PointXYZ> (cloud)); //针对平面模型的对象
if(pcl::console::find_argument (argc, argv, "-f") >= 0)
{
//根据命令行参数,来随机估算对应平面模型,并存储估计的局内点
pcl::RandomSampleConsensus<pcl::PointXYZ> ransac (model_p);
ransac.setDistanceThreshold (.01); //与平面距离小于0.01 的点称为局内点考虑,注意局内点设置参数
ransac.computeModel(); //执行随机参数估计
ransac.getInliers(inliers); //存储估计所得的局内点
}
深度图
在PCL 中深度图像与点云最主要的区别在于其近邻的检索方式的不同,并且可以互相转换。
深度图像(Depth Images)也被称为距离影像(Range Image),是指将从图像采集器到场景中各点的距离值作为像素值的图像,它直接反应了景物可见表面的几何形状,利用它可以很方便的解决3D目标描述中的许多问题,深度图像经过点云变换可以计算为点云数据,有规则及有必要信息的点云数据可以反算为深度图像数据。
重要函数及操作
createFromPointCloud(pointCloud, angularResolution, maxAngleWidth, maxAngleHeight,
sensorPose, coordinate_frame, noiseLevel, minRange, borderSize)
range_image.integrateFarRanges (far_ranges);
if (setUnseenToMaxRange)
{range_image.setUnseenToMaxRange ();}
pcl::RangeImageBorderExtractor border_extractor (&range_image);
pcl::PointCloud<pcl::BorderDescription> border_descriptions;
border_extractor.compute (border_descriptions); //提取边界计算描述子
IO输入输出
顾名思义,实现读写pcd文件,点云pcd的合并,以及OPENNI数据流读取点云。
重要函数及操作
pcl::io::savePCDFileASCII("../test_pcd.pcd", cloud);
pcl::io::loadPCDFile<pcl::PointXYZ>("../test_pcd.pcd", *cloud) ;
滤波
在获取点云数据时,由于设备精度、操作者经验、环境因素等影响,点云数据中将不可避免地出现一些噪声点。由于外界干扰如视线遮挡、障碍物等,往往也会存在一些离主体数据较远的离群点。而滤波可以将噪声点、离群点、数据压缩等按照后续要求处理,更好地进行配准、特征提取、曲面重建、可视化,所以常作为点云处理的第一步。
PCL点云滤波模块提供了很多灵活的滤波算法,如直通滤波,双边滤波,体素法滤波,高斯滤波,条件滤波,基于随机采样一致性滤波等。
如果是线结构光的采集方式得到的点云,则沿z向的分布较广,但沿x、y方向的分布则处于有限的范围内。此时,可采用直通滤波,确定x或者y方向的范围,快速裁剪离群点。
如果使用高分辨率相机等设备对点云进行采集,则点云往往较为密集。过多的点云数据对后续的分割工作带来困难。体素法滤波可以达到下采样的同时不破坏点云本身几何结构的功能,可以提高配准、曲面重建、形状识别等算法的速度,并保证准确性。
统计滤波器用于去除明显的离群点(离群点往往由噪声引入)。噪声信息属于无用信息,信息量较小。所以离群点表达的信息可以忽略不计。考虑到离群点的特征,则可以定义某处点云小于某个密度,既点云无效。计算每个点到其最近的k个点平均距离。则点云中所有点的距离应构成高斯分布。给定均值与方差,可剔除3∑之外的点。对离群点的处理可以提高对局部点云特征提取的准确性,从而提高点云配准的正确率。
重要函数及其操作
对一个点云进行降采样:
输入input.pcd,输出output.pdf
pcl_voxel_grid input.pcd output.pcd -leaf 0.03,0.03,0.03
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud (cloud); //设置输入点云
pass.setFilterFieldName ("z"); //设置过滤时所需要点云类型的Z字段
pass.setFilterLimits (0.0, 1.0); //设置在过滤字段的范围
pass.setFilterLimitsNegative (true); //设置保留范围内还是过滤掉范围内
pass.filter (*cloud_filtered); //执行滤波,保存过滤结果在cloud_filtered
//从一个点云提取索引
pcl::ExtractIndices<pcl::PointXYZ> extract; //创建点云提取对象
//为了处理点云包含的多个模型,在一个循环中执行该过程并在每次模型被提取后,保存剩余的点进行迭代
//通过分割得到内点inliers
// Extract the inliers
extract.setInputCloud (cloud_filtered);
extract.setIndices (inliers); //
extract.setNegative (false);
extract.filter (*cloud_p);
特征提取
3D 点云特征描述与提取是点云信息处理中的最基础也是最关键的部分,点云的识别、分割、重采样、配准、曲面重建等大部分算法,都十分依赖特征描述与提取的结果。
输入方式:
通过setInputCloud(PointCloudConstPtr&)给出的整个点云数据集, 任何试图在给定输入云中的每个点上进行特征估计。
通过setInputCloud(PointCloudConstPtr&)和setIndices(IndicesConstPtr&)(可选)给出的点云数据集的子集, 所有特征估计方式都将用来尝试估计输入云中每个在indices索引列表中点的特征。默认情况下,如果未给出索引集,则将考虑云中的所有点。
额外的调用setSearchSurface(PointCloudConstPtr&)来指定要搜索使用的点邻居集。
使用setSearchSurface的最有用的情况是:当我们有一个非常密集的输入数据集,但我们不想估计其中所有点的特征,而是要估计使用pcl_keypoints中的方法发现的某些关键点,或者在点云的降采样版本上(例如,使用pcl::VoxelGrid
点云特征包括法线的计算、PFH点特征直方图描述子、FPFH快速点特征直方图描述子、VFH视点特征直方图描述子、NARF (Normal Aligned Radial Feature)法线对齐径向特征。
PFH通过参数化查询点与邻域点之间的空间差异信息,形成一个多维直方图对点的k邻域的几何特征进行描述。使点云对应的6DOF(degree of freedom自由度)姿态来说具有不变性。通过建立uvw坐标系,减少与法向量有关参数至四元组。
VFH源于FPFH描述子,为了使构造的特征保持缩放不变性的同时,还要区分不同的位姿,因而计算时,需要加入视点信息。
重要函数及操作
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
//创建一个空的KdTree对象,并把它传递给法线估计向量
//基于给出的输入数据集,KdTree将被建立
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>);
//使用半径在查询点周围3厘米范围内的所有临近元素
ne.setRadiusSearch(0.03);
//计算特征值
ne.compute(*cloud_normals);
// cloud_normals->points.size ()应该与input cloud_downsampled->points.size ()有相同的尺寸
表面
使用统计分析很难消除某些数据不规则性(由较小的距离测量误差引起)。要创建完整的模型,必须考虑光滑的表面以及数据中的遮挡。重采样算法尝试通过周围数据点之间的高阶多项式插值来重新创建表面的缺失部分.
相关函数及操作
pcl::MovingLeastSquares<pcl::PointXYZ, pcl::PointNormal> mls;
mls.setComputeNormals(true);
// Set parameters
mls.setInputCloud(cloud);
mls.setPolynomialOrder(2);
mls.setSearchMethod(tree);
mls.setSearchRadius(0.03);
// Reconstruct
mls.process(mls_points);
点云分割
点云分割是根据空间、几何和纹理等特征对点云进行划分,使得同一划分区域内的点云拥有相似的特征 。
现有实现的分割算法是鲁棒性比较好的Cluster聚类分割和RANSAC基于随机采样一致性的分割。
简而言之就是分而治之,将物体分开处理。
相关函数及操作
/**
* 创建分割时所需要的模型系数对象 coefficients 及存储内点的点索引集合对象 inliers .
* 这也是我们指定“阈值距离DistanceThreshold”的地方,该距离阈值确定点必须与模型有多远才能被视为离群点。
* 这里距离阔值是 0.01m ,即只要点到 z=1 平面距离小于该阈值的点都作为内部点看待,而大于该阁值的则看做离群点。
* 我们将使用RANSAC方法(`pcl::SAC_RANSAC`)作为可靠的估计器。因为RANSAC比较简单(其他强大的估算工具也以此为基础,并添加了其他更复杂的概念)。
*/
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
// Create the segmentation object
pcl::SACSegmentation<pcl::PointXYZ> seg;
// 可选配置:是否优化模型系数
seg.setOptimizeCoefficients(true);
// 必选配置:设置分割的模型类型、分割算法、距离阈值、输入点云
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.01);
seg.setInputCloud(cloud);
// 执行分割操作,并存储分割结果保存到点集合 inliers 及存储平面模型系数 coefficients
seg.segment(*inliers, *coefficients);
// Extract the planar inliers from the input cloud
extract.setInputCloud(cloud_filtered);
extract.setIndices(inliers_plane);
extract.setNegative(false);
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
因为点云是PointXYZ类型的,所以这里用PointXYZ创建一个欧氏聚类对象,并设置提取的参数和变量。
ec.setClusterTolerance(0.02); // 设置临近搜索的搜索半径(搜索容差)为2cm
设置一个合适的聚类搜索半径 ClusterTolerance,如果搜索半径取一个非常小的值,那么一个实际独立的对象就会被分割为多个聚类;如果将值设置得太高,那么多个对象就会被分割为一个聚类,所以需要进行测试找出最合适的ClusterTolerance.
ec.setMinClusterSize(100); // 每个簇(集群)的最小大小
ec.setMaxClusterSize(25000); // 每个簇(集群)的最大大小
点云配准
由于三维扫描仪设备受到测量方式和被测物体形状的条件限制,一次扫描往往只能获取到局部的点云信息,进而需要进行多次扫描,然后每次扫描时得到的点云都有独立的坐标系,不可以直接进行拼接。
为了得到被测物体的完整数据模型,需要确定一个合适的坐标变换 ,将从各个视角得到的点集合并到一个统一的坐标系下形成一个完整的数据点云,然后就可以方便地进行可视化等操作,这就是点云数据的配准。
粗配准是指在点云相对位姿完全未知的情况下对点云进行配准,找到一个可以让两块点云相对近似的旋转平移变换矩阵,进而将待配准点云数据转换到统一的坐标系内,可以为精配准提供良好的初始值。
精配准是指在粗配准的基础上,让点云之间的空间位置差异最小化,得到一个更加精准的旋转平移变换矩阵。该算法的运行速度以及向全局最优化的收敛性却在很大程度上依赖于给定的初始变换估计以及在迭代过程中对应关系的确立。
依赖平台设备:将被测物体放在平台上,利用控制器对平台进行控制,使之按照指定角度转动,通过多次测量可以得到不同视角下的点云,由于提前获知了距离及角度信息,则可以直接对所有点云进行配准。
辅助标志点:通过在被测物体表面粘贴标签,将这些标签作为标志点,对多次测量得到的点云数据进行配准时,对这些有显著特征的标签进行识别配准,代替了对整体点云的配准,提高效率,精确度。
pcl的自动配准:
两两配准(pairwise registration):我们称一对点云数据集的配准问题为两两配准(pairwise registration)。通常通过应用一个估算得到的表示平移和旋转的 4 × 4 刚体变换矩阵来使一个点云数据集精确地与另一个点云数据集(目标数据集)进行完美配准。
具体操作为:提取关键点特征描述子,去除噪声点,得到对应关系矩阵。
配准算法有迭代最近点算法(ICP)、正态分布变换配准(NDT)、刚性物体的鲁棒姿态估计等。
具体函数及操作
pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
icp.setInputSource(cloud_in);
icp.setInputTarget(cloud_out);
pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt;
// Setting scale dependent NDT parameters
// Setting minimum transformation difference for termination condition.
ndt.setTransformationEpsilon(0.01);
// Setting maximum step size for More-Thuente line search.
ndt.setStepSize(0.1);
//Setting Resolution of NDT grid structure (VoxelGridCovariance).
ndt.setResolution(1.0);
// Setting max number of registration iterations.
ndt.setMaximumIterations(35);
// Setting point cloud to be aligned.
ndt.setInputSource(filtered_cloud);
// Setting point cloud to be aligned to.
ndt.setInputTarget(target_cloud);
// Set initial alignment estimate found using robot odometry.
Eigen::AngleAxisf init_rotation(0.6931, Eigen::Vector3f::UnitZ());
Eigen::Translation3f init_translation(1.79387, 0, 0);
Eigen::Matrix4f init_guess = (init_translation * init_rotation).matrix();
// Calculating required rigid transform to align the input cloud to the target cloud.
pcl::PointCloud<pcl::PointXYZ>::Ptr output_cloud(new pcl::PointCloud<pcl::PointXYZ>);
ndt.align(*output_cloud, init_guess);
可视化
可视化能够把科学数据(包括测量获得的数值、图像或者计算中设计、产生的数字信息)变为直观的、可以用以图形图像信息表示的、随时间和空间变化的物理现象或物理量。进而呈现在研究者面前,使他们可以方便的观察、模拟和计算。
重要函数及操作
// 定义对象
pcl::visualization::PCLVisualizer viewer;
//设置背景颜色,默认黑色
viewer.setBackgroundColor(100, 100, 100); // rgb
// --- 显示点云数据 ----
// "cloud1" 为显示id,默认cloud,显示多个点云时用默认会报警告。
viewer.addPointCloud(cloud1, "cloud1");
pcl::visualization::PointCloudColorHandlerCustom<PointT> red(cloud2, 255, 0, 0); // rgb
// 将点云设置颜色,默认白色
viewer.addPointCloud(cloud2, red, "cloud2");
// 将两个点连线
PointT temp1 = cloud1->points[0];
PointT temp2 = cloud1->points[1];
viewer.addLine(temp1, temp2, "line0");
while (!viewer.wasStopped())
{
viewer.spinOnce(100);
boost::this_thread::sleep(boost::posix_time::microseconds(100000));
// 可添加其他操作,注意Cmakelists链接boost.
}
system("pause");
return 0;
关键点
关键点是 可以通过检测标准来获取的具有稳定性、区别性的点集。从技术上来说,关键点的数量比原始点云或图像的数据量少很多,其与局部特征描述子结合组成关键点描述子。常用来构成原始数据的紧凑表示 ,具有代表性与描述性,从而加快后续识别、追踪等对数据的处理速度 。
关键点提取过程有以下要求:
- 提取的过程必须考虑边缘以及物体表面变化信息
- 即使换了不同的视角,关键点的位置必须稳定的可以被重复探测
- 关键点所在的位置必须有稳定的支持区域,可以计算描述子和估计唯一的法向量。
重要函数及操作
pcl::NarfKeypoint narf_keypoint_detector(&range_image_border_extractor);
narf_keypoint_detector.setRangeImage(&range_image);
narf_keypoint_detector.getParameters().support_size = support_size;
//narf_keypoint_detector.getParameters ().add_points_on_straight_edges = true;
//narf_keypoint_detector.getParameters ().distance_for_additional_points = 0.5;
pcl::PointCloud<int> keypoint_indices;
narf_keypoint_detector.compute(keypoint_indices);
std::cout << "Found " << keypoint_indices.points.size() << " key points.\n";
// ----------------------------------------------
// -----Show keypoints in range image widget-----
// ----------------------------------------------
//for (size_t i=0; i<keypoint_indices.points.size (); ++i)
//range_image_widget.markPoint (keypoint_indices.points[i]%range_image.width,
//keypoint_indices.points[i]/range_image.width);
// -------------------------------------
// -----Show keypoints in 3D viewer-----
// -------------------------------------
pcl::PointCloud<pcl::PointXYZ>::Ptr keypoints_ptr(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ> &keypoints = *keypoints_ptr;
keypoints.points.resize(keypoint_indices.points.size());