00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037 #include <string>
00038
00039 #include <ros/ros.h>
00040
00041 #include <sensor_msgs/PointCloud.h>
00042 #include <sensor_msgs/PointCloud2.h>
00043 #include <sensor_msgs/point_cloud_conversion.h>
00044 #include <visualization_msgs/Marker.h>
00045
00046 #include <tf/transform_listener.h>
00047 #include <tf/transform_broadcaster.h>
00048
00049 #include <pcl/point_cloud.h>
00050 #include <pcl/point_types.h>
00051 #include <pcl/io/io.h>
00052 #include <pcl/filters/voxel_grid.h>
00053 #include <pcl/filters/passthrough.h>
00054 #include <pcl/filters/extract_indices.h>
00055 #include <pcl/features/normal_3d.h>
00056 #include <pcl/kdtree/kdtree_flann.h>
00057 #include <pcl/sample_consensus/method_types.h>
00058 #include <pcl/sample_consensus/model_types.h>
00059 #include <pcl/segmentation/sac_segmentation.h>
00060 #include <pcl/filters/project_inliers.h>
00061 #include <pcl/surface/convex_hull.h>
00062 #include <pcl/segmentation/extract_polygonal_prism_data.h>
00063 #include <pcl/segmentation/extract_clusters.h>
00064
00065 #include "tabletop_object_detector/marker_generator.h"
00066 #include "tabletop_object_detector/TabletopSegmentation.h"
00067
00068 namespace tabletop_object_detector {
00069
00070 class TabletopSegmentor
00071 {
00072 typedef pcl::PointXYZ Point;
00073 typedef pcl::KdTree<Point>::Ptr KdTreePtr;
00074
00075 private:
00077 ros::NodeHandle nh_;
00079 ros::NodeHandle priv_nh_;
00081 ros::Publisher marker_pub_;
00083 ros::ServiceServer segmentation_srv_;
00084
00086 int num_markers_published_;
00088 int current_marker_id_;
00089
00091 int inlier_threshold_;
00093 double plane_detection_voxel_size_;
00095 double clustering_voxel_size_;
00097 double z_filter_min_, z_filter_max_;
00099 double table_z_filter_min_, table_z_filter_max_;
00101 double cluster_distance_;
00103 int min_cluster_size_;
00106 std::string processing_frame_;
00108 double up_direction_;
00109
00111 tf::TransformListener listener_;
00112
00113
00114
00116 bool serviceCallback(TabletopSegmentation::Request &request, TabletopSegmentation::Response &response);
00117
00118
00119
00121 template <class PointCloudType>
00122 Table getTable(std_msgs::Header cloud_header, const tf::Transform &table_plane_trans,
00123 const PointCloudType &table_points);
00124
00126 template <class PointCloudType>
00127 void publishClusterMarkers(const std::vector<PointCloudType> &clusters, std_msgs::Header cloud_header);
00128
00129
00130
00132 void processCloud(const sensor_msgs::PointCloud2 &cloud,
00133 TabletopSegmentation::Response &response);
00134
00136 void clearOldMarkers(std::string frame_id);
00137
00138 public:
00140
00141 TabletopSegmentor(ros::NodeHandle nh) : nh_(nh), priv_nh_("~")
00142 {
00143 num_markers_published_ = 1;
00144 current_marker_id_ = 1;
00145
00146 marker_pub_ = nh_.advertise<visualization_msgs::Marker>(nh_.resolveName("markers_out"), 10);
00147
00148 segmentation_srv_ = nh_.advertiseService(nh_.resolveName("segmentation_srv"),
00149 &TabletopSegmentor::serviceCallback, this);
00150
00151
00152 priv_nh_.param<int>("inlier_threshold", inlier_threshold_, 300);
00153 priv_nh_.param<double>("plane_detection_voxel_size", plane_detection_voxel_size_, 0.01);
00154 priv_nh_.param<double>("clustering_voxel_size", clustering_voxel_size_, 0.003);
00155 priv_nh_.param<double>("z_filter_min", z_filter_min_, 0.4);
00156 priv_nh_.param<double>("z_filter_max", z_filter_max_, 1.25);
00157 priv_nh_.param<double>("table_z_filter_min", table_z_filter_min_, 0.01);
00158 priv_nh_.param<double>("table_z_filter_max", table_z_filter_max_, 0.50);
00159 priv_nh_.param<double>("cluster_distance", cluster_distance_, 0.03);
00160 priv_nh_.param<int>("min_cluster_size", min_cluster_size_, 300);
00161 priv_nh_.param<std::string>("processing_frame", processing_frame_, "");
00162 priv_nh_.param<double>("up_direction", up_direction_, -1.0);
00163 }
00164
00166 ~TabletopSegmentor() {}
00167 };
00168
00171 bool TabletopSegmentor::serviceCallback(TabletopSegmentation::Request &request,
00172 TabletopSegmentation::Response &response)
00173 {
00174
00175 std::string topic = nh_.resolveName("cloud_in");
00176 ROS_INFO("Tabletop detection service called; waiting for a point_cloud2 on topic %s", topic.c_str());
00177
00178 sensor_msgs::PointCloud2::ConstPtr recent_cloud =
00179 ros::topic::waitForMessage<sensor_msgs::PointCloud2>(topic, nh_, ros::Duration(3.0));
00180
00181 if (!recent_cloud)
00182 {
00183 ROS_ERROR("Tabletop object detector: no point_cloud2 has been received");
00184 response.result = response.NO_CLOUD_RECEIVED;
00185 return true;
00186 }
00187
00188 ROS_INFO("Point cloud received; processing");
00189 if (!processing_frame_.empty())
00190 {
00191
00192 sensor_msgs::PointCloud old_cloud;
00193 sensor_msgs::convertPointCloud2ToPointCloud (*recent_cloud, old_cloud);
00194 try
00195 {
00196 listener_.transformPointCloud(processing_frame_, old_cloud, old_cloud);
00197 }
00198 catch (tf::TransformException ex)
00199 {
00200 ROS_ERROR("Failed to transform cloud from frame %s into frame %s", old_cloud.header.frame_id.c_str(),
00201 processing_frame_.c_str());
00202 response.result = response.OTHER_ERROR;
00203 return true;
00204 }
00205 sensor_msgs::PointCloud2 converted_cloud;
00206 sensor_msgs::convertPointCloudToPointCloud2 (old_cloud, converted_cloud);
00207 ROS_INFO("Input cloud converted to %s frame", processing_frame_.c_str());
00208 processCloud(converted_cloud, response);
00209 clearOldMarkers(converted_cloud.header.frame_id);
00210 }
00211 else
00212 {
00213 processCloud(*recent_cloud, response);
00214 clearOldMarkers(recent_cloud->header.frame_id);
00215 }
00216 return true;
00217 }
00218
00219 template <class PointCloudType>
00220 Table TabletopSegmentor::getTable(std_msgs::Header cloud_header,
00221 const tf::Transform &table_plane_trans,
00222 const PointCloudType &table_points)
00223 {
00224 Table table;
00225
00226
00227 if (!table_points.points.empty())
00228 {
00229 table.x_min = table_points.points[0].x;
00230 table.x_max = table_points.points[0].x;
00231 table.y_min = table_points.points[0].y;
00232 table.y_max = table_points.points[0].y;
00233 }
00234 for (size_t i=1; i<table_points.points.size(); ++i)
00235 {
00236 if (table_points.points[i].x<table.x_min && table_points.points[i].x>-3.0) table.x_min = table_points.points[i].x;
00237 if (table_points.points[i].x>table.x_max && table_points.points[i].x< 3.0) table.x_max = table_points.points[i].x;
00238 if (table_points.points[i].y<table.y_min && table_points.points[i].y>-3.0) table.y_min = table_points.points[i].y;
00239 if (table_points.points[i].y>table.y_max && table_points.points[i].y< 3.0) table.y_max = table_points.points[i].y;
00240 }
00241
00242 geometry_msgs::Pose table_pose;
00243 tf::poseTFToMsg(table_plane_trans, table_pose);
00244 table.pose.pose = table_pose;
00245 table.pose.header = cloud_header;
00246
00247 visualization_msgs::Marker tableMarker = MarkerGenerator::getTableMarker(table.x_min, table.x_max,
00248 table.y_min, table.y_max);
00249 tableMarker.header = cloud_header;
00250 tableMarker.pose = table_pose;
00251 tableMarker.ns = "tabletop_node";
00252 tableMarker.id = current_marker_id_++;
00253 marker_pub_.publish(tableMarker);
00254
00255 return table;
00256 }
00257
00258 template <class PointCloudType>
00259 void TabletopSegmentor::publishClusterMarkers(const std::vector<PointCloudType> &clusters, std_msgs::Header cloud_header)
00260 {
00261 for (size_t i=0; i<clusters.size(); i++)
00262 {
00263 visualization_msgs::Marker cloud_marker = MarkerGenerator::getCloudMarker(clusters[i]);
00264 cloud_marker.header = cloud_header;
00265 cloud_marker.pose.orientation.w = 1;
00266 cloud_marker.ns = "tabletop_node";
00267 cloud_marker.id = current_marker_id_++;
00268 marker_pub_.publish(cloud_marker);
00269 }
00270 }
00271
00272 void TabletopSegmentor::clearOldMarkers(std::string frame_id)
00273 {
00274 for (int id=current_marker_id_; id < num_markers_published_; id++)
00275 {
00276 visualization_msgs::Marker delete_marker;
00277 delete_marker.header.stamp = ros::Time::now();
00278 delete_marker.header.frame_id = frame_id;
00279 delete_marker.id = id;
00280 delete_marker.action = visualization_msgs::Marker::DELETE;
00281 delete_marker.ns = "tabletop_node";
00282 marker_pub_.publish(delete_marker);
00283 }
00284 num_markers_published_ = current_marker_id_;
00285 current_marker_id_ = 0;
00286 }
00287
00289 tf::Transform getPlaneTransform (pcl::ModelCoefficients coeffs, double up_direction)
00290 {
00291 ROS_ASSERT(coeffs.values.size() > 3);
00292 double a = coeffs.values[0], b = coeffs.values[1], c = coeffs.values[2], d = coeffs.values[3];
00293
00294 btVector3 position(-a*d, -b*d, -c*d);
00295 btVector3 z(a, b, c);
00296
00297 ROS_DEBUG("z.dot: %0.3f", z.dot(btVector3(0,0,1)));
00298 ROS_DEBUG("in getPlaneTransform, z: %0.3f, %0.3f, %0.3f", z[0], z[1], z[2]);
00299 if ( z.dot( btVector3(0, 0, up_direction) ) < 0)
00300 {
00301 z = -1.0 * z;
00302 ROS_INFO("flipped z");
00303 }
00304 ROS_DEBUG("in getPlaneTransform, z: %0.3f, %0.3f, %0.3f", z[0], z[1], z[2]);
00305
00306
00307
00308 btVector3 x(1, 0, 0);
00309 if ( fabs(z.dot(x)) > 1.0 - 1.0e-4) x = btVector3(0, 1, 0);
00310 btVector3 y = z.cross(x).normalized();
00311 x = y.cross(z).normalized();
00312
00313 btMatrix3x3 rotation;
00314 rotation[0] = x;
00315 rotation[1] = y;
00316 rotation[2] = z;
00317 rotation = rotation.transpose();
00318 btQuaternion orientation;
00319 rotation.getRotation(orientation);
00320 return tf::Transform(orientation, position);
00321 }
00322
00323 template <typename PointT>
00324 bool getPlanePoints (const pcl::PointCloud<PointT> &table,
00325 const tf::Transform& table_plane_trans,
00326 sensor_msgs::PointCloud &table_points)
00327 {
00328
00329 table_points.header = table.header;
00330 table_points.points.resize (table.points.size ());
00331 for (size_t i = 0; i < table.points.size (); ++i)
00332 {
00333 table_points.points[i].x = table.points[i].x;
00334 table_points.points[i].y = table.points[i].y;
00335 table_points.points[i].z = table.points[i].z;
00336 }
00337
00338
00339 tf::TransformListener listener;
00340 tf::StampedTransform table_pose_frame(table_plane_trans, table.header.stamp,
00341 table.header.frame_id, "table_frame");
00342 listener.setTransform(table_pose_frame);
00343 std::string error_msg;
00344 if (!listener.canTransform("table_frame", table_points.header.frame_id, table_points.header.stamp, &error_msg))
00345 {
00346 ROS_ERROR("Can not transform point cloud from frame %s to table frame; error %s",
00347 table_points.header.frame_id.c_str(), error_msg.c_str());
00348 return false;
00349 }
00350 try
00351 {
00352 listener.transformPointCloud("table_frame", table_points, table_points);
00353 }
00354 catch (tf::TransformException ex)
00355 {
00356 ROS_ERROR("Failed to transform point cloud from frame %s into table_frame; error %s",
00357 table_points.header.frame_id.c_str(), ex.what());
00358 return false;
00359 }
00360 table_points.header.stamp = table.header.stamp;
00361 table_points.header.frame_id = "table_frame";
00362 return true;
00363 }
00364
00365 template <typename PointT> void
00366 getClustersFromPointCloud2 (const pcl::PointCloud<PointT> &cloud_objects,
00367 const std::vector<pcl::PointIndices> &clusters2,
00368 std::vector<sensor_msgs::PointCloud> &clusters)
00369 {
00370 clusters.resize (clusters2.size ());
00371 for (size_t i = 0; i < clusters2.size (); ++i)
00372 {
00373 clusters[i].header.frame_id = cloud_objects.header.frame_id;
00374 clusters[i].header.stamp = ros::Time(0);
00375 clusters[i].points.resize (clusters2[i].indices.size ());
00376 for (size_t j = 0; j < clusters[i].points.size (); ++j)
00377 {
00378 clusters[i].points[j].x = cloud_objects.points[clusters2[i].indices[j]].x;
00379 clusters[i].points[j].y = cloud_objects.points[clusters2[i].indices[j]].y;
00380 clusters[i].points[j].z = cloud_objects.points[clusters2[i].indices[j]].z;
00381 }
00382 }
00383 }
00384
00385 void TabletopSegmentor::processCloud(const sensor_msgs::PointCloud2 &cloud,
00386 TabletopSegmentation::Response &response)
00387 {
00388 ROS_INFO("Starting process on new cloud");
00389 ROS_INFO("In frame %s", cloud.header.frame_id.c_str());
00390
00391
00392
00393 KdTreePtr normals_tree_, clusters_tree_;
00394 pcl::VoxelGrid<Point> grid_, grid_objects_;
00395 pcl::PassThrough<Point> pass_;
00396 pcl::NormalEstimation<Point, pcl::Normal> n3d_;
00397 pcl::SACSegmentationFromNormals<Point, pcl::Normal> seg_;
00398 pcl::ProjectInliers<Point> proj_;
00399 pcl::ConvexHull<Point> hull_;
00400 pcl::ExtractPolygonalPrismData<Point> prism_;
00401 pcl::EuclideanClusterExtraction<Point> pcl_cluster_;
00402
00403
00404 grid_.setLeafSize (plane_detection_voxel_size_, plane_detection_voxel_size_, plane_detection_voxel_size_);
00405 grid_objects_.setLeafSize (clustering_voxel_size_, clustering_voxel_size_, clustering_voxel_size_);
00406 grid_.setFilterFieldName ("z");
00407 pass_.setFilterFieldName ("z");
00408
00409 pass_.setFilterLimits (z_filter_min_, z_filter_max_);
00410 grid_.setFilterLimits (z_filter_min_, z_filter_max_);
00411 grid_.setDownsampleAllData (false);
00412 grid_objects_.setDownsampleAllData (false);
00413
00414 normals_tree_ = boost::make_shared<pcl::KdTreeFLANN<Point> > ();
00415 clusters_tree_ = boost::make_shared<pcl::KdTreeFLANN<Point> > ();
00416
00417
00418 n3d_.setKSearch (10);
00419 n3d_.setSearchMethod (normals_tree_);
00420
00421 seg_.setDistanceThreshold (0.05);
00422 seg_.setMaxIterations (10000);
00423 seg_.setNormalDistanceWeight (0.1);
00424 seg_.setOptimizeCoefficients (true);
00425 seg_.setModelType (pcl::SACMODEL_NORMAL_PLANE);
00426 seg_.setMethodType (pcl::SAC_RANSAC);
00427 seg_.setProbability (0.99);
00428
00429 proj_.setModelType (pcl::SACMODEL_PLANE);
00430
00431
00432 pcl_cluster_.setClusterTolerance (cluster_distance_);
00433 pcl_cluster_.setMinClusterSize (min_cluster_size_);
00434 pcl_cluster_.setSearchMethod (clusters_tree_);
00435
00436
00437 pcl::PointCloud<Point> cloud_t;
00438 pcl::fromROSMsg (cloud, cloud_t);
00439 pcl::PointCloud<Point>::ConstPtr cloud_ptr = boost::make_shared<const pcl::PointCloud<Point> > (cloud_t);
00440
00441 pcl::PointCloud<Point> cloud_filtered;
00442 pass_.setInputCloud (cloud_ptr);
00443 pass_.filter (cloud_filtered);
00444 pcl::PointCloud<Point>::ConstPtr cloud_filtered_ptr =
00445 boost::make_shared<const pcl::PointCloud<Point> > (cloud_filtered);
00446 ROS_INFO("Step 1 done");
00447 if (cloud_filtered.points.size() < (unsigned int)min_cluster_size_)
00448 {
00449 ROS_INFO("Filtered cloud only has %d points", (int)cloud_filtered.points.size());
00450 response.result = response.NO_TABLE;
00451 return;
00452 }
00453
00454 pcl::PointCloud<Point> cloud_downsampled;
00455 grid_.setInputCloud (cloud_filtered_ptr);
00456 grid_.filter (cloud_downsampled);
00457 pcl::PointCloud<Point>::ConstPtr cloud_downsampled_ptr =
00458 boost::make_shared<const pcl::PointCloud<Point> > (cloud_downsampled);
00459 if (cloud_downsampled.points.size() < (unsigned int)min_cluster_size_)
00460 {
00461 ROS_INFO("Downsampled cloud only has %d points", (int)cloud_downsampled.points.size());
00462 response.result = response.NO_TABLE;
00463 return;
00464 }
00465
00466
00467 pcl::PointCloud<pcl::Normal> cloud_normals;
00468 n3d_.setInputCloud (cloud_downsampled_ptr);
00469 n3d_.compute (cloud_normals);
00470 pcl::PointCloud<pcl::Normal>::ConstPtr cloud_normals_ptr =
00471 boost::make_shared<const pcl::PointCloud<pcl::Normal> > (cloud_normals);
00472 ROS_INFO("Step 2 done");
00473
00474
00475 pcl::PointIndices table_inliers; pcl::ModelCoefficients table_coefficients;
00476 seg_.setInputCloud (cloud_downsampled_ptr);
00477 seg_.setInputNormals (cloud_normals_ptr);
00478 seg_.segment (table_inliers, table_coefficients);
00479 pcl::PointIndices::ConstPtr table_inliers_ptr = boost::make_shared<const pcl::PointIndices> (table_inliers);
00480 pcl::ModelCoefficients::ConstPtr table_coefficients_ptr =
00481 boost::make_shared<const pcl::ModelCoefficients> (table_coefficients);
00482
00483 if (table_coefficients.values.size () <=3)
00484 {
00485 ROS_INFO("Failed to detect table in scan");
00486 response.result = response.NO_TABLE;
00487 return;
00488 }
00489
00490 if ( table_inliers.indices.size() < (unsigned int)inlier_threshold_)
00491 {
00492 ROS_INFO("Plane detection has %d inliers, below min threshold of %d", (int)table_inliers.indices.size(),
00493 inlier_threshold_);
00494 response.result = response.NO_TABLE;
00495 return;
00496 }
00497
00498 ROS_INFO ("[TableObjectDetector::input_callback] Model found with %d inliers: [%f %f %f %f].",
00499 (int)table_inliers.indices.size (),
00500 table_coefficients.values[0], table_coefficients.values[1],
00501 table_coefficients.values[2], table_coefficients.values[3]);
00502 ROS_INFO("Step 3 done");
00503
00504
00505 pcl::PointCloud<Point> table_projected;
00506 proj_.setInputCloud (cloud_downsampled_ptr);
00507 proj_.setIndices (table_inliers_ptr);
00508 proj_.setModelCoefficients (table_coefficients_ptr);
00509 proj_.filter (table_projected);
00510 pcl::PointCloud<Point>::ConstPtr table_projected_ptr =
00511 boost::make_shared<const pcl::PointCloud<Point> > (table_projected);
00512 ROS_INFO("Step 4 done");
00513
00514 sensor_msgs::PointCloud table_points;
00515 tf::Transform table_plane_trans = getPlaneTransform (table_coefficients, up_direction_);
00516
00517
00518 if (!getPlanePoints<Point> (table_projected, table_plane_trans, table_points))
00519 {
00520 response.result = response.OTHER_ERROR;
00521 return;
00522 }
00523 ROS_INFO("Table computed");
00524
00525 response.table = getTable<sensor_msgs::PointCloud>(cloud.header, table_plane_trans, table_points);
00526 response.result = response.SUCCESS;
00527
00528
00529
00530 pcl::PointCloud<Point> table_hull;
00531 hull_.setInputCloud (table_projected_ptr);
00532 hull_.reconstruct (table_hull);
00533 pcl::PointCloud<Point>::ConstPtr table_hull_ptr = boost::make_shared<const pcl::PointCloud<Point> > (table_hull);
00534
00535
00536 pcl::PointIndices cloud_object_indices;
00537
00538 prism_.setInputCloud (cloud_filtered_ptr);
00539 prism_.setInputPlanarHull (table_hull_ptr);
00540 ROS_INFO("Using table prism: %f to %f", table_z_filter_min_, table_z_filter_max_);
00541 prism_.setHeightLimits (table_z_filter_min_, table_z_filter_max_);
00542 prism_.segment (cloud_object_indices);
00543
00544 pcl::PointCloud<Point> cloud_objects;
00545 pcl::ExtractIndices<Point> extract_object_indices;
00546 extract_object_indices.setInputCloud (cloud_filtered_ptr);
00547 extract_object_indices.setIndices (boost::make_shared<const pcl::PointIndices> (cloud_object_indices));
00548 extract_object_indices.filter (cloud_objects);
00549 pcl::PointCloud<Point>::ConstPtr cloud_objects_ptr = boost::make_shared<const pcl::PointCloud<Point> > (cloud_objects);
00550 ROS_INFO (" Number of object point candidates: %d.", (int)cloud_objects.points.size ());
00551
00552 if (cloud_objects.points.empty ())
00553 {
00554 ROS_INFO("No objects on table");
00555 return;
00556 }
00557
00558
00559 pcl::PointCloud<Point> cloud_objects_downsampled;
00560 grid_objects_.setInputCloud (cloud_objects_ptr);
00561 grid_objects_.filter (cloud_objects_downsampled);
00562 pcl::PointCloud<Point>::ConstPtr cloud_objects_downsampled_ptr
00563 = boost::make_shared <const pcl::PointCloud<Point> > (cloud_objects_downsampled);
00564
00565
00566 std::vector<pcl::PointIndices> clusters2;
00567
00568 pcl_cluster_.setInputCloud (cloud_objects_downsampled_ptr);
00569 pcl_cluster_.extract (clusters2);
00570 ROS_INFO ("Number of clusters found matching the given constraints: %d.", (int)clusters2.size ());
00571
00572
00573 std::vector<sensor_msgs::PointCloud> clusters;
00574 getClustersFromPointCloud2<Point> (cloud_objects_downsampled, clusters2, clusters);
00575 ROS_INFO("Clusters converted");
00576 response.clusters = clusters;
00577
00578 publishClusterMarkers(clusters, cloud.header);
00579 }
00580
00581
00582 }
00583
00584 int main(int argc, char **argv)
00585 {
00586 ros::init(argc, argv, "tabletop_segmentation_node");
00587 ros::NodeHandle nh;
00588
00589 tabletop_object_detector::TabletopSegmentor node(nh);
00590 ros::spin();
00591 return 0;
00592 }