SLAM and Localization
The Two Jobs, and the Frames That Encode Them
SLAM builds a map while estimating the pose in it. Localization estimates the pose in a map that already exists. They are different problems and different nodes, and a robot normally does SLAM once and localization forever after.
Both publish the same edge: map -> odom. Nothing else should.
map --(SLAM or AMCL: the correction, jumps)--> odom
odom --(wheel odometry or an EKF: smooth, drifts)--> base_link
That split is REP-105 and it is the single most useful thing to keep straight, because it tells you which node to suspect. A pose that jumps is the localiser correcting; a pose that drifts is odometry doing its job. See ../03_Spatial_and_Temporal/02_Conventions_and_Time.ipynb.
Exactly one node may publish map -> odom. Running SLAM and AMCL together, or AMCL and an EKF configured to publish it, gives the flickering tree from ../03_Spatial_and_Temporal/00_tf2.ipynb. The same applies to odom -> base_link: if diff_drive_controller publishes it and an EKF also does, turn one off. See ../04_Simulation_and_Hardware/02_ros2_control.ipynb.
slam_toolbox
The default for 2D lidar SLAM on Jazzy. It replaces gmapping and cartographer for most wheeled robots, and it does both mapping and localization.
sudo apt install ros-jazzy-slam-toolbox
ros2 launch slam_toolbox online_async_launch.py use_sim_time:=trueslam_toolbox:
ros__parameters:
mode: mapping # or localization
odom_frame: odom
map_frame: map
base_frame: base_link
scan_topic: /scan
resolution: 0.05
max_laser_range: 12.0
minimum_travel_distance: 0.3 # only add a node after moving this far
minimum_travel_heading: 0.3
loop_search_maximum_distance: 3.0
do_loop_closing: trueModes worth knowing: mapping builds a new map; localization loads a serialised pose graph and tracks within it; lifelong keeps updating a map while running, which is attractive and expensive.
Saving, and the distinction that catches people:
ros2 service call /slam_toolbox/save_map slam_toolbox/srv/SaveMap "{name: {data: my_map}}"
ros2 service call /slam_toolbox/serialize_map slam_toolbox/srv/SerializePoseGraph "{filename: my_map}"save_map writes the .pgm plus .yaml that AMCL and Nav2’s map server consume. serialize_map writes slam_toolbox’s own pose graph (.posegraph plus .data), which is what localization mode needs. They are not interchangeable, and discovering that at the point of switching to localization is the usual way to learn it.
Three practical points:
- Odometry quality sets map quality. slam_toolbox corrects odometry; it cannot rescue odometry that is wrong in scale or sign. Drive a 2 m square and check
odombefore blaming SLAM. minimum_travel_distancecontrols node density. Too small and the graph bloats; too large and loop closure has nothing to match.- Loop closure is what makes a map metric. A long corridor traversed once produces a map that looks fine and is skewed. Drive loops deliberately.
AMCL
Localization in an existing map, by particle filter: nav2_amcl maintains a cloud of pose hypotheses, weights them by how well the scan matches the map, and resamples.
amcl:
ros__parameters:
min_particles: 500
max_particles: 2000
laser_model_type: likelihood_field
max_beams: 60
update_min_d: 0.25 # resample after this much motion
update_min_a: 0.2
alpha1: 0.2 # rotation noise from rotation
alpha2: 0.2 # rotation noise from translation
alpha3: 0.2 # translation noise from translation
alpha4: 0.2 # translation noise from rotation
set_initial_pose: false
tf_broadcast: trueWhat goes wrong, in order of frequency:
- No initial pose, so the filter never converges. AMCL starts ignorant. Give it a pose with the RViz “2D Pose Estimate” tool,
set_initial_poseplusinitial_poseparameters, or a/initialposepublication. A robot that localises nowhere has usually just never been told where it is. - The
alpha*motion-noise parameters are wrong for the robot. Too low and the filter is overconfident and cannot recover from a bump; too high and the pose is permanently fuzzy. They are the main tuning knobs and they are robot-specific. - The map does not match the world. Furniture moved, or the map was built at a different lidar height. AMCL will confidently localise to the map’s version of reality.
- Symmetry. A long empty corridor is genuinely ambiguous along its length, and no tuning fixes that; the particle cloud will stretch, correctly.
ros2 topic echo /amcl_pose gives the pose with its covariance, which is the honest measure of whether it has converged. Watching the particle cloud in RViz is the fast version.
Odometry Sources and robot_localization
Wheel odometry alone drifts, mostly in heading, and heading error turns into position error linearly with distance. Fusing it with an IMU fixes most of that. robot_localization provides an EKF for this.
ekf_filter_node:
ros__parameters:
frequency: 30.0
two_d_mode: true # true for a wheeled ground robot: locks z, roll, pitch
publish_tf: true
map_frame: map
odom_frame: odom
base_link_frame: base_link
world_frame: odom # this choice decides which edge is published
odom0: /wheel/odometry
odom0_config: [false, false, false, # x, y, z
false, false, false, # roll, pitch, yaw
true, true, false, # vx, vy, vz
false, false, true, # vroll, vpitch, vyaw
false, false, false] # ax, ay, az
imu0: /imu/data
imu0_config: [false, false, false,
false, false, true, # yaw
false, false, false,
false, false, true, # vyaw
true, false, false] # axTwo decisions dominate the configuration:
world_framechooses the edge.world_frame: odomfuses continuous sources and publishesodom -> base_link.world_frame: mapfuses global sources (GPS, a localiser) and publishesmap -> odom. A common setup runs two EKF instances, one of each. Settingworld_frame: mapon the instance that also haspublish_tf: truewhile AMCL is running is how you get two publishers of the same edge.- Fuse velocities from wheels, not positions. Wheel odometry’s absolute position is an integral of its own errors; its velocity is a measurement. The
odom0_configabove reflects that: velocities true, positions false.
Things that cost time here: covariances that are left at zero or at defaults, which makes the filter trust a source absolutely; an IMU whose yaw is in the wrong convention (REP-103 is counter-clockwise from east, most datasheets are clockwise from north, see ../03_Spatial_and_Temporal/02_Conventions_and_Time.ipynb); and fusing two sources that are not independent, such as an IMU and a wheel odometry that already has the IMU’s yaw folded in, which makes the filter overconfident.
Visual and RGB-D SLAM
When there is no lidar, or the map needs to be 3D, the camera becomes the sensor. rtabmap_ros is the mature ROS 2 option, taking RGB-D or stereo plus odometry and producing a 3D map with loop closure.
sudo apt install ros-jazzy-rtabmap-ros
ros2 launch rtabmap_launch rtabmap.launch.py \
rgb_topic:=/camera/color/image_raw \
depth_topic:=/camera/depth/image_rect_raw \
camera_info_topic:=/camera/color/camera_info \
frame_id:=base_link approx_sync:=falseNotes from actually running this on modest hardware (the public piros2 project, a Raspberry Pi 5 streaming to a dev box, with monocular neural depth rather than an RGB-D sensor):
- A motionless scene produces a one-node map, and that is correct. RTAB-Map merges lookalike frames, so a static bag yields a single graph node rather than a map. Measured odometry quality in the 447 to 563 range on such a bag is the pipeline working, not failing.
- Its 5-second no-data warnings are a watchdog, not a fault, when synced pairs arrive at 1 to 2 Hz. Its
delay=figure is the age of the data, which on a replayed bag is the bag’s age. camera_infowith an all-zerokmakes a recording unusable for mapping, and nothing says so until RTAB-Map refuses to initialise. Recordcamera_infoand check it; see ../05_Perception/00_Images_and_Calibration.ipynb.- Looping a bag teleports odometry. Play once (
ros2 bag playwithout--loop) when feeding SLAM, or the jump between the end and the start is interpreted as motion. - Exact sync needs a deep queue. With
approx_sync:=falseand the default queue of 5, pairing was a coin toss: 0 to 6 odometry updates on a replay where a 30-deep queue deterministically gave- Queue depth is not a tuning nicety here. See ../03_Spatial_and_Temporal/03_Message_Synchronisation.ipynb.
- Monocular depth has no absolute scale. A neural depth model gives relative depth, so a scale factor must be calibrated against a measured distance, and it wobbles per frame (about 4% on a static scene in that project). A 2.50 m wall read as 9.30 m until the scale was fixed. That wobble, not the pose estimate, was the limit on map quality.
- Pure-rotation visual odometry is a trap worth naming. The essential matrix is degenerate under pure rotation, so rotation-only estimators are used instead; but a hand-held “rotation only” pan actually contains translation (0.9 m of arm arc, measured), which smears the map radially.
For 3D mapping on better hardware the alternatives are ORB-SLAM3, and NVIDIA’s isaac_ros_visual_slam if the Isaac stack is already in play; see ../04_Simulation_and_Hardware/01_Other_Simulators_and_Sim_to_Real.ipynb.
Choosing, and Diagnosing
| Situation | Use |
|---|---|
| 2D lidar, flat floors | slam_toolbox to map, then AMCL or slam_toolbox localization |
| RGB-D or stereo, 3D map needed | rtabmap_ros |
| no lidar, monocular camera only | visual SLAM, and expect to calibrate scale |
| outdoor with GPS | robot_localization with navsat_transform_node |
| known map, just need a pose | AMCL; do not run SLAM |
Diagnosing, in order:
ros2 run tf2_tools view_frames # is map -> odom -> base_link present, once each
ros2 topic hz /scan /odom /tf
ros2 topic echo /amcl_pose # pose and covariance
ros2 topic echo /odom --field pose.pose.position| Symptom | Likely cause |
|---|---|
| map drifts and shears over a long run | no loop closure; drive loops |
| pose jumps constantly in a static robot | localiser resampling on a poor scan match, or alpha* too high |
| pose never converges | no initial pose given |
| robot position in RViz lags reality | stamps, or use_sim_time set on some nodes only |
| map built but localization mode fails to load it | save_map output used where a serialised pose graph is required |
| SLAM produces a one-node map | the robot never moved far enough, by design |
| two poses flickering | two publishers of map -> odom |
The conceptual background on SLAM algorithms (graph optimisation, loop closure, scan matching) is covered in study notes kept outside this site and deliberately not linked here; this notebook is about the ROS 2 side.