Two numbers in every ROS map's YAML file have consequences far beyond the map itself. The resolution decides how much memory the costmaps use, how fast the planner runs and how precisely the robot can thread a doorway. The origin decides where the map sits in the world, and therefore where every saved goal, dock and zone ends up. This guide explains how to choose both and how to change them safely.
What resolution means
resolution is the size of one map cell in metres. 0.05, the default in slam_toolbox and the most common choice, means 5 cm squares. The number of cells grows with the square of the resolution:
| Resolution | Cells for a 50 m × 50 m building | Robot 0.5 m wide spans | Typical use |
|---|---|---|---|
| 0.10 m | 500 × 500 = 250,000 | 5 cells | Large warehouses, outdoor areas, slow planners on small computers |
| 0.05 m | 1000 × 1000 = 1,000,000 | 10 cells | Most indoor robots |
| 0.025 m | 2000 × 2000 = 4,000,000 | 20 cells | Small robots in tight spaces, precise docking areas |
Halving the cell size quadruples the cell count. The global costmap normally matches the map, and it holds several layers of those cells, so memory use and the time spent updating layers and planning grow roughly in proportion. On a laptop this rarely matters; on a Raspberry Pi running Nav2 on a large site, it is the difference between a planner that answers instantly and one that takes seconds.
How fine is fine enough?
Resolution has to be good enough to tell open space from obstacles at the scale of your tightest squeeze. Rounding to cells can cost up to about one cell of clearance on each side of a gap, and the costmap's inflation is also counted in cells. A useful rule: make the cell size no more than about a tenth of the smallest clearance you care about. A robot that must pass through 80 cm doorways with 15 cm to spare on each side is comfortable at 5 cm and tight at 10 cm.
Finer is not automatically better. A 2D laser's range noise is typically a few centimetres, so very fine maps record noise as detail and fill up with speckles, and AMCL does more work per scan for little gain. Match the resolution to your sensor and your robot, not to the screen.
The origin
origin: [x, y, yaw] is the pose of the map's lower-left corner in the map frame. SLAM tools choose it so that the robot's starting position is at (0, 0), which means the origin is usually a pair of negative numbers. Two facts follow:
- Changing the origin moves the whole map in the world frame. Every goal, waypoint, docking pose and keep-out zone expressed in map coordinates now points somewhere else. Do it only deliberately, and update everything that depends on it.
- Keep the yaw at 0. Many ROS tools ignore the yaw in the origin. If the map needs rotating, rotate the image itself, or simply give the robot its initial pose with the correct heading.
Good reasons to set the origin by hand: making the map frame line up with a building's coordinate system, so that positions from floor plans can be used directly, or placing (0, 0) at the charging dock, which makes the dock pose trivial to configure. If you do, also make every mask used by Nav2's costmap filters use the same origin and resolution.
Changing the resolution of an existing map
Re-mapping at the new resolution gives the best result, but you can also downsample a map you already have. The catch is that ordinary image resizing averages pixels, so one-pixel walls fade into grey that the map server reads as unknown or free. The safe way is to give each block the darkest pixel in it, so any obstacle survives. This script halves (or quarters) a map's resolution that way:
"""Downsample a ROS map without losing thin walls. Needs numpy, Pillow, PyYAML."""
import sys
import numpy as np
import yaml
from PIL import Image
src_yaml, dst_name, factor = sys.argv[1], sys.argv[2], int(sys.argv[3])
meta = yaml.safe_load(open(src_yaml))
img = np.array(Image.open(meta['image'])) # run it from the map's folder
h, w = img.shape
h2, w2 = h // factor, w // factor
# drop rows at the top and columns at the right that don't fill a whole
# block, so the lower-left corner (the origin) stays where it was
img = img[h - h2 * factor:, :w2 * factor]
small = img.reshape(h2, factor, w2, factor).min(axis=(1, 3)).astype(np.uint8)
Image.fromarray(small).save(dst_name + '.pgm')
meta['image'] = dst_name + '.pgm'
meta['resolution'] = meta['resolution'] * factor
with open(dst_name + '.yaml', 'w') as f:
yaml.safe_dump(meta, f, default_flow_style=None, sort_keys=False)
Run it as python3 downsample_map.py depot.yaml depot_10cm 2. On Nav2's depot map it turned 604 × 307 pixels at 5 cm into 302 × 153 pixels at 10 cm with the origin unchanged, and every obstacle pixel of the original fell inside an obstacle pixel of the result. Because the darkest value wins, unknown also beats free in mixed blocks, which is the conservative choice. Check the result in the ROS Map Editor before using it.
Going the other way, to a finer resolution, does not add information. If you need more detail, map again with a smaller resolution in slam_toolbox's parameters, as described in mapping with slam_toolbox.
Summary
- Start at 5 cm. Go coarser for very large sites or slow computers, finer only for tight spaces and precise docking.
- Keep the cell size well below your smallest important clearance.
- Treat the origin as part of your robot's configuration: change it only on purpose, keep the yaw at 0, and keep masks in step.
- When downsampling, keep the darkest pixel, never the average.