3D Object Recognition based on Correspondence Grouping
This tutorial aims at explaining how to perform 3D Object Recognition based on the pcl_recognition module. Specifically, it explains how to use Correspondence Grouping algorithms in order to cluster the set of point-to-point correspondences obtained after the 3D descriptor matching stage into model instances that are present in the current scene. For each cluster, representing a possible model instance in the scene, the Correspondence Grouping algorithms also output the transformation matrix identifying the 6DOF pose estimation of that model in the current scene.
The code
Before you begin, you should download the PCD dataset used in this tutorial from GitHub (milk.pcd and milk_cartoon_all_small_clorox.pcd) and put the files in a folder of your convenience.
Also, copy and paste the following code into your editor and save it as correspondence_grouping.cpp
(or download the source file here
).
1#include <pcl/io/pcd_io.h>
2#include <pcl/point_cloud.h>
3#include <pcl/correspondence.h>
4#include <pcl/features/normal_3d_omp.h>
5#include <pcl/features/shot_omp.h>
6#include <pcl/features/board.h>
7#include <pcl/filters/uniform_sampling.h>
8#include <pcl/recognition/cg/hough_3d.h>
9#include <pcl/recognition/cg/geometric_consistency.h>
10#include <pcl/visualization/pcl_visualizer.h>
11#include <pcl/kdtree/kdtree_flann.h>
12#include <pcl/kdtree/impl/kdtree_flann.hpp>
13#include <pcl/common/transforms.h>
14#include <pcl/console/parse.h>
15
16typedef pcl::PointXYZRGBA PointType;
17typedef pcl::Normal NormalType;
18typedef pcl::ReferenceFrame RFType;
19typedef pcl::SHOT352 DescriptorType;
20
21std::string model_filename_;
22std::string scene_filename_;
23
24//Algorithm params
25bool show_keypoints_ (false);
26bool show_correspondences_ (false);
27bool use_cloud_resolution_ (false);
28bool use_hough_ (true);
29float model_ss_ (0.01f);
30float scene_ss_ (0.03f);
31float rf_rad_ (0.015f);
32float descr_rad_ (0.02f);
33float cg_size_ (0.01f);
34float cg_thresh_ (5.0f);
35
36void
37showHelp (char *filename)
38{
39 std::cout << std::endl;
40 std::cout << "***************************************************************************" << std::endl;
41 std::cout << "* *" << std::endl;
42 std::cout << "* Correspondence Grouping Tutorial - Usage Guide *" << std::endl;
43 std::cout << "* *" << std::endl;
44 std::cout << "***************************************************************************" << std::endl << std::endl;
45 std::cout << "Usage: " << filename << " model_filename.pcd scene_filename.pcd [Options]" << std::endl << std::endl;
46 std::cout << "Options:" << std::endl;
47 std::cout << " -h: Show this help." << std::endl;
48 std::cout << " -k: Show used keypoints." << std::endl;
49 std::cout << " -c: Show used correspondences." << std::endl;
50 std::cout << " -r: Compute the model cloud resolution and multiply" << std::endl;
51 std::cout << " each radius given by that value." << std::endl;
52 std::cout << " --algorithm (Hough|GC): Clustering algorithm used (default Hough)." << std::endl;
53 std::cout << " --model_ss val: Model uniform sampling radius (default 0.01)" << std::endl;
54 std::cout << " --scene_ss val: Scene uniform sampling radius (default 0.03)" << std::endl;
55 std::cout << " --rf_rad val: Reference frame radius (default 0.015)" << std::endl;
56 std::cout << " --descr_rad val: Descriptor radius (default 0.02)" << std::endl;
57 std::cout << " --cg_size val: Cluster size (default 0.01)" << std::endl;
58 std::cout << " --cg_thresh val: Clustering threshold (default 5)" << std::endl << std::endl;
59}
60
61void
62parseCommandLine (int argc, char *argv[])
63{
64 //Show help
65 if (pcl::console::find_switch (argc, argv, "-h"))
66 {
67 showHelp (argv[0]);
68 exit (0);
69 }
70
71 //Model & scene filenames
72 std::vector<int> filenames;
73 filenames = pcl::console::parse_file_extension_argument (argc, argv, ".pcd");
74 if (filenames.size () != 2)
75 {
76 std::cout << "Filenames missing.\n";
77 showHelp (argv[0]);
78 exit (-1);
79 }
80
81 model_filename_ = argv[filenames[0]];
82 scene_filename_ = argv[filenames[1]];
83
84 //Program behavior
85 if (pcl::console::find_switch (argc, argv, "-k"))
86 {
87 show_keypoints_ = true;
88 }
89 if (pcl::console::find_switch (argc, argv, "-c"))
90 {
91 show_correspondences_ = true;
92 }
93 if (pcl::console::find_switch (argc, argv, "-r"))
94 {
95 use_cloud_resolution_ = true;
96 }
97
98 std::string used_algorithm;
99 if (pcl::console::parse_argument (argc, argv, "--algorithm", used_algorithm) != -1)
100 {
101 if (used_algorithm.compare ("Hough") == 0)
102 {
103 use_hough_ = true;
104 }else if (used_algorithm.compare ("GC") == 0)
105 {
106 use_hough_ = false;
107 }
108 else
109 {
110 std::cout << "Wrong algorithm name.\n";
111 showHelp (argv[0]);
112 exit (-1);
113 }
114 }
115
116 //General parameters
117 pcl::console::parse_argument (argc, argv, "--model_ss", model_ss_);
118 pcl::console::parse_argument (argc, argv, "--scene_ss", scene_ss_);
119 pcl::console::parse_argument (argc, argv, "--rf_rad", rf_rad_);
120 pcl::console::parse_argument (argc, argv, "--descr_rad", descr_rad_);
121 pcl::console::parse_argument (argc, argv, "--cg_size", cg_size_);
122 pcl::console::parse_argument (argc, argv, "--cg_thresh", cg_thresh_);
123}
124
125double
126computeCloudResolution (const pcl::PointCloud<PointType>::ConstPtr &cloud)
127{
128 double res = 0.0;
129 int n_points = 0;
130 int nres;
131 std::vector<int> indices (2);
132 std::vector<float> sqr_distances (2);
133 pcl::search::KdTree<PointType> tree;
134 tree.setInputCloud (cloud);
135
136 for (std::size_t i = 0; i < cloud->size (); ++i)
137 {
138 if (! std::isfinite ((*cloud)[i].x))
139 {
140 continue;
141 }
142 //Considering the second neighbor since the first is the point itself.
143 nres = tree.nearestKSearch (i, 2, indices, sqr_distances);
144 if (nres == 2)
145 {
146 res += sqrt (sqr_distances[1]);
147 ++n_points;
148 }
149 }
150 if (n_points != 0)
151 {
152 res /= n_points;
153 }
154 return res;
155}
156
157int
158main (int argc, char *argv[])
159{
160 parseCommandLine (argc, argv);
161
162 pcl::PointCloud<PointType>::Ptr model (new pcl::PointCloud<PointType> ());
163 pcl::PointCloud<PointType>::Ptr model_keypoints (new pcl::PointCloud<PointType> ());
164 pcl::PointCloud<PointType>::Ptr scene (new pcl::PointCloud<PointType> ());
165 pcl::PointCloud<PointType>::Ptr scene_keypoints (new pcl::PointCloud<PointType> ());
166 pcl::PointCloud<NormalType>::Ptr model_normals (new pcl::PointCloud<NormalType> ());
167 pcl::PointCloud<NormalType>::Ptr scene_normals (new pcl::PointCloud<NormalType> ());
168 pcl::PointCloud<DescriptorType>::Ptr model_descriptors (new pcl::PointCloud<DescriptorType> ());
169 pcl::PointCloud<DescriptorType>::Ptr scene_descriptors (new pcl::PointCloud<DescriptorType> ());
170
171 //
172 // Load clouds
173 //
174 if (pcl::io::loadPCDFile (model_filename_, *model) < 0)
175 {
176 std::cout << "Error loading model cloud." << std::endl;
177 showHelp (argv[0]);
178 return (-1);
179 }
180 if (pcl::io::loadPCDFile (scene_filename_, *scene) < 0)
181 {
182 std::cout << "Error loading scene cloud." << std::endl;
183 showHelp (argv[0]);
184 return (-1);
185 }
186
187 //
188 // Set up resolution invariance
189 //
190 if (use_cloud_resolution_)
191 {
192 float resolution = static_cast<float> (computeCloudResolution (model));
193 if (resolution != 0.0f)
194 {
195 model_ss_ *= resolution;
196 scene_ss_ *= resolution;
197 rf_rad_ *= resolution;
198 descr_rad_ *= resolution;
199 cg_size_ *= resolution;
200 }
201
202 std::cout << "Model resolution: " << resolution << std::endl;
203 std::cout << "Model sampling size: " << model_ss_ << std::endl;
204 std::cout << "Scene sampling size: " << scene_ss_ << std::endl;
205 std::cout << "LRF support radius: " << rf_rad_ << std::endl;
206 std::cout << "SHOT descriptor radius: " << descr_rad_ << std::endl;
207 std::cout << "Clustering bin size: " << cg_size_ << std::endl << std::endl;
208 }
209
210 //
211 // Compute Normals
212 //
213 pcl::NormalEstimationOMP<PointType, NormalType> norm_est;
214 norm_est.setKSearch (10);
215 norm_est.setInputCloud (model);
216 norm_est.compute (*model_normals);
217
218 norm_est.setInputCloud (scene);
219 norm_est.compute (*scene_normals);
220
221 //
222 // Downsample Clouds to Extract keypoints
223 //
224
225 pcl::UniformSampling<PointType> uniform_sampling;
226 uniform_sampling.setInputCloud (model);
227 uniform_sampling.setRadiusSearch (model_ss_);
228 uniform_sampling.filter (*model_keypoints);
229 std::cout << "Model total points: " << model->size () << "; Selected Keypoints: " << model_keypoints->size () << std::endl;
230
231 uniform_sampling.setInputCloud (scene);
232 uniform_sampling.setRadiusSearch (scene_ss_);
233 uniform_sampling.filter (*scene_keypoints);
234 std::cout << "Scene total points: " << scene->size () << "; Selected Keypoints: " << scene_keypoints->size () << std::endl;
235
236
237 //
238 // Compute Descriptor for keypoints
239 //
240 pcl::SHOTEstimationOMP<PointType, NormalType, DescriptorType> descr_est;
241 descr_est.setRadiusSearch (descr_rad_);
242
243 descr_est.setInputCloud (model_keypoints);
244 descr_est.setInputNormals (model_normals);
245 descr_est.setSearchSurface (model);
246 descr_est.compute (*model_descriptors);
247
248 descr_est.setInputCloud (scene_keypoints);
249 descr_est.setInputNormals (scene_normals);
250 descr_est.setSearchSurface (scene);
251 descr_est.compute (*scene_descriptors);
252
253 //
254 // Find Model-Scene Correspondences with KdTree
255 //
256 pcl::CorrespondencesPtr model_scene_corrs (new pcl::Correspondences ());
257
258 pcl::KdTreeFLANN<DescriptorType> match_search;
259 match_search.setInputCloud (model_descriptors);
260
261 // For each scene keypoint descriptor, find nearest neighbor into the model keypoints descriptor cloud and add it to the correspondences vector.
262 for (std::size_t i = 0; i < scene_descriptors->size (); ++i)
263 {
264 std::vector<int> neigh_indices (1);
265 std::vector<float> neigh_sqr_dists (1);
266 if (!std::isfinite (scene_descriptors->at (i).descriptor[0])) //skipping NaNs
267 {
268 continue;
269 }
270 int found_neighs = match_search.nearestKSearch (scene_descriptors->at (i), 1, neigh_indices, neigh_sqr_dists);
271 if(found_neighs == 1 && neigh_sqr_dists[0] < 0.25f) // add match only if the squared descriptor distance is less than 0.25 (SHOT descriptor distances are between 0 and 1 by design)
272 {
273 pcl::Correspondence corr (neigh_indices[0], static_cast<int> (i), neigh_sqr_dists[0]);
274 model_scene_corrs->push_back (corr);
275 }
276 }
277 std::cout << "Correspondences found: " << model_scene_corrs->size () << std::endl;
278
279 //
280 // Actual Clustering
281 //
282 std::vector<Eigen::Matrix4f, Eigen::aligned_allocator<Eigen::Matrix4f> > rototranslations;
283 std::vector<pcl::Correspondences> clustered_corrs;
284
285 // Using Hough3D
286 if (use_hough_)
287 {
288 //
289 // Compute (Keypoints) Reference Frames only for Hough
290 //
291 pcl::PointCloud<RFType>::Ptr model_rf (new pcl::PointCloud<RFType> ());
292 pcl::PointCloud<RFType>::Ptr scene_rf (new pcl::PointCloud<RFType> ());
293
294 pcl::BOARDLocalReferenceFrameEstimation<PointType, NormalType, RFType> rf_est;
295 rf_est.setFindHoles (true);
296 rf_est.setRadiusSearch (rf_rad_);
297
298 rf_est.setInputCloud (model_keypoints);
299 rf_est.setInputNormals (model_normals);
300 rf_est.setSearchSurface (model);
301 rf_est.compute (*model_rf);
302
303 rf_est.setInputCloud (scene_keypoints);
304 rf_est.setInputNormals (scene_normals);
305 rf_est.setSearchSurface (scene);
306 rf_est.compute (*scene_rf);
307
308 // Clustering
309 pcl::Hough3DGrouping<PointType, PointType, RFType, RFType> clusterer;
310 clusterer.setHoughBinSize (cg_size_);
311 clusterer.setHoughThreshold (cg_thresh_);
312 clusterer.setUseInterpolation (true);
313 clusterer.setUseDistanceWeight (false);
314
315 clusterer.setInputCloud (model_keypoints);
316 clusterer.setInputRf (model_rf);
317 clusterer.setSceneCloud (scene_keypoints);
318 clusterer.setSceneRf (scene_rf);
319 clusterer.setModelSceneCorrespondences (model_scene_corrs);
320
321 //clusterer.cluster (clustered_corrs);
322 clusterer.recognize (rototranslations, clustered_corrs);
323 }
324 else // Using GeometricConsistency
325 {
326 pcl::GeometricConsistencyGrouping<PointType, PointType> gc_clusterer;
327 gc_clusterer.setGCSize (cg_size_);
328 gc_clusterer.setGCThreshold (cg_thresh_);
329
330 gc_clusterer.setInputCloud (model_keypoints);
331 gc_clusterer.setSceneCloud (scene_keypoints);
332 gc_clusterer.setModelSceneCorrespondences (model_scene_corrs);
333
334 //gc_clusterer.cluster (clustered_corrs);
335 gc_clusterer.recognize (rototranslations, clustered_corrs);
336 }
337
338 //
339 // Output results
340 //
341 std::cout << "Model instances found: " << rototranslations.size () << std::endl;
342 for (std::size_t i = 0; i < rototranslations.size (); ++i)
343 {
344 std::cout << "\n Instance " << i + 1 << ":" << std::endl;
345 std::cout << " Correspondences belonging to this instance: " << clustered_corrs[i].size () << std::endl;
346
347 // Print the rotation matrix and translation vector
348 Eigen::Matrix3f rotation = rototranslations[i].block<3,3>(0, 0);
349 Eigen::Vector3f translation = rototranslations[i].block<3,1>(0, 3);
350
351 printf ("\n");
352 printf (" | %6.3f %6.3f %6.3f | \n", rotation (0,0), rotation (0,1), rotation (0,2));
353 printf (" R = | %6.3f %6.3f %6.3f | \n", rotation (1,0), rotation (1,1), rotation (1,2));
354 printf (" | %6.3f %6.3f %6.3f | \n", rotation (2,0), rotation (2,1), rotation (2,2));
355 printf ("\n");
356 printf (" t = < %0.3f, %0.3f, %0.3f >\n", translation (0), translation (1), translation (2));
357 }
358
359 //
360 // Visualization
361 //
362 pcl::visualization::PCLVisualizer viewer ("Correspondence Grouping");
363 viewer.addPointCloud (scene, "scene_cloud");
364
365 pcl::PointCloud<PointType>::Ptr off_scene_model (new pcl::PointCloud<PointType> ());
366 pcl::PointCloud<PointType>::Ptr off_scene_model_keypoints (new pcl::PointCloud<PointType> ());
367
368 if (show_correspondences_ || show_keypoints_)
369 {
370 // We are translating the model so that it doesn't end in the middle of the scene representation
371 pcl::transformPointCloud (*model, *off_scene_model, Eigen::Vector3f (-1,0,0), Eigen::Quaternionf (1, 0, 0, 0));
372 pcl::transformPointCloud (*model_keypoints, *off_scene_model_keypoints, Eigen::Vector3f (-1,0,0), Eigen::Quaternionf (1, 0, 0, 0));
373
374 pcl::visualization::PointCloudColorHandlerCustom<PointType> off_scene_model_color_handler (off_scene_model, 255, 255, 128);
375 viewer.addPointCloud (off_scene_model, off_scene_model_color_handler, "off_scene_model");
376 }
377
378 if (show_keypoints_)
379 {
380 pcl::visualization::PointCloudColorHandlerCustom<PointType> scene_keypoints_color_handler (scene_keypoints, 0, 0, 255);
381 viewer.addPointCloud (scene_keypoints, scene_keypoints_color_handler, "scene_keypoints");
382 viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "scene_keypoints");
383
384 pcl::visualization::PointCloudColorHandlerCustom<PointType> off_scene_model_keypoints_color_handler (off_scene_model_keypoints, 0, 0, 255);
385 viewer.addPointCloud (off_scene_model_keypoints, off_scene_model_keypoints_color_handler, "off_scene_model_keypoints");
386 viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "off_scene_model_keypoints");
387 }
388
389 for (std::size_t i = 0; i < rototranslations.size (); ++i)
390 {
391 pcl::PointCloud<PointType>::Ptr rotated_model (new pcl::PointCloud<PointType> ());
392 pcl::transformPointCloud (*model, *rotated_model, rototranslations[i]);
393
394 std::stringstream ss_cloud;
395 ss_cloud << "instance" << i;
396
397 pcl::visualization::PointCloudColorHandlerCustom<PointType> rotated_model_color_handler (rotated_model, 255, 0, 0);
398 viewer.addPointCloud (rotated_model, rotated_model_color_handler, ss_cloud.str ());
399
400 if (show_correspondences_)
401 {
402 for (std::size_t j = 0; j < clustered_corrs[i].size (); ++j)
403 {
404 std::stringstream ss_line;
405 ss_line << "correspondence_line" << i << "_" << j;
406 PointType& model_point = off_scene_model_keypoints->at (clustered_corrs[i][j].index_query);
407 PointType& scene_point = scene_keypoints->at (clustered_corrs[i][j].index_match);
408
409 // We are drawing a line for each pair of clustered correspondences found between the model and the scene
410 viewer.addLine<PointType, PointType> (model_point, scene_point, 0, 255, 0, ss_line.str ());
411 }
412 }
413 }
414
415 while (!viewer.wasStopped ())
416 {
417 viewer.spinOnce ();
418 }
419
420 return (0);
421}
Walkthrough
Now let’s take a look at the various parts of the code to see how it works.
Helper Functions
Let’s start with a couple of useful functions: the first one prints on the console a short explanation of the several command line switches that the program can accept.
void
showHelp (char *filename)
{
std::cout << std::endl;
std::cout << "***************************************************************************" << std::endl;
std::cout << "* *" << std::endl;
std::cout << "* Correspondence Grouping Tutorial - Usage Guide *" << std::endl;
std::cout << "* *" << std::endl;
std::cout << "***************************************************************************" << std::endl << std::endl;
std::cout << "Usage: " << filename << " model_filename.pcd scene_filename.pcd [Options]" << std::endl << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " -h: Show this help." << std::endl;
std::cout << " -k: Show used keypoints." << std::endl;
std::cout << " -c: Show used correspondences." << std::endl;
std::cout << " -r: Compute the model cloud resolution and multiply" << std::endl;
std::cout << " each radius given by that value." << std::endl;
std::cout << " --algorithm (Hough|GC): Clustering algorithm used (default Hough)." << std::endl;
std::cout << " --model_ss val: Model uniform sampling radius (default 0.01)" << std::endl;
std::cout << " --scene_ss val: Scene uniform sampling radius (default 0.03)" << std::endl;
std::cout << " --rf_rad val: Reference frame radius (default 0.015)" << std::endl;
std::cout << " --descr_rad val: Descriptor radius (default 0.02)" << std::endl;
std::cout << " --cg_size val: Cluster size (default 0.01)" << std::endl;
std::cout << " --cg_thresh val: Clustering threshold (default 5)" << std::endl << std::endl;
}
The second function does the actual parsing of the command line arguments in order to set the correct parameters for the execution.
void
parseCommandLine (int argc, char *argv[])
{
//Show help
if (pcl::console::find_switch (argc, argv, "-h"))
{
showHelp (argv[0]);
exit (0);
}
//Model & scene filenames
std::vector<int> filenames;
filenames = pcl::console::parse_file_extension_argument (argc, argv, ".pcd");
if (filenames.size () != 2)
{
std::cout << "Filenames missing.\n";
showHelp (argv[0]);
exit (-1);
}
model_filename_ = argv[filenames[0]];
scene_filename_ = argv[filenames[1]];
//Program behavior
if (pcl::console::find_switch (argc, argv, "-k"))
{
show_keypoints_ = true;
}
if (pcl::console::find_switch (argc, argv, "-c"))
{
show_correspondences_ = true;
}
if (pcl::console::find_switch (argc, argv, "-r"))
{
use_cloud_resolution_ = true;
}
std::string used_algorithm;
if (pcl::console::parse_argument (argc, argv, "--algorithm", used_algorithm) != -1)
{
if (used_algorithm.compare ("Hough") == 0)
{
use_hough_ = true;
}else if (used_algorithm.compare ("GC") == 0)
{
use_hough_ = false;
}
else
{
std::cout << "Wrong algorithm name.\n";
showHelp (argv[0]);
exit (-1);
}
}
//General parameters
pcl::console::parse_argument (argc, argv, "--model_ss", model_ss_);
pcl::console::parse_argument (argc, argv, "--scene_ss", scene_ss_);
pcl::console::parse_argument (argc, argv, "--rf_rad", rf_rad_);
pcl::console::parse_argument (argc, argv, "--descr_rad", descr_rad_);
pcl::console::parse_argument (argc, argv, "--cg_size", cg_size_);
pcl::console::parse_argument (argc, argv, "--cg_thresh", cg_thresh_);
}
It’s important to say that the only command line parameters required when executing this tutorial are the filenames of the model and the scene, in this exact order. All other parameters are set to a default value that will make the tutorial work correctly with the supplied dataset, although with different models and scene some parameter values might need to be adjusted. You can play around with them to see how they influence the final result.
You can choose between two correspondence clustering algorithms with the command line switch --algorithm (Hough|GC)
- Hough (default)
This is a clustering algorithm based on a 3D Hough voting scheme described in:
F. Tombari and L. Di Stefano: “Object recognition in 3D scenes with occlusions and clutter by Hough voting”, 4th Pacific-Rim Symposium on Image and Video Technology, 2010.
- GC
This is a geometric consistency clustering algorithm enforcing simple geometric constraints between pairs of correspondences. It builds on the proposal presented in:
H. Chen and B. Bhanu: “3D free-form object recognition in range images using local surface patches”, Pattern Recognition Letters, vol. 28, no. 10, pp. 1252-1262, 2007.
Some other interesting switches are -k
, -c
and -r
:
-k
shows the keypoints used to compute the correspondences as a blue overlay into the PCL visualizer.
-c
draws a line connecting each pair of model-scene correspondences that survived the clustering process.
-r
estimates the spatial resolution for the model point cloud and afterwards considers the radii used as parameters as if they were given in units of cloud resolution; thus achieving some sort of resolution invariance that might be useful when using this tutorial with the same command line and different point clouds.
The next function performs the spatial resolution computation for a given point cloud averaging the distance between each cloud point and its nearest neighbor.
double
computeCloudResolution (const pcl::PointCloud<PointType>::ConstPtr &cloud)
{
double res = 0.0;
int n_points = 0;
int nres;
std::vector<int> indices (2);
std::vector<float> sqr_distances (2);
pcl::search::KdTree<PointType> tree;
tree.setInputCloud (cloud);
for (std::size_t i = 0; i < cloud->size (); ++i)
{
if (! std::isfinite ((*cloud)[i].x))
{
continue;
}
//Considering the second neighbor since the first is the point itself.
nres = tree.nearestKSearch (i, 2, indices, sqr_distances);
if (nres == 2)
{
res += sqrt (sqr_distances[1]);
++n_points;
}
}
if (n_points != 0)
{
res /= n_points;
}
return res;
}
Clustering Pipeline
The main function, which performs the actual clustering, is quite straightforward. We will take a look at each part of code as they appear in the proposed example.
First, the program parses the command line arguments and loads the model and scene clouds from disk (using the filenames supplied by the user).
parseCommandLine (argc, argv);
if (pcl::io::loadPCDFile (model_filename_, *model) < 0)
{
std::cout << "Error loading model cloud." << std::endl;
showHelp (argv[0]);
return (-1);
}
if (pcl::io::loadPCDFile (scene_filename_, *scene) < 0)
{
std::cout << "Error loading scene cloud." << std::endl;
showHelp (argv[0]);
return (-1);
}
As a second step, only if resolution invariance flag has been enabled in the command line, the program adjusts the radii that will be used in the next sections by multiplying them for the estimated model cloud resolution.
if (use_cloud_resolution_)
{
float resolution = static_cast<float> (computeCloudResolution (model));
if (resolution != 0.0f)
{
model_ss_ *= resolution;
scene_ss_ *= resolution;
rf_rad_ *= resolution;
descr_rad_ *= resolution;
cg_size_ *= resolution;
}
std::cout << "Model resolution: " << resolution << std::endl;
std::cout << "Model sampling size: " << model_ss_ << std::endl;
std::cout << "Scene sampling size: " << scene_ss_ << std::endl;
std::cout << "LRF support radius: " << rf_rad_ << std::endl;
std::cout << "SHOT descriptor radius: " << descr_rad_ << std::endl;
std::cout << "Clustering bin size: " << cg_size_ << std::endl << std::endl;
}
Next, it computes the normals for each point of both the model and the scene cloud with the NormalEstimationOMP estimator, using the 10 nearest neighbors of each point (this parameter seems to be fairly ok for many datasets, not just for the one provided).
pcl::NormalEstimationOMP<PointType, NormalType> norm_est;
norm_est.setKSearch (10);
norm_est.setInputCloud (model);
norm_est.compute (*model_normals);
norm_est.setInputCloud (scene);
norm_est.compute (*scene_normals);
Then it downsamples each cloud in order to find a small number of keypoints, which will then be associated to a 3D descriptor in order to perform keypoint matching and determine point-to-point correspondences. The radii used for the UniformSampling are either the ones set with the command line switches or the defaults.
pcl::UniformSampling<PointType> uniform_sampling;
uniform_sampling.setInputCloud (model);
uniform_sampling.setRadiusSearch (model_ss_);
uniform_sampling.filter (*model_keypoints);
std::cout << "Model total points: " << model->size () << "; Selected Keypoints: " << model_keypoints->size () << std::endl;
uniform_sampling.setInputCloud (scene);
uniform_sampling.setRadiusSearch (scene_ss_);
uniform_sampling.filter (*scene_keypoints);
std::cout << "Scene total points: " << scene->size () << "; Selected Keypoints: " << scene_keypoints->size () << std::endl;
//
The next stage consists in associating a 3D descriptor to each model and scene keypoint. In our tutorial, we compute SHOT descriptors using SHOTEstimationOMP.
descr_est.setInputCloud (model_keypoints);
descr_est.setInputNormals (model_normals);
descr_est.setSearchSurface (model);
descr_est.compute (*model_descriptors);
descr_est.setInputCloud (scene_keypoints);
descr_est.setInputNormals (scene_normals);
descr_est.setSearchSurface (scene);
descr_est.compute (*scene_descriptors);
//
// Find Model-Scene Correspondences with KdTree
Now we need to determine point-to-point correspondences between model descriptors and scene descriptors. To do this, the program uses a KdTreeFLANN whose input cloud has been set to the cloud containing the model descriptors. For each descriptor associated to a scene keypoint, it efficiently finds the most similar model descriptor based on the Euclidean distance, and it adds this pair to a Correspondences vector (only if the two descriptors are similar enough, i.e. their squared distance is less than a threshold, set to 0.25).
match_search.setInputCloud (model_descriptors);
// For each scene keypoint descriptor, find nearest neighbor into the model keypoints descriptor cloud and add it to the correspondences vector.
for (std::size_t i = 0; i < scene_descriptors->size (); ++i)
{
std::vector<int> neigh_indices (1);
std::vector<float> neigh_sqr_dists (1);
if (!std::isfinite (scene_descriptors->at (i).descriptor[0])) //skipping NaNs
{
continue;
}
int found_neighs = match_search.nearestKSearch (scene_descriptors->at (i), 1, neigh_indices, neigh_sqr_dists);
if(found_neighs == 1 && neigh_sqr_dists[0] < 0.25f) // add match only if the squared descriptor distance is less than 0.25 (SHOT descriptor distances are between 0 and 1 by design)
{
pcl::Correspondence corr (neigh_indices[0], static_cast<int> (i), neigh_sqr_dists[0]);
model_scene_corrs->push_back (corr);
}
}
std::cout << "Correspondences found: " << model_scene_corrs->size () << std::endl;
//
// Actual Clustering
The last stage of the pipeline is the actual clustering of the previously found correspondences.
The default algorithm is Hough3DGrouping, that is based on an Hough Voting process. Please note that this algorithm needs to associate a Local Reference Frame (LRF) for each keypoint belonging to the clouds which are passed as arguments! In this example, we explicitly compute the set of LRFs using the BOARDLocalReferenceFrameEstimation estimator before calling the clustering algorithm.
// Compute (Keypoints) Reference Frames only for Hough
//
pcl::PointCloud<RFType>::Ptr model_rf (new pcl::PointCloud<RFType> ());
pcl::PointCloud<RFType>::Ptr scene_rf (new pcl::PointCloud<RFType> ());
pcl::BOARDLocalReferenceFrameEstimation<PointType, NormalType, RFType> rf_est;
rf_est.setFindHoles (true);
rf_est.setRadiusSearch (rf_rad_);
rf_est.setInputCloud (model_keypoints);
rf_est.setInputNormals (model_normals);
rf_est.setSearchSurface (model);
rf_est.compute (*model_rf);
rf_est.setInputCloud (scene_keypoints);
rf_est.setInputNormals (scene_normals);
rf_est.setSearchSurface (scene);
rf_est.compute (*scene_rf);
// Clustering
pcl::Hough3DGrouping<PointType, PointType, RFType, RFType> clusterer;
clusterer.setHoughBinSize (cg_size_);
clusterer.setHoughThreshold (cg_thresh_);
clusterer.setUseInterpolation (true);
clusterer.setUseDistanceWeight (false);
clusterer.setInputCloud (model_keypoints);
clusterer.setInputRf (model_rf);
clusterer.setSceneCloud (scene_keypoints);
clusterer.setSceneRf (scene_rf);
clusterer.setModelSceneCorrespondences (model_scene_corrs);
//clusterer.cluster (clustered_corrs);
clusterer.recognize (rototranslations, clustered_corrs);
}
else // Using GeometricConsistency
{
pcl::GeometricConsistencyGrouping<PointType, PointType> gc_clusterer;
Note
It’s not necessary to explicitly compute the LRFs before calling the clustering algorithm. If the clouds which are fetched to the clustering algorithm do not have a set of LRFs associated, Hough3DGrouping automatically computes them before performing clustering. In particular, this happens when calling the recognize
(or cluster
) method without setting the LRFs: in this case you need to specify the radius of the LRF as an additional parameter for the clustering algorithm (with the setLocalRfSearchRadius
method).
Alternatively to Hough3DGrouping, and by means of the appropriate command line switch described before, you might choose to employ the GeometricConsistencyGrouping algorithm. In this case the LRF computation is not needed so we are simply creating an instance of the algorithm class, passing the right parameters and invoking the recognize
method.
gc_clusterer.setGCSize (cg_size_);
gc_clusterer.setGCThreshold (cg_thresh_);
gc_clusterer.setInputCloud (model_keypoints);
gc_clusterer.setSceneCloud (scene_keypoints);
gc_clusterer.setModelSceneCorrespondences (model_scene_corrs);
//gc_clusterer.cluster (clustered_corrs);
gc_clusterer.recognize (rototranslations, clustered_corrs);
}
//
// Output results
Note
The recognize
method returns a vector of Eigen::Matrix4f
representing a transformation (rotation + translation) for each instance of the model found in the scene (obtained via Absolute Orientation) and a vector of Correspondences (a vector of vectors of Correspondence) representing the output of the clustering i.e. each element of this vector is in turn a set of correspondences, representing the correspondences associated to a specific model instance in the scene.
If you only need the clustered correspondences because you are planning to use them in a different way, you can use the cluster
method.
Output and Visualization
We are almost at the end of this tutorial. The last few words are related to the part of the program that displays the results on the console and over a PCL Visualizer window.
As a first thing we are showing, for each instance of the model found into the scene, the transformation matrix and the number of correspondences extracted by the clustering method.
std::cout << "\n Instance " << i + 1 << ":" << std::endl;
std::cout << " Correspondences belonging to this instance: " << clustered_corrs[i].size () << std::endl;
// Print the rotation matrix and translation vector
Eigen::Matrix3f rotation = rototranslations[i].block<3,3>(0, 0);
Eigen::Vector3f translation = rototranslations[i].block<3,1>(0, 3);
printf ("\n");
printf (" | %6.3f %6.3f %6.3f | \n", rotation (0,0), rotation (0,1), rotation (0,2));
printf (" R = | %6.3f %6.3f %6.3f | \n", rotation (1,0), rotation (1,1), rotation (1,2));
printf (" | %6.3f %6.3f %6.3f | \n", rotation (2,0), rotation (2,1), rotation (2,2));
printf ("\n");
printf (" t = < %0.3f, %0.3f, %0.3f >\n", translation (0), translation (1), translation (2));
}
//
// Visualization
The program then shows in a PCLVisualizer window the scene cloud with a red overlay where an instance of the model has been found.
If the command line switches -k
and -c
have been used, the program also shows a “stand-alone” rendering of the model cloud. If keypoint visualization is enabled, keypoints are displayed as blue dots and if correspondence visualization has been enabled they are shown as a green line for each correspondence which survived the clustering process.
pcl::PointCloud<PointType>::Ptr off_scene_model (new pcl::PointCloud<PointType> ());
pcl::PointCloud<PointType>::Ptr off_scene_model_keypoints (new pcl::PointCloud<PointType> ());
if (show_correspondences_ || show_keypoints_)
{
// We are translating the model so that it doesn't end in the middle of the scene representation
pcl::transformPointCloud (*model, *off_scene_model, Eigen::Vector3f (-1,0,0), Eigen::Quaternionf (1, 0, 0, 0));
pcl::transformPointCloud (*model_keypoints, *off_scene_model_keypoints, Eigen::Vector3f (-1,0,0), Eigen::Quaternionf (1, 0, 0, 0));
pcl::visualization::PointCloudColorHandlerCustom<PointType> off_scene_model_color_handler (off_scene_model, 255, 255, 128);
viewer.addPointCloud (off_scene_model, off_scene_model_color_handler, "off_scene_model");
}
if (show_keypoints_)
{
pcl::visualization::PointCloudColorHandlerCustom<PointType> scene_keypoints_color_handler (scene_keypoints, 0, 0, 255);
viewer.addPointCloud (scene_keypoints, scene_keypoints_color_handler, "scene_keypoints");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "scene_keypoints");
pcl::visualization::PointCloudColorHandlerCustom<PointType> off_scene_model_keypoints_color_handler (off_scene_model_keypoints, 0, 0, 255);
viewer.addPointCloud (off_scene_model_keypoints, off_scene_model_keypoints_color_handler, "off_scene_model_keypoints");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "off_scene_model_keypoints");
}
for (std::size_t i = 0; i < rototranslations.size (); ++i)
{
pcl::PointCloud<PointType>::Ptr rotated_model (new pcl::PointCloud<PointType> ());
pcl::transformPointCloud (*model, *rotated_model, rototranslations[i]);
std::stringstream ss_cloud;
ss_cloud << "instance" << i;
pcl::visualization::PointCloudColorHandlerCustom<PointType> rotated_model_color_handler (rotated_model, 255, 0, 0);
viewer.addPointCloud (rotated_model, rotated_model_color_handler, ss_cloud.str ());
if (show_correspondences_)
{
for (std::size_t j = 0; j < clustered_corrs[i].size (); ++j)
{
std::stringstream ss_line;
ss_line << "correspondence_line" << i << "_" << j;
PointType& model_point = off_scene_model_keypoints->at (clustered_corrs[i][j].index_query);
PointType& scene_point = scene_keypoints->at (clustered_corrs[i][j].index_match);
// We are drawing a line for each pair of clustered correspondences found between the model and the scene
viewer.addLine<PointType, PointType> (model_point, scene_point, 0, 255, 0, ss_line.str ());
}
}
}
while (!viewer.wasStopped ())
{
viewer.spinOnce ();
}
return (0);
}
Compiling and running the program
Create a CMakeLists.txt file and add the following lines into it:
1cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
2
3project(correspondence_grouping)
4
5find_package(PCL 1.5 REQUIRED)
6
7include_directories(${PCL_INCLUDE_DIRS})
8link_directories(${PCL_LIBRARY_DIRS})
9add_definitions(${PCL_DEFINITIONS})
10
11add_executable (correspondence_grouping correspondence_grouping.cpp)
12target_link_libraries (correspondence_grouping ${PCL_LIBRARIES})
After you have created the executable, you can then launch it following this example:
$ ./correspondence_grouping milk.pcd milk_cartoon_all_small_clorox.pcd
Or, alternatively, if you prefer specifying the radii in units of cloud resolution:
$ ./correspondence_grouping milk.pcd milk_cartoon_all_small_clorox.pcd -r --model_ss 7.5 --scene_ss 20 --rf_rad 10 --descr_rad 15 --cg_size 10
Remember to replace milk.pcd
and milk_cartoon_all_small_clorox.pcd
with model and scene filenames, in this exact order. If you want you can add other command line options as described at the beginning of this tutorial.
Note
If you are using different point clouds and you don’t know how to set the various parameters for this tutorial you can use the -r
flag and try setting the LRF and descriptor radii to 5, 10, 15 or 20 times the actual cloud resolution. After that you probably will have to tweak the values by hand to achieve the best results.
After a few seconds, you will see an output similar to:
Model total points: 13704; Selected Keypoints: 732
Scene total points: 307200; Selected Keypoints: 3747
Correspondences found: 1768
Model instances found: 1
Instance 1:
Correspondences belonging to this instance: 24
| 0.968 -0.148 0.201 |
R = | -0.146 -0.989 -0.023 |
| 0.202 -0.007 -0.979 |
t = < -0.171, -0.204, 0.043 >
You may see warnings about invalid reference frames (this can happen if a keypoint does not have enough other points in its neighborhood). If these warnings are only displayed for few points and the results look good otherwise, you can ignore them, else you should try to adapt the parameters.
The output window should look like this (depending on the command line options used):