Context
sweep_node (simulation) publishes a sparse sensor_msgs/LaserScan on /drone/sweep/result — 181 ranges, one scan every ~22 seconds (settle_ms=120). Not a continuous lidar. ROS 2 Jazzy (2024) shifted 2D-SLAM package availability: slam_toolbox became the Nav2 default; cartographer_ros is now a community fork with variable freshness.
This is the kind of comparison where the wrong choice is easy to make from a Google search — Cartographer has more academic prestige, and a casual reader might assume that’s the better pick. Our experiment data says otherwise, and we want to be specific about why.
Evaluation matrix (6 axes, 1–5 stars)
| Candidate | Jazzy | Sparse-robust | Maintenance | CPU/RAM | Config | Loop closure | Σ |
|---|---|---|---|---|---|---|---|
| slam_toolbox | 5★ apt | 4★ async + sparse Ceres | 5★ Macenski + ROS 2 default | 4★ ~70% weak HW, ≪i7 | 5★ template + Nav2 | 4★ pose-graph + lifelong | 27 |
| cartographer_ros | 3★ Jazzy 2.0.9003 (community fork) | 3★ works, harder to tune | 2★ #1536 plan since 2020 | 3★ slower map generation | 2★ Lua, sensitive tuning | 5★ sparse pose graph + Ceres GOLD STANDARD | 18 |
| hector_slam | 0★ no ROS 2 port (#83 open >5 years) | n/a | 0★ | n/a | n/a | n/a | 0 |
| RTAB-Map (2D mode) | 3★ available | 3★ works | 4★ Mathieu Labbé | 2★ overkill, RGBD-oriented | 2★ many params | 4★ appearance-based | 18 |
Winner: slam_toolbox, by a wide margin.
Final recommendation: slam_toolbox (online_async mode)
Parameters (for researchbest_drone/config/slam_async_params.yaml):
slam_toolbox:
ros__parameters:
use_sim_time: True
odom_frame: 'odom'
base_frame: 'base_link'
map_frame: 'map'
scan_topic: '/scan' # remap from /drone/sweep/result
mode: 'mapping'
resolution: 0.05 # 5 cm/cell
min_laser_range: 0.2
max_laser_range: 8.0 # TF-Luna max
transform_publish_period: 0.05
map_update_interval: 5.0 # less frequent — scans are sparse
minimum_time_interval: 0.5
ceres_linear_solver: 'SPARSE_NORMAL_CHOLESKY'
ceres_preconditioner: 'SCHUR_JACOBI'
Apt install:
sudo apt install ros-jazzy-slam-toolbox # 2.8.4+ — ROS 2 default
Drone-specific TF tree (per the ArduPilot ros-slam.html reference):
ros2 run tf2_ros static_transform_publisher 0 0 0 0 0 0 base_link laser_frame
The drone operates in a 2D plane at flight altitude → base_footprint = base_link, laser frame collocated with base.
Numbers from academic literature
| Source | slam_toolbox | Cartographer |
|---|---|---|
| MDPI Electronics 2025 “Sim2Real ROS 2” | ATE 0.13 m, position error ≤ 10 cm, CPU notably lower | ATE 0.21 m, position error 1.4 m (×14 worse!) |
| Authorea/ResearchGate 2024 | 70% CPU + 293 MB RAM (weak HW), 5.2 s startup | Slower map generation, parameter-sensitive |
| MacOpenJOSS 2021 (Macenski & Jambrecic) | Async mode recommended “when the robot doesn’t have to wait for SLAM” — our scenario exactly | n/a |
Why NOT Cartographer (despite academic prestige)
- Cartographer position error of 1.4 m — for our 18 m zigzag world, that disqualifies it immediately (frontier exploration would mis-target on every single hop).
- ROS 2 port is community-driven since 2020; the cartographer-project/cartographer_ros#1536 thread reads “rough plan, will probably need to spawn new issues for refining work packages” — the bus factor is too high to bet on.
- Lua config with delicate tuning — for our sparse-scan case, every parameter would need recalibration.
Cartographer’s only real win is loop closure quality on long cycles (200+ m) — our 18 m zigzag closes in under a minute, so the advantage doesn’t matter for us.
Sparse-scan + Cartographer fallback (Alternative #2)
If slam_toolbox shows drift > 0.5 m on the 18 m zigzag in practice, the fallback is TumAro D-Map port (HKU-MARS “Occupancy Grid Mapping Without Ray-Casting,” TRO 2023):
- Octree occupancy + segment-tree range-maximum queries + on-tree updates.
- Sensor-agnostic (181-sample LaserScan → depth image).
- 3D-ready out of the box (useful for future multi-layer).
- Own odometry from MAVROS
/mavros/local_position/odom+ IMU.
Switch trigger: scan-matching failures + drift > 0.5 m over 5 frontier hops.
Hookup pattern (from ArduPilot ros-slam.html)
# slam.launch.py
Node(package='slam_toolbox', executable='async_slam_toolbox_node',
parameters=[slam_params_yaml],
remappings=[('/scan', '/drone/sweep/result')])
# laser_frame TF (drone-specific — laser collocated with base_link)
Node(package='tf2_ros', executable='static_transform_publisher',
arguments=['0','0','0', '0','0','0', 'base_link','laser_frame'])
Smoke acceptance (post bring-up)
ros2 topic echo /map | headshows anOccupancyGridmessage.- RViz2 → load
/map→ zigzag world is visualized. tf2_echo map odomafter 18 m of flight → drift < 0.5 m.top -p $(pgrep slam_toolbox)→ CPU < 30% on i7-12700F.
Sources
- slam_toolbox (SteveMacenski/slam_toolbox) — latest release 2.8.5 (2026-04-29), 566 commits on the ros2 branch.
- JOSS paper Macenski & Jambrecic 2021 — async-mode justification.
- MDPI Electronics 2025 — “From Simulation to Reality: SLAM Toolbox vs Cartographer in ROS 2.”
- ArduPilot dev wiki —
ros-slam.htmlfor drone-specific TF tree. - Nav2 mapping docs —
slam_toolboxis the default mapping solution. - cartographer-project/cartographer_ros#1536 — ROS 2 port tracking, community fork.
- TumAro/occupancy-grid-mapping — D-Map fallback (TRO 2023).