在实际实现3D目标检测时,在不依靠深度学习的训练模型时,仅采用传统方法实现目标检测。而在聚类领域里面,多采用欧几里得、区域生长、Ransac等方法实现聚类。其实还有一种方法可实现聚类,也就是CloudCompare软件内的标签连通域聚类方法。这种方法的好处是耗时较短,也同样能够实现较高精度的距离聚类。
本文根据其特性做出相对应的修改,使其能够脱离CC软件插件,用C++程序以及PCL点云库实现。
标签连通域算法起源来自于图像聚类,其聚类思路简单易懂。
阐述完图像的标签连通域算法的基本流程后,我们需要进一步了解在空间点云上是如何实现标签连通域算法的。
CloudCompare上的连通域算法思路:
其实整体看下来,距离判定处理的方式与欧几里得聚类方法相同,不同的是一个采用 K n n \ Knn Knn搜索,一个为半径搜索。那么相信在讲解该算法的运行流程之后,大家会理解基于八叉树的标签连通域聚类原理。然后只需要一步步实现其代码即可。
首先头文件。
#pragma once
#ifndef OCTREE_CONNECTION
#define OCTREE_CONNECTION
#define PCL_NO_PRECOMPILE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include //文件输入输出
using namespace pcl;
using namespace std;
/*
*@brief 输入需要聚类的点云,参数;输出聚类完成带标签的点云
*/
class OTC
{
private:
typedef pcl::PointXYZ VPoint; ///< 定义点云类型
typedef std::vector<VPoint,Eigen::aligned_allocator<VPoint>> AlignedPointTVector;
/*
* @brief 计算点云至原点的距离
* @param points 输入三维点
* @return double 返回距离长度
*/
double get_points2dis(VPoint points);
float octree_leaf_size = 0.3f; ///< 八叉树深度参数
int max_points_size = 20000; ///< 聚类最大点云数
int min_points_size = 2; ///< 聚类最小点云数
int sourch_k_num = 10; ///< 聚类K近邻搜索数
double dis_th = 0.1; ///< 聚类放大比例系数
public:
/*
* @brief 八叉树结构去噪算法
* @param cloud 输入需要去噪点云
* @param filter_cloud 输出去噪后的点云
* @return bool 返回是否成功去噪
*/
bool octree_denoise( const pcl::PointCloud<VPoint>::Ptr cloud,
pcl::PointCloud<VPoint> & filter_cloud);
/*
* @brief 设定八叉树最小深度值
* @param size 输入定义深度值
*/
void set_octree_leafsize(const float size){
octree_leaf_size = size;
};
/*
* @brief 设定聚类最小值
* @param min_size 输入定义聚类最小值
*/
void set_min_points_size(const int min_size){
min_points_size = min_size;
};
/*
* @brief 设定聚类比例系数
* @param distance_th 输入定义聚类比例系数
*/
void set_distance_th(const double distance_th){
dis_th = distance_th;
};
/*
* @brief 欧几里得聚类算法(采用KNN搜索)
* @param cloud 输入需要聚类的点云
* @param tree 输入搜索方式,默认为KDtree结构
* @param tolerance 输入八叉树搜索间隔
* @param clusters 输出聚类后的类别集合
* @param min_pts_per_cluster 输入聚类点云的最小值
* @param max_pts_per_cluster 输入聚类点云的最大值
*/
void euclidean_clusters(const PointCloud<VPoint> cloud,
const typename search::Search<VPoint>::Ptr &tree,
double tolerance, std::vector<PointIndices> &clusters,
unsigned int min_pts_per_cluster,
unsigned int max_pts_per_cluster);
/*
* @brief 八叉树连通域聚类算法
* @param input_cloud 输入需要聚类的点云
* @param octree_connect_cloud 输出聚类后的点云集合
*/
void octree_connection(const PointCloud<VPoint>::Ptr input_cloud,
std::vector<pcl::PointCloud<VPoint>>& octree_connect_cloud);
OTC(){};
~OTC(){};
};
#endif
后续对应的cpp文件,参考欧几里得的聚类源代码实现。
void OTC::euclidean_clusters(const PointCloud<VPoint> cloud,
const typename search::Search<VPoint>::Ptr & tree,
double tolerance,
std::vector<PointIndices> & clusters,
unsigned int min_pts_per_cluster,
unsigned int max_pts_per_cluster){
if (tree->getInputCloud ()->points.size () != cloud.points.size ()){
PCL_ERROR ("[pcl::extracteuclidean_clusters] Tree built for a different point cloud dataset (%lu) than the input cloud (%lu)!\n", tree->getInputCloud ()->points.size (), cloud.points.size ());
return;
}
int nn_start_idx = tree->getSortedResults () ? 1 : 0;
std::vector<bool> processed (cloud.points.size (), false);
std::vector<int> nn_indices;
std::vector<float> nn_distances;
//遍历点云中的每一个点
for (int i = 0; i < static_cast<int> (cloud.points.size ()); ++i) {
if (processed[i]) ///< 如果该点已经处理则跳过
continue;
std::vector<int> seed_queue; ///< 定义一个种子队列
int sq_idx = 0;
seed_queue.push_back(i); ///< 加入一个种子
processed[i] = true;
while (sq_idx < static_cast<int> (seed_queue.size())) {
/*采用KNN搜索方式进行相邻点判定*/
if (!tree->nearestKSearch(seed_queue[sq_idx], sourch_k_num, nn_indices, nn_distances)) {
sq_idx++;
continue; ///< 没找到近邻点就继续
}
double dis = get_points2dis(cloud.points[seed_queue[sq_idx]]);
for (size_t j = nn_start_idx; j < nn_indices.size (); ++j){
if (nn_indices[j] == -1 || processed[nn_indices[j]])
continue; ///< 种子点的近邻点中如果已经处理就跳出此次循环继续
if(nn_distances[j]>max(dis_th * tolerance * dis ,tolerance))
continue;
seed_queue.push_back (nn_indices[j]); ///< 将此种子点的临近点作为新的种子点。入队操作
processed[nn_indices[j]] = true; ///< 该点已经处理,打标签
}
sq_idx++;
}
/*最大点数和最小点数的类过滤*/
if (seed_queue.size () >= min_pts_per_cluster && seed_queue.size () <= max_pts_per_cluster){
pcl::PointIndices r;
r.indices.resize (seed_queue.size ());
for (size_t j = 0; j < seed_queue.size (); ++j)
r.indices[j] = seed_queue[j];
std::sort (r.indices.begin (), r.indices.end ());
r.indices.erase (std::unique (r.indices.begin (), r.indices.end ()), r.indices.end ());
r.header = cloud.header;
clusters.push_back (r);
}
}
}
进一步分析连通域聚类算法,通过八叉树建立点云数据,采用距离聚类,并最后恢复点云标签,从而完成整个连通域聚类算法。
void OTC::octree_connection(const pcl::PointCloud<VPoint>::Ptr input_cloud,
std::vector<pcl::PointCloud<VPoint>>& octree_connect_cloud){
AlignedPointTVector voxel_center_list_arg;
voxel_center_list_arg.clear();
octree_connect_cloud.clear();
pcl::octree::OctreePointCloudSearch<VPoint> octree(octree_leaf_size);
octree.setInputCloud(input_cloud);
octree.addPointsFromInputCloud();
octree.getOccupiedVoxelCenters(voxel_center_list_arg);
// 等同于 occupiedVoxelCenters
pcl::PointCloud<VPoint> v_cloud,euc_cloud;
v_cloud.resize(voxel_center_list_arg.size());
for (size_t i = 0; i < voxel_center_list_arg.size(); i++){
v_cloud[i].x = voxel_center_list_arg[i].x;
v_cloud[i].y = voxel_center_list_arg[i].y;
v_cloud[i].z = voxel_center_list_arg[i].z;
}
//欧几里得聚类
float dis_th = pow(pow((octree_leaf_size * 2 ),2) + pow((octree_leaf_size * 2 ),2),0.5)+0.001; ///< 计算聚类深度阈值
pcl::search::KdTree<VPoint>::Ptr tree(new pcl::search::KdTree<VPoint>);
std::vector<pcl::PointIndices>ece_inlier;
tree->setInputCloud(v_cloud.makeShared());
euclidean_clusters(v_cloud,tree,dis_th, ece_inlier,1,max_points_size); ///< 聚类
pcl::visualization::PCLVisualizer viewer("cloud viewer");
viewer.setBackgroundColor(0, 0, 0);
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "cloud viewer");
int num = 0;
for (int i = 0; i < ece_inlier.size();i++){
/*聚类完成后,需要重新找到八叉树内部所有点(体素搜索)*/
std::vector<int> ece_inlier_ext = ece_inlier[i].indices; ///< 输入所聚类到的体素中心点
pcl::PointCloud<VPoint> voxel_cloud,cloud_copy;
pcl::copyPointCloud(v_cloud, ece_inlier_ext, cloud_copy); ///< 按照索引提取点云数据
for (int j =0 ; j<cloud_copy.points.size();j++){
std::vector<int> pointIdxVec; ///< 保存体素近邻搜索的结果向量
if (octree.voxelSearch(cloud_copy.points[j], pointIdxVec)){
for (size_t k = 0; k < pointIdxVec.size(); ++k){
voxel_cloud.push_back(input_cloud->points[pointIdxVec[k]]);
}
}
}
if(voxel_cloud.points.size()>min_points_size){
uint8_t R = rand() % (256) + 0;
uint8_t G = rand() % (256) + 0;
uint8_t B = rand() % (256) + 0;
pcl::visualization::PointCloudColorHandlerCustom<VPoint> ramdonColor(input_cloud, R, G, B);
viewer.addPointCloud(voxel_cloud.makeShared(), ramdonColor, std::to_string(num));
num++;
octree_connect_cloud.push_back(voxel_cloud);
}
}
while(!viewer.wasStopped())//要想让自己所创窗口一直显示
{
viewer.spinOnce();
};
}
测试结合地面分割算法实现,在地面分割的基础上完成连通域聚类可以有效防止错误聚类的情况发生。地面分割算法可以参考上一篇博客 [自动驾驶-目标检测] C++ PCL 地面点云分割。
优点:
缺点:
[1] opencv connectedComponents函数
[2] Vo A V , Truong-Hong L , Laefer D F , et al. Octree-based region growing for point cloud segmentation[J]. ISPRS Journal of Photogrammetry and Remote Sensing, 2015, 104:88-100.
[3] Leon_Chan0 OpenCV学习(二十一) :计算图像连通分量
…