A ROS map is two small files: a greyscale image and a YAML file that says how to read it. They look simple, and they are, but a few of their details decide whether your robot treats unexplored space as a wall, as unknown, or as open floor. This guide explains both files and shows, with measurements from Nav2's own map server, exactly how pixels become an occupancy grid.
The image: a PGM file
Maps are usually saved as PGM (portable greymap) images: a short text header followed by one byte per pixel.
P5
# CREATOR: map_saver
604 307
255
…604 × 307 bytes of pixel data…
P5 means binary greyscale, then come the width and height in pixels and the maximum value (255). Each pixel is one cell of the map. The image is stored top row first, but a ROS occupancy grid counts rows from the bottom, so the map server flips it: the bottom-left pixel of the image is cell (0, 0) of the map. Maps saved by Nav2 use three values: 254 (white) for free space, 0 (black) for obstacles and 205 (grey) for unknown space.
The metadata: a YAML file
Here is the YAML of the depot map that ships with Nav2 on ROS 2 Jazzy:
image: depot.pgm
mode: trinary
resolution: 0.05
origin: [-7.14, -7.83, 0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25
| Field | Meaning |
|---|---|
image | The image file, relative to the YAML file. |
mode | How pixel values become occupancy: trinary (the default), scale or raw. See below. |
resolution | Metres per pixel. 0.05 means each pixel is a 5 cm square, so this 604 × 307 image covers 30.2 × 15.35 m. |
origin | The pose of the lower-left corner of the map in the map frame: x, y in metres and a yaw angle. Many parts of ROS ignore the yaw, so keep it 0 and rotate the image instead. |
negate | If 1, white means occupied and black means free. Almost always 0. |
occupied_thresh | Pixels darker than this occupancy probability count as obstacles. |
free_thresh | Pixels lighter than this occupancy probability count as free. |
From pixel to occupancy
The map server first turns each pixel into an occupancy probability, where darker means more likely occupied:
To be sure how this behaves in practice, we fed a strip of eight test pixels to the Nav2 map server on ROS 2 Jazzy (thresholds 0.65 and 0.196) and read back the occupancy grid it published:
| Pixel | 0 | 50 | 100 | 150 | 180 | 205 | 230 | 254 |
|---|---|---|---|---|---|---|---|---|
trinary | 100 | 100 | −1 | −1 | −1 | −1 | 0 | 0 |
scale | 100 | 100 | 91 | 48 | 22 | 0 | 0 | 0 |
raw | 0 | 50 | 100 | −1 | −1 | −1 | −1 | −1 |
- trinary gives three classes. It is what navigation maps normally use.
- scale keeps in-between greys as intermediate values between the two thresholds. It is useful for masks such as speed zones, as our guide to Nav2 keep-out and speed zones shows.
- raw uses the pixel value itself as the occupancy (0 to 100), and anything above 100 is unknown. Notice that the meaning is reversed compared with the other modes: a white pixel is not free in raw mode.
The free_thresh trap
Look at pixel 205, the grey that means "unknown". Its occupancy probability is 50 ÷ 255 ≈ 0.19608, a hair above 0.196. So it is read as unknown only if free_thresh is 0.196 or lower. With a higher value, it falls below the free threshold and becomes free space.
We checked this with the three maps that ship with Nav2. With free_thresh 0.196 (tb3_sandbox) and 0.1 (warehouse), all grey pixels loaded as unknown. With 0.25 (depot), all 8,894 grey pixels loaded as free. Maps saved with Nav2's map_saver_cli on Jazzy are written with 0.196 (we saved one with --free 0.19 and still got 0.196 in the YAML), so the trap mostly comes from hand-written or edited YAML files. Whether it matters depends on your planner: if it is configured not to plan through unknown space, turning unknown into free lets it plan routes through areas the robot has never seen.
The ROS Map Editor shows how your current thresholds will read white, grey and black pixels, and warns you when grey would become free.
Pixel to world coordinates
For a map with zero yaw, the centre of the pixel in column c and row r (counted from the top of an image H pixels high) is at:
For the depot map, the top-left pixel (c = 0, r = 0) is at x = −7.14 + 0.025 = −7.115 m and y = −7.83 + (307 − 0.5) × 0.05 = 7.495 m. This is handy for checking where a goal will land, or for drawing a keep-out zone at known coordinates.
Saving and loading
# save the map currently published on /map as office.pgm + office.yaml
ros2 run nav2_map_server map_saver_cli -f ~/maps/office
# serve it (map_server is a lifecycle node; autostart activates it)
ros2 run nav2_map_server map_server --ros-args \
-p yaml_filename:=$HOME/maps/office.yaml -p autostart_node:=true
map_saver_cli also accepts --fmt png, --mode scale or raw, and threshold options. In a full Nav2 bringup you normally pass the map with the map:= launch argument instead of starting the server yourself.
Keep the files together
The YAML refers to the image by name, so move and rename the two files together, and keep them under version control: they are small, and when a robot starts behaving strangely after a map edit, being able to diff and roll back the map is invaluable. To clean a map up before navigating, see how to clean up a SLAM map.