perception.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2015 Fetch Robotics Inc.
3  * Author: Michael Ferguson, Sriramvarun Nidamarthy
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <fetch_auto_dock/icp_2d.h>
21 
22 #include <angles/angles.h>
23 #include <tf/tf.h>
24 #include <sensor_msgs/PointCloud2.h>
26 
27 #include <list>
28 #include <queue>
29 #include <string>
30 #include <vector>
31 
32 double getPoseDistance(const geometry_msgs::Pose a,
33  const geometry_msgs::Pose b)
34 {
35  double dx = a.position.x - b.position.x;
36  double dy = a.position.y - b.position.y;
37  return sqrt(dx*dx + dy*dy);
38 }
39 
41  running_(false),
42  tracking_frame_("odom"),
43  found_dock_(false)
44 {
45  ros::NodeHandle pnh("~");
46 
47  // Should we publish the debugging cloud
48  if (!pnh.getParam("debug", debug_))
49  {
50  debug_ = false;
51  }
52 
53  // Create coefficient vectors for a second order butterworth filter
54  // with a cutoff frequency of 10 Hz assuming the loop is updated at 50 Hz
55  // [b, a] = butter(order, cutoff_frequ/half_sampling_freq)
56  // [b, a] = butter(2, 10/25)
57  float b_arr[] = {0.20657, 0.41314, 0.20657};
58  float a_arr[] = {1.00000, -0.36953, 0.19582};
59  std::vector<float> b(b_arr, b_arr + sizeof(b_arr)/sizeof(float));
60  std::vector<float> a(a_arr, a_arr + sizeof(a_arr)/sizeof(float));
61  dock_pose_filter_.reset(new LinearPoseFilter2D(b, a));
62 
63  // Limit the average reprojection error of points onto
64  // the ideal dock. This prevents the robot docking
65  // with something that is very un-dock-like.
66  if (!pnh.getParam("max_alignment_error", max_alignment_error_))
67  {
68  max_alignment_error_ = 0.01;
69  }
70 
71  // Create ideal cloud
72  // Front face is 300mm long
73  for (double y = -0.15; y <= 0.15; y += 0.001)
74  {
75  geometry_msgs::Point p;
76  p.x = p.z = 0.0;
77  p.y = y;
78  ideal_cloud_.push_back(p);
79  front_cloud_.push_back(p);
80  }
81  // Each side is 100mm long, at 45 degree angle
82  for (double x = 0.0; x < 0.05 /*0.0707106*/; x += 0.001)
83  {
84  geometry_msgs::Point p;
85  p.x = x;
86  p.y = 0.15 + x;
87  p.z = 0.0;
88  ideal_cloud_.push_back(p);
89  p.y = -0.15 - x;
90  ideal_cloud_.insert(ideal_cloud_.begin(), p);
91  }
92 
93  // Debugging publishers first
94  if (debug_)
95  {
96  debug_points_ = nh.advertise<sensor_msgs::PointCloud2>("dock_points", 10);
97  }
98 
99  // Init base scan only after publishers are created
100  scan_sub_ = nh.subscribe("base_scan", 1, &DockPerception::callback, this);
101 
102  ROS_INFO_NAMED("perception","Dock perception initialized");
103 }
104 
105 bool DockPerception::start(const geometry_msgs::PoseStamped& pose)
106 {
107  running_ = false;
108  found_dock_ = false;
109  dock_ = pose;
110  running_ = true;
111  return true;
112 }
113 
115 {
116  running_ = false;
117  return true;
118 }
119 
120 bool DockPerception::getPose(geometry_msgs::PoseStamped& pose, std::string frame)
121 {
122  // All of this requires a lock on the dock_
123  boost::mutex::scoped_lock lock(dock_mutex_);
124 
125  if (!found_dock_)
126  return false;
127 
128  if (ros::Time::now() > dock_stamp_ + ros::Duration(0.35))
129  {
130  ROS_DEBUG_NAMED("dock_perception", "Dock pose timed out");
131  return false;
132  }
133 
134  // Check for a valid orientation.
135  tf::Quaternion q;
136  tf::quaternionMsgToTF(dock_.pose.orientation, q);
137  if (!isValid(q))
138  {
139  ROS_ERROR_STREAM_NAMED("perception",
140  "Dock orientation invalid.");
141  ROS_DEBUG_STREAM_NAMED("perception",
142  "Quaternion magnitude is "
143  << q.length()
144  << " Quaternion is ["
145  << q.x() << ", " << q.y() << ", "
146  << q.z() << ", " << q.w() << "]"
147  );
148  return false;
149  }
150 
151  pose = dock_;
152 
153  if (frame != "")
154  {
155  // Transform to desired frame
156  try
157  {
159  pose.header.frame_id,
160  pose.header.stamp,
161  ros::Duration(0.1));
162  listener_.transformPose(frame, pose, pose);
163  }
164  catch (tf::TransformException const &ex)
165  {
166  ROS_WARN_STREAM_THROTTLE(1.0, "Couldn't transform dock pose");
167  return false;
168  }
169  }
170 
171  return found_dock_;
172 }
173 
174 void DockPerception::callback(const sensor_msgs::LaserScanConstPtr& scan)
175 {
176  // Be lazy about search
177  if (!running_)
178  {
179  return;
180  }
181 
182  // Make sure goal is valid (orientation != 0 0 0 0)
183  if (dock_.header.frame_id == "" ||
184  (dock_.pose.orientation.z == 0.0 && dock_.pose.orientation.w == 0.0))
185  {
186  // Lock the dock_
187  boost::mutex::scoped_lock lock(dock_mutex_);
188 
189  // If goal is invalid, set to a point directly ahead of robot
190  for (size_t i = scan->ranges.size()/2; i < scan->ranges.size(); i++)
191  {
192  if (std::isfinite(scan->ranges[i]))
193  {
194  double angle = scan->angle_min + i * scan->angle_increment;
195  dock_.header = scan->header;
196  dock_.pose.position.x = cos(angle) * scan->ranges[i];
197  dock_.pose.position.y = sin(angle) * scan->ranges[i];
198  dock_.pose.orientation.x = 1.0;
199  dock_.pose.orientation.y = 0.0;
200  dock_.pose.orientation.z = 0.0;
201  dock_.pose.orientation.w = 0.0;
202  ROS_DEBUG_NAMED("dock_perception", "Set initial pose to (%f, %f, %f)",
203  dock_.pose.position.x, dock_.pose.position.y,
204  icp_2d::thetaFromQuaternion(dock_.pose.orientation));
205  break;
206  }
207  }
208  }
209 
210  // Make sure goal is in the tracking frame
211  if (dock_.header.frame_id != tracking_frame_)
212  {
213  boost::mutex::scoped_lock lock(dock_mutex_);
214  try
215  {
217  dock_.header.frame_id,
218  dock_.header.stamp,
219  ros::Duration(0.1));
221  }
222  catch (tf::TransformException const &ex)
223  {
224  ROS_WARN_STREAM_THROTTLE(1.0, "Couldn't transform dock pose to tracking frame");
225  return;
226  }
227  ROS_DEBUG_NAMED("dock_perception", "Transformed initial pose to (%f, %f, %f)",
228  dock_.pose.position.x, dock_.pose.position.y,
229  icp_2d::thetaFromQuaternion(dock_.pose.orientation));
230  }
231 
232  // Cluster the laser scan
234  laser_processor::ScanProcessor processor(*scan, mask);
235  processor.splitConnected(0.04); // TODO(enhancement) parameterize
236  processor.removeLessThan(5);
237 
238  // Sort clusters based on distance to last dock
239  std::priority_queue<DockCandidatePtr, std::vector<DockCandidatePtr>, CompareCandidates> candidates;
240  for (std::list<laser_processor::SampleSet*>::iterator i = processor.getClusters().begin();
241  i != processor.getClusters().end();
242  i++)
243  {
244  DockCandidatePtr c = extract(*i);
245  if (c && c->valid(found_dock_))
246  {
247  candidates.push(c);
248  }
249  }
250  ROS_DEBUG_STREAM_NAMED("dock_perception", "Extracted " << candidates.size() << " clusters");
251 
252  // Extract ICP pose/fit on best clusters
253  DockCandidatePtr best;
254  geometry_msgs::Pose best_pose;
255  while (!candidates.empty())
256  {
257  geometry_msgs::Pose pose = dock_.pose;
258  double score = fit(candidates.top(), pose);
259  if (score >= 0)
260  {
261  best = candidates.top();
262  best_pose = pose;
263  break;
264  }
265  else // Let's see what's wrong with this point cloud.
266  {
267  if (debug_)
268  {
269  DockCandidatePtr not_best = candidates.top();
270 
271  // Create point cloud
272  sensor_msgs::PointCloud2 cloud;
273  cloud.header.stamp = scan->header.stamp;
274  cloud.header.frame_id = tracking_frame_;
275  cloud.width = cloud.height = 0;
276 
277  // Allocate space for points
278  sensor_msgs::PointCloud2Modifier cloud_mod(cloud);
279  cloud_mod.setPointCloud2FieldsByString(1, "xyz");
280  cloud_mod.resize(not_best->points.size());
281 
282  // Fill in points
283  sensor_msgs::PointCloud2Iterator<float> cloud_iter(cloud, "x");
284  for (size_t i = 0; i < not_best->points.size(); i++)
285  {
286  cloud_iter[0] = not_best->points[i].x;
287  cloud_iter[1] = not_best->points[i].y;
288  cloud_iter[2] = not_best->points[i].z;
289  ++cloud_iter;
290  }
291  debug_points_.publish(cloud);
292  }
293  }
294  candidates.pop();
295  }
296 
297  // Did we find dock?
298  if (!best)
299  {
300  ROS_DEBUG_NAMED("dock_perception", "DID NOT FIND THE DOCK");
301  return;
302  }
303 
304  ROS_DEBUG_NAMED("dock_perception", "Found the dock.");
305 
306  // Update
307  if (debug_)
308  {
309  // Create point cloud
310  sensor_msgs::PointCloud2 cloud;
311  cloud.header.stamp = scan->header.stamp;
312  cloud.header.frame_id = tracking_frame_;
313  cloud.width = cloud.height = 0;
314 
315  // Allocate space for points
316  sensor_msgs::PointCloud2Modifier cloud_mod(cloud);
317  cloud_mod.setPointCloud2FieldsByString(1, "xyz");
318  cloud_mod.resize(best->points.size());
319 
320  // Fill in points
321  sensor_msgs::PointCloud2Iterator<float> cloud_iter(cloud, "x");
322  for (size_t i = 0; i < best->points.size(); i++)
323  {
324  cloud_iter[0] = best->points[i].x;
325  cloud_iter[1] = best->points[i].y;
326  cloud_iter[2] = best->points[i].z;
327  ++cloud_iter;
328  }
329  debug_points_.publish(cloud);
330  }
331 
332  // Everything after this modifies the dock_
333  boost::mutex::scoped_lock lock(dock_mutex_);
334 
335  // Update stamp
336  dock_.header.stamp = scan->header.stamp;
337  dock_.header.frame_id = tracking_frame_;
338 
339  // If this is the first time we've found dock, take whole pose
340  if (!found_dock_)
341  {
342  dock_.pose = best_pose;
343  // Reset the dock pose filter.
344  dock_pose_filter_->reset();
345  // Set the filter state to the current pose estimate.
346  dock_pose_filter_->setFilterState(dock_.pose, dock_.pose);
347  }
348  else
349  {
350  // Check that pose is not too far from current pose
351  double d = getPoseDistance(dock_.pose, best_pose);
352  if (d > 0.05)
353  {
354  ROS_DEBUG_STREAM_NAMED("dock_perception", "Dock pose jumped: " << d);
355  return;
356  }
357  }
358 
359  // Filter the pose esitmate.
360  dock_.pose = dock_pose_filter_->filter(best_pose);
361  dock_stamp_ = scan->header.stamp;
362  found_dock_ = true;
363 }
364 
366 {
367  DockCandidatePtr candidate(new DockCandidate());
368 
369  tf::Point tf_point;
370  tf::StampedTransform t_frame;
371  try
372  {
374  cluster->header.frame_id,
375  cluster->header.stamp,
376  ros::Duration(0.1));
378  cluster->header.frame_id,
379  ros::Time(0),
380  t_frame);
381  }
382  catch (tf::TransformException const &ex)
383  {
384  ROS_WARN_STREAM_THROTTLE(1.0, "Couldn't transform laser point");
385  return candidate;
386  }
387  // Transform each point into tracking frame
388  size_t i = 0;
389  for (laser_processor::SampleSet::iterator p = cluster->begin();
390  p != cluster->end();
391  p++, i++)
392  {
393  geometry_msgs::PointStamped pt;
394  pt.header = cluster->header;
395  pt.point.x = (*p)->x;
396  pt.point.y = (*p)->y;
397  pt.point.z = 0;
398  tf::pointMsgToTF(pt.point, tf_point);
399  tf_point = t_frame*tf_point;
400  tf::pointTFToMsg(tf_point, pt.point);
401  candidate->points.push_back(pt.point);
402  }
403 
404  // Get distance from cloud center to previous pose
405  geometry_msgs::Point centroid = icp_2d::getCentroid(candidate->points);
406  double dx = centroid.x - dock_.pose.position.x;
407  double dy = centroid.y - dock_.pose.position.y;
408  candidate->dist = (dx*dx + dy*dy);
409 
410  return candidate;
411 }
412 
413 double DockPerception::fit(const DockCandidatePtr& candidate, geometry_msgs::Pose& pose)
414 {
415  // Setup initial pose
416  geometry_msgs::Transform transform;
417  transform.translation.x = pose.position.x;
418  transform.translation.y = pose.position.y;
419  transform.rotation = pose.orientation;
420 
421  // Initial yaw. Presumably the initial goal orientation estimate.
422  tf::Quaternion init_pose, cand_pose;
423  quaternionMsgToTF(pose.orientation, init_pose);
424  if (!isValid(init_pose))
425  {
426  ROS_ERROR_STREAM_NAMED("perception",
427  "Initial dock orientation estimate is invalid.");
428  ROS_DEBUG_STREAM_NAMED("perception",
429  "Quaternion magnitude is "
430  << init_pose.length()
431  << " Quaternion is ["
432  << init_pose.x() << ", " << init_pose.y() << ", "
433  << init_pose.z() << ", " << init_pose.w() << "]"
434  );
435  return -1.0;
436  }
437 
438  // ICP the dock
439  double fitness = icp_2d::alignICP(ideal_cloud_, candidate->points, transform);
440  quaternionMsgToTF(transform.rotation, cand_pose);
441  if (!isValid(cand_pose))
442  {
443  ROS_WARN_STREAM_NAMED("perception",
444  "Dock candidate orientation estimate is invalid.");
445  ROS_DEBUG_STREAM_NAMED("perception",
446  "Quaternion magnitude is "
447  << cand_pose.length()
448  << " Quaternion is ["
449  << cand_pose.x() << ", " << cand_pose.y() << ", "
450  << cand_pose.z() << ", " << cand_pose.w() << "]"
451  );
452  }
453 
454  // If the dock orientation seems flipped, flip it.
455  // Check it by finding the relative roation between the two quaternions.
456  if (fabs(angles::normalize_angle(tf::getYaw(tf::inverse(cand_pose)*init_pose))) > 3.1415*(2.0/3.0) )
457  {
458  transform.rotation = tf::createQuaternionMsgFromYaw(3.1415 + tf::getYaw(transform.rotation));
459  }
460 
461  if (fitness >= 0.0)
462  {
463  // Initialize the number of times we retry if the fitness is bad.
464  double retry = 5;
465  // If the fitness is hosed or the angle is borked, try again.
466  quaternionMsgToTF(transform.rotation, cand_pose);
467  while (retry-- &&
468  (
469  fitness > max_alignment_error_ ||
470  fabs(angles::normalize_angle(tf::getYaw(tf::inverse(cand_pose)*init_pose))) > 3.1415/4.0
471  )
472  )
473  {
474  // Try one more time.
475 
476  // Perturb the pose to try to get it out of the local minima.
477  transform.translation.x += retry*(0.75/100.0)*static_cast<double>((rand() % 200) - 100);
478  transform.translation.y += retry*(0.75/100.0)*static_cast<double>((rand() % 200) - 100);
479  transform.rotation = tf::createQuaternionMsgFromYaw(retry*(0.28/100.0)*double((rand() % 200) - 100) + tf::getYaw(transform.rotation));
480 
481  // Rerun ICP.
482  fitness = icp_2d::alignICP(ideal_cloud_, candidate->points, transform);
483 
484  // If the dock orientation seems flipped, flip it.
485  quaternionMsgToTF(transform.rotation, cand_pose);
486  if (fabs(angles::normalize_angle(tf::getYaw(tf::inverse(cand_pose)*init_pose))) > 3.1415*(2.0/3.0) )
487  {
488  transform.rotation = tf::createQuaternionMsgFromYaw(3.1415 + tf::getYaw(transform.rotation));
489  }
490  }
491 
492  // If the dock orientation is still really borked, fail.
493  quaternionMsgToTF(transform.rotation, cand_pose);
494  if (!isValid(cand_pose))
495  {
496  ROS_ERROR_STREAM_NAMED("perception",
497  "Dock candidate orientation estimate is invalid.");
498  ROS_DEBUG_STREAM_NAMED("perception","Quaternion magnitude is "
499  << cand_pose.length()
500  << " Orientation is ["
501  << cand_pose.x() << ", " << cand_pose.y() << ", "
502  << cand_pose.z() << ", " << cand_pose.w() << "]"
503  );
504  return -1.0;
505  }
506  if (fabs(angles::normalize_angle(tf::getYaw(tf::inverse(cand_pose)*init_pose))) > 3.1415/2.0 )
507  {
508  fitness = -1.0;
509  }
510 
511  // Check that fitness is good enough
512  if (!found_dock_ && fabs(fitness) > max_alignment_error_)
513  {
514  // If not, signal no fit
515  fitness = -1.0;
516  }
517 
518  // If width of candidate is smaller than the width of dock
519  // then the whole dock is not visible...
520  if (candidate->width() < 0.375)
521  {
522  // ... and heading is unreliable when close to dock
523  ROS_DEBUG_STREAM_NAMED("perception", "Dock candidate width is unreliable.");
524  transform.rotation = pose.orientation;
525  fitness = 0.001234;
526  // Probably can use a different algorithm here, if necessary, which it might not be.
527  }
528 
529  // Transform ideal cloud, and store for visualization
530  candidate->points = icp_2d::transform(ideal_cloud_,
531  transform.translation.x,
532  transform.translation.y,
533  icp_2d::thetaFromQuaternion(transform.rotation));
534 
535  // Get pose
536  pose.position.x = transform.translation.x;
537  pose.position.y = transform.translation.y;
538  pose.position.z = transform.translation.z;
539  pose.orientation = transform.rotation;
540  return fitness;
541  }
542 
543  // Signal no fit
544  ROS_DEBUG_NAMED("dock_perception", "Did not converge");
545  ROS_INFO_STREAM("Did not converge.");
546  return -1.0;
547 }
548 
550 {
551  return 1e-3 >= fabs(1.0 - q.length());
552 }
d
#define ROS_INFO_NAMED(name,...)
DockPerception(ros::NodeHandle &nh)
Definition: perception.cpp:40
static void pointMsgToTF(const geometry_msgs::Point &msg_v, Point &bt_v)
#define ROS_WARN_STREAM_THROTTLE(period, args)
#define ROS_DEBUG_STREAM_NAMED(name, args)
void transformPose(const std::string &target_frame, const geometry_msgs::PoseStamped &stamped_in, geometry_msgs::PoseStamped &stamped_out) const
#define ROS_ERROR_STREAM_NAMED(name, args)
Subscriber subscribe(const std::string &topic, uint32_t queue_size, void(T::*fp)(M), T *obj, const TransportHints &transport_hints=TransportHints())
DockCandidatePtr extract(laser_processor::SampleSet *cluster)
Extract a DockCandidate from a cluster, filling in the lengths and slopes of each line found using ra...
Definition: perception.cpp:365
double getPoseDistance(const geometry_msgs::Pose a, const geometry_msgs::Pose b)
Definition: perception.cpp:32
static double getYaw(const Quaternion &bt_q)
bool waitForTransform(const std::string &target_frame, const std::string &source_frame, const ros::Time &time, const ros::Duration &timeout, const ros::Duration &polling_sleep_duration=ros::Duration(0.01), std::string *error_msg=NULL) const
geometry_msgs::PoseStamped dock_
Definition: perception.h:92
std::vector< geometry_msgs::Point > transform(const std::vector< geometry_msgs::Point > &points, double x, double y, double theta)
Transform a vector of points in 2d.
Definition: icp_2d.cpp:41
tf::Vector3 Point
bool getPose(geometry_msgs::PoseStamped &pose, std::string frame="")
Get the pose of the dock.
Definition: perception.cpp:120
ros::Publisher debug_points_
Definition: perception.h:103
TFSIMD_FORCE_INLINE tfScalar angle(const Quaternion &q1, const Quaternion &q2)
boost::mutex dock_mutex_
Definition: perception.h:94
std::vector< geometry_msgs::Point > ideal_cloud_
Definition: perception.h:106
#define ROS_DEBUG_NAMED(name,...)
void publish(const boost::shared_ptr< M > &message) const
std::vector< geometry_msgs::Point > front_cloud_
Definition: perception.h:108
double alignICP(const std::vector< geometry_msgs::Point > source, const std::vector< geometry_msgs::Point > target, geometry_msgs::Transform &transform, size_t max_iterations=10, double min_delta_rmsd=0.000001)
Perform Iterative Closest Point (ICP) algorithm to align two point clouds in a two dimensional plane...
Definition: icp_2d.cpp:289
static void quaternionMsgToTF(const geometry_msgs::Quaternion &msg, Quaternion &bt)
double max_alignment_error_
Definition: perception.h:100
bool getParam(const std::string &key, std::string &s) const
double thetaFromQuaternion(const geometry_msgs::Quaternion &q)
Get the 2d rotation from a quaternion.
Definition: icp_2d.cpp:28
TFSIMD_FORCE_INLINE Quaternion inverse(const Quaternion &q)
An ordered set of Samples.
tfScalar length() const
ros::Subscriber scan_sub_
Definition: perception.h:81
geometry_msgs::Point getCentroid(const std::vector< geometry_msgs::Point > points)
Get the centroid of a set of points.
Definition: icp_2d.cpp:66
Publisher advertise(const std::string &topic, uint32_t queue_size, bool latch=false)
def normalize_angle(angle)
std::string tracking_frame_
Low pass filter object for filtering dock poses.
Definition: perception.h:90
std::list< SampleSetConstPtr > & getClusters()
void callback(const sensor_msgs::LaserScanConstPtr &scan)
Callback to process laser scans.
Definition: perception.cpp:174
bool found_dock_
Definition: perception.h:96
static geometry_msgs::Quaternion createQuaternionMsgFromYaw(double yaw)
#define ROS_INFO_STREAM(args)
void lookupTransform(const std::string &target_frame, const std::string &source_frame, const ros::Time &time, StampedTransform &transform) const
static bool isValid(const tf::Quaternion &q)
Method to check if the quaternion is valid.
Definition: perception.cpp:549
bool start(const geometry_msgs::PoseStamped &pose)
Start dock detection.
Definition: perception.cpp:105
double fit(const DockCandidatePtr &candidate, geometry_msgs::Pose &pose)
Try to fit a dock to candidate.
Definition: perception.cpp:413
static Time now()
LinearPoseFilter2DPtr dock_pose_filter_
Definition: perception.h:87
A cluster which is a candidate for a dock.
A mask for filtering out Samples based on range.
bool stop()
Stop tracking the dock.
Definition: perception.cpp:114
void setPointCloud2FieldsByString(int n_fields,...)
ros::Time dock_stamp_
Definition: perception.h:98
tf::TransformListener listener_
Definition: perception.h:82
#define ROS_WARN_STREAM_NAMED(name, args)
static void pointTFToMsg(const Point &bt_v, geometry_msgs::Point &msg_v)


fetch_open_auto_dock
Author(s): Michael Ferguson, Griswald Brooks
autogenerated on Mon Feb 28 2022 22:21:37