条件欧几里得聚类
本教程介绍如何使用类: 一种分割算法,根据欧几里得距离和需要保持的用户可自定义条件对点进行聚类。pcl::ConditionalEuclideanClustering
此类使用与 Euclidean Cluster Extraction、Region growing segmentation 和 Color-based regional growing segmentation 中使用的相同 greedy-like /region-growing /flood-fill 方法。 与其他类相比,使用此类的优势在于,聚类的约束(纯欧几里得、平滑度、RGB)现在可以由用户自定义。 一些缺点包括:没有初始种子系统,没有过度和不足分段控制,以及从主计算循环内部调用条件函数的时间效率较低。
理论入门
Euclidean Cluster Extraction 和 Region growing segmentation 教程已经非常准确地解释了区域增长算法。 对这些解释的唯一补充是,现在可以完全自定义将邻居合并到当前集群中所需的条件。
随着聚类的增长,它将评估聚类内已有的点与附近的候选点之间的用户定义条件。 候选点(最近邻点)使用围绕聚类中每个点的欧氏半径搜索来查找。 对于生成的聚类中的每个点,条件需要至少与一个相邻要素保持,而不是与其所有相邻要素保持。
Conditional Euclidean Clustering 类还可以根据大小约束自动筛选聚类。 分类为“太小”或“太大”的集群仍可在之后检索。
代码
首先,下载数据集 Statues_4.pcd 并从存档中提取 PCD 文件。 这是一个非常大的室外环境数据集,我们的目标是将单独的对象聚集在一起,并且还希望将建筑物与地平面分开,即使它是以欧几里得意义连接的。
现在,在您最喜欢的编辑器中创建一个文件,并将以下内容放入其中:conditional_euclidean_clustering.cpp
1#include <pcl/point_types.h>
2#include <pcl/io/pcd_io.h>
3#include <pcl/console/time.h>
4
5#include <pcl/filters/voxel_grid.h>
6#include <pcl/features/normal_3d.h>
7#include <pcl/segmentation/conditional_euclidean_clustering.h>
8
9typedef pcl::PointXYZI PointTypeIO;
10typedef pcl::PointXYZINormal PointTypeFull;
11
12bool
13enforceIntensitySimilarity (const PointTypeFull& point_a, const PointTypeFull& point_b, float /*squared_distance*/)
14{
15 if (std::abs (point_a.intensity - point_b.intensity) < 5.0f)
16 return (true);
17 else
18 return (false);
19}
20
21bool
22enforceNormalOrIntensitySimilarity (const PointTypeFull& point_a, const PointTypeFull& point_b, float /*squared_distance*/)
23{
24 Eigen::Map<const Eigen::Vector3f> point_a_normal = point_a.getNormalVector3fMap (), point_b_normal = point_b.getNormalVector3fMap ();
25 if (std::abs (point_a.intensity - point_b.intensity) < 5.0f)
26 return (true);
27 if (std::abs (point_a_normal.dot (point_b_normal)) > std::cos (30.0f / 180.0f * static_cast<float> (M_PI)))
28 return (true);
29 return (false);
30}
31
32bool
33customRegionGrowing (const PointTypeFull& point_a, const PointTypeFull& point_b, float squared_distance)
34{
35 Eigen::Map<const Eigen::Vector3f> point_a_normal = point_a.getNormalVector3fMap (), point_b_normal = point_b.getNormalVector3fMap ();
36 if (squared_distance < 10000)
37 {
38 if (std::abs (point_a.intensity - point_b.intensity) < 8.0f)
39 return (true);
40 if (std::abs (point_a_normal.dot (point_b_normal)) > std::cos (30.0f / 180.0f * static_cast<float> (M_PI)))
41 return (true);
42 }
43 else
44 {
45 if (std::abs (point_a.intensity - point_b.intensity) < 3.0f)
46 return (true);
47 }
48 return (false);
49}
50
51int
52main ()
53{
54 // Data containers used
55 pcl::PointCloud<PointTypeIO>::Ptr cloud_in (new pcl::PointCloud<PointTypeIO>), cloud_out (new pcl::PointCloud<PointTypeIO>);
56 pcl::PointCloud<PointTypeFull>::Ptr cloud_with_normals (new pcl::PointCloud<PointTypeFull>);
57 pcl::IndicesClustersPtr clusters (new pcl::IndicesClusters), small_clusters (new pcl::IndicesClusters), large_clusters (new pcl::IndicesClusters);
58 pcl::search::KdTree<PointTypeIO>::Ptr search_tree (new pcl::search::KdTree<PointTypeIO>);
59 pcl::console::TicToc tt;
60
61 // Load the input point cloud
62 std::cerr << "Loading...\n", tt.tic ();
63 pcl::io::loadPCDFile ("Statues_4.pcd", *cloud_in);
64 std::cerr << ">> Done: " << tt.toc () << " ms, " << cloud_in->size () << " points\n";
65
66 // Downsample the cloud using a Voxel Grid class
67 std::cerr << "Downsampling...\n", tt.tic ();
68 pcl::VoxelGrid<PointTypeIO> vg;
69 vg.setInputCloud (cloud_in);
70 vg.setLeafSize (80.0, 80.0, 80.0);
71 vg.setDownsampleAllData (true);
72 vg.filter (*cloud_out);
73 std::cerr << ">> Done: " << tt.toc () << " ms, " << cloud_out->size () << " points\n";
74
75 // Set up a Normal Estimation class and merge data in cloud_with_normals
76 std::cerr << "Computing normals...\n", tt.tic ();
77 pcl::copyPointCloud (*cloud_out, *cloud_with_normals);
78 pcl::NormalEstimation<PointTypeIO, PointTypeFull> ne;
79 ne.setInputCloud (cloud_out);
80 ne.setSearchMethod (search_tree);
81 ne.setRadiusSearch (300.0);
82 ne.compute (*cloud_with_normals);
83 std::cerr << ">> Done: " << tt.toc () << " ms\n";
84
85 // Set up a Conditional Euclidean Clustering class
86 std::cerr << "Segmenting to clusters...\n", tt.tic ();
87 pcl::ConditionalEuclideanClustering<PointTypeFull> cec (true);
88 cec.setInputCloud (cloud_with_normals);
89 cec.setConditionFunction (&customRegionGrowing);
90 cec.setClusterTolerance (500.0);
91 cec.setMinClusterSize (cloud_with_normals->size () / 1000);
92 cec.setMaxClusterSize (cloud_with_normals->size () / 5);
93 cec.segment (*clusters);
94 cec.getRemovedClusters (small_clusters, large_clusters);
95 std::cerr << ">> Done: " << tt.toc () << " ms\n";
96
97 // Using the intensity channel for lazy visualization of the output
98 for (const auto& small_cluster : (*small_clusters))
99 for (const auto& j : small_cluster.indices)
100 (*cloud_out)[j].intensity = -2.0;
101 for (const auto& large_cluster : (*large_clusters))
102 for (const auto& j : large_cluster.indices)
103 (*cloud_out)[j].intensity = +10.0;
104 for (const auto& cluster : (*clusters))
105 {
106 int label = rand () % 8;
107 for (const auto& j : cluster.indices)
108 (*cloud_out)[j].intensity = label;
109 }
110
111 // Save the output point cloud
112 std::cerr << "Saving...\n", tt.tic ();
113 pcl::io::savePCDFile ("output.pcd", *cloud_out);
114 std::cerr << ">> Done: " << tt.toc () << " ms\n";
115
116 return (0);
117}
解释
由于 Conditional Euclidean Clustering 类适用于更高级的用户,因此我将跳过对代码中更明显的部分的解释:
pcl::io::loadPCDFile
,用于加载和保存点云数据。pcl::io::savePCDFile
pcl::console::TicToc
用于轻松输出计时结果。使用 VoxelGrid 过滤器对 PointCloud 进行下采样(第 66-73 行)来对云进行下采样并提供更均衡的点密度。
在 PointCloud 中估计表面法线(第 75-83 行)用于估计将附加到点信息的法线; 条件欧几里得聚类类将使用 模板化,其中包含要在条件函数中使用的 x、y、z、强度、法线和曲率信息。
pcl::PointXYZINormal
第 85-95 行设置了要使用的 Conditional Euclidean Clustering 类:
// Set up a Conditional Euclidean Clustering class
std::cerr << "Segmenting to clusters...\n", tt.tic ();
pcl::ConditionalEuclideanClustering<PointTypeFull> cec (true);
cec.setInputCloud (cloud_with_normals);
cec.setConditionFunction (&customRegionGrowing);
cec.setClusterTolerance (500.0);
cec.setMinClusterSize (cloud_with_normals->size () / 1000);
cec.setMaxClusterSize (cloud_with_normals->size () / 5);
cec.segment (*clusters);
cec.getRemovedClusters (small_clusters, large_clusters);
std::cerr << ">> Done: " << tt.toc () << " ms\n";
对不同代码行的更详细描述:
该类使用 TRUE 进行初始化。 这将允许提取太小或太大的集群。 如果在没有 this 的情况下初始化类,它可以节省一些计算时间和内存。
可以使用从类派生的方法指定类的输入数据,即: 和 .
PCLBase
setInputCloud
setIndices
随着集群的增长,它将评估集群内已有的点与附近的候选点之间的用户定义条件。 有关 condition 函数的更多信息可以在下面进一步阅读。
拓扑容差是 k-NN 搜索的半径,用于查找候选点。
占云总点数 0.1% 的集群被认为太小。
占云总点数 20% 以上的集群被视为太大。
生成的集群以索引数组的格式存储,该格式是索引数组,即输入点云的索引点。
pcl::IndicesClusters
太小或太大的集群不会传递到主输出,而是可以在单独的数据容器中检索,但前提是类是使用 TRUE 初始化的。
pcl::IndicesClusters
第 12-49 行显示了条件函数的一些示例:
bool
enforceIntensitySimilarity (const PointTypeFull& point_a, const PointTypeFull& point_b, float /*squared_distance*/)
{
if (std::abs (point_a.intensity - point_b.intensity) < 5.0f)
return (true);
else
return (false);
}
bool
enforceNormalOrIntensitySimilarity (const PointTypeFull& point_a, const PointTypeFull& point_b, float /*squared_distance*/)
{
Eigen::Map<const Eigen::Vector3f> point_a_normal = point_a.getNormalVector3fMap (), point_b_normal = point_b.getNormalVector3fMap ();
if (std::abs (point_a.intensity - point_b.intensity) < 5.0f)
return (true);
if (std::abs (point_a_normal.dot (point_b_normal)) > std::cos (30.0f / 180.0f * static_cast<float> (M_PI)))
return (true);
return (false);
}
bool
customRegionGrowing (const PointTypeFull& point_a, const PointTypeFull& point_b, float squared_distance)
{
Eigen::Map<const Eigen::Vector3f> point_a_normal = point_a.getNormalVector3fMap (), point_b_normal = point_b.getNormalVector3fMap ();
if (squared_distance < 10000)
{
if (std::abs (point_a.intensity - point_b.intensity) < 8.0f)
return (true);
if (std::abs (point_a_normal.dot (point_b_normal)) > std::cos (30.0f / 180.0f * static_cast<float> (M_PI)))
return (true);
}
else
{
if (std::abs (point_a.intensity - point_b.intensity) < 3.0f)
return (true);
}
return (false);
}
条件函数的格式是固定的:
前两个输入参数的类型必须与 Conditional Euclidean Clustering 类中使用的模板类型相同。 这些参数将传递当前种子点(第一个参数)和当前候选点(第二个参数)的点信息。
第三个 input 参数需要是 float。 此参数将传递种子点和候选点之间的平方距离。 虽然这个信息也可以使用前两个参数进行计算,但它已经由底层的最近邻搜索提供,并且可以很容易地用于制作一个与距离相关的条件函数。
output 参数需要是布尔值。 返回 TRUE 会将候选点合并到种子点的集群中。 返回 FALSE 不会通过此特定点对合并候选点,但是,这两个点仍有可能通过不同的点对关系最终位于同一集群中。
这些示例条件函数只是为了指示如何使用它们。 例如,只要集群在表面法线方向上相似或强度值相似,第二个条件函数就会增加集群。 这应该希望将纹理相似的建筑物群集为一个群集,但不会将它们与相邻对象合并到同一群集中。 这将计算出强度是否与附近的对象足够不同,并且附近的对象没有共享具有相同法线的附近表面。 第三个条件函数与第二个条件函数类似,但根据点之间的距离具有不同的约束。
第 97-109 行包含一段代码,该代码是一个快速而肮脏的修复,用于可视化结果:
// Using the intensity channel for lazy visualization of the output
for (const auto& small_cluster : (*small_clusters))
for (const auto& j : small_cluster.indices)
(*cloud_out)[j].intensity = -2.0;
for (const auto& large_cluster : (*large_clusters))
for (const auto& j : large_cluster.indices)
(*cloud_out)[j].intensity = +10.0;
for (const auto& cluster : (*clusters))
{
int label = rand () % 8;
for (const auto& j : cluster.indices)
(*cloud_out)[j].intensity = label;
}
当使用 PCL 的标准 PCD 查看器打开输出点云时,按“5”将切换到强度通道可视化。 太小的群集将变为红色,太大的群集将变为蓝色,实际感兴趣的群集/对象将在黄色和青色之间随机着色。
编译和运行程序
将以下行添加到您的CMakeLists.txt
1cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
2
3project(conditional_euclidean_clustering)
4
5find_package(PCL 1.7 REQUIRED)
6
7include_directories(${PCL_INCLUDE_DIRS})
8link_directories(${PCL_LIBRARY_DIRS})
9add_definitions(${PCL_DEFINITIONS})
10
11add_executable (conditional_euclidean_clustering conditional_euclidean_clustering.cpp)
12target_link_libraries (conditional_euclidean_clustering ${PCL_LIBRARIES})
创建可执行文件后,您可以运行它。只需:
$ ./conditional_euclidean_clustering
生成的输出点云可以按如下方式打开:
$ ./pcl_viewer output.pcd
您应该会看到类似于以下内容的内容: