Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
inflation_layer.cpp
1 // Copyright (c) 2026, Dexory (Tony Najjar)
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "nav2_costmap_2d/inflation_layer.hpp"
16 
17 #include <limits>
18 #include <map>
19 #include <vector>
20 #include <algorithm>
21 #include <utility>
22 #include <cmath>
23 
24 #ifdef _OPENMP
25 #include <omp.h>
26 #endif
27 
28 #include "nav2_costmap_2d/costmap_math.hpp"
29 #include "nav2_costmap_2d/footprint.hpp"
30 #include "nav2_costmap_2d/distance_transform.hpp"
31 #include "pluginlib/class_list_macros.hpp"
32 #include "rclcpp/parameter_events_filter.hpp"
33 
35 
36 using nav2_costmap_2d::LETHAL_OBSTACLE;
37 using nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE;
38 using nav2_costmap_2d::NO_INFORMATION;
39 using rcl_interfaces::msg::ParameterType;
40 
41 namespace nav2_costmap_2d
42 {
43 
45 : inflation_radius_(0),
46  inscribed_radius_(0),
47  custom_inscribed_radius_(-1.0),
48  cost_scaling_factor_(0),
49  inflate_unknown_(false),
50  inflate_around_unknown_(false),
51  cell_inflation_radius_(0),
52  num_threads_(-1),
53  resolution_(0),
54  last_min_x_(std::numeric_limits<double>::lowest()),
55  last_min_y_(std::numeric_limits<double>::lowest()),
56  last_max_x_(std::numeric_limits<double>::max()),
57  last_max_y_(std::numeric_limits<double>::max())
58 {
59  access_ = new mutex_t();
60 }
61 
63 {
64  delete access_;
65 }
66 
67 void
69 {
70  if (custom_inscribed_radius_ >= 0.0) {
71  RCLCPP_WARN(
72  logger_,
73  "POTENTIAL SAFETY ISSUE!!! Make sure you fully understand the consequences of changing the "
74  "inscribed radius! Inflation layer is set to use a custom inscribed radius of %.3f m instead "
75  "of the footprint's inscribed radius of %.3f m. This can have serious implications on the "
76  "robot's safety and is not recommended unless specifically needed. This practice is intended "
77  "only for controllers that are customized explicitly to feed on such data. It is NOT "
78  "intended for global path planners or setups that depend on the footprint inscribed radius!",
79  custom_inscribed_radius_, layered_costmap_->getInscribedRadius());
80  inscribed_radius_ = custom_inscribed_radius_;
81  } else {
82  inscribed_radius_ = layered_costmap_->getInscribedRadius();
83  }
84 }
85 
86 void
88 {
89  {
90  auto node = node_.lock();
91  if (!node) {
92  throw std::runtime_error{"Failed to lock node"};
93  }
94  enabled_ = node->declare_or_get_parameter(name_ + "." + "enabled", true);
95  inflation_radius_ = node->declare_or_get_parameter(name_ + "." + "inflation_radius", 0.55);
96  custom_inscribed_radius_ = node->declare_or_get_parameter(
97  name_ + "." + "custom_inscribed_radius", -1.0);
98  cost_scaling_factor_ = node->declare_or_get_parameter(
99  name_ + "." + "cost_scaling_factor", 10.0);
100  inflate_unknown_ = node->declare_or_get_parameter(name_ + "." + "inflate_unknown", false);
101  inflate_around_unknown_ = node->declare_or_get_parameter(
102  name_ + "." + "inflate_around_unknown", false);
103  num_threads_ = node->declare_or_get_parameter(
104  name_ + "." + "num_threads", -1);
105  }
107  setCurrent(true);
108  need_reinflation_ = false;
109  matchSize();
110 }
111 
113 {
114  auto node = node_.lock();
115  post_set_params_handler_ = node->add_post_set_parameters_callback(
116  std::bind(
118  this, std::placeholders::_1));
119  on_set_params_handler_ = node->add_on_set_parameters_callback(
120  std::bind(
122  this, std::placeholders::_1));
123 }
124 
126 {
127  auto node = node_.lock();
128  if (post_set_params_handler_ && node) {
129  node->remove_post_set_parameters_callback(post_set_params_handler_.get());
130  }
131  post_set_params_handler_.reset();
132  if (on_set_params_handler_ && node) {
133  node->remove_on_set_parameters_callback(on_set_params_handler_.get());
134  }
135  on_set_params_handler_.reset();
136 }
137 
138 void
140 {
141  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
142  nav2_costmap_2d::Costmap2D * costmap = layered_costmap_->getCostmap();
143  resolution_ = costmap->getResolution();
144  cell_inflation_radius_ = cellDistance(inflation_radius_);
145  computeCaches();
146 }
147 
148 void
150  double /*robot_x*/, double /*robot_y*/, double /*robot_yaw*/, double * min_x,
151  double * min_y, double * max_x, double * max_y)
152 {
153  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
154  if (need_reinflation_) {
155  // Reset last_* to "no expansion" values so the next cycle won't
156  // merge with these full-map bounds (avoids double full-map update after reset)
157  last_min_x_ = last_min_y_ = std::numeric_limits<double>::max();
158  last_max_x_ = last_max_y_ = std::numeric_limits<double>::lowest();
159 
160  *min_x = std::numeric_limits<double>::lowest();
161  *min_y = std::numeric_limits<double>::lowest();
162  *max_x = std::numeric_limits<double>::max();
163  *max_y = std::numeric_limits<double>::max();
164  need_reinflation_ = false;
165  } else {
166  double tmp_min_x = last_min_x_;
167  double tmp_min_y = last_min_y_;
168  double tmp_max_x = last_max_x_;
169  double tmp_max_y = last_max_y_;
170  last_min_x_ = *min_x;
171  last_min_y_ = *min_y;
172  last_max_x_ = *max_x;
173  last_max_y_ = *max_y;
174  *min_x = std::min(tmp_min_x, *min_x) - inflation_radius_;
175  *min_y = std::min(tmp_min_y, *min_y) - inflation_radius_;
176  *max_x = std::max(tmp_max_x, *max_x) + inflation_radius_;
177  *max_y = std::max(tmp_max_y, *max_y) + inflation_radius_;
178  }
179 }
180 
181 int
183 {
184 #ifdef _OPENMP
185  // If num_threads parameter is explicitly set (> 0), use it
186  if (num_threads_ > 0) {
187  RCLCPP_INFO_ONCE(
188  logger_,
189  "OpenMP: Using configured num_threads: %d",
190  num_threads_);
191  return num_threads_;
192  }
193 
194  // Otherwise use auto-detection: half the available cores for memory-bound algorithms
195  // Balances performance with memory bandwidth and safety on constrained systems
196  // Respects OMP_NUM_THREADS environment variable
197  int cpu_cores = omp_get_max_threads();
198  int optimal = std::max(1, cpu_cores / 2);
199 
200  RCLCPP_INFO_ONCE(
201  logger_,
202  "OpenMP: %d cores available, using %d threads (auto)",
203  cpu_cores, optimal);
204 
205  return optimal;
206 #else
207  return 1;
208 #endif
209 }
210 
211 void
213 {
214  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
216  cell_inflation_radius_ = cellDistance(inflation_radius_);
217  computeCaches();
218  need_reinflation_ = true;
219 
220  if (inflation_radius_ < inscribed_radius_) {
221  RCLCPP_WARN(
222  logger_,
223  "The configured inflation radius (%.3f) is smaller than "
224  "the computed/custom inscribed radius (%.3f) of your footprint, "
225  "it is highly recommended to set inflation radius to be at "
226  "least as big as the inscribed radius to avoid collisions",
227  inflation_radius_, inscribed_radius_);
228  }
229 
230  RCLCPP_DEBUG(
231  logger_, "InflationLayer::onFootprintChanged(): num footprint points: %zu,"
232  " inscribed_radius_ = %.3f, inflation_radius_ = %.3f",
233  layered_costmap_->getFootprint().size(), inscribed_radius_, inflation_radius_);
234 }
235 
236 void
238  unsigned char * master_array,
239  const MatrixXfRM & distance_map,
240  int min_i, int min_j, int max_i, int max_j,
241  int roi_min_i, int roi_min_j,
242  unsigned int size_x)
243 {
244  const float cell_inflation_radius_f = static_cast<float>(cell_inflation_radius_);
245  const int lut_max = static_cast<int>(cost_lut_.size() - 1);
246  const unsigned char * lut_data = cost_lut_.data();
247  const int lut_precision = COST_LUT_PRECISION;
248  const bool inflate_unk = inflate_unknown_;
249 
250 #ifdef _OPENMP
251  const int num_threads = getOptimalThreadCount();
252  #pragma omp parallel for num_threads(num_threads) schedule(dynamic, 16)
253 #endif
254  for (int j = min_j; j < max_j; ++j) {
255  const int row_offset = j * static_cast<int>(size_x);
256  const int dist_row = j - roi_min_j;
257 
258  for (int i = min_i; i < max_i; ++i) {
259  const float distance_cells = distance_map(dist_row, i - roi_min_i);
260  if (distance_cells > cell_inflation_radius_f) {
261  continue;
262  }
263 
264  const unsigned int index = row_offset + i;
265  const unsigned char old_cost = master_array[index];
266  const unsigned int d_scaled = std::min(
267  static_cast<unsigned int>(lut_max),
268  static_cast<unsigned int>(distance_cells * lut_precision + 0.5f));
269  const unsigned char cost = lut_data[d_scaled];
270 
271  if (old_cost == NO_INFORMATION &&
272  (inflate_unk ? (cost > FREE_SPACE) : (cost >= INSCRIBED_INFLATED_OBSTACLE)))
273  {
274  master_array[index] = cost;
275  } else {
276  master_array[index] = std::max(old_cost, cost);
277  }
278  }
279  }
280 }
281 
282 void
284  nav2_costmap_2d::Costmap2D & master_grid, int min_i, int min_j,
285  int max_i,
286  int max_j)
287 {
288  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
289  if (!enabled_ || (cell_inflation_radius_ == 0)) {
290  return;
291  }
292 
293  unsigned char * master_array = master_grid.getCharMap();
294  const unsigned int size_x = master_grid.getSizeInCellsX();
295  const unsigned int size_y = master_grid.getSizeInCellsY();
296 
297  min_i = std::max(0, min_i);
298  min_j = std::max(0, min_j);
299  max_i = std::min(static_cast<int>(size_x), max_i);
300  max_j = std::min(static_cast<int>(size_y), max_j);
301 
302  // Compute padded ROI bounds for distance transform
303  const int padding = static_cast<int>(cell_inflation_radius_);
304  int roi_min_i = std::max(0, min_i - padding);
305  int roi_min_j = std::max(0, min_j - padding);
306  int roi_max_i = std::min(static_cast<int>(size_x), max_i + padding);
307  int roi_max_j = std::min(static_cast<int>(size_y), max_j + padding);
308 
309  const int roi_width = roi_max_i - roi_min_i;
310  const int roi_height = roi_max_j - roi_min_j;
311 
312  // Create distance map: obstacles = 0, free space = INF
313  MatrixXfRM distance_map(roi_height, roi_width);
314 
315  // Initialize mask (parallelized)
316 #ifdef _OPENMP
317  const int num_threads = getOptimalThreadCount();
318  #pragma omp parallel for num_threads(num_threads) schedule(dynamic, 16)
319 #endif
320  for (int y = 0; y < roi_height; y++) {
321  const int src_y = y + roi_min_j;
322  for (int x = 0; x < roi_width; x++) {
323  const int src_x = x + roi_min_i;
324  const unsigned char cell = master_array[src_y * size_x + src_x];
325 
326  if (inflate_around_unknown_) {
327  // Treat both LETHAL_OBSTACLE and NO_INFORMATION as obstacles
328  distance_map(y,
329  x) = (cell != LETHAL_OBSTACLE &&
330  cell != NO_INFORMATION) ? DistanceTransform::DT_INF : 0.0f;
331  } else {
332  // Only LETHAL_OBSTACLE is treated as obstacle
333  distance_map(y, x) = (cell != LETHAL_OBSTACLE) ? DistanceTransform::DT_INF : 0.0f;
334  }
335  }
336  }
337 
338  // Perform Felzenszwalb-Huttenlocher distance transform
339  DistanceTransform::distanceTransform2D(distance_map, roi_height, roi_width);
340 
341  // Apply inflation costs
343  master_array, distance_map,
344  min_i, min_j, max_i, max_j,
345  roi_min_i, roi_min_j, size_x);
346 
347  setCurrent(true);
348 }
349 
350 
351 void
353 {
354  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
355  if (cell_inflation_radius_ == 0) {
356  return;
357  }
358 
359  // Generate cost lookup table for distance -> cost mapping
360  const unsigned int max_dist_scaled = cell_inflation_radius_ * COST_LUT_PRECISION + 1;
361  cost_lut_.resize(max_dist_scaled + 1);
362  for (unsigned int d_scaled = 0; d_scaled <= max_dist_scaled; ++d_scaled) {
363  const double distance = static_cast<double>(d_scaled) / COST_LUT_PRECISION;
364  cost_lut_[d_scaled] = computeCost(distance);
365  }
366 }
367 
368 
369 rcl_interfaces::msg::SetParametersResult InflationLayer::validateParameterUpdatesCallback(
370  const std::vector<rclcpp::Parameter> & parameters)
371 {
372  rcl_interfaces::msg::SetParametersResult result;
373  result.successful = true;
374  for (const auto & parameter : parameters) {
375  const auto & param_type = parameter.get_type();
376  const auto & param_name = parameter.get_name();
377  if (param_name.find(name_ + ".") != 0) {
378  continue;
379  }
380  if (param_type == ParameterType::PARAMETER_DOUBLE) {
381  if (param_name != name_ + "." + "custom_inscribed_radius" &&
382  parameter.as_double() < 0.0)
383  {
384  RCLCPP_WARN(
385  logger_, "The value of parameter '%s' is incorrectly set to %f, "
386  "it should be >=0. Ignoring parameter update.",
387  param_name.c_str(), parameter.as_double());
388  result.successful = false;
389  }
390  }
391  }
392  return result;
393 }
394 
395 void
397  const std::vector<rclcpp::Parameter> & parameters)
398 {
399  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
400 
401  bool need_cache_recompute = false;
402 
403  for (const auto & parameter : parameters) {
404  const auto & param_type = parameter.get_type();
405  const auto & param_name = parameter.get_name();
406  if (param_name.find(name_ + ".") != 0) {
407  continue;
408  }
409 
410  if (param_type == ParameterType::PARAMETER_DOUBLE) {
411  if (param_name == name_ + "." + "inflation_radius" &&
412  inflation_radius_ != parameter.as_double())
413  {
414  inflation_radius_ = parameter.as_double();
415  need_reinflation_ = true;
416  need_cache_recompute = true;
417  setCurrent(false);
418  } else if (param_name == name_ + "." + "custom_inscribed_radius" && // NOLINT
419  custom_inscribed_radius_ != parameter.as_double())
420  {
421  custom_inscribed_radius_ = parameter.as_double();
423  need_reinflation_ = true;
424  need_cache_recompute = true;
425  } else if (param_name == name_ + "." + "cost_scaling_factor" && // NOLINT
426  getCostScalingFactor() != parameter.as_double())
427  {
428  cost_scaling_factor_ = parameter.as_double();
429  need_reinflation_ = true;
430  need_cache_recompute = true;
431  setCurrent(false);
432  }
433  } else if (param_type == ParameterType::PARAMETER_INTEGER) {
434  if (param_name == name_ + "." + "num_threads" && // NOLINT
435  num_threads_ != parameter.as_int())
436  {
437  int new_value = parameter.as_int();
438 #ifdef _OPENMP
439  if (new_value < -1) {
440  RCLCPP_WARN(
441  logger_,
442  "Invalid num_threads value %d, must be -1 (auto) or > 0. Ignoring.",
443  new_value);
444  } else {
445  int available_cores = omp_get_max_threads();
446  if (new_value > available_cores) {
447  RCLCPP_WARN(
448  logger_,
449  "num_threads=%d exceeds available cores (%d). Ignoring.",
450  new_value, available_cores);
451  } else {
452  num_threads_ = new_value;
453  RCLCPP_INFO(
454  logger_,
455  "Updated num_threads to %d %s",
456  num_threads_,
457  num_threads_ == -1 ? "(auto)" : "");
458  }
459  }
460 #else
461  RCLCPP_WARN(
462  logger_,
463  "num_threads parameter ignored - OpenMP support not available. "
464  "Inflation layer will use single thread.");
465  num_threads_ = new_value;
466 #endif
467  }
468  } else if (param_type == ParameterType::PARAMETER_BOOL) {
469  if (param_name == name_ + "." + "enabled" && enabled_ != parameter.as_bool()) {
470  enabled_ = parameter.as_bool();
471  need_reinflation_ = true;
472  setCurrent(false);
473  } else if (param_name == name_ + "." + "inflate_unknown" && // NOLINT
474  inflate_unknown_ != parameter.as_bool())
475  {
476  inflate_unknown_ = parameter.as_bool();
477  need_reinflation_ = true;
478  setCurrent(false);
479  } else if (param_name == name_ + "." + "inflate_around_unknown" && // NOLINT
480  inflate_around_unknown_ != parameter.as_bool())
481  {
482  inflate_around_unknown_ = parameter.as_bool();
483  need_reinflation_ = true;
484  setCurrent(false);
485  }
486  }
487  }
488 
489  if (need_cache_recompute) {
490  matchSize();
491  }
492 }
493 
494 } // namespace nav2_costmap_2d
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:69
unsigned char * getCharMap() const
Will return a pointer to the underlying unsigned char array used as the costmap.
Definition: costmap_2d.cpp:260
double getResolution() const
Accessor for the resolution of the costmap.
Definition: costmap_2d.cpp:578
unsigned int getSizeInCellsX() const
Accessor for the x size of the costmap in cells.
Definition: costmap_2d.cpp:548
unsigned int getSizeInCellsY() const
Accessor for the y size of the costmap in cells.
Definition: costmap_2d.cpp:553
static constexpr float DT_INF
Infinity constant for distance transform.
static void distanceTransform2D(MatrixXfRM &img, int height, int width)
Perform 2D Euclidean distance transform using separable passes.
Layer to convolve costmap by robot's radius or footprint using Eigen-based Felzenszwalb-Huttenlocher ...
double getCostScalingFactor() override
Get the cost scaling factor.
void computeCaches()
Generate cost lookup table for distance to cost mapping.
void updateCosts(nav2_costmap_2d::Costmap2D &master_grid, int min_i, int min_j, int max_i, int max_j) override
Update the costs in the master costmap in the window.
int getOptimalThreadCount()
Determine optimal thread count based on system resources.
unsigned int cellDistance(double world_dist)
Convert world distance to cell distance.
void deactivate() override
Deactivate the layer.
void onInitialize() override
Initialization process of layer on startup.
void matchSize() override
Match the size of the master costmap.
void onFootprintChanged() override
Process updates on footprint changes to the inflation layer.
unsigned char computeCost(double distance) const override
Given a distance, compute a cost.
void updateParametersCallback(const std::vector< rclcpp::Parameter > &parameters)
Apply parameter updates after validation This callback is executed when parameters have been successf...
void updateBounds(double robot_x, double robot_y, double robot_yaw, double *min_x, double *min_y, double *max_x, double *max_y) override
Update the bounds of the master costmap by this layer's update dimensions.
rcl_interfaces::msg::SetParametersResult validateParameterUpdatesCallback(const std::vector< rclcpp::Parameter > &parameters)
Validate incoming parameter updates before applying them. This callback is triggered when one or more...
void activate() override
Activate the layer.
void applyInflation(unsigned char *master_array, const MatrixXfRM &distance_map, int min_i, int min_j, int max_i, int max_j, int roi_min_i, int roi_min_j, unsigned int size_x)
Apply inflation costs from distance map to costmap.
void updateInscribedRadius()
Update inscribed_radius_ from custom_inscribed_radius_ or the footprint, logging a warning when the c...
mutex_t * getMutex() override
Get the mutex of the inflation information.
Abstract class for layered costmap plugin implementations.
Definition: layer.hpp:60
void setCurrent(bool current)
Set whether the data in the layer is up to date.
Definition: layer.hpp:147
Costmap2D * getCostmap()
Get the costmap pointer to the master costmap.
double getInscribedRadius()
The radius of a circle centered at the origin of the robot which is just within all points and edges ...
const std::vector< geometry_msgs::msg::Point > & getFootprint()
Returns the latest footprint stored with setFootprint().
Eigen::Matrix< float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > MatrixXfRM
Row-major float matrix type for efficient row-wise access.