ni_linemod.cpp
Go to the documentation of this file.
00001 /*
00002  * Software License Agreement (BSD License)
00003  *
00004  *  Point Cloud Library (PCL) - www.pointclouds.org
00005  *  Copyright (c) 2010-2012, Willow Garage, Inc.
00006  * 
00007  *  All rights reserved.
00008  *
00009  *  Redistribution and use in source and binary forms, with or without
00010  *  modification, are permitted provided that the following conditions
00011  *  are met:
00012  *
00013  *   * Redistributions of source code must retain the above copyright
00014  *     notice, this list of conditions and the following disclaimer.
00015  *   * Redistributions in binary form must reproduce the above
00016  *     copyright notice, this list of conditions and the following
00017  *     disclaimer in the documentation and/or other materials provided
00018  *     with the distribution.
00019  *   * Neither the name of Willow Garage, Inc. nor the names of its
00020  *     contributors may be used to endorse or promote products derived
00021  *     from this software without specific prior written permission.
00022  *
00023  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00024  *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00025  *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00026  *  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00027  *  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00028  *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00029  *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00030  *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00031  *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00032  *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00033  *  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00034  *  POSSIBILITY OF SUCH DAMAGE.
00035  *
00036  * $Id: openni_viewer.cpp 5059 2012-03-14 02:12:17Z gedikli $
00037  *
00038  */
00039 
00040 #include <pcl/apps/timer.h>
00041 #include <pcl/common/common.h>
00042 #include <pcl/common/angles.h>
00043 #include <pcl/common/time.h>
00044 #include <pcl/io/openni_grabber.h>
00045 #include <pcl/io/pcd_io.h>
00046 #include <pcl/search/organized.h>
00047 #include <pcl/features/integral_image_normal.h>
00048 #include <pcl/filters/extract_indices.h>
00049 #include <pcl/segmentation/organized_multi_plane_segmentation.h>
00050 #include <pcl/segmentation/sac_segmentation.h>
00051 #include <pcl/segmentation/extract_polygonal_prism_data.h>
00052 #include <pcl/sample_consensus/sac_model_plane.h>
00053 //#include <pcl/io/tar_io.h>
00054 #include <pcl/surface/convex_hull.h>
00055 #include <pcl/visualization/point_cloud_handlers.h>
00056 #include <pcl/visualization/pcl_visualizer.h>
00057 #include <pcl/visualization/image_viewer.h>
00058 #include <pcl/console/print.h>
00059 #include <pcl/console/parse.h>
00060 #include <pcl/segmentation/euclidean_cluster_comparator.h>
00061 #include <pcl/segmentation/organized_connected_component_segmentation.h>
00062 #include <pcl/segmentation/edge_aware_plane_comparator.h>
00063 #include <pcl/geometry/polygon_operations.h>
00064 
00065 using namespace pcl;
00066 using namespace std;
00067 
00068 typedef PointXYZRGBA PointT;
00069 
00070 #define SHOW_FPS 1
00071 
00073 class NILinemod
00074 {
00075   public:
00076     typedef PointCloud<PointT> Cloud;
00077     typedef Cloud::Ptr CloudPtr;
00078     typedef Cloud::ConstPtr CloudConstPtr;
00079     bool added;
00080 
00081     NILinemod (Grabber& grabber)
00082       : cloud_viewer_ ("PointCloud")
00083       , grabber_ (grabber)
00084       , image_viewer_ ("Image")
00085       , first_frame_ (true)
00086     {
00087       added = false;
00088 
00089       // Set the parameters for normal estimation
00090       ne_.setNormalEstimationMethod (ne_.COVARIANCE_MATRIX);
00091       ne_.setMaxDepthChangeFactor (0.02f);
00092       ne_.setNormalSmoothingSize (20.0f);
00093 
00094       // Set the parameters for planar segmentation
00095       plane_comparator_.reset (new EdgeAwarePlaneComparator<PointT, Normal>);
00096         plane_comparator_->setDistanceThreshold (0.01f, false);
00097       mps_.setMinInliers (5000);
00098       mps_.setAngularThreshold (pcl::deg2rad (3.0)); // 3 degrees
00099       mps_.setDistanceThreshold (0.02); // 2 cm
00100       mps_.setMaximumCurvature (0.001); // a small curvature
00101       mps_.setProjectPoints (true);
00102       mps_.setComparator (plane_comparator_);
00103     }
00104 
00106     void
00107     cloud_callback (const CloudConstPtr& cloud)
00108     {
00109       FPS_CALC ("cloud callback");
00110       boost::mutex::scoped_lock lock (cloud_mutex_);
00111       cloud_ = cloud;
00112       search_.setInputCloud (cloud);
00113 
00114       // Subsequent frames are segmented by "tracking" the parameters of the previous frame
00115       // We do this by using the estimated inliers from previous frames in the current frame, 
00116       // and refining the coefficients
00117 
00118       if (!first_frame_)
00119       {
00120         if (!plane_indices_ || plane_indices_->indices.empty () || !search_.getInputCloud ())
00121         {
00122           PCL_ERROR ("Lost tracking. Select the object again to continue.\n");
00123           first_frame_ = true;
00124           return;
00125         }
00126         SACSegmentation<PointT> seg;
00127         seg.setOptimizeCoefficients (true);
00128         seg.setModelType (SACMODEL_PLANE);
00129         seg.setMethodType (SAC_RANSAC);
00130         seg.setMaxIterations (1000);
00131         seg.setDistanceThreshold (0.01);
00132         seg.setInputCloud (search_.getInputCloud ());
00133         seg.setIndices (plane_indices_);
00134         ModelCoefficients coefficients;
00135         PointIndices inliers;
00136         seg.segment (inliers, coefficients);
00137 
00138         if (inliers.indices.empty ())
00139         {
00140           PCL_ERROR ("No planar model found. Select the object again to continue.\n");
00141           first_frame_ = true;
00142           return;
00143         }
00144 
00145         // Visualize the object in 3D...
00146         CloudPtr plane_inliers (new Cloud);
00147         pcl::copyPointCloud (*search_.getInputCloud (), inliers.indices, *plane_inliers);
00148         if (plane_inliers->points.empty ())
00149         {
00150           PCL_ERROR ("No planar model found. Select the object again to continue.\n");
00151           first_frame_ = true;
00152           return;
00153         }
00154         else
00155         {
00156           plane_.reset (new Cloud);
00157 
00158           // Compute the convex hull of the plane
00159           ConvexHull<PointT> chull;
00160           chull.setDimension (2);
00161           chull.setInputCloud (plane_inliers);
00162           chull.reconstruct (*plane_);
00163         }
00164       }
00165     }
00166 
00168     CloudConstPtr
00169     getLatestCloud ()
00170     {
00171       // Lock while we swap our cloud and reset it.
00172       boost::mutex::scoped_lock lock (cloud_mutex_);
00173       CloudConstPtr temp_cloud;
00174       temp_cloud.swap (cloud_);
00175       return (temp_cloud);
00176     }
00177 
00179     void 
00180     keyboard_callback (const visualization::KeyboardEvent&, void*)
00181     {
00182       //if (event.getKeyCode())
00183       //  cout << "the key \'" << event.getKeyCode() << "\' (" << event.getKeyCode() << ") was";
00184       //else
00185       //  cout << "the special key \'" << event.getKeySym() << "\' was";
00186       //if (event.keyDown())
00187       //  cout << " pressed" << endl;
00188       //else
00189       //  cout << " released" << endl;
00190     }
00191     
00193     void 
00194     mouse_callback (const visualization::MouseEvent&, void*)
00195     {
00196       //if (mouse_event.getType() == visualization::MouseEvent::MouseButtonPress && mouse_event.getButton() == visualization::MouseEvent::LeftButton)
00197       //{
00198       //  cout << "left button pressed @ " << mouse_event.getX () << " , " << mouse_event.getY () << endl;
00199       //}
00200     }
00201 
00203 
00212     void
00213     segmentObject (int picked_idx, 
00214                    const CloudConstPtr &cloud, 
00215                    const PointIndices::Ptr &plane_indices, 
00216                    const PointIndices::Ptr &plane_boundary_indices, 
00217                    Cloud &object)
00218     {
00219       CloudPtr plane_hull (new Cloud);
00220 
00221       // Compute the convex hull of the plane
00222       ConvexHull<PointT> chull;
00223       chull.setDimension (2);
00224       chull.setInputCloud (cloud);
00225       chull.setIndices (plane_boundary_indices);
00226       chull.reconstruct (*plane_hull);
00227 
00228       // Remove the plane indices from the data
00229       PointIndices::Ptr everything_but_the_plane (new PointIndices);
00230       if (indices_fullset_.size () != cloud->points.size ())
00231       {
00232         indices_fullset_.resize (cloud->points.size ());
00233         for (int p_it = 0; p_it < static_cast<int> (indices_fullset_.size ()); ++p_it)
00234           indices_fullset_[p_it] = p_it;
00235       }
00236       std::vector<int> indices_subset = plane_indices->indices;
00237       std::sort (indices_subset.begin (), indices_subset.end ());
00238       set_difference (indices_fullset_.begin (), indices_fullset_.end (), 
00239                       indices_subset.begin (), indices_subset.end (), 
00240                       inserter (everything_but_the_plane->indices, everything_but_the_plane->indices.begin ()));
00241 
00242       // Extract all clusters above the hull
00243       PointIndices::Ptr points_above_plane (new PointIndices);
00244       ExtractPolygonalPrismData<PointT> exppd;
00245       exppd.setInputCloud (cloud);
00246       exppd.setInputPlanarHull (plane_hull);
00247       exppd.setIndices (everything_but_the_plane);
00248       exppd.setHeightLimits (0.0, 0.5);           // up to half a meter
00249       exppd.segment (*points_above_plane);
00250 
00251       // Use an organized clustering segmentation to extract the individual clusters
00252       EuclideanClusterComparator<PointT, Normal, Label>::Ptr euclidean_cluster_comparator (new EuclideanClusterComparator<PointT, Normal, Label>);
00253       euclidean_cluster_comparator->setInputCloud (cloud);
00254       euclidean_cluster_comparator->setDistanceThreshold (0.03f, false);
00255       // Set the entire scene to false, and the inliers of the objects located on top of the plane to true
00256       Label l; l.label = 0;
00257       PointCloud<Label>::Ptr scene (new PointCloud<Label> (cloud->width, cloud->height, l));
00258       // Mask the objects that we want to split into clusters
00259       for (int i = 0; i < static_cast<int> (points_above_plane->indices.size ()); ++i)
00260         scene->points[points_above_plane->indices[i]].label = 1;
00261       euclidean_cluster_comparator->setLabels (scene);
00262 
00263       vector<bool> exclude_labels (2);  exclude_labels[0] = true; exclude_labels[1] = false;
00264       euclidean_cluster_comparator->setExcludeLabels (exclude_labels);
00265 
00266       OrganizedConnectedComponentSegmentation<PointT, Label> euclidean_segmentation (euclidean_cluster_comparator);
00267       euclidean_segmentation.setInputCloud (cloud);
00268 
00269       PointCloud<Label> euclidean_labels;
00270       vector<PointIndices> euclidean_label_indices;
00271       euclidean_segmentation.segment (euclidean_labels, euclidean_label_indices);
00272 
00273       // For each cluster found
00274       bool cluster_found = false;
00275       for (size_t i = 0; i < euclidean_label_indices.size (); i++)
00276       {
00277         if (cluster_found)
00278           break;
00279         // Check if the point that we picked belongs to it
00280         for (size_t j = 0; j < euclidean_label_indices[i].indices.size (); ++j)
00281         {
00282           if (picked_idx != euclidean_label_indices[i].indices[j])
00283             continue;
00284           //pcl::PointCloud<PointT> cluster;
00285           pcl::copyPointCloud (*cloud, euclidean_label_indices[i].indices, object);
00286           cluster_found = true;
00287           break;
00288           //object_indices = euclidean_label_indices[i].indices;
00289           //clusters.push_back (cluster);
00290         }
00291       }
00292     }
00293 
00294 
00296     void
00297     segment (const PointT &picked_point, 
00298              int picked_idx,
00299              PlanarRegion<PointT> &region,
00300              PointIndices &,
00301              CloudPtr &object)
00302     {
00303       // First frame is segmented using an organized multi plane segmentation approach from points and their normals
00304       if (!first_frame_)
00305         return;
00306 
00307       // Estimate normals in the cloud
00308       PointCloud<Normal>::Ptr normal_cloud (new PointCloud<Normal>);
00309       ne_.setInputCloud (search_.getInputCloud ());
00310       ne_.compute (*normal_cloud);
00311 
00312       plane_comparator_->setDistanceMap (ne_.getDistanceMap ());
00313 
00314       // Segment out all planes
00315       mps_.setInputNormals (normal_cloud);
00316       mps_.setInputCloud (search_.getInputCloud ());
00317 
00318       // Use one of the overloaded segmentAndRefine calls to get all the information that we want out
00319       vector<PlanarRegion<PointT>, Eigen::aligned_allocator<PlanarRegion<PointT> > > regions;
00320       vector<ModelCoefficients> model_coefficients;
00321       vector<PointIndices> inlier_indices;  
00322       PointCloud<Label>::Ptr labels (new PointCloud<Label>);
00323       vector<PointIndices> label_indices;
00324       vector<PointIndices> boundary_indices;
00325       mps_.segmentAndRefine (regions, model_coefficients, inlier_indices, labels, label_indices, boundary_indices);
00326       PCL_DEBUG ("Number of planar regions detected: %zu for a cloud of %zu points and %zu normals.\n", regions.size (), search_.getInputCloud ()->points.size (), normal_cloud->points.size ());
00327 
00328       double max_dist = numeric_limits<double>::max ();
00329       // Compute the distances from all the planar regions to the picked point, and select the closest region
00330       int idx = -1;
00331       for (size_t i = 0; i < regions.size (); ++i)
00332       {
00333         double dist = pointToPlaneDistance (picked_point, regions[i].getCoefficients ()); 
00334         if (dist < max_dist)
00335         {
00336           max_dist = dist;
00337           idx = static_cast<int> (i);
00338         }
00339       }
00340 
00341       PointIndices::Ptr plane_boundary_indices;
00342       // Get the plane that holds the object of interest
00343       if (idx != -1)
00344       {
00345         region = regions[idx]; 
00346         plane_indices_.reset (new PointIndices (inlier_indices[idx]));
00347         plane_boundary_indices.reset (new PointIndices (boundary_indices[idx]));
00348       }
00349 
00350       // Segment the object of interest
00351       if (plane_boundary_indices && !plane_boundary_indices->indices.empty ())
00352       {
00353         object.reset (new Cloud);
00354         segmentObject (picked_idx, search_.getInputCloud (), plane_indices_, plane_boundary_indices, *object);
00355 
00356         // Save to disk
00357         //pcl::io::saveTARPointCloud ("output.ltm", *object, "object.pcd");
00358       }
00359       first_frame_ = false;
00360     }
00361 
00363 
00368     void 
00369     pp_callback (const visualization::PointPickingEvent& event, void*)
00370     {
00371       // Check to see if we got a valid point. Early exit.
00372       int idx = event.getPointIndex ();
00373       if (idx == -1)
00374         return;
00375 
00376       vector<int> indices (1);
00377       vector<float> distances (1);
00378 
00379       // Use mutices to make sure we get the right cloud
00380       boost::mutex::scoped_lock lock1 (cloud_mutex_);
00381 
00382       // Get the point that was picked
00383       PointT picked_pt;
00384       event.getPoint (picked_pt.x, picked_pt.y, picked_pt.z);
00385 
00386       // Add a sphere to it in the PCLVisualizer window
00387       stringstream ss;
00388       ss << "sphere_" << idx;
00389       cloud_viewer_.addSphere (picked_pt, 0.01, 1.0, 0.0, 0.0, ss.str ());
00390 
00391       // Check to see if we have access to the actual cloud data. Use the previously built search object.
00392       if (!search_.isValid ())
00393         return;
00394 
00395       // Because VTK/OpenGL stores data without NaN, we lose the 1-1 correspondence, so we must search for the real point
00396       search_.nearestKSearch (picked_pt, 1, indices, distances);
00397 
00398       // Get the [u, v] in pixel coordinates for the ImageViewer. Remember that 0,0 is bottom left.
00399       uint32_t width  = search_.getInputCloud ()->width;
00400 //               height = search_.getInputCloud ()->height;
00401       int v = indices[0] / width,
00402           u = indices[0] % width;
00403 
00404       // Add some marker to the image
00405       image_viewer_.addCircle (u, v, 5, 1.0, 0.0, 0.0, "circles", 1.0);
00406       image_viewer_.addFilledRectangle (u-5, u+5, v-5, v+5, 0.0, 1.0, 0.0, "boxes", 0.5);
00407       image_viewer_.markPoint (u, v, visualization::red_color, visualization::blue_color, 10);
00408 
00409       // Segment the region that we're interested in, by employing a two step process:
00410       //  * first, segment all the planes in the scene, and find the one closest to our picked point
00411       //  * then, use euclidean clustering to find the object that we clicked on and return it
00412       PlanarRegion<PointT> region;
00413       CloudPtr object;
00414       PointIndices region_indices;
00415       segment (picked_pt, indices[0], region, region_indices, object);
00416 
00417       // If no region could be determined, exit
00418       if (region.getContour ().empty ())
00419       {
00420         PCL_ERROR ("No planar region detected. Please select another point or relax the thresholds and continue.\n");
00421         return;
00422       }
00423       // Else, draw it on screen
00424       else
00425       {
00426         //cloud_viewer_.addPolygon (region, 1.0, 0.0, 0.0, "region");
00427         //cloud_viewer_.setShapeRenderingProperties (visualization::PCL_VISUALIZER_LINE_WIDTH, 10, "region");
00428 
00429         PlanarRegion<PointT> refined_region;
00430         pcl::approximatePolygon (region, refined_region, 0.01, false, true);
00431         PCL_INFO ("Planar region: %zu points initial, %zu points after refinement.\n", region.getContour ().size (), refined_region.getContour ().size ());
00432         cloud_viewer_.addPolygon (refined_region, 0.0, 0.0, 1.0, "refined_region");
00433         cloud_viewer_.setShapeRenderingProperties (visualization::PCL_VISUALIZER_LINE_WIDTH, 10, "refined_region");
00434 
00435         // Draw in image space
00436         image_viewer_.addPlanarPolygon (search_.getInputCloud (), refined_region, 0.0, 0.0, 1.0, "refined_region", 1.0);
00437       }
00438 
00439       // If no object could be determined, exit
00440       if (!object)
00441       {
00442         PCL_ERROR ("No object detected. Please select another point or relax the thresholds and continue.\n");
00443         return;
00444       }
00445       else
00446       {
00447         // Visualize the object in 3D...
00448         visualization::PointCloudColorHandlerCustom<PointT> red (object, 255, 0, 0);
00449         if (!cloud_viewer_.updatePointCloud (object, red, "object"))
00450           cloud_viewer_.addPointCloud (object, red, "object");
00451         // ...and 2D
00452         image_viewer_.removeLayer ("object");
00453         image_viewer_.addMask (search_.getInputCloud (), *object, "object");
00454 
00455         // Compute the min/max of the object
00456         PointT min_pt, max_pt;
00457         getMinMax3D (*object, min_pt, max_pt);
00458         stringstream ss;
00459         ss << "cube_" << idx;
00460         // Visualize the bounding box in 3D...
00461         cloud_viewer_.addCube (min_pt.x, max_pt.x, min_pt.y, max_pt.y, min_pt.z, max_pt.z, 0.0, 1.0, 0.0, ss.str ());
00462         cloud_viewer_.setShapeRenderingProperties (visualization::PCL_VISUALIZER_LINE_WIDTH, 10, ss.str ());
00463 
00464         // ...and 2D
00465         image_viewer_.addRectangle (search_.getInputCloud (), *object);
00466       }
00467     }
00468     
00470     void
00471     init ()
00472     {
00473       cloud_viewer_.registerMouseCallback (&NILinemod::mouse_callback, *this);
00474       cloud_viewer_.registerKeyboardCallback(&NILinemod::keyboard_callback, *this);
00475       cloud_viewer_.registerPointPickingCallback (&NILinemod::pp_callback, *this);
00476       boost::function<void (const CloudConstPtr&) > cloud_cb = boost::bind (&NILinemod::cloud_callback, this, _1);
00477       cloud_connection = grabber_.registerCallback (cloud_cb);
00478       
00479       image_viewer_.registerMouseCallback (&NILinemod::mouse_callback, *this);
00480       image_viewer_.registerKeyboardCallback(&NILinemod::keyboard_callback, *this);
00481     }
00482 
00484     void
00485     run ()
00486     {
00487       grabber_.start ();
00488       
00489       bool image_init = false, cloud_init = false;
00490 
00491       while (!cloud_viewer_.wasStopped () && !image_viewer_.wasStopped ())
00492       {
00493         if (cloud_)
00494         {
00495           CloudConstPtr cloud = getLatestCloud ();
00496           if (!cloud_init)
00497           {
00498             cloud_viewer_.setPosition (0, 0);
00499             cloud_viewer_.setSize (cloud->width, cloud->height);
00500             cloud_init = !cloud_init;
00501           }
00502 
00503           if (!cloud_viewer_.updatePointCloud (cloud, "OpenNICloud"))
00504           {
00505             cloud_viewer_.addPointCloud (cloud, "OpenNICloud");
00506             cloud_viewer_.resetCameraViewpoint ("OpenNICloud");
00507           }
00508 
00509           if (!image_init)
00510           {
00511             image_viewer_.setPosition (cloud->width, 0);
00512             image_viewer_.setSize (cloud->width, cloud->height);
00513             image_init = !image_init;
00514           }
00515 
00516           image_viewer_.showRGBImage<PointT> (cloud);
00517         }
00518 
00519 
00520         // Add the plane that we're tracking to the cloud visualizer
00521         CloudPtr plane (new Cloud);
00522         if (plane_)
00523           *plane = *plane_;
00524         visualization::PointCloudColorHandlerCustom<PointT> blue (plane, 0, 255, 0);
00525         if (!cloud_viewer_.updatePointCloud (plane, blue, "plane"))
00526           cloud_viewer_.addPointCloud (plane, "plane");
00527         cloud_viewer_.spinOnce ();
00528 
00529         image_viewer_.spinOnce ();
00530         boost::this_thread::sleep (boost::posix_time::microseconds (100));
00531       }
00532 
00533       grabber_.stop ();
00534       
00535       cloud_connection.disconnect ();
00536     }
00537     
00538     visualization::PCLVisualizer cloud_viewer_;
00539     Grabber& grabber_;
00540     boost::mutex cloud_mutex_;
00541     CloudConstPtr cloud_;
00542     
00543     visualization::ImageViewer image_viewer_;
00544 
00545     search::OrganizedNeighbor<PointT> search_;
00546   private:
00547     boost::signals2::connection cloud_connection, image_connection;
00548     bool first_frame_;
00549     
00550     // Segmentation
00551     std::vector<int> indices_fullset_;
00552     PointIndices::Ptr plane_indices_;
00553     CloudPtr plane_;
00554     IntegralImageNormalEstimation<PointT, Normal> ne_;
00555     OrganizedMultiPlaneSegmentation<PointT, Normal, Label> mps_;
00556     EdgeAwarePlaneComparator<PointT, Normal>::Ptr plane_comparator_;
00557 };
00558 
00559 /* ---[ */
00560 int
00561 main (int, char**)
00562 {
00563   string device_id ("#1");
00564   OpenNIGrabber grabber (device_id);
00565   NILinemod openni_viewer (grabber);
00566 
00567   openni_viewer.init ();
00568   openni_viewer.run ();
00569   
00570   return (0);
00571 }
00572 /* ]--- */


pcl
Author(s): Open Perception
autogenerated on Wed Aug 26 2015 15:25:49