15 #include "nav2_costmap_2d/asymmetric_inflation_layer.hpp"
21 #include <unordered_map>
29 #include "nav2_ros_common/node_utils.hpp"
30 #include "nav2_util/robot_utils.hpp"
31 #include "pluginlib/class_list_macros.hpp"
32 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
33 #include "geometry_msgs/msg/pose_stamped.hpp"
34 #include "geometry_msgs/msg/transform_stamped.hpp"
38 using nav2_costmap_2d::LETHAL_OBSTACLE;
39 using nav2_costmap_2d::NO_INFORMATION;
40 using rcl_interfaces::msg::ParameterType;
45 AsymmetricInflationLayer::AsymmetricInflationLayer()
46 : cost_scaling_factor_left_(0),
47 cost_scaling_factor_right_(0),
48 goal_distance_threshold_(0)
53 AsymmetricInflationLayer::onInitialize()
56 double temp_tf_tol = 0.0;
58 auto node = node_.lock();
60 throw std::runtime_error{
"Failed to lock node"};
63 enabled_ = node->declare_or_get_parameter(name_ +
"." +
"enabled",
true);
64 inflation_radius_ = node->declare_or_get_parameter(
65 name_ +
"." +
"inflation_radius", 2.0);
66 inflate_unknown_ = node->declare_or_get_parameter(name_ +
"." +
"inflate_unknown",
false);
67 inflate_around_unknown_ = node->declare_or_get_parameter(
68 name_ +
"." +
"inflate_around_unknown",
false);
69 num_threads_ = node->declare_or_get_parameter(
70 name_ +
"." +
"num_threads", -1);
71 cost_scaling_factor_left_ = node->declare_or_get_parameter(
72 name_ +
"." +
"cost_scaling_factor_left", 4.0);
73 cost_scaling_factor_right_ = node->declare_or_get_parameter(
74 name_ +
"." +
"cost_scaling_factor_right", 4.0);
75 plan_topic_ = node->declare_or_get_parameter<std::string>(
76 name_ +
"." +
"plan_topic",
"plan");
77 goal_distance_threshold_ = node->declare_or_get_parameter(
78 name_ +
"." +
"goal_distance_threshold", 1.5);
79 transform_staleness_threshold_ = node->declare_or_get_parameter(
80 name_ +
"." +
"transform_staleness_threshold", 0.0);
83 node->get_parameter(
"transform_tolerance", temp_tf_tol);
84 transform_tolerance_ = tf2::durationFromSec(temp_tf_tol);
86 if (inflation_radius_ < 0.0) {
87 throw std::runtime_error(
88 "AsymmetricInflationLayer: inflation_radius must be >= 0");
90 if (cost_scaling_factor_left_ < 0.0) {
91 throw std::runtime_error(
92 "AsymmetricInflationLayer: cost_scaling_factor_left must be >= 0");
94 if (cost_scaling_factor_right_ < 0.0) {
95 throw std::runtime_error(
96 "AsymmetricInflationLayer: cost_scaling_factor_right must be >= 0");
98 if (goal_distance_threshold_ < 0.0) {
99 throw std::runtime_error(
100 "AsymmetricInflationLayer: goal_distance_threshold must be >= 0");
102 if (num_threads_ < -1) {
103 throw std::runtime_error(
104 "AsymmetricInflationLayer: num_threads must be -1 (auto) or > 0");
106 if (temp_tf_tol < 0.0) {
107 throw std::runtime_error(
108 "AsymmetricInflationLayer: transform_tolerance must be >= 0");
111 cost_scaling_factor_ =
112 std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
114 plan_topic_ = joinWithParentNamespace(plan_topic_);
115 path_sub_ = node->create_subscription<nav_msgs::msg::Path>(
118 &AsymmetricInflationLayer::globalPathCallback,
119 this, std::placeholders::_1),
124 need_reinflation_ =
false;
125 asymmetry_active_ =
false;
126 cell_inflation_radius_ = cellDistance(inflation_radius_);
131 AsymmetricInflationLayer::activate()
133 auto node = node_.lock();
135 throw std::runtime_error{
"Failed to lock node"};
137 on_set_params_handler_ = node->add_on_set_parameters_callback(
139 &AsymmetricInflationLayer::validateParameterUpdatesCallback,
140 this, std::placeholders::_1));
141 post_set_params_handler_ = node->add_post_set_parameters_callback(
143 &AsymmetricInflationLayer::updateParametersCallback,
144 this, std::placeholders::_1));
148 AsymmetricInflationLayer::deactivate()
150 auto node = node_.lock();
151 if (on_set_params_handler_ && node) {
152 node->remove_on_set_parameters_callback(on_set_params_handler_.get());
154 on_set_params_handler_.reset();
155 if (post_set_params_handler_ && node) {
156 node->remove_post_set_parameters_callback(post_set_params_handler_.get());
158 post_set_params_handler_.reset();
162 AsymmetricInflationLayer::globalPathCallback(
const nav_msgs::msg::Path::ConstSharedPtr msg)
164 if (latest_global_path_ && *latest_global_path_ == *msg) {
return;}
167 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
169 std::lock_guard<std::mutex> lock(path_mutex_);
170 latest_global_path_ = msg;
173 need_reinflation_ =
true;
178 AsymmetricInflationLayer::matchSize()
180 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
181 InflationLayer::matchSize();
183 computeAsymmetricCaches();
187 AsymmetricInflationLayer::updateBounds(
188 double robot_x,
double robot_y,
double robot_yaw,
double * min_x,
189 double * min_y,
double * max_x,
double * max_y)
191 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
194 current_robot_x_ = robot_x;
195 current_robot_y_ = robot_y;
197 InflationLayer::updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y);
201 AsymmetricInflationLayer::onFootprintChanged()
203 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
204 InflationLayer::onFootprintChanged();
205 computeAsymmetricCaches();
208 std::vector<AsymmetricPathSegment>
209 AsymmetricInflationLayer::extractLocalPathSegments(
212 std::vector<AsymmetricPathSegment> local_path_segments;
213 nav_msgs::msg::Path current_path;
215 std::lock_guard<std::mutex> lock(path_mutex_);
216 if (!latest_global_path_ || latest_global_path_->poses.size() < 2) {
217 return local_path_segments;
219 current_path = *latest_global_path_;
223 std::string global_frame = layered_costmap_->getGlobalFrameID();
224 std::string path_frame = current_path.header.frame_id;
225 bool need_transform = (global_frame != path_frame && !path_frame.empty());
228 geometry_msgs::msg::TransformStamped transform;
229 if (need_transform) {
230 if (!nav2_util::lookupTransformWithStalenessCheck(
231 *tf_, global_frame, path_frame, clock_->now(),
232 transform_staleness_threshold_, transform))
234 RCLCPP_WARN_THROTTLE(
235 logger_, *clock_, 1000,
236 "AsymmetricInflationLayer: TF lookup failed or returned stale data (%s -> %s). "
237 "Falling back to symmetric inflation.",
238 path_frame.c_str(), global_frame.c_str());
239 return local_path_segments;
244 geometry_msgs::msg::PoseStamped goal_pose = current_path.poses.back();
245 if (need_transform) {
246 tf2::doTransform(goal_pose, goal_pose, transform);
250 double dist_to_goal = std::hypot(
251 goal_pose.pose.position.x - current_robot_x_,
252 goal_pose.pose.position.y - current_robot_y_);
254 if (dist_to_goal <= goal_distance_threshold_) {
255 return local_path_segments;
259 const double map_min_x = master_grid.
getOriginX();
260 const double map_min_y = master_grid.
getOriginY();
261 const double map_max_x = map_min_x +
263 const double map_max_y = map_min_y +
267 double win_min_x = map_min_x, win_max_x = map_max_x;
268 double win_min_y = map_min_y, win_max_y = map_max_y;
269 if (need_transform) {
271 tf2::fromMsg(transform.transform, tf);
272 const tf2::Transform inv = tf.inverse();
274 win_min_x = win_min_y = std::numeric_limits<double>::max();
275 win_max_x = win_max_y = std::numeric_limits<double>::lowest();
277 const double cxs[4] = {map_min_x, map_max_x, map_max_x, map_min_x};
278 const double cys[4] = {map_min_y, map_min_y, map_max_y, map_max_y};
279 for (
int c = 0; c < 4; ++c) {
280 const tf2::Vector3 p = inv * tf2::Vector3(cxs[c], cys[c], 0.0);
281 win_min_x = std::min(win_min_x, p.x());
282 win_max_x = std::max(win_max_x, p.x());
283 win_min_y = std::min(win_min_y, p.y());
284 win_max_y = std::max(win_max_y, p.y());
289 for (
size_t i = 1; i < current_path.poses.size(); ++i) {
290 const auto & pa = current_path.poses[i - 1].pose.position;
291 const auto & pb = current_path.poses[i].pose.position;
294 const double seg_min_x = std::min(pa.x, pb.x) - inflation_radius_;
295 const double seg_max_x = std::max(pa.x, pb.x) + inflation_radius_;
296 const double seg_min_y = std::min(pa.y, pb.y) - inflation_radius_;
297 const double seg_max_y = std::max(pa.y, pb.y) + inflation_radius_;
298 if (seg_max_x < win_min_x || seg_min_x > win_max_x ||
299 seg_max_y < win_min_y || seg_min_y > win_max_y)
305 double ax = pa.x, ay = pa.y, bx = pb.x, by = pb.y;
306 if (need_transform) {
307 geometry_msgs::msg::PoseStamped transformed_start;
308 geometry_msgs::msg::PoseStamped transformed_end;
309 tf2::doTransform(current_path.poses[i - 1], transformed_start, transform);
310 tf2::doTransform(current_path.poses[i], transformed_end, transform);
311 ax = transformed_start.pose.position.x;
312 ay = transformed_start.pose.position.y;
313 bx = transformed_end.pose.position.x;
314 by = transformed_end.pose.position.y;
317 const double min_x = std::min(ax, bx) - inflation_radius_;
318 const double max_x = std::max(ax, bx) + inflation_radius_;
319 const double min_y = std::min(ay, by) - inflation_radius_;
320 const double max_y = std::max(ay, by) + inflation_radius_;
321 if (max_x < map_min_x || min_x > map_max_x || max_y < map_min_y || min_y > map_max_y) {
326 local_path_segments.push_back({{ax, ay}, {bx, by}});
328 return local_path_segments;
332 AsymmetricInflationLayer::computeObstacleSide(
333 double cx,
double cy,
334 const std::vector<size_t> & candidates,
335 const std::vector<AsymmetricPathSegment> & local_path_segments)
337 const double inflation_radius_sq = inflation_radius_ * inflation_radius_;
339 double min_dist_sq = std::numeric_limits<double>::max();
340 double best_cross = 0.0;
343 for (
size_t p : candidates) {
345 const auto & segment = local_path_segments[p];
346 double ax = segment.start.first;
347 double ay = segment.start.second;
348 double bx = segment.end.first;
349 double by = segment.end.second;
352 double min_x = std::min(ax, bx) - inflation_radius_;
353 double max_x = std::max(ax, bx) + inflation_radius_;
354 if (cx < min_x || cx > max_x) {
continue;}
356 double min_y = std::min(ay, by) - inflation_radius_;
357 double max_y = std::max(ay, by) + inflation_radius_;
358 if (cy < min_y || cy > max_y) {
continue;}
362 double abx = bx - ax;
363 double aby = by - ay;
364 double len_sq = abx * abx + aby * aby;
366 double acx = cx - ax;
367 double acy = cy - ay;
373 double min_segment_length_sq = 1e-6;
376 if (len_sq < min_segment_length_sq) {
377 dist_sq = acx * acx + acy * acy;
381 double t = std::clamp((acx * abx + acy * aby) / len_sq, 0.0, 1.0);
384 double proj_dx = acx - t * abx;
385 double proj_dy = acy - t * aby;
386 dist_sq = proj_dx * proj_dx + proj_dy * proj_dy;
389 cross = abx * acy - aby * acx;
393 if (dist_sq < min_dist_sq) {
394 min_dist_sq = dist_sq;
400 if (min_dist_sq > inflation_radius_sq) {
401 return Side::Neutral;
405 if (best_cross > 0.0) {
return Side::Left;}
406 if (best_cross < 0.0) {
return Side::Right;}
408 return Side::Neutral;
412 AsymmetricInflationLayer::updateCosts(
414 int max_i,
int max_j)
416 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
418 if (!enabled_ || cell_inflation_radius_ == 0) {
423 InflationLayer::updateCosts(master_grid, min_i, min_j, max_i, max_j);
425 std::vector<AsymmetricPathSegment> local_path_segments = extractLocalPathSegments(master_grid);
428 const bool asymmetry_active =
429 !local_path_segments.empty() && cost_scaling_factor_left_ != cost_scaling_factor_right_;
432 if (asymmetry_active != asymmetry_active_) {
433 asymmetry_active_ = asymmetry_active;
434 need_reinflation_ =
true;
439 if (!asymmetry_active) {
445 unsigned char * master_array = master_grid.
getCharMap();
450 const int cmin_i = std::max(0, min_i);
451 const int cmin_j = std::max(0, min_j);
452 const int cmax_i = std::min(
static_cast<int>(size_x), max_i);
453 const int cmax_j = std::min(
static_cast<int>(size_y), max_j);
456 const int padding =
static_cast<int>(cell_inflation_radius_);
457 const int roi_min_i = std::max(0, cmin_i - padding);
458 const int roi_min_j = std::max(0, cmin_j - padding);
459 const int roi_max_i = std::min(
static_cast<int>(size_x), cmax_i + padding);
460 const int roi_max_j = std::min(
static_cast<int>(size_y), cmax_j + padding);
461 const int roi_width = roi_max_i - roi_min_i;
462 const int roi_height = roi_max_j - roi_min_j;
464 auto spatial_hash = buildPathSpatialHash(local_path_segments);
467 master_grid, roi_min_i, roi_min_j, roi_width, roi_height,
468 spatial_hash, local_path_segments);
470 DistanceTransform::distanceTransform2D(dist_map, roi_height, roi_width);
473 master_array, dist_map,
474 cmin_i, cmin_j, cmax_i, cmax_j,
475 roi_min_i, roi_min_j, size_x);
480 std::unordered_map<uint64_t, std::vector<size_t>>
481 AsymmetricInflationLayer::buildPathSpatialHash(
482 const std::vector<AsymmetricPathSegment> & local_path_segments)
484 std::unordered_map<uint64_t, std::vector<size_t>> spatial_hash;
486 for (
size_t p = 0; p < local_path_segments.size(); ++p) {
488 const auto & segment = local_path_segments[p];
489 double ax = segment.start.first;
490 double ay = segment.start.second;
491 double bx = segment.end.first;
492 double by = segment.end.second;
495 double min_x = std::min(ax, bx) - inflation_radius_;
496 double max_x = std::max(ax, bx) + inflation_radius_;
497 double min_y = std::min(ay, by) - inflation_radius_;
498 double max_y = std::max(ay, by) + inflation_radius_;
501 int64_t min_bx =
static_cast<int64_t
>(std::floor(min_x / inflation_radius_));
502 int64_t max_bx =
static_cast<int64_t
>(std::floor(max_x / inflation_radius_));
503 int64_t min_by =
static_cast<int64_t
>(std::floor(min_y / inflation_radius_));
504 int64_t max_by =
static_cast<int64_t
>(std::floor(max_y / inflation_radius_));
506 for (int64_t b_x = min_bx; b_x <= max_bx; ++b_x) {
507 for (int64_t b_y = min_by; b_y <= max_by; ++b_y) {
509 uint64_t key = (
static_cast<uint64_t
>(
static_cast<uint32_t
>(b_x)) << 32) |
510 (
static_cast<uint32_t
>(b_y));
512 spatial_hash[key].push_back(p);
521 AsymmetricInflationLayer::seedDistanceMap(
523 int roi_min_i,
int roi_min_j,
int roi_width,
int roi_height,
524 const std::unordered_map<uint64_t, std::vector<size_t>> & spatial_hash,
525 const std::vector<AsymmetricPathSegment> & local_path_segments)
527 unsigned char * master_array = master_grid.
getCharMap();
532 dist_map.setConstant(DistanceTransform::DT_INF);
534 Side disfavored_side = (cost_scaling_factor_left_ < cost_scaling_factor_right_) ?
535 Side::Left : Side::Right;
536 const int roi_max_i = roi_min_i + roi_width;
537 const int roi_max_j = roi_min_j + roi_height;
540 auto is_traversable = [&](
int nx,
int ny) {
541 unsigned char c = master_array[master_grid.
getIndex(nx, ny)];
542 return inflate_around_unknown_ ?
543 (c != LETHAL_OBSTACLE && c != NO_INFORMATION) : (c != LETHAL_OBSTACLE);
547 for (
int j = roi_min_j; j < roi_max_j; ++j) {
548 for (
int i = roi_min_i; i < roi_max_i; ++i) {
549 unsigned char cost = master_array[master_grid.
getIndex(i, j)];
552 if (cost != LETHAL_OBSTACLE && !(inflate_around_unknown_ && cost == NO_INFORMATION)) {
557 bool is_on_map_edge = (i == 0 || i ==
static_cast<int>(size_x) - 1 ||
558 j == 0 || j ==
static_cast<int>(size_y) - 1);
561 bool is_boundary = is_on_map_edge ||
562 is_traversable(i - 1, j) || is_traversable(i + 1, j) ||
563 is_traversable(i, j - 1) || is_traversable(i, j + 1);
573 int64_t b_x =
static_cast<int64_t
>(std::floor(cx / inflation_radius_));
574 int64_t b_y =
static_cast<int64_t
>(std::floor(cy / inflation_radius_));
575 uint64_t key = (
static_cast<uint64_t
>(
static_cast<uint32_t
>(b_x)) << 32) |
576 static_cast<uint32_t
>(b_y);
580 auto it = spatial_hash.find(key);
581 if (it == spatial_hash.end()) {
586 Side side = computeObstacleSide(cx, cy, it->second, local_path_segments);
587 if (side != Side::Neutral && side == disfavored_side) {
588 dist_map(j - roi_min_j, i - roi_min_i) = 0.0f;
597 AsymmetricInflationLayer::applyInflation(
598 unsigned char * master_array,
600 int min_i,
int min_j,
int max_i,
int max_j,
601 int roi_min_i,
int roi_min_j,
604 if (cost_lut_disfavored_.empty()) {
608 const float cell_inflation_radius_f =
static_cast<float>(cell_inflation_radius_);
609 const int lut_max =
static_cast<int>(cost_lut_disfavored_.size() - 1);
610 const unsigned char * lut_data = cost_lut_disfavored_.data();
611 const int lut_precision = COST_LUT_PRECISION;
614 const int num_threads = getOptimalThreadCount();
615 #pragma omp parallel for num_threads(num_threads) schedule(dynamic, 16)
617 for (
int j = min_j; j < max_j; ++j) {
618 const int row_offset = j *
static_cast<int>(size_x);
619 const int dist_row = j - roi_min_j;
621 for (
int i = min_i; i < max_i; ++i) {
622 const float distance_cells = distance_map(dist_row, i - roi_min_i);
623 if (distance_cells > cell_inflation_radius_f) {
627 const unsigned int index = row_offset + i;
628 const unsigned char old_cost = master_array[index];
629 const unsigned int d_scaled = std::min(
630 static_cast<unsigned int>(lut_max),
631 static_cast<unsigned int>(distance_cells * lut_precision + 0.5f));
632 const unsigned char new_cost = lut_data[d_scaled];
634 if (new_cost > old_cost) {
635 master_array[index] = new_cost;
642 AsymmetricInflationLayer::computeAsymmetricCaches()
644 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
646 if (cell_inflation_radius_ == 0) {
652 const double c_side = std::min(cost_scaling_factor_left_, cost_scaling_factor_right_);
653 const unsigned int max_dist_scaled = cell_inflation_radius_ * COST_LUT_PRECISION + 1;
655 cost_lut_disfavored_.resize(max_dist_scaled + 1);
656 for (
unsigned int d_scaled = 0; d_scaled <= max_dist_scaled; ++d_scaled) {
657 const double distance =
static_cast<double>(d_scaled) / COST_LUT_PRECISION;
658 unsigned char cost = 0;
659 if (distance == 0.0) {
660 cost = LETHAL_OBSTACLE;
661 }
else if (distance * resolution_ <= inscribed_radius_) {
662 cost = INSCRIBED_INFLATED_OBSTACLE;
664 double factor = exp(-c_side * (distance * resolution_ - inscribed_radius_));
665 cost =
static_cast<unsigned char>((INSCRIBED_INFLATED_OBSTACLE - 1) * factor);
667 cost_lut_disfavored_[d_scaled] = cost;
671 rcl_interfaces::msg::SetParametersResult
672 AsymmetricInflationLayer::validateParameterUpdatesCallback(
673 const std::vector<rclcpp::Parameter> & parameters)
677 return InflationLayer::validateParameterUpdatesCallback(parameters);
681 AsymmetricInflationLayer::updateParametersCallback(
682 const std::vector<rclcpp::Parameter> & parameters)
684 std::vector<rclcpp::Parameter> base_parameters;
685 base_parameters.reserve(parameters.size());
686 for (
const auto & parameter : parameters) {
687 if (parameter.get_name() != name_ +
".cost_scaling_factor") {
688 base_parameters.push_back(parameter);
691 InflationLayer::updateParametersCallback(base_parameters);
693 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
694 bool side_scaling_changed =
false;
696 for (
const auto & parameter : parameters) {
697 const auto & param_type = parameter.get_type();
698 const auto & param_name = parameter.get_name();
699 if (param_name.find(name_ +
".") != 0) {
703 if (param_type == ParameterType::PARAMETER_DOUBLE) {
704 if (param_name == name_ +
".cost_scaling_factor_left" &&
705 cost_scaling_factor_left_ != parameter.as_double())
707 cost_scaling_factor_left_ = parameter.as_double();
708 side_scaling_changed =
true;
709 }
else if (param_name == name_ +
".cost_scaling_factor_right" &&
710 cost_scaling_factor_right_ != parameter.as_double())
712 cost_scaling_factor_right_ = parameter.as_double();
713 side_scaling_changed =
true;
714 }
else if (param_name == name_ +
".goal_distance_threshold" &&
715 goal_distance_threshold_ != parameter.as_double())
717 goal_distance_threshold_ = parameter.as_double();
718 need_reinflation_ =
true;
720 }
else if (param_name == name_ +
".transform_staleness_threshold") {
721 transform_staleness_threshold_ = parameter.as_double();
722 need_reinflation_ =
true;
728 if (side_scaling_changed) {
729 cost_scaling_factor_ =
730 std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
731 need_reinflation_ =
true;
A QoS profile for standard reliable topics with a history of 10 messages.
Costmap layer that inflates obstacles asymmetrically relative to the global path, biasing the navigab...
A 2D costmap provides a mapping between points in the world and their associated "costs".
void mapToWorld(unsigned int mx, unsigned int my, double &wx, double &wy) const
Convert from map coordinates to world coordinates.
unsigned int getIndex(unsigned int mx, unsigned int my) const
Given two map coordinates... compute the associated index.
unsigned char * getCharMap() const
Will return a pointer to the underlying unsigned char array used as the costmap.
double getResolution() const
Accessor for the resolution of the costmap.
unsigned int getSizeInCellsX() const
Accessor for the x size of the costmap in cells.
double getOriginY() const
Accessor for the y origin of the costmap.
unsigned int getSizeInCellsY() const
Accessor for the y size of the costmap in cells.
double getOriginX() const
Accessor for the x origin of the costmap.
Abstract class for layered costmap plugin implementations.
Eigen::Matrix< float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > MatrixXfRM
Row-major float matrix type for efficient row-wise access.