本教程首先对PCL中实现的双边滤波算法以及所涉及的概念进行简介,其次对PCL的滤波相关模块及类进行简单介绍,最后通过在PCL中使用直通滤波器对点云进行滤波处理,使用VoxelGrid滤波器对点云进行下采样,使用StatisticalOutlierRemoval滤波器移除离群点,使用参数化模型投影点云,从点云中提取索引,使用ConditionalRemoval或RadiusOutlierRemoval移除离群点6个应用实例来展示如何对PCL中的滤波模块进行灵活运用。
注意:本教程是在Ubuntu系统下使用Cmake进行编译运行,所以需要对Cmake有一定的了解。
PCL中总结了几种需要进行点云滤波处理的情况,这几种情况如下:
∙ \bullet ∙ 点云数据密度不规则需要平滑
∙ \bullet ∙ 因为遮挡等问题造成离群点需要去除
∙ \bullet ∙ 大量数据需要下采样(Downsample
)
∙ \bullet ∙ 噪音数据需要去除
对应的方法如下:
∙ \bullet ∙ 按具体给定的规则限制过滤去除点
∙ \bullet ∙ 通过常用滤波算法修改点的部分属性
∙ \bullet ∙ 对数据进行下采样
双边滤波算法
,是通过取邻近采样点的加权平均来修正当前采样点的位置,从而达到滤波效果。同时也会有选择的剔除部分与当前采用点“差异”太大的相邻采样点,从而达到保持原特征的目的。
pcl_filters模块提供了对噪声点和离群点去除的具体实现。filters模块利用32个类和5个函数实现了对点云数据进行不同的滤波以达到除去不需要的点的目的,其依赖于pcl::common、pcl::sample_consensus、pcl::search、pcl::kdtree、pcl::octree模块。函数及类的接口请查看官网。
本小节我们将学习如何对指定的某一维度实行一个简单的滤波,即去掉在用户指定外围内部(或外部)的点。
首先创建一个工作空间passthrough
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p passthrough/src
接着,在passthrough/src
路径下,创建一个文件并命名为passthrough.cpp
,拷贝如下代码:
#include
#include
#include
#include
int main (int argc, char** argv)
{
srand(time(0));
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);
/* 生成并填充点云数据 */
cloud->width = 5; // 设置点云宽度或数量,这里为数量
cloud->height = 1; // 设置点云高度或标准,其为无序点云
cloud->points.resize (cloud->width * cloud->height);
for (size_t i = 0; i < cloud->points.size (); ++i) // 为点云填充数据
{
cloud->points[i].x = rand () / (RAND_MAX + 1.0f)-0.5;
cloud->points[i].y = rand () / (RAND_MAX + 1.0f)-0.5;
cloud->points[i].z = rand () / (RAND_MAX + 1.0f)-0.5;
}
std::cerr << "Cloud before filtering: " << std::endl;
for (size_t i = 0; i < cloud->points.size (); ++i)
std::cerr << " " << cloud->points[i].x << " "
<< cloud->points[i].y << " "
<< cloud->points[i].z << std::endl;
/* 创建滤波器对象 */
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
std::cerr << "Cloud after filtering: " << std::endl;
for (size_t i = 0; i < cloud_filtered->points.size (); ++i)
std::cerr << " " << cloud_filtered->points[i].x << " "
<< cloud_filtered->points[i].y << " "
<< cloud_filtered->points[i].z << std::endl;
return (0);
}
首先,利用随机数生成了点云,作为滤波的输入点云数据,并将其打印到终端。
接下来,创建了直通滤波器的对象,设立它的参数,滤波字段名被设为Z
轴方向,可接受的范围设为(0.0,1.0)
,即将点云中所有点的z坐标不在该范围内的点过滤掉或保留,这里是过滤,由函数setFilterLimitsNegative
设定。
最后打印出过滤后的点云数据。
在工作空间根目录passthrough
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(passthrough)
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/passthrough.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录passthrough
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件passthrough_node
,运行该可执行文件:
./passthrough_node
在终端中可以看到如下输出结果:
Cloud before filtering:
-0.413473 0.464987 0.170971
0.468947 0.0822083 -0.180129
-0.00256902 -0.250155 0.0773935
0.117259 -0.0797942 -0.110821
0.0489048 -0.316323 0.254254
Cloud after filtering:
-0.413473 0.464987 0.170971
-0.00256902 -0.250155 0.0773935
0.0489048 -0.316323 0.254254
注意:由于是利用随机数生成的点云数据,所以每次运行结果都会不一样,但都会将点云中的z坐标在(0,1)范围外的点过滤掉。
本小节我们将学习如何使用体素化网格方法实现下采样,即减少点的数量,并同时保持点云的形状特征,在提高配准、曲面重建、形状识别等算法速度中非常实用。PCL实现的VoxelGrid类通过输入的点云数据创建一个三维体素栅格(可把体素栅格想象为微小的空间三维立方体的集合),然后在每个体素(即三维立方体)内,用体素中所有点的重心来近似显示体素中其他点,这样该体素内所有点就用一个重心点最终表示,对所有体素处理后得到过滤后的点云。这种方法比用体素中心来逼近的方法更慢,但它对于采样点对应曲面的表示更为准确。
首先创建一个工作空间voxel_grid
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p voxel_grid/src
接着,在voxel_grid/src
路径下,创建一个文件并命名为voxel_grid.cpp
,拷贝如下代码:
#include
#include
#include
#include
#include
#include
using namespace std;
int user_data;
void viewerOneOff (pcl::visualization::PCLVisualizer& viewer)
{
viewer.setBackgroundColor(0,0,0); //设置窗口背景颜色(这里是黑色)
}
void viewerPsycho (pcl::visualization::PCLVisualizer& viewer)
{
static unsigned count = 0;
std::stringstream ss;
ss << "Once per viewer loop: " << count++;
viewer.removeShape ("text", 0);
viewer.addText (ss.str(), 200, 300, "text", 0);
//FIXME: possible race condition here:
user_data++;
cout << "user_data = " << user_data << endl;
}
int main (int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>);
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZI>);
pcl::io::loadPCDFile("../pcd/filter_before/table_scene_lms400.pcd", *cloud); // 从pcd文件中读取点云数据
std::cerr << "PointCloud before filtering: " << cloud->width * cloud->height
<< " data points (" << pcl::getFieldsList (*cloud) << ")." << endl; // 输出滤波前的点云信息
/* 可视化原始点云 */
pcl::visualization::CloudViewer viewer("PointCloud before filtering");
viewer.showCloud(cloud);
viewer.runOnVisualizationThreadOnce (viewerOneOff);
pcl::VoxelGrid<pcl::PointXYZI> sor; // 创建滤波对象
sor.setInputCloud (cloud); // 给滤波对象设置需要过滤的点云
sor.setLeafSize (0.01f, 0.01f, 0.01f); // 设置滤波时创建的体素大小为1cm立方体
sor.filter (*cloud_filtered); // 执行滤波处理,存储输出到cloud_filtered
pcl::io::savePCDFileBinary("../pcd/filter_after/table_scene_lms400_downsampled.pcd",*cloud); // 保存滤波后的点云数据
while(!viewer.wasStopped()) {} // 判断是否关闭了滤波之前的点云显示窗口,如果没关闭就继续等待
if(viewer.wasStopped()) // 如果关闭了窗口则重新显示滤波之后的点云
{
pcl::visualization::CloudViewer viewer("PointCloud after filtering");
viewer.showCloud(cloud_filtered);
viewer.runOnVisualizationThread (viewerPsycho);
while (!viewer.wasStopped()) {}
}
std::cerr << "PointCloud after filtering: " << cloud_filtered->width * cloud_filtered->height
<< " data points (" << pcl::getFieldsList (*cloud_filtered) << ")." << endl; // 输出滤波后的点云信息
return (0);
}
注意:读取原始pcd文件以及保存滤波后pcd文件的路径需要根据自己的需要进行修改,原始pcd文件可以在此处下载。
为了更加直观的对比滤波前后的点云数据,本教程定义了两个子线程来显示滤波前和滤波后的点云。主要是因为点云显示与主线程不相关,是一个单独的线程。如果在主线程中对点云进行操作,很有可能会发生线程争用的问题。viewer.runOnVisualizationThreadOnce()
和viewer.runOnVisualizationThread()
可以用于点云显示线程,避免部分问题。两者的区别在于前者只回调一次函数viewerOneOff
,后者则是每次迭代都会回调一次函数viewerPsycho
。
在工作空间根目录voxel_grid
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(voxel_grid)
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/voxel_grid.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录voxel_grid
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件voxel_grid_node
,运行该可执行文件:
./voxel_grid_node
滤波前和滤波后的结果分别为:
同时,在终端中输出如下信息:
PointCloud before filtering: 460400 data points (x y z intensity).
PointCloud after filtering: 41049 data points (x y z intensity).
从上述结果可以看出,过滤后的数据量大大减少,处理前点云数为460400,而处理后为41049。根据原始点云与滤波后的点云可视化结果可以看出,点的密度大小与整齐程度不同,虽然处理后数据量大大减少,但很明显其所含有的形状特征和空间结构信息与原始点云差不多。
本小节我们将学习如何使用统计分析技术,从一个点云数据集中移除测量噪声点(也就是离群点)。
激光扫描通常会产生密度不均匀的点云数据集。另外,测量中的误差会产生稀疏的离群点,使效果更糟糕。估计局部点云特征(例如采样点处的法向量或曲率变化率)的运算很复杂,这会导致错误的数值,反过来有可能导致点云的配准等后期处理失败。以下方法可以解决其中部分问题:对每个点的邻域进行一个统计分析,并修剪掉那些不符合一定标准的点。我们的稀疏离群点移除方法基于在输入数据中对点到临近点的距离分布的计算。对每个点,我们计算它到它所有临近点的平均距离。假设得到的结果是一个高斯分布,其形状由均值和标准差决定,平均距离在标准范围(由全局距离平均值和方差定义)之外的点,可被定义为离群点并可从数据集中去除掉。
首先创建一个工作空间statistical_removal
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p statistical_removal/src
接着,在statistical_removal/src
路径下,创建一个文件并命名为statistical_removal.cpp
,拷贝如下代码:
#include
#include
#include
#include
#include
#include
using namespace std;
int user_data;
void viewerOneOff (pcl::visualization::PCLVisualizer& viewer)
{
viewer.setBackgroundColor(0,0,0); //设置窗口背景颜色(这里是黑色)
}
void viewerPsycho (pcl::visualization::PCLVisualizer& viewer)
{
static unsigned count = 0;
std::stringstream ss;
ss << "Once per viewer loop: " << count++;
viewer.removeShape ("text", 0);
viewer.addText (ss.str(), 200, 300, "text", 0);
//FIXME: possible race condition here:
user_data++;
}
int main (int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>);
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud_filtered_inliers(new pcl::PointCloud<pcl::PointXYZI>); // 存储滤波后剩下的点云数据
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud_filtered_outliers(new pcl::PointCloud<pcl::PointXYZI>); // 存储滤波后滤除的点云数据
pcl::io::loadPCDFile("../pcd/filter_before/table_scene_lms400.pcd", *cloud); // 从pcd文件中读取点云数据
std::cerr << "Cloud before filtering: " << std::endl;
std::cerr << *cloud << std::endl;
/* 可视化原始点云 */
pcl::visualization::CloudViewer viewer("PointCloud before filtering");
viewer.showCloud(cloud);
viewer.runOnVisualizationThreadOnce (viewerOneOff);
pcl::StatisticalOutlierRemoval<pcl::PointXYZI> sor; // 创建滤波器对象
sor.setInputCloud (cloud); // 设置待滤波的点云
sor.setMeanK (50); // 设置在进行统计时考虑查询临近点数
sor.setStddevMulThresh (1.0); // 设置判断是否为离群点的阈值,设置为1表明一个点的距离超出平均距离一个标准差以上,则被标记为离群点,并将被移除
sor.filter (*cloud_filtered_inliers); // 执行滤波,并将结果保存到cloud_filtered_inliers中
std::cerr << "Cloud after filtering: " << std::endl;
std::cerr << *cloud_filtered_inliers << std::endl;
pcl::io::savePCDFile("../pcd/filter_after/table_scene_lms400_inliers.pcd",*cloud_filtered_inliers); // 保存滤波后剩下的点云数据
while(!viewer.wasStopped()) {} // 判断是否关闭了滤波之前的点云显示窗口,如果没关闭就继续等待
if(viewer.wasStopped()) // 如果关闭了窗口则重新显示滤波之后的点云
{
pcl::visualization::CloudViewer viewer("PointCloud after filtering(inliers)");
viewer.showCloud(cloud_filtered_inliers);
viewer.runOnVisualizationThread (viewerPsycho);
while (!viewer.wasStopped()) {}
}
sor.setNegative (true); // 设置输出取外点,以获取离群点数据(也就是原本滤除掉的点)
sor.filter (*cloud_filtered_outliers); // 将滤除掉的点云存储在cloud_filtered_outliers中
pcl::io::savePCDFile("../pcd/filter_after/table_scene_lms400_outliers.pcd",*cloud_filtered_outliers); // 保存滤波后除掉的点云数据
while(!viewer.wasStopped()) {} // 判断是否关闭了滤波之前的点云显示窗口,如果没关闭就继续等待
if(viewer.wasStopped()) // 如果关闭了窗口则重新显示滤波之后的点云
{
pcl::visualization::CloudViewer viewer("PointCloud after filtering(outliers)");
viewer.showCloud(cloud_filtered_outliers);
viewer.runOnVisualizationThread (viewerPsycho);
while (!viewer.wasStopped()) {}
}
return (0);
}
在工作空间根目录statistical_removal
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(statistical_removal)
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/statistical_removal.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录statistical_removal
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件statistical_removal_node
,运行该可执行文件:
./statistical_removal_node
滤波前和滤波后的结果分别为:
其中,第一幅图为原始点云的可视化,第二幅图为滤波后内点的可视化,第三幅图为滤波后外点的可视化。从上述结果可以看出,该滤波处理非常适合对点云中的离群点进行去除。
本小节将学习如何将点投影到一个参数化模型上(例如平面或球等)。参数化模型是通过一组参数来设定。对于平面来说,使用其等式形式 a x + b y + c z + d = 0 ax+by+cz+d=0 ax+by+cz+d=0,在PCL中有特意存储常见模型系数的数据结构。
首先创建一个工作空间project_inliers
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p project_inliers/src
接着,在project_inliers/src
路径下,创建一个文件并命名为project_inliers.cpp
,拷贝如下代码:
#include
#include
#include
#include // 模型系数定义头文件
#include // 投影滤波类头文件
using namespace std;
int main (int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_projected (new pcl::PointCloud<pcl::PointXYZ>);
/* 填充点云数据 */
cloud->width = 5;
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 * rand () / (RAND_MAX + 1.0f);
cloud->points[i].y = 1024 * rand () / (RAND_MAX + 1.0f);
cloud->points[i].z = 1024 * rand () / (RAND_MAX + 1.0f);
}
std::cerr << "Cloud before projection: " << std::endl;
for (size_t i = 0; i < cloud->points.size (); ++i)
{
std::cerr << " " << cloud->points[i].x << " "
<< cloud->points[i].y << " "
<< cloud->points[i].z << std::endl;
}
/* 定义模型系数对象,并填充对应的数据 */
pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients ());
coefficients->values.resize (4);
coefficients->values[0] = coefficients->values[1] = 0;
coefficients->values[2] = 1.0;
coefficients->values[3] = 0;
/* 创建ProjectInliers对象,并使用刚刚定义好的ModelCoefficients作为投影对象的模型参数 */
pcl::ProjectInliers<pcl::PointXYZ> proj; // 创建投影滤波对象
proj.setModelType (pcl::SACMODEL_PLANE); // 设置对象对应的投影模型
proj.setInputCloud (cloud); // 设置输入点云
proj.setModelCoefficients (coefficients); // 设置模型对应的系数
proj.filter (*cloud_projected); // 执行投影滤波,并将结果存储在cloud_projected中
std::cerr << "Cloud after projection: " << std::endl;
for (size_t i = 0; i < cloud_projected->points.size (); ++i)
{
std::cerr << " " << cloud_projected->points[i].x << " "
<< cloud_projected->points[i].y << " "
<< cloud_projected->points[i].z << std::endl;
}
return (0);
}
上述代码中,在填充ModelCoefficients
的值时,我们使用了 a x + b y + c z + d = 0 ax+by+cz+d=0 ax+by+cz+d=0 的平面模型,其中 a = b = d = 0 , c = 1 a=b=d=0, c=1 a=b=d=0,c=1 ,即 X − Y X-Y X−Y 平面,用户可以任意定义PCL中支持的模型圆球、圆柱、锥形等进行投影滤波。
在工作空间根目录project_inliers
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(project_inliers)
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/project_inliers.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录project_inliers
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件project_inliers_node
,运行该可执行文件:
./project_inliers_node
可以看到在终端中输出如下运行结果:
Cloud before projection:
0.352222 -0.151883 -0.106395
-0.397406 -0.473106 0.292602
-0.731898 0.667105 0.441304
-0.734766 0.854581 -0.0361733
-0.4607 -0.277468 -0.916762
Cloud after projection:
0.352222 -0.151883 0
-0.397406 -0.473106 0
-0.731898 0.667105 0
-0.734766 0.854581 0
-0.4607 -0.277468 0
根据上述输出结果可以看出,投影前的Z轴都不为0,是随机产生的数,投影之后,xy没有变化,z都变为0,符合程序的逻辑,该投影滤波类输入为点云和投影模型,输出为投影到模型上之后的点云。
在本小节中,我们将学习如何使用一个ExtractIndices滤波器,基于某一分割算法提取点云中的一个子集。
首先创建一个工作空间extract_indices
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p extract_indices/src
接着,在extract_indices/src
路径下,创建一个文件并命名为extract_indices.cpp
,拷贝如下代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int user_data;
void viewerOneOff (pcl::visualization::PCLVisualizer& viewer)
{
viewer.setBackgroundColor(0,0,0); //设置窗口背景颜色(这里是黑色)
}
void viewerPsycho (pcl::visualization::PCLVisualizer& viewer)
{
static unsigned count = 0;
std::stringstream ss;
ss << "Once per viewer loop: " << count++;
viewer.removeShape ("text", 0);
viewer.addText (ss.str(), 200, 300, "text", 0);
//FIXME: possible race condition here:
user_data++;
}
int main (int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZI>),
cloud_filtered (new pcl::PointCloud<pcl::PointXYZI>);
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud_p (new pcl::PointCloud<pcl::PointXYZI>),
cloud_f (new pcl::PointCloud<pcl::PointXYZI>);
pcl::io::loadPCDFile("../pcd/filter_before/table_scene_lms400.pcd", *cloud); // 从pcd文件中读取点云数据
std::cerr << "PointCloud before filtering: " << cloud->width * cloud->height << " data points." << std::endl;
/* 可视化原始点云 */
pcl::visualization::CloudViewer viewer("PointCloud before filtering");
viewer.showCloud(cloud);
viewer.runOnVisualizationThreadOnce (viewerOneOff);
/* 首先执行体素栅格下采样 */
pcl::VoxelGrid<pcl::PointXYZI> sor; // 创建体素栅格下采样对象
sor.setInputCloud (cloud); // 设置下采样输入点云数据
sor.setLeafSize (0.01f, 0.01f, 0.01f); // 设置采样的体素大小
sor.filter (*cloud_filtered); // 执行体素栅格下采样,并将结果保存到cloud_filtered_blob中
std::cerr << "PointCloud after filtering: " << cloud_filtered->width * cloud_filtered->height << " data points." << std::endl;
pcl::io::savePCDFile("../pcd/filter_after/table_scene_lms400_downsampled.pcd",*cloud_filtered); // 保存下采样后的点云数据
while(!viewer.wasStopped()) {} // 判断是否关闭了下采样之前的点云显示窗口,如果没关闭就继续等待
if(viewer.wasStopped()) // 如果关闭了窗口则重新显示下采样之后的点云
{
pcl::visualization::CloudViewer viewer("PointCloud after filtering");
viewer.showCloud(cloud_filtered);
viewer.runOnVisualizationThread (viewerPsycho);
while (!viewer.wasStopped()) {}
}
pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients ());
pcl::PointIndices::Ptr inliers (new pcl::PointIndices ());
pcl::SACSegmentation<pcl::PointXYZI> seg; // 创建分割对象
seg.setOptimizeCoefficients (true); // 设置对估计的模型参数进行优化处理
seg.setModelType (pcl::SACMODEL_PLANE); // 设置分割模型类别
seg.setMethodType (pcl::SAC_RANSAC); // 设置用哪个随机参数估计方法
seg.setMaxIterations (1000); // 设置最大迭代次数
seg.setDistanceThreshold (0.01); // 设置判断是否为模型内点的距离阈值
pcl::ExtractIndices<pcl::PointXYZI> extract; // 设置点云提取对象
int i = 0, nr_points = (int) cloud_filtered->points.size ();
/* 当还有30%原始点云数据时 */
while (cloud_filtered->points.size () > 0.3 * nr_points)
{
/* 从余下的点云中分割最大平面组成部分 */
seg.setInputCloud (cloud_filtered);
seg.segment (*inliers, *coefficients);
if (inliers->indices.size () == 0)
{
std::cerr << "Could not estimate a planar model for the given dataset." << std::endl;
break;
}
/* 分离内层 */
extract.setInputCloud (cloud_filtered); // 设置输入点云
extract.setIndices (inliers); // 设置分割后的内点为需要提取的点集
extract.setNegative (false); // 设置提取内点而不是外点
extract.filter (*cloud_p); // 执行提取,并将结果保存到cloud_p
std::cerr << "PointCloud representing the planar component: " << cloud_p->width * cloud_p->height << " data points." << std::endl;
std::stringstream ss;
ss << "../pcd/filter_after/table_scene_lms400_plane_" << i << ".pcd";
pcl::io::savePCDFile(ss.str(), *cloud_p);
while(!viewer.wasStopped()) {}
if(viewer.wasStopped())
{
pcl::visualization::CloudViewer viewer("PointCloud after filtering");
viewer.showCloud(cloud_p);
viewer.runOnVisualizationThread (viewerPsycho);
while (!viewer.wasStopped()) {}
}
extract.setNegative (true);
extract.filter (*cloud_f);
cloud_filtered.swap (cloud_f);
i++;
}
return (0);
}
注意:上述在分割之前创建了一个VoxelGrid
滤波器对数据进行下采样,主要的原因是为了加速处理过程,因为越少的点意味着分割循环中处理起来越快。
在工作空间根目录extract_indices
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(extract_indices)
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/extract_indices.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录extract_indices
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件extract_indices_node
,运行该可执行文件:
./extract_indices_node
在本小节中,我们将学习在滤波器模块使用ConditionalRemoval
和RadiusOutlierRemoval
方法来移除离群点。其中ConditionalRemoval
滤波器用于删除点云中不符合用户指定的一个或多个条件的数据点,而RadiusOutlierRemoval
滤波器可以删除在输入的点云一定范围内没有达到足够多近邻的所有数据点。
首先创建一个工作空间remove_outliers
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p remove_outliers/src
接着,在remove_outliers/src
路径下,创建一个文件并命名为remove_outliers.cpp
,拷贝如下代码:
#include
#include
#include
#include
int main (int argc, char** argv)
{
if (argc != 2)
{
std::cerr << "please specify command line arg '-r' or '-c'" << std::endl;
exit(0);
}
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);
/* 填入点云数据 */
cloud->width = 10;
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 * rand () / (RAND_MAX + 1.0f);
cloud->points[i].y = 1024 * rand () / (RAND_MAX + 1.0f);
cloud->points[i].z = 1024 * rand () / (RAND_MAX + 1.0f);
}
if (strcmp(argv[1], "-r") == 0)
{
pcl::RadiusOutlierRemoval<pcl::PointXYZ> outrem; // 创建滤波器
outrem.setInputCloud(cloud); // 设置输入点云
outrem.setRadiusSearch(0.8); // 设置在0.8m半径的范围内找邻近点
outrem.setMinNeighborsInRadius (2); // 设置查询点的邻近点集数少于2的删除
outrem.filter (*cloud_filtered); // 执行滤波,并将结果存储到cloud_filtered
}
else if (strcmp(argv[1], "-c") == 0)
{
pcl::ConditionAnd<pcl::PointXYZ>::Ptr range_cond (new pcl::ConditionAnd<pcl::PointXYZ> ()); // 创建条件定义对象
/* 为条件定义对象添加比较算子 */
range_cond->addComparison (pcl::FieldComparison<pcl::PointXYZ>::ConstPtr
(new pcl::FieldComparison<pcl::PointXYZ> ("z", pcl::ComparisonOps::GT, 0.0))); // 添加在z字段上大于0的比较算子
range_cond->addComparison (pcl::FieldComparison<pcl::PointXYZ>::ConstPtr
(new pcl::FieldComparison<pcl::PointXYZ> ("z", pcl::ComparisonOps::LT, 0.8))); // 添加在z字段上小于0.8的比较算子
pcl::ConditionalRemoval<pcl::PointXYZ> condrem; // 创建滤波器
condrem.setCondition(range_cond); // 为滤波器设置条件
condrem.setInputCloud (cloud); // 设置输入点云
condrem.setKeepOrganized(true); // 设置保持点云的结构
condrem.filter (*cloud_filtered); // 执行条件滤波,并将结果存储到cloud_filtered
}
else
{
std::cerr << "please specify command line arg '-r' or '-c'" << std::endl;
exit(0);
}
std::cerr << "Cloud before filtering: " << std::endl;
for (size_t i = 0; i < cloud->points.size (); ++i)
{
std::cerr << " " << cloud->points[i].x << " "
<< cloud->points[i].y << " "
<< cloud->points[i].z << std::endl;
}
std::cerr << "Cloud after filtering: " << std::endl;
for (size_t i = 0; i < cloud_filtered->points.size (); ++i)
{
std::cerr << " " << cloud_filtered->points[i].x << " "
<< cloud_filtered->points[i].y << " "
<< cloud_filtered->points[i].z << std::endl;
}
return (0);
}
在工作空间根目录remove_outliers
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(extract_indices)
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/extract_indices.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录remove_outliers
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件remove_outliers_node
,运行该可执行文件:
./remove_outliers_node -c
./remove_outliers_node -r
当用户希望使用ConditionalRemoval
滤波器,则需要指定命令行参数-c
,此时在终端中将会输出如下结果:
Cloud before filtering:
0.352222 -0.151883 -0.106395
-0.397406 -0.473106 0.292602
-0.731898 0.667105 0.441304
-0.734766 0.854581 -0.0361733
-0.4607 -0.277468 -0.916762
0.183749 0.968809 0.512055
-0.998983 -0.463871 0.691785
0.716053 0.525135 -0.523004
0.439387 0.56706 0.905417
-0.579787 0.898706 -0.504929
Cloud after filtering:
nan nan nan
-0.397406 -0.473106 0.292602
-0.731898 0.667105 0.441304
nan nan nan
nan nan nan
0.183749 0.968809 0.512055
-0.998983 -0.463871 0.691785
nan nan nan
nan nan nan
nan nan nan
注意:在输出结果中可以看到很多nan
数据,这是因为在代码中添加了这一句:
condrem.setKeepOrganized(true); // 设置保持点云的结构
当用户希望使用RadiusOutlierRemoval
滤波器,则需要指定命令行参数-r
,此时在终端中将会输出如下结果:
Cloud before filtering:
0.352222 -0.151883 -0.106395
-0.397406 -0.473106 0.292602
-0.731898 0.667105 0.441304
-0.734766 0.854581 -0.0361733
-0.4607 -0.277468 -0.916762
0.183749 0.968809 0.512055
-0.998983 -0.463871 0.691785
0.716053 0.525135 -0.523004
0.439387 0.56706 0.905417
-0.579787 0.898706 -0.504929
Cloud after filtering:
-0.734766 0.854581 -0.0361733
RadiusOutlierRemoval
滤波器非常适合去除单个的离群点,而ConditionalRemoval
滤波器比较灵活,可以根据用户设置的条件灵活过滤。
本小节将学习如何利用CropHull
滤波器得到2D封闭多边形内部或者外部的点云。
首先创建一个工作空间CropHull
,然后再在工作空间创建一个文件夹src
用于存放源代码:
mkdir -p CropHull/src
接着,在CropHull/src
路径下,创建一个文件并命名为CropHull.cpp
,拷贝如下代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile("../pcd/filter_before/pig.pcd", *cloud); // 从pcd文件中读取点云数据
/* 输入2D平面点云 */
pcl::PointCloud<pcl::PointXYZ>::Ptr boundingbox_ptr (new pcl::PointCloud<pcl::PointXYZ>);
boundingbox_ptr->push_back(pcl::PointXYZ(0.1, 0.1, 0));
boundingbox_ptr->push_back(pcl::PointXYZ(0.1, -0.1,0 ));
boundingbox_ptr->push_back(pcl::PointXYZ(-0.1, 0.1,0 ));
boundingbox_ptr->push_back(pcl::PointXYZ(-0.1, -0.1,0 ));
boundingbox_ptr->push_back(pcl::PointXYZ(0.15, 0.1,0 ));
/* 构造2D凸包 */
pcl::ConvexHull<pcl::PointXYZ> hull; // 创建凸包对象
hull.setInputCloud(boundingbox_ptr); // 设置输入点云
hull.setDimension(2); // 设置凸包维度
std::vector<pcl::Vertices> polygons; // 设置pcl::Vertices类型的向量,用于保存凸包顶点
pcl::PointCloud<pcl::PointXYZ>::Ptr surface_hull (new pcl::PointCloud<pcl::PointXYZ>); // 该点云用于描述凸包形状
hull.reconstruct(*surface_hull, polygons); // 计算2D凸包结果
pcl::PointCloud<pcl::PointXYZ>::Ptr objects (new pcl::PointCloud<pcl::PointXYZ>);
pcl::CropHull<pcl::PointXYZ> bb_filter; // 创建CropHull对象
bb_filter.setDim(2); // 设置维度,应与输入的凸包维度相一致
bb_filter.setInputCloud(cloud); // 设置需要滤波的点云
bb_filter.setHullIndices(polygons); // 输入封闭多边形的顶点
bb_filter.setHullCloud(surface_hull); // 输入封闭多边形的形状
bb_filter.filter(*objects); // 执行滤波,并将结果保存到objects
std::cout << objects->size() << std::endl;
//visualize
boost::shared_ptr<pcl::visualization::PCLVisualizer> for_visualizer_v (new pcl::visualization::PCLVisualizer ("crophull display"));
for_visualizer_v->setBackgroundColor(255,255,255);
int v1(0);
for_visualizer_v->createViewPort (0.0, 0.0, 0.33, 1, v1);
for_visualizer_v->setBackgroundColor (255, 255, 255, v1);
for_visualizer_v->addPointCloud (cloud,"cloud",v1);
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR,255,0,0,"cloud");
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,3,"cloud");
for_visualizer_v->addPolygon<pcl::PointXYZ>(surface_hull,0,.069*255,0.2*255,"backview_hull_polyline1",v1);
int v2(0);
for_visualizer_v->createViewPort (0.33, 0.0, 0.66, 1, v2);
for_visualizer_v->setBackgroundColor (255, 255, 255, v2);
for_visualizer_v->addPointCloud (surface_hull,"surface_hull",v2);
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR,255,0,0,"surface_hull");
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,8,"surface_hull");
for_visualizer_v->addPolygon<pcl::PointXYZ>(surface_hull,0,.069*255,0.2*255,"backview_hull_polyline",v2);
int v3(0);
for_visualizer_v->createViewPort (0.66, 0.0, 1, 1, v3);
for_visualizer_v->setBackgroundColor (255, 255, 255, v3);
for_visualizer_v->addPointCloud (objects,"objects",v3);
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR,255,0,0,"objects");
for_visualizer_v->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,3,"objects");
while (!for_visualizer_v->wasStopped())
{
for_visualizer_v->spinOnce(1000);
}
system("pause");
}
在上述代码中,首先从pcd文件中读取待滤波对象的点云,为了构造2D封闭多边形,需要输入2D平面点云,这些平面点是2D封闭多边形的顶点,然后对输入的2D平面点构造2D凸包,凸包的构造需要使用ConvexHull
类,接下来创建CropHull
对象,滤波得到2D封闭凸包范围内的点云,此处需要注意的是CropHull
类的setDim
函数设置的维度应与输入的凸包维度相一致,最后是可视化部分。
在工作空间根目录CropHull
下,编写CMakeLists.txt
文件如下:
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(crophull)
find_package(PCL 1.7 REQUIRED)
include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})
add_executable (${PROJECT_NAME}_node src/crophull.cpp)
target_link_libraries (${PROJECT_NAME}_node ${PCL_LIBRARIES})
在工作空间根目录CropHull
下创建一个build
文件夹,用于存放编译过程中产生的文件,然后执行编译:
mkdir build
cd build
cmake ..
make
此时,会在build
文件夹下生成一个可执行文件crophull_node
,运行该可执行文件:
./crophull_node
可以看到弹出三个视口显示的分别是原始输入点云、封闭2D多边形凸包结果和CropHull滤波结果: