← BlogAMR Navigation Basics· 2/12

From Static Map to Nav2 Costmap — A Map That Knows Walls vs. One That Doesn't

How an occupancy grid flows through Nav2's map_server into a static-layer costmap that the planner can actually use, and why substituting an all-free empty map makes the robot believe the world has no walls.

~5 min
Nav2costmapstatic-mapmap-serverAMR

From Static Map to Nav2 Costmap

In the previous article we covered representing an environment as an occupancy grid. But the grid alone does not let the robot plan a path. It must be converted into a map that Nav2 (the ROS 2 navigation stack) can consume, then loaded as a layer of the costmap before path planning becomes possible.

Occupancy Grid → Map → Costmap

The flow looks like this.

Rendering diagram…
  • map.yaml — grid image (pgm/png) + resolution (meters per pixel) + origin (world coordinate of the map's lower-left corner)
  • map_server — reads the map file and publishes it once on the /map topic (latched)
  • static_layer — subscribes to /map and initializes the "unchanging layer" of the costmap — fixed obstacles such as walls and shelving
  • global_costmap — the static layer is the foundation; real-time obstacle and inflation layers are stacked on top to produce the final cost grid

The critical requirement here is coordinate-frame alignment. The ROS standard frame chain (REP 105) is map → odom → base_link; if those TF transforms are not ready, the costmap does not know where it is. In practice a late TF means the costmap stalls for tens of seconds waiting for a valid transform.

"A Map That Knows Walls" — and the Empty Map Trap

There is a failure mode that shows up repeatedly. An empty map — one where every cell is marked free.

An empty map is convenient for debugging or bootstrapping, but feeding it into the static layer means the robot believes there are no walls anywhere in the world.

Left: a map that knows its walls — the path routes around the racking. Right: an empty map, where the hatching marks structure that is physically there but absent from the map. To the planner the second grid contains no blocked cells at all, so the shortest path is a straight line — and that line goes through the racking. For reference, our warehouse run's global costmap held 22,340 lethal cells, 56,790 free, and 0 unknown — the zero unknown is a simulation artefact: the map comes from ground truth, whereas any SLAM-built map always retains unobserved regions.Left: a map that knows its walls — the path routes around the racking. Right: an empty map, where the hatching marks structure that is physically there but absent from the map. To the planner the second grid contains no blocked cells at all, so the shortest path is a straight line — and that line goes through the racking. For reference, our warehouse run's global costmap held 22,340 lethal cells, 56,790 free, and 0 unknown — the zero unknown is a simulation artefact: the map comes from ground truth, whereas any SLAM-built map always retains unobserved regions.

Rendering diagram…

We hit exactly this regression bug in production. During a version cutover, an empty map was hardcoded into the execution path somewhere. The environment had real walls; the static layer said "all free." Robots drove directly toward solid walls.

The fix is straightforward: generate a real occupancy grid from the environment geometry and load it as the Nav2 map. After the fix, the number of lethal cells in the costmap jumped from zero to tens of thousands, and the robots recognized walls again. The lesson: "an empty map makes the robot fearless."

Why the "Map → Topic → Layer" Indirection?

The pipeline looks like many steps. Why not feed the grid directly to the planner? The indirection serves a purpose.

  • Source independence — whether the map comes from SLAM, simulation, or manual editing, map_server supplies it through the same /map interface
  • Layer composition — the static layer is written once; a separate real-time sensor obstacle layer is updated on top, decoupling update costs
  • Multiple subscribers — global costmap, local costmap, and visualization tools all share the same /map topic

This "map → costmap layer" architecture is what enables the three-layer costmap (static / obstacle / inflation) composition covered in the next article.

In a nutshell

📌 An occupancy grid does not feed the planner directly — it must flow through Nav2 map (map.yaml) → map_server (/map) → static layer → costmap before the robot has any knowledge of walls. Slip an empty map into that chain anywhere and the robot believes the world is obstacle-free and drives straight into walls.

See standard test results with videos and verdict evidence on the imSim Portal.

View public results →