FixTheRobot Explore Robotics / Systems Engineering / Engineering Guides / Module 13

Engineering Guides · Module 13

Tuning costmaps, inflation and footprint

Most “the robot will not go through that door” and “it scrapes the wall” problems are costmap numbers. Start from the official values, then change one parameter from evidence.

  • Advanced
  • Module 13: Navigation Stack & Autonomous Motion Planning
  • Written for ROS 2 Jazzy where ROS 2 is used
  • Sources checked 2026-11-04

Assumptions: Nav2 on ROS 2 Jazzy, planar robot with a LaserScan. The YAML is the costmap section of the official nav2_params.yaml (jazzy branch, fetched 2026-11-04) and is parsed by the site’s tests with every key checked against that file. The inflation-cost formula is the layer’s documented exponential model as recalled by the author and was not re-fetched; the example is executed by the tests but does not prove the formula matches your Nav2 build. Nothing here has been run in Nav2 by the site’s tests.

1. Engineering problem

The costmap is the planner’s picture of where the robot may go and how close to obstacles it should stay. If the robot is modelled too large, or the inflation too wide, doors become impassable; too small and it brushes obstacles.

2. When is this needed?

  • Bringing up Nav2 on a new robot.
  • Robot refuses narrow passages, hesitates near obstacles, or clips corners.
  • Changing the LiDAR, its mounting, or the robot’s size.

3. Selection and design decisions

Parameters (names confirmed from the official file)

Costmap parameters
ParameterRoleTune when
robot_radiusCircular robot footprint radius; cells closer than this to an obstacle are impassableAlways set to the real circumscribed radius plus a small padding; wrong values dominate everything
inflation_layer.inflation_radiusHow far from an obstacle cost is spreadRobot hugs walls (increase) or cannot pass doors (decrease)
inflation_layer.cost_scaling_factorExponential decay of cost with distance: larger → cost drops faster, planner may run closer to obstaclesPath keeps too close to walls (decrease) or detours too much (increase)
resolutionMetres per cellFiner grids resolve narrow gaps but cost CPU and memory
update_frequency, publish_frequencyCostmap update and publish rates (local is faster than global)Latency to see new obstacles vs CPU
observation_sources and scan.*Which sensors mark and clear obstacles; obstacle_max_range limits marking, raytrace_max_range limits clearingGhost obstacles remain (clearing range too small) or distant clutter appears (marking range too large)
rolling_window, width, heightLocal costmap follows the robot in a window of this size (metres)Controller needs to see farther ahead
track_unknown_spaceKeep unknown cells distinct from freePlanning through unexplored space is wrong for your application

4. Advantages and disadvantages

  • Wide inflation: keeps the robot away from walls (safer, smoother), but narrows or closes corridors.
  • Fine resolution: accurate near obstacles, more CPU. 5 cm is the official default; coarser is cheaper and rounds gaps closed.
  • Long marking range: early obstacle awareness, more clutter and false obstacles.

5. Requirements and constraints

  • A corridor narrower than 2 × robot_radius is impassable in the model; the planner can use it, with cost, only if wider than that.
  • The footprint (radius) must cover the robot at every orientation, including sensor masts and bumpers.
  • Sensor frames and the LiDAR height must be right for the scan to mark obstacles at all.

6. Architecture

/scan ──▶ obstacle/voxel layer ──┐
/map  ──▶ static layer ──────────┼──▶ inflation layer ──▶ costmap ──▶ planner / controller
(odom frame: local costmap; map frame: global costmap)

7. Implementation

The costmap section of the official Jazzy parameter file (parsed and name-checked by the site’s tests; global costmap has a static and obstacle layer, local costmap a voxel layer, both inflate):

local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      rolling_window: true
      width: 3
      height: 3
      resolution: 0.05
      robot_radius: 0.22
      plugins: ["voxel_layer", "inflation_layer"]
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.70
      voxel_layer:
        plugin: "nav2_costmap_2d::VoxelLayer"
        enabled: true
        publish_voxel_map: true
        origin_z: 0.0
        z_resolution: 0.05
        z_voxels: 16
        max_obstacle_height: 2.0
        mark_threshold: 0
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          clearing: true
          marking: true
          data_type: "LaserScan"
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0
      always_send_full_costmap: true
global_costmap:
  global_costmap:
    ros__parameters:
      update_frequency: 1.0
      publish_frequency: 1.0
      global_frame: map
      robot_base_frame: base_link
      robot_radius: 0.22
      resolution: 0.05
      track_unknown_space: true
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        enabled: true
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          clearing: true
          marking: true
          data_type: "LaserScan"
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_subscribe_transient_local: true
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        cost_scaling_factor: 3.0
        inflation_radius: 0.7
      always_send_full_costmap: true

What the inflation numbers mean

import math
robot_radius, inflation_radius, scale = 0.22, 0.70, 3.0
INSCRIBED = 253                                      # cost of a cell the robot cannot enter without touching
def cost(d):                                         # d: distance from the obstacle to the cell centre, metres
    if d <= robot_radius:
        return INSCRIBED
    if d > inflation_radius:
        return 0
    return int(math.exp(-scale * (d - robot_radius)) * (INSCRIBED - 1))
for d in (0.20, 0.22, 0.30, 0.45, 0.70, 0.75):
    print(d, cost(d))
# narrowest corridor the planner can use, and the narrowest with a free (zero-cost) lane:
print(round(2 * robot_radius, 2), round(2 * inflation_radius, 2))

Printed (checked by the site’s tests):

0.2 253
0.22 253
0.3 198
0.45 126
0.7 59
0.75 0
0.44 1.4

With robot_radius 0.22 m, inflation 0.70 m and scaling 3.0, cost is 253 (inscribed: forbidden) up to 0.22 m, falls through 198 at 0.30 m and 126 at 0.45 m, and ends at 59 at the inflation radius before dropping to 0. The last line gives the key corridor widths: 0.44 m is the narrowest the model allows; a corridor must be wider than 1.4 m (2 × inflation radius) to contain a lane of zero inflated cost.

Inspect and change on the running system

Not executed by the site’s automated tests (they need a ROS 2 installation).

ros2 param get /local_costmap/local_costmap inflation_layer.inflation_radius
ros2 param set /local_costmap/local_costmap inflation_layer.inflation_radius 0.5
ros2 topic hz /local_costmap/costmap
# in RViz: add the costmap displays and compare them with the real walls

A ros2 param set is a temporary test; write the final value into your parameter file.

8. Real examples

  1. Robot will not enter an 0.8 m doorway: 2 × inflation radius (1.4 m) is larger than the door, so the whole doorway is inflated; the planner cost there is high but not lethal. Lower the inflation radius or the cost scaling so the centre line is cheap.
  2. Ghost obstacles trail behind the robot: clearing needs a long enough raytrace_max_range and the clearing: true flag.
  3. Robot cuts corners: robot_radius smaller than the real robot.

9. Failure scenarios

  • Setting robot_radius to the body radius but forgetting a protruding sensor.
  • Inflation so wide that every corridor looks closed.
  • LiDAR frame wrong or scan below the obstacle height, so nothing is marked.
  • Different resolution in local and global costmaps causing disagreements near obstacles.

10. Troubleshooting

  1. View both costmaps in RViz against the real environment: measure a doorway and compare.
  2. Compute your corridor numbers: 2 × robot_radius and 2 × inflation_radius against the real width.
  3. Change one parameter at a time with ros2 param set and repeat the same manoeuvre.
  4. Check the scan frame and range parameters if obstacles are missing or never clear.
  5. Record the final values and re-test the full route (a bag of the route for regression).

11. Validation

  • The robot passes the narrowest real doorway on its route with the margin you require, repeatedly (at least 20 attempts, count successes).
  • Minimum clearance to walls along the route, measured, stays above your requirement.
  • No ghost obstacles remain after a person walks through.
  • The YAML parses and every key is one confirmed from the official file (checked by the site’s tests).

12. Engineering checklist

13. Further references

  • Nav2 default parameter file, jazzy branch (costmap section confirmed on 2026-11-04): nav2_bringup/params/nav2_params.yaml.
  • Nav2 documentation for the full parameter reference and the inflation formula: docs.nav2.org. The inflation formula in this guide was verified against the source (read 2026-11-06): inflation_layer.hpp, computeCost: cost is LETHAL_OBSTACLE (254) at distance 0, INSCRIBED_INFLATED_OBSTACLE (253) at or inside the inscribed radius, and otherwise (253 − 1)·exp(−cost_scaling_factor·(distance − inscribed_radius)) truncated to an integer; the constants are in cost_values.hpp (NO_INFORMATION 255, LETHAL_OBSTACLE 254, INSCRIBED_INFLATED_OBSTACLE 253, MAX_NON_OBSTACLE 252, FREE_SPACE 0).

Related on FixTheRobot