15 #include "nav2_costmap_2d/asymmetric_inflation_layer.hpp"
21 #include <unordered_map>
29 #include "nav2_ros_common/node_utils.hpp"
30 #include "pluginlib/class_list_macros.hpp"
31 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
32 #include "geometry_msgs/msg/pose_stamped.hpp"
33 #include "geometry_msgs/msg/transform_stamped.hpp"
37 using nav2_costmap_2d::LETHAL_OBSTACLE;
38 using nav2_costmap_2d::NO_INFORMATION;
39 using rcl_interfaces::msg::ParameterType;
44 AsymmetricInflationLayer::AsymmetricInflationLayer()
45 : cost_scaling_factor_left_(0),
46 cost_scaling_factor_right_(0),
47 goal_distance_threshold_(0)
52 AsymmetricInflationLayer::onInitialize()
55 double temp_tf_tol = 0.0;
57 auto node = node_.lock();
59 throw std::runtime_error{
"Failed to lock node"};
62 enabled_ = node->declare_or_get_parameter(name_ +
"." +
"enabled",
true);
63 inflation_radius_ = node->declare_or_get_parameter(
64 name_ +
"." +
"inflation_radius", 2.0);
65 inflate_unknown_ = node->declare_or_get_parameter(name_ +
"." +
"inflate_unknown",
false);
66 inflate_around_unknown_ = node->declare_or_get_parameter(
67 name_ +
"." +
"inflate_around_unknown",
false);
68 num_threads_ = node->declare_or_get_parameter(
69 name_ +
"." +
"num_threads", -1);
70 cost_scaling_factor_left_ = node->declare_or_get_parameter(
71 name_ +
"." +
"cost_scaling_factor_left", 4.0);
72 cost_scaling_factor_right_ = node->declare_or_get_parameter(
73 name_ +
"." +
"cost_scaling_factor_right", 4.0);
74 plan_topic_ = node->declare_or_get_parameter<std::string>(
75 name_ +
"." +
"plan_topic",
"plan");
76 goal_distance_threshold_ = node->declare_or_get_parameter(
77 name_ +
"." +
"goal_distance_threshold", 1.5);
80 node->get_parameter(
"transform_tolerance", temp_tf_tol);
81 transform_tolerance_ = tf2::durationFromSec(temp_tf_tol);
83 if (inflation_radius_ < 0.0) {
84 throw std::runtime_error(
85 "AsymmetricInflationLayer: inflation_radius must be >= 0");
87 if (cost_scaling_factor_left_ < 0.0) {
88 throw std::runtime_error(
89 "AsymmetricInflationLayer: cost_scaling_factor_left must be >= 0");
91 if (cost_scaling_factor_right_ < 0.0) {
92 throw std::runtime_error(
93 "AsymmetricInflationLayer: cost_scaling_factor_right must be >= 0");
95 if (goal_distance_threshold_ < 0.0) {
96 throw std::runtime_error(
97 "AsymmetricInflationLayer: goal_distance_threshold must be >= 0");
99 if (num_threads_ < -1) {
100 throw std::runtime_error(
101 "AsymmetricInflationLayer: num_threads must be -1 (auto) or > 0");
103 if (temp_tf_tol < 0.0) {
104 throw std::runtime_error(
105 "AsymmetricInflationLayer: transform_tolerance must be >= 0");
108 cost_scaling_factor_ =
109 std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
111 plan_topic_ = joinWithParentNamespace(plan_topic_);
112 path_sub_ = node->create_subscription<nav_msgs::msg::Path>(
115 &AsymmetricInflationLayer::globalPathCallback,
116 this, std::placeholders::_1),
121 need_reinflation_ =
false;
122 asymmetry_active_ =
false;
123 cell_inflation_radius_ = cellDistance(inflation_radius_);
128 AsymmetricInflationLayer::activate()
130 auto node = node_.lock();
132 throw std::runtime_error{
"Failed to lock node"};
134 on_set_params_handler_ = node->add_on_set_parameters_callback(
136 &AsymmetricInflationLayer::validateParameterUpdatesCallback,
137 this, std::placeholders::_1));
138 post_set_params_handler_ = node->add_post_set_parameters_callback(
140 &AsymmetricInflationLayer::updateParametersCallback,
141 this, std::placeholders::_1));
145 AsymmetricInflationLayer::deactivate()
147 auto node = node_.lock();
148 if (on_set_params_handler_ && node) {
149 node->remove_on_set_parameters_callback(on_set_params_handler_.get());
151 on_set_params_handler_.reset();
152 if (post_set_params_handler_ && node) {
153 node->remove_post_set_parameters_callback(post_set_params_handler_.get());
155 post_set_params_handler_.reset();
159 AsymmetricInflationLayer::globalPathCallback(
const nav_msgs::msg::Path::ConstSharedPtr msg)
161 if (latest_global_path_ && *latest_global_path_ == *msg) {
return;}
164 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
166 std::lock_guard<std::mutex> lock(path_mutex_);
167 latest_global_path_ = msg;
170 need_reinflation_ =
true;
175 AsymmetricInflationLayer::matchSize()
177 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
178 InflationLayer::matchSize();
180 computeAsymmetricCaches();
184 AsymmetricInflationLayer::updateBounds(
185 double robot_x,
double robot_y,
double robot_yaw,
double * min_x,
186 double * min_y,
double * max_x,
double * max_y)
188 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
191 current_robot_x_ = robot_x;
192 current_robot_y_ = robot_y;
194 InflationLayer::updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y);
198 AsymmetricInflationLayer::onFootprintChanged()
200 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
201 InflationLayer::onFootprintChanged();
202 computeAsymmetricCaches();
205 std::vector<AsymmetricPathSegment>
206 AsymmetricInflationLayer::extractLocalPathSegments(
209 std::vector<AsymmetricPathSegment> local_path_segments;
210 nav_msgs::msg::Path current_path;
212 std::lock_guard<std::mutex> lock(path_mutex_);
213 if (!latest_global_path_ || latest_global_path_->poses.size() < 2) {
214 return local_path_segments;
216 current_path = *latest_global_path_;
220 std::string global_frame = layered_costmap_->getGlobalFrameID();
221 std::string path_frame = current_path.header.frame_id;
222 bool need_transform = (global_frame != path_frame && !path_frame.empty());
225 geometry_msgs::msg::TransformStamped transform;
226 if (need_transform) {
228 transform = tf_->lookupTransform(
229 global_frame, path_frame, tf2::TimePointZero, transform_tolerance_);
230 }
catch (
const tf2::TransformException & ex) {
231 RCLCPP_WARN_THROTTLE(
232 logger_, *clock_, 1000,
233 "AsymmetricInflationLayer: TF lookup failed (%s -> %s): %s. "
234 "Falling back to symmetric inflation.",
235 path_frame.c_str(), global_frame.c_str(), ex.what());
236 return local_path_segments;
241 geometry_msgs::msg::PoseStamped goal_pose = current_path.poses.back();
242 if (need_transform) {
243 tf2::doTransform(goal_pose, goal_pose, transform);
247 double dist_to_goal = std::hypot(
248 goal_pose.pose.position.x - current_robot_x_,
249 goal_pose.pose.position.y - current_robot_y_);
251 if (dist_to_goal <= goal_distance_threshold_) {
252 return local_path_segments;
256 const double map_min_x = master_grid.
getOriginX();
257 const double map_min_y = master_grid.
getOriginY();
258 const double map_max_x = map_min_x +
260 const double map_max_y = map_min_y +
264 double win_min_x = map_min_x, win_max_x = map_max_x;
265 double win_min_y = map_min_y, win_max_y = map_max_y;
266 if (need_transform) {
268 tf2::fromMsg(transform.transform, tf);
269 const tf2::Transform inv = tf.inverse();
271 win_min_x = win_min_y = std::numeric_limits<double>::max();
272 win_max_x = win_max_y = std::numeric_limits<double>::lowest();
274 const double cxs[4] = {map_min_x, map_max_x, map_max_x, map_min_x};
275 const double cys[4] = {map_min_y, map_min_y, map_max_y, map_max_y};
276 for (
int c = 0; c < 4; ++c) {
277 const tf2::Vector3 p = inv * tf2::Vector3(cxs[c], cys[c], 0.0);
278 win_min_x = std::min(win_min_x, p.x());
279 win_max_x = std::max(win_max_x, p.x());
280 win_min_y = std::min(win_min_y, p.y());
281 win_max_y = std::max(win_max_y, p.y());
286 for (
size_t i = 1; i < current_path.poses.size(); ++i) {
287 const auto & pa = current_path.poses[i - 1].pose.position;
288 const auto & pb = current_path.poses[i].pose.position;
291 const double seg_min_x = std::min(pa.x, pb.x) - inflation_radius_;
292 const double seg_max_x = std::max(pa.x, pb.x) + inflation_radius_;
293 const double seg_min_y = std::min(pa.y, pb.y) - inflation_radius_;
294 const double seg_max_y = std::max(pa.y, pb.y) + inflation_radius_;
295 if (seg_max_x < win_min_x || seg_min_x > win_max_x ||
296 seg_max_y < win_min_y || seg_min_y > win_max_y)
302 double ax = pa.x, ay = pa.y, bx = pb.x, by = pb.y;
303 if (need_transform) {
304 geometry_msgs::msg::PoseStamped transformed_start;
305 geometry_msgs::msg::PoseStamped transformed_end;
306 tf2::doTransform(current_path.poses[i - 1], transformed_start, transform);
307 tf2::doTransform(current_path.poses[i], transformed_end, transform);
308 ax = transformed_start.pose.position.x;
309 ay = transformed_start.pose.position.y;
310 bx = transformed_end.pose.position.x;
311 by = transformed_end.pose.position.y;
314 const double min_x = std::min(ax, bx) - inflation_radius_;
315 const double max_x = std::max(ax, bx) + inflation_radius_;
316 const double min_y = std::min(ay, by) - inflation_radius_;
317 const double max_y = std::max(ay, by) + inflation_radius_;
318 if (max_x < map_min_x || min_x > map_max_x || max_y < map_min_y || min_y > map_max_y) {
323 local_path_segments.push_back({{ax, ay}, {bx, by}});
325 return local_path_segments;
329 AsymmetricInflationLayer::computeObstacleSide(
330 double cx,
double cy,
331 const std::vector<size_t> & candidates,
332 const std::vector<AsymmetricPathSegment> & local_path_segments)
334 const double inflation_radius_sq = inflation_radius_ * inflation_radius_;
336 double min_dist_sq = std::numeric_limits<double>::max();
337 double best_cross = 0.0;
340 for (
size_t p : candidates) {
342 const auto & segment = local_path_segments[p];
343 double ax = segment.start.first;
344 double ay = segment.start.second;
345 double bx = segment.end.first;
346 double by = segment.end.second;
349 double min_x = std::min(ax, bx) - inflation_radius_;
350 double max_x = std::max(ax, bx) + inflation_radius_;
351 if (cx < min_x || cx > max_x) {
continue;}
353 double min_y = std::min(ay, by) - inflation_radius_;
354 double max_y = std::max(ay, by) + inflation_radius_;
355 if (cy < min_y || cy > max_y) {
continue;}
359 double abx = bx - ax;
360 double aby = by - ay;
361 double len_sq = abx * abx + aby * aby;
363 double acx = cx - ax;
364 double acy = cy - ay;
370 double min_segment_length_sq = 1e-6;
373 if (len_sq < min_segment_length_sq) {
374 dist_sq = acx * acx + acy * acy;
378 double t = std::clamp((acx * abx + acy * aby) / len_sq, 0.0, 1.0);
381 double proj_dx = acx - t * abx;
382 double proj_dy = acy - t * aby;
383 dist_sq = proj_dx * proj_dx + proj_dy * proj_dy;
386 cross = abx * acy - aby * acx;
390 if (dist_sq < min_dist_sq) {
391 min_dist_sq = dist_sq;
397 if (min_dist_sq > inflation_radius_sq) {
398 return Side::Neutral;
402 if (best_cross > 0.0) {
return Side::Left;}
403 if (best_cross < 0.0) {
return Side::Right;}
405 return Side::Neutral;
409 AsymmetricInflationLayer::updateCosts(
411 int max_i,
int max_j)
413 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
415 if (!enabled_ || cell_inflation_radius_ == 0) {
420 InflationLayer::updateCosts(master_grid, min_i, min_j, max_i, max_j);
422 std::vector<AsymmetricPathSegment> local_path_segments = extractLocalPathSegments(master_grid);
425 const bool asymmetry_active =
426 !local_path_segments.empty() && cost_scaling_factor_left_ != cost_scaling_factor_right_;
429 if (asymmetry_active != asymmetry_active_) {
430 asymmetry_active_ = asymmetry_active;
431 need_reinflation_ =
true;
436 if (!asymmetry_active) {
442 unsigned char * master_array = master_grid.
getCharMap();
447 const int cmin_i = std::max(0, min_i);
448 const int cmin_j = std::max(0, min_j);
449 const int cmax_i = std::min(
static_cast<int>(size_x), max_i);
450 const int cmax_j = std::min(
static_cast<int>(size_y), max_j);
453 const int padding =
static_cast<int>(cell_inflation_radius_);
454 const int roi_min_i = std::max(0, cmin_i - padding);
455 const int roi_min_j = std::max(0, cmin_j - padding);
456 const int roi_max_i = std::min(
static_cast<int>(size_x), cmax_i + padding);
457 const int roi_max_j = std::min(
static_cast<int>(size_y), cmax_j + padding);
458 const int roi_width = roi_max_i - roi_min_i;
459 const int roi_height = roi_max_j - roi_min_j;
461 auto spatial_hash = buildPathSpatialHash(local_path_segments);
464 master_grid, roi_min_i, roi_min_j, roi_width, roi_height,
465 spatial_hash, local_path_segments);
467 DistanceTransform::distanceTransform2D(dist_map, roi_height, roi_width);
470 master_array, dist_map,
471 cmin_i, cmin_j, cmax_i, cmax_j,
472 roi_min_i, roi_min_j, size_x);
477 std::unordered_map<uint64_t, std::vector<size_t>>
478 AsymmetricInflationLayer::buildPathSpatialHash(
479 const std::vector<AsymmetricPathSegment> & local_path_segments)
481 std::unordered_map<uint64_t, std::vector<size_t>> spatial_hash;
483 for (
size_t p = 0; p < local_path_segments.size(); ++p) {
485 const auto & segment = local_path_segments[p];
486 double ax = segment.start.first;
487 double ay = segment.start.second;
488 double bx = segment.end.first;
489 double by = segment.end.second;
492 double min_x = std::min(ax, bx) - inflation_radius_;
493 double max_x = std::max(ax, bx) + inflation_radius_;
494 double min_y = std::min(ay, by) - inflation_radius_;
495 double max_y = std::max(ay, by) + inflation_radius_;
498 int64_t min_bx =
static_cast<int64_t
>(std::floor(min_x / inflation_radius_));
499 int64_t max_bx =
static_cast<int64_t
>(std::floor(max_x / inflation_radius_));
500 int64_t min_by =
static_cast<int64_t
>(std::floor(min_y / inflation_radius_));
501 int64_t max_by =
static_cast<int64_t
>(std::floor(max_y / inflation_radius_));
503 for (int64_t b_x = min_bx; b_x <= max_bx; ++b_x) {
504 for (int64_t b_y = min_by; b_y <= max_by; ++b_y) {
506 uint64_t key = (
static_cast<uint64_t
>(
static_cast<uint32_t
>(b_x)) << 32) |
507 (
static_cast<uint32_t
>(b_y));
509 spatial_hash[key].push_back(p);
518 AsymmetricInflationLayer::seedDistanceMap(
520 int roi_min_i,
int roi_min_j,
int roi_width,
int roi_height,
521 const std::unordered_map<uint64_t, std::vector<size_t>> & spatial_hash,
522 const std::vector<AsymmetricPathSegment> & local_path_segments)
524 unsigned char * master_array = master_grid.
getCharMap();
529 dist_map.setConstant(DistanceTransform::DT_INF);
531 Side disfavored_side = (cost_scaling_factor_left_ < cost_scaling_factor_right_) ?
532 Side::Left : Side::Right;
533 const int roi_max_i = roi_min_i + roi_width;
534 const int roi_max_j = roi_min_j + roi_height;
537 auto is_traversable = [&](
int nx,
int ny) {
538 unsigned char c = master_array[master_grid.
getIndex(nx, ny)];
539 return inflate_around_unknown_ ?
540 (c != LETHAL_OBSTACLE && c != NO_INFORMATION) : (c != LETHAL_OBSTACLE);
544 for (
int j = roi_min_j; j < roi_max_j; ++j) {
545 for (
int i = roi_min_i; i < roi_max_i; ++i) {
546 unsigned char cost = master_array[master_grid.
getIndex(i, j)];
549 if (cost != LETHAL_OBSTACLE && !(inflate_around_unknown_ && cost == NO_INFORMATION)) {
554 bool is_on_map_edge = (i == 0 || i ==
static_cast<int>(size_x) - 1 ||
555 j == 0 || j ==
static_cast<int>(size_y) - 1);
558 bool is_boundary = is_on_map_edge ||
559 is_traversable(i - 1, j) || is_traversable(i + 1, j) ||
560 is_traversable(i, j - 1) || is_traversable(i, j + 1);
570 int64_t b_x =
static_cast<int64_t
>(std::floor(cx / inflation_radius_));
571 int64_t b_y =
static_cast<int64_t
>(std::floor(cy / inflation_radius_));
572 uint64_t key = (
static_cast<uint64_t
>(
static_cast<uint32_t
>(b_x)) << 32) |
573 static_cast<uint32_t
>(b_y);
577 auto it = spatial_hash.find(key);
578 if (it == spatial_hash.end()) {
583 Side side = computeObstacleSide(cx, cy, it->second, local_path_segments);
584 if (side != Side::Neutral && side == disfavored_side) {
585 dist_map(j - roi_min_j, i - roi_min_i) = 0.0f;
594 AsymmetricInflationLayer::applyInflation(
595 unsigned char * master_array,
597 int min_i,
int min_j,
int max_i,
int max_j,
598 int roi_min_i,
int roi_min_j,
601 if (cost_lut_disfavored_.empty()) {
605 const float cell_inflation_radius_f =
static_cast<float>(cell_inflation_radius_);
606 const int lut_max =
static_cast<int>(cost_lut_disfavored_.size() - 1);
607 const unsigned char * lut_data = cost_lut_disfavored_.data();
608 const int lut_precision = COST_LUT_PRECISION;
611 const int num_threads = getOptimalThreadCount();
612 #pragma omp parallel for num_threads(num_threads) schedule(dynamic, 16)
614 for (
int j = min_j; j < max_j; ++j) {
615 const int row_offset = j *
static_cast<int>(size_x);
616 const int dist_row = j - roi_min_j;
618 for (
int i = min_i; i < max_i; ++i) {
619 const float distance_cells = distance_map(dist_row, i - roi_min_i);
620 if (distance_cells > cell_inflation_radius_f) {
624 const unsigned int index = row_offset + i;
625 const unsigned char old_cost = master_array[index];
626 const unsigned int d_scaled = std::min(
627 static_cast<unsigned int>(lut_max),
628 static_cast<unsigned int>(distance_cells * lut_precision + 0.5f));
629 const unsigned char new_cost = lut_data[d_scaled];
631 if (new_cost > old_cost) {
632 master_array[index] = new_cost;
639 AsymmetricInflationLayer::computeAsymmetricCaches()
641 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
643 if (cell_inflation_radius_ == 0) {
649 const double c_side = std::min(cost_scaling_factor_left_, cost_scaling_factor_right_);
650 const unsigned int max_dist_scaled = cell_inflation_radius_ * COST_LUT_PRECISION + 1;
652 cost_lut_disfavored_.resize(max_dist_scaled + 1);
653 for (
unsigned int d_scaled = 0; d_scaled <= max_dist_scaled; ++d_scaled) {
654 const double distance =
static_cast<double>(d_scaled) / COST_LUT_PRECISION;
655 unsigned char cost = 0;
656 if (distance == 0.0) {
657 cost = LETHAL_OBSTACLE;
658 }
else if (distance * resolution_ <= inscribed_radius_) {
659 cost = INSCRIBED_INFLATED_OBSTACLE;
661 double factor = exp(-c_side * (distance * resolution_ - inscribed_radius_));
662 cost =
static_cast<unsigned char>((INSCRIBED_INFLATED_OBSTACLE - 1) * factor);
664 cost_lut_disfavored_[d_scaled] = cost;
668 rcl_interfaces::msg::SetParametersResult
669 AsymmetricInflationLayer::validateParameterUpdatesCallback(
670 const std::vector<rclcpp::Parameter> & parameters)
674 return InflationLayer::validateParameterUpdatesCallback(parameters);
678 AsymmetricInflationLayer::updateParametersCallback(
679 const std::vector<rclcpp::Parameter> & parameters)
681 std::vector<rclcpp::Parameter> base_parameters;
682 base_parameters.reserve(parameters.size());
683 for (
const auto & parameter : parameters) {
684 if (parameter.get_name() != name_ +
".cost_scaling_factor") {
685 base_parameters.push_back(parameter);
688 InflationLayer::updateParametersCallback(base_parameters);
690 std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
691 bool side_scaling_changed =
false;
693 for (
const auto & parameter : parameters) {
694 const auto & param_type = parameter.get_type();
695 const auto & param_name = parameter.get_name();
696 if (param_name.find(name_ +
".") != 0) {
700 if (param_type == ParameterType::PARAMETER_DOUBLE) {
701 if (param_name == name_ +
".cost_scaling_factor_left" &&
702 cost_scaling_factor_left_ != parameter.as_double())
704 cost_scaling_factor_left_ = parameter.as_double();
705 side_scaling_changed =
true;
706 }
else if (param_name == name_ +
".cost_scaling_factor_right" &&
707 cost_scaling_factor_right_ != parameter.as_double())
709 cost_scaling_factor_right_ = parameter.as_double();
710 side_scaling_changed =
true;
711 }
else if (param_name == name_ +
".goal_distance_threshold" &&
712 goal_distance_threshold_ != parameter.as_double())
714 goal_distance_threshold_ = parameter.as_double();
715 need_reinflation_ =
true;
721 if (side_scaling_changed) {
722 cost_scaling_factor_ =
723 std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
724 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.