A ROS 2 package that generates Gazebo (gz sim) SDF world files from
ROS 2 occupancy grid maps in the nav2 map_server format (YAML + PGM/PNG).
- Tested on: ROS 2 Jazzy / Gazebo Harmonic (gz sim 8)
- No extra Python dependencies (numpy / OpenCV / PyYAML only)
| Input: occupancy grid map | Output: Gazebo world |
|---|---|
![]() |
![]() |
- Loads the map YAML and image, and binarizes occupied cells using the same thresholding as nav2.
- Traces the occupied regions as boundary polygons with holes
(
cv2.findContours), optionally simplified with Douglas-Peucker (--simplify), and offset onto cell boundaries so even one-cell walls keep their thickness. - Extrudes the polygons into watertight prisms (side walls plus triangulated top/bottom caps), writes them as a single binary STL mesh, and generates an SDF world that references it. With only one collision/visual pair, even large maps load fast.
The origin: [x, y, yaw] from the map YAML is applied to the wall model
pose, so the coordinate frame of the generated world matches the original
map frame.
cd ~/dev_ws
colcon build --packages-select map2sdf
source install/setup.bashros2 run map2sdf map2sdf --map <map.yaml> -o <output-dir> [options]| Option | Default | Description |
|---|---|---|
--map |
(required) | path to the map YAML file |
-o, --out |
. |
output directory |
--wall-height |
2.0 |
wall height in meters |
--world-name |
map_world |
SDF world/model name, also used as the output file stem |
--format {world,model} |
world |
output a complete world, or a Gazebo model directory for <include> |
--no-ground |
- | do not add a ground plane |
--shadows |
- | enable shadows (disabled by default for lighter rendering) |
--unknown-as {free,occupied} |
free |
how to treat unknown cells |
--occupied-thresh |
YAML value | override the occupied threshold |
--simplify TOL |
0 (off) |
approximate wall contours within TOL meters before meshing; greatly reduces the triangle count for jagged SLAM maps (features thinner than TOL may disappear) |
ros2 run map2sdf map2sdf \
--map $(ros2 pkg prefix map2sdf)/share/map2sdf/maps/sample.yaml \
-o /tmp/map_world
gz sim -r /tmp/map_world/map_world.sdfVia the launch file (uses ros_gz_sim):
ros2 launch map2sdf map2sdf_demo.launch.py world:=/tmp/map_world/map_world.sdfmap2sdf_node subscribes to a nav_msgs/OccupancyGrid topic (latched,
as published by map_server or a SLAM node) and writes the same output
files, so a world can be generated directly from a running SLAM session:
ros2 run map2sdf map2sdf_node --ros-args -p out:=/tmp/map_world \
-p simplify:=0.05 -r map:=/your_map_topicParameters mirror the CLI options (out, world_name, wall_height,
simplify, ground, shadows, unknown_as, occupied_thresh). With
one_shot (default true) the node exits after the first conversion;
set it to false to regenerate on every map update.
--format model writes a Gazebo model directory
(<out>/<name>/model.config, model.sdf, and the STL) instead of a
world, so the walls can be dropped into an existing world:
<include>
<uri>file:///path/to/out/map_walls</uri>
</include>The map origin pose is embedded in the model, so including it without a pose keeps the walls aligned to the original map frame.
The output directory will contain map_world.sdf and map_world_walls.stl.
The SDF references the mesh by a path relative to the world file, so the
pair keeps working as long as both files stay in the same directory —
no GZ_SIM_RESOURCE_PATH or other environment variables required.
image (relative to the YAML), resolution, origin, negate,
occupied_thresh, free_thresh, mode (trinary / scale / raw)
Note: with the nav2 threshold semantics, a free_thresh of 0.25 classifies
the conventional unknown gray (205) as free. Use the classic 0.196 if you
want gray pixels to stay in the unknown band (see --unknown-as).
colcon test --packages-select map2sdf && colcon test-result --verboseApache License 2.0 — see LICENSE.

