Euclidean Cluster Extraction

In this tutorial we will learn how to extract Euclidean clusters with the pcl::EuclideanClusterExtraction class. In order to not complicate the tutorial, certain elements of it such as the plane segmentation algorithm, will not be explained here. Please check the Plane model segmentation tutorial for more information.

Theoretical Primer

A clustering method needs to divide an unorganized point cloud model P into smaller parts so that the overall processing time for P is significantly reduced. A simple data clustering approach in an Euclidean sense can be implemented by making use of a 3D grid subdivision of the space using fixed-width boxes, or more generally, an octree data structure. This particular representation is very fast to build and is useful for situations where either a volumetric representation of the occupied space is needed, or the data in each resultant 3D box (or octree leaf) can be approximated with a different structure. In a more general sense however, we can make use of nearest neighbors and implement a clustering technique that is essentially similar to a flood fill algorithm.

Let’s assume we are given a point cloud with a table and objects on top of it. We want to find and segment the individual object point clusters lying on the plane. Assuming that we use a Kd-tree structure for finding the nearest neighbors, the algorithmic steps for that would be (from [RusuDissertation]):

  1. create a Kd-tree representation for the input point cloud dataset P;

  2. set up an empty list of clusters C, and a queue of the points that need to be checked Q;

  3. then for every point \boldsymbol{p}_i \in P, perform the following steps:

    • add \boldsymbol{p}_i to the current queue Q;

    • for every point \boldsymbol{p}_i \in Q do:

      • search for the set P^k_i of point neighbors of \boldsymbol{p}_i in a sphere with radius r < d_{th};

      • for every neighbor \boldsymbol{p}^k_i \in P^k_i, check if the point has already been processed, and if not add it to Q;

    • when the list of all points in Q has been processed, add Q to the list of clusters C, and reset Q to an empty list

  4. the algorithm terminates when all points \boldsymbol{p}_i \in P have been processed and are now part of the list of point clusters C

The Code

First, download the dataset table_scene_lms400.pcd and save it somewhere to disk.

Then, create a file, let’s say, cluster_extraction.cpp in your favorite editor, and place the following inside it:

  1#include <pcl/ModelCoefficients.h>
  2#include <pcl/point_types.h>
  3#include <pcl/io/pcd_io.h>
  4#include <pcl/filters/extract_indices.h>
  5#include <pcl/filters/voxel_grid.h>
  6#include <pcl/features/normal_3d.h>
  7#include <pcl/search/kdtree.h>
  8#include <pcl/sample_consensus/method_types.h>
  9#include <pcl/sample_consensus/model_types.h>
 10#include <pcl/segmentation/sac_segmentation.h>
 11#include <pcl/segmentation/extract_clusters.h>
 12#include <iomanip> // for setw, setfill
 13
 14int 
 15main ()
 16{
 17  // Read in the cloud data
 18  pcl::PCDReader reader;
 19  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>), cloud_f (new pcl::PointCloud<pcl::PointXYZ>);
 20  reader.read ("table_scene_lms400.pcd", *cloud);
 21  std::cout << "PointCloud before filtering has: " << cloud->size () << " data points." << std::endl; //*
 22
 23  // Create the filtering object: downsample the dataset using a leaf size of 1cm
 24  pcl::VoxelGrid<pcl::PointXYZ> vg;
 25  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);
 26  vg.setInputCloud (cloud);
 27  vg.setLeafSize (0.01f, 0.01f, 0.01f);
 28  vg.filter (*cloud_filtered);
 29  std::cout << "PointCloud after filtering has: " << cloud_filtered->size ()  << " data points." << std::endl; //*
 30
 31  // Create the segmentation object for the planar model and set all the parameters
 32  pcl::SACSegmentation<pcl::PointXYZ> seg;
 33  pcl::PointIndices::Ptr inliers (new pcl::PointIndices);
 34  pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients);
 35  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_plane (new pcl::PointCloud<pcl::PointXYZ> ());
 36  pcl::PCDWriter writer;
 37  seg.setOptimizeCoefficients (true);
 38  seg.setModelType (pcl::SACMODEL_PLANE);
 39  seg.setMethodType (pcl::SAC_RANSAC);
 40  seg.setMaxIterations (100);
 41  seg.setDistanceThreshold (0.02);
 42
 43  int nr_points = (int) cloud_filtered->size ();
 44  while (cloud_filtered->size () > 0.3 * nr_points)
 45  {
 46    // Segment the largest planar component from the remaining cloud
 47    seg.setInputCloud (cloud_filtered);
 48    seg.segment (*inliers, *coefficients);
 49    if (inliers->indices.size () == 0)
 50    {
 51      std::cout << "Could not estimate a planar model for the given dataset." << std::endl;
 52      break;
 53    }
 54
 55    // Extract the planar inliers from the input cloud
 56    pcl::ExtractIndices<pcl::PointXYZ> extract;
 57    extract.setInputCloud (cloud_filtered);
 58    extract.setIndices (inliers);
 59    extract.setNegative (false);
 60
 61    // Get the points associated with the planar surface
 62    extract.filter (*cloud_plane);
 63    std::cout << "PointCloud representing the planar component: " << cloud_plane->size () << " data points." << std::endl;
 64
 65    // Remove the planar inliers, extract the rest
 66    extract.setNegative (true);
 67    extract.filter (*cloud_f);
 68    *cloud_filtered = *cloud_f;
 69  }
 70
 71  // Creating the KdTree object for the search method of the extraction
 72  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
 73  tree->setInputCloud (cloud_filtered);
 74
 75  std::vector<pcl::PointIndices> cluster_indices;
 76  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
 77  ec.setClusterTolerance (0.02); // 2cm
 78  ec.setMinClusterSize (100);
 79  ec.setMaxClusterSize (25000);
 80  ec.setSearchMethod (tree);
 81  ec.setInputCloud (cloud_filtered);
 82  ec.extract (cluster_indices);
 83
 84  int j = 0;
 85  for (const auto& cluster : cluster_indices)
 86  {
 87    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster (new pcl::PointCloud<pcl::PointXYZ>);
 88    for (const auto& idx : cluster.indices) {
 89      cloud_cluster->push_back((*cloud_filtered)[idx]);
 90    } //*
 91    cloud_cluster->width = cloud_cluster->size ();
 92    cloud_cluster->height = 1;
 93    cloud_cluster->is_dense = true;
 94
 95    std::cout << "PointCloud representing the Cluster: " << cloud_cluster->size () << " data points." << std::endl;
 96    std::stringstream ss;
 97    ss << std::setw(4) << std::setfill('0') << j;
 98    writer.write<pcl::PointXYZ> ("cloud_cluster_" + ss.str () + ".pcd", *cloud_cluster, false); //*
 99    j++;
100  }
101
102  return (0);
103}

The Explanation

Now, let’s break down the code piece by piece, skipping the obvious.

    // Read in the cloud data
    pcl::PCDReader reader;
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>), cloud_f (new pcl::PointCloud<pcl::PointXYZ>);
    reader.read ("table_scene_lms400.pcd", *cloud);
    std::cout << "PointCloud before filtering has: " << cloud->size () << " data points." << std::endl;

              .
              .
              .

    while (cloud_filtered->size () > 0.3 * nr_points)
    {

              .
              .
              .

            // Remove the plane inliers, extract the rest
      extract.setNegative (true);
      extract.filter (*cloud_f);
cloud_filtered = cloud_f;
    }

The code above is already described in other tutorials, so you can read the explanation there (in particular Plane model segmentation and Extracting indices from a PointCloud).

  // Creating the KdTree object for the search method of the extraction
  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
  tree->setInputCloud (cloud_filtered);

There we are creating a KdTree object for the search method of our extraction algorithm.

  std::vector<pcl::PointIndices> cluster_indices;

Here we are creating a vector of PointIndices, which contain the actual index information in a vector<int>. The indices of each detected cluster are saved here - please take note of the fact that cluster_indices is a vector containing one instance of PointIndices for each detected cluster. So cluster_indices[0] contains all indices of the first cluster in our point cloud.

  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
  ec.setClusterTolerance (0.02); // 2cm
  ec.setMinClusterSize (100);
  ec.setMaxClusterSize (25000);
  ec.setSearchMethod (tree);
  ec.setInputCloud (cloud_filtered);
  ec.extract (cluster_indices);

Here we are creating a EuclideanClusterExtraction object with point type PointXYZ since our point cloud is of type PointXYZ. We are also setting the parameters and variables for the extraction. Be careful setting the right value for setClusterTolerance(). If you take a very small value, it can happen that an actual object can be seen as multiple clusters. On the other hand, if you set the value too high, it could happen, that multiple objects are seen as one cluster. So our recommendation is to just test and try out which value suits your dataset.

We impose that the clusters found must have at least setMinClusterSize() points and maximum setMaxClusterSize() points.

Now we extracted the clusters out of our point cloud and saved the indices in cluster_indices. To separate each cluster out of the vector<PointIndices> we have to iterate through cluster_indices, create a new PointCloud for each entry and write all points of the current cluster in the PointCloud.

  int j = 0;
  for (const auto& cluster : cluster_indices)
  {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster (new pcl::PointCloud<pcl::PointXYZ>);
    for (const auto& idx : cluster.indices) {
      cloud_cluster->push_back((*cloud_filtered)[idx]);
    } //*
    cloud_cluster->width = cloud_cluster->size ();
    cloud_cluster->height = 1;
    cloud_cluster->is_dense = true;

Compiling and running the program

Add the following lines to your CMakeLists.txt

 1cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
 2
 3project(cluster_extraction)
 4
 5find_package(PCL 1.2 REQUIRED)
 6
 7include_directories(${PCL_INCLUDE_DIRS})
 8link_directories(${PCL_LIBRARY_DIRS})
 9add_definitions(${PCL_DEFINITIONS})
10
11add_executable (cluster_extraction cluster_extraction.cpp)
12target_link_libraries (cluster_extraction ${PCL_LIBRARIES})

After you have made the executable, you can run it. Simply do:

$ ./cluster_extraction

You will see something similar to:

PointCloud before filtering has: 460400 data points.
PointCloud after filtering has: 41049 data points.
[SACSegmentation::initSACModel] Using a model of type: SACMODEL_PLANE
[SACSegmentation::initSAC] Using a method of type: SAC_RANSAC with a model threshold of 0.020000
[SACSegmentation::initSAC] Setting the maximum number of iterations to 100
PointCloud representing the planar component: 20522 data points.
[SACSegmentation::initSACModel] Using a model of type: SACMODEL_PLANE
[SACSegmentation::initSAC] Using a method of type: SAC_RANSAC with a model threshold of 0.020000
[SACSegmentation::initSAC] Setting the maximum number of iterations to 100
PointCloud representing the planar component: 12429 data points.
PointCloud representing the Cluster: 4883 data points.
PointCloud representing the Cluster: 1386 data points.
PointCloud representing the Cluster: 320 data points.
PointCloud representing the Cluster: 290 data points.
PointCloud representing the Cluster: 120 data points.

You can also look at your outputs cloud_cluster_0000.pcd, cloud_cluster_0001.pcd, cloud_cluster_0002.pcd, cloud_cluster_0003.pcd and cloud_cluster_0004.pcd:

$ ./pcl_viewer cloud_cluster_0000.pcd cloud_cluster_0001.pcd cloud_cluster_0002.pcd cloud_cluster_0003.pcd cloud_cluster_0004.pcd

You are now able to see the different clusters in one viewer. You should see something similar to this:

Output Cluster Extraction