tutorial
robot_localization in ROS 2: configure an odometry and IMU filter
Run the ROS 2 EKF with supplied wheel odometry and IMU data. Select measurement fields, check frames and covariance, and see how a biased gyro changes the estimated path.
What you will learn
- Run the real robot_localization EKF with supplied ROS messages.
- Read the fifteen-field measurement-selection arrays.
- Assign pose, twist, and IMU measurements to their declared frames.
- Distinguish measurement covariance from a persistent sensor bias.
- Compare a wheel-only baseline with a filter that accepts a biased gyro.
- Check filtered odometry and its matching transform against a known reference.
Before you start
- Prediction, measurement correction, and uncertainty
- Odometry messages and timestamped transforms
- A terminal and an installed, running Docker engine
The ROS 2 robot_localization package combines selected motion measurements into a state estimate. Its ekf_node can consume wheel odometry and IMU messages, publish odometry/filtered, and broadcast the corresponding transform. The configuration determines which fields the filter accepts.
This tutorial runs the real estimator on a supplied straight-line sequence. A wheel-only baseline follows that reference; enabling a deliberately biased gyro makes the estimated path turn. You can inspect the messages, change one selection bit, and check the resulting error without a physical robot.
What does robot_localization estimate?
The EKF predicts motion, then corrects that prediction using incoming measurements and their reported uncertainty. Its state contains position, orientation, linear and angular velocity, and linear acceleration. Each sensor's configuration chooses which parts of its message contribute to those corrections.
Review the extended Kalman filter lesson for the prediction, Jacobian, and measurement-update calculation. That lesson uses a separate landmark model. Here the task is configuring and checking the ROS package's own estimator.
A populated message field does not automatically enter the filter. A valid quaternion, for example, can accompany an odometry message whose configuration selects only velocity. Read the selection arrays before assuming that the filter receives an absolute heading.
Run the checked Jazzy example
Save these five files in one directory: Dockerfile, localization_workflow.py, verify.sh, baseline.yaml, and fuse-biased-gyro.yaml.
Open a host terminal there and run:
docker build --platform linux/arm64 -t dustin-robot-localization-lesson:jazzy .
mkdir -p localization-output
docker run --rm --network none --platform linux/arm64 \
--mount "type=bind,source=$(pwd),target=/lesson,readonly" \
--mount "type=bind,source=$(pwd)/localization-output,target=/output" \
dustin-robot-localization-lesson:jazzy \
bash /lesson/verify.sh --output /output/run
The build needs network access to install robot_localization into the pinned ROS base image. It requests one exact ARM64 package version; other dependency versions resolve during the build. The experiment itself runs without external network access and changes no host ROS installation.
Choose a new directory name under /output when repeating the run. The script refuses an existing output directory. It saves supplied inputs, both filtered trajectories, TF samples, parameter readbacks, logs, and report.json under localization-output/run on your host.
Validation used ROS 2 Jazzy, robot_localization 3.8.3, and Python 3.12.3 in Ubuntu 24.04 on ARM64 through Docker Desktop for macOS. See the environment record for package versions and the captured output for a checked run. This build targets ARM64; other architectures are unverified.
The README explains the output files and checks. If the pinned Debian package becomes unavailable, the build fails rather than silently substituting a newer estimator.
Define the motion and sensor messages
The supplied reference describes a body moving forward at 1 m/s with no rotation. If τ is elapsed time from the first sample, its pose is:
x(τ) = τ meters, y(τ) = 0 meters, yaw(τ) = 0 radians
The fixture supplies wheel odometry with forward velocity 1 m/s, lateral velocity zero, and yaw rate zero. A second stream supplies IMU angular velocity about the body z axis at +0.2 rad/s, despite the reference's zero turn rate. That fixed offset is an injected gyro bias.
Each run publishes 51 odometry messages and 51 IMU messages, stamped from 100.0 through 105.0 seconds in 0.1-second steps. The input topics are /fixture/odom and /fixture/imu. The runner supplies /clock, and the estimator uses use_sim_time: true.
It queues each pair before advancing simulated time to that pair's stamp, giving the estimator's timer and subscription callbacks time to run. Bounded waits use elapsed wall time. The script starts a fresh estimator process for each configuration and checks the loaded parameters through its ROS service.
The odometry pose fields contain the reference for inspection, but both filter masks exclude them. The checker uses the reference to measure error; no reference position enters a state update. The same sensor streams are published for both runs; the baseline excludes the IMU, while the changed configuration accepts its yaw rate. This is an intentionally inconsistent teaching dataset; it does not represent measurements from Dustin's robot or characterize a real IMU.
Choose which fields enter the filter
Every odomN_config or imuN_config array has fifteen booleans in a fixed order. The 3.8.3 parameter template defines them as:
x, y, z,
roll, pitch, yaw,
vx, vy, vz,
vroll, vpitch, vyaw,
ax, ay, az
The mask selects components in the input frame for that part of the message. Odometry twist uses its child frame; IMU angular velocity uses its header frame. Those axes matter when a sensor is rotated.
The v entries describe velocities; vyaw is angular velocity about z. An IMU still uses the full fifteen-entry array, even though it does not provide a position measurement.
Both runs select these wheel-odometry fields:
odom0_config: [false, false, false,
false, false, false,
true, true, false,
false, false, true,
false, false, false]
That chooses vx, vy, and vyaw. The measured zero lateral velocity is a useful constraint for this ideal body, which cannot move sideways. It would be a poor assumption for a slipping robot or a platform that can translate laterally.
The baseline has an all-false imu0_config. The changed configuration enables just vyaw, entry 12 when counting from one:
imu0_config: [false, false, false,
false, false, false,
false, false, false,
false, false, true,
false, false, false]
The downloadable YAML files wrap these settings in a node name and ros__parameters:
ekf_filter_node:
ros__parameters:
use_sim_time: true
two_d_mode: true
frequency: 20.0
world_frame: odom
odom_frame: odom
base_link_frame: base_link
publish_tf: true
odom0: /fixture/odom
imu0: /fixture/imu
This excerpt omits the masks and other settings; run the complete supplied files. The runner starts ekf_node with the matching node name and passes the selected YAML through --ros-args --params-file. A YAML file under a different node name can leave the intended parameters unapplied.
The IMU stream exists in both cases. The single changed bit makes its biased turn rate an accepted measurement in the second run. Orientation and linear acceleration remain excluded.
Both configurations use two_d_mode: true. This constrains out-of-plane motion for the planar example. It does not supply an absolute x, y, or yaw observation and cannot correct an incorrect turn-rate measurement.
The odometry pose fields remain excluded. On a real wheel-odometry source, pose and twist often derive from the same encoders; fusing both as independent evidence can count the same information twice. The package's configuration guide explains this dependency.
Keep input and output frames explicit
The Odometry message contract assigns pose to header.frame_id and twist to child_frame_id. The package transforms selected pose measurements into its world frame and selected twist measurements into its body frame.
This example uses:
- Input odometry pose:
header.frame_id: odom. Pose coordinates refer to the local odometry frame; pose fields are excluded from fusion. - Input odometry twist:
child_frame_id: base_link. Forward and lateral velocity refer to the body axes. - Input IMU:
header.frame_id: base_link. The fixture already expresses angular velocity in body axes. - Filter world frame:
world_frame: odom. The output is a local drifting estimate. - Filter body frame:
base_link_frame: base_link. The estimated body has this frame ID. - Filter TF output:
publish_tf: true. The filter ownsodom→base_link.
Using base_link directly for this synthetic IMU removes a mounting transform from the experiment. For a real IMU with its own frame, provide the correct static transform between the body and sensor. Follow the package's sensor-data conventions, including ENU orientation conventions and right-handed axes; renaming a frame does not rotate its measurements.
Only the filter broadcasts odom → base_link here. If a wheel driver already publishes that same edge, choose one authority and disable the other. Publishing an Odometry message and broadcasting a TF edge are separate actions, as the tf2 tutorial demonstrates.
Report uncertainty without hiding bias
The wheel message assigns variance 0.01 to each selected velocity component. The IMU assigns yaw-rate variance 0.0001 rad²/s². Its reported standard deviation is therefore 0.01 rad/s, while the injected bias is 0.2 rad/s: twenty times that value.
Both configurations use those same reported variances. The IMU's claimed yaw-rate uncertainty is much smaller than the wheel source's, so this setup gives the wrong gyro reading substantial influence. The values are deliberate teaching inputs, not recommendations for tuning real sensors.
The fixture marks IMU orientation and acceleration unavailable, and both masks exclude them. Process noise and initial state covariance retain this package version's defaults; the README records that choice. No covariance tuning or bias calibration occurs during the run.
A diagonal covariance entry is a variance, with squared units. Linear-velocity variance uses m²/s²; angular-velocity variance uses rad²/s². Off-diagonal entries describe cross-covariance between quantities. These values affect the correction together with the predicted state covariance and process model.
Setting a selected field's variance to zero does not tell the filter to ignore it. Use the selection mask to exclude a field. The IMU message definition has separate conventions: an all-zero covariance block means unknown covariance, while a first entry of −1 marks an unavailable measurement block.
The package's fifteen-state model has no gyro-bias state. Enabling a biased rate therefore does not ask it to estimate and remove that bias. In this fixture the sensor disagreement reaches the filter as competing turn-rate measurements, and accepting the bad rate bends the estimated trajectory.
Lower reported uncertainty cannot establish that a sensor is correct. Calibration, frame conventions, units, and time alignment all need their own checks. See the Kalman filter lesson for how measurement uncertainty affects the gain.
Compare the two estimated paths
At the final input stamp of 105 seconds, the supplied reference is (5, 0) m with zero yaw. One captured run produced these rounded results:
| Quantity | Wheel-only baseline | With biased IMU yaw rate |
|---|---|---|
| Final x (m) | 5.000 | 4.245 |
| Final y (m) | 0.000 | 2.238 |
| Yaw (rad) | 0.000 | 0.990 |
| Position error (m) | 0.000 | 2.362 |
The position error uses the same origin and axes as the supplied reference:
error = √((estimated x − 5)² + estimated y²)
There is no fitted rotation or translation that aligns the estimated path afterward. The changed run's heading reaches roughly 57°, so integrating its forward body velocity carries it away from the reference line.
Open baseline-filtered.csv and fuse-biased-gyro-filtered.csv to inspect the received estimates. Their matching -tf.csv files preserve the transform samples. report.json records the final errors and checks; the -parameters.json files record settings read from the running nodes.
Equal-timestamp inputs can initialize the filter in a different order. If the IMU arrives first, the default initial covariance makes forward velocity rise gradually from near zero; wheel-first initialization starts it at 1 m/s. A later verified run ended at (3.993, 2.232) m with 2.448 m error, while reaching the same rounded yaw.
The table therefore describes one captured run. Startup order can change the transient and endpoint, and output counts can vary. The checks use bounds: less than 0.05 m baseline position error, and more than 1.5 m error with positive turning in the biased-gyro case. Those bounds verify this fixture's intended effect; they are not an accuracy specification for a robot.
The comparison changes the IMU selection bit while keeping the supplied sequence and covariance settings fixed. The wheel-only case has an advantage by construction: its velocities agree with the reference. This result demonstrates a configuration effect, not a general reason to omit IMUs.
The script checks output timestamps, frame IDs, finite values, and the transform associated with the output pose. A topic that publishes successfully can still contain an incorrect estimate; the reference comparison checks a different part of the problem.
Diagnose an unexpected estimate
| Observation | Check before tuning |
|---|---|
| No filtered messages | Inspect sensor topics, types, selected fields, compatible QoS, and clock readiness. |
| A valid IMU appears to have no effect | Read imu0_config; orientation, angular velocity, and acceleration are separate choices. |
| The robot curves while the reference stays straight | Compare both yaw-rate measurements, units, axis signs, and reported variances. |
| TF consumers see conflicting motion | Check whether two nodes publish the same parent-child edge. |
| The filter is certain but wrong | Check bias, duplicated evidence, timing, and model assumptions before reducing covariance. |
Use ROS 2 QoS diagnosis when endpoints exist but messages do not arrive. Use the bag recording and replay lesson to separate message stamps from a node's current time. The download here supplies its own messages and clock; those other tutorials use separate fixtures.
The settings differential and relative solve different pose-input problems. Differential mode turns successive pose changes into velocity measurements; relative mode expresses poses relative to the first received pose. Neither is a gyro-bias correction, and neither is needed for the selected velocity fields here.
Separate local odometry from global localization
The output integrates local motion from a chosen origin. Without an absolute position or heading source, it cannot recover a global map pose or remove accumulated drift. Producing an odom → base_link transform does not create a map.
The SLAM Toolbox tutorial adds a mapping node that consumes scans and odometry TF, publishes a grid, and owns map → odom. Its supplied odometry is a separate fixture; the two downloads do not run as one combined stack.
The package also supports other sensors and frame arrangements. With a map world frame, its TF role changes and another source must supply odom → base_link; read the state-estimation node reference before copying a configuration into a larger stack.
This check covers supplied planar velocity measurements and one persistent gyro error. It does not test calibration, hardware timing, wheel slip, accelerometer integration, GNSS, a UKF, or statistical consistency across real sensor runs. The saved reference errors do not establish a localization benchmark.
For a complete trajectory comparison, continue with timestamp association and trajectory-error metrics. For the separate problem of estimating a pose while building a map, continue with SLAM.
Check two configuration decisions
Exercise 1. Your IMU message contains orientation, angular velocity, and linear acceleration. Its imu0_config enables only entry 12. Which quantity enters the filter? Would giving the orientation a smaller covariance change this selection?
Check the selected IMU quantity
Entry 12 is vyaw, the angular velocity about z. Orientation and acceleration remain excluded. Changing the orientation covariance does not enable its fields; selection comes from the configuration mask. First decide which measurements you intend to fuse, then provide their frames and justified uncertainty.
Exercise 2. A wheel driver and the EKF both publish odom → base_link. The EKF also publishes odometry/filtered. Does subscribing to that odometry topic remove the conflicting TF authority?
Check the transform authority
No. Topic subscriptions do not disable transform publishers. Choose the source that should own the edge and disable the other TF publication. The driver can continue publishing wheel odometry as a filter input while the EKF broadcasts the filtered transform.
Sources and further study
- robot_localization 3.8.3 EKF parameter template, for the field order, sensor selection, and frame settings.
- Configuring robot_localization, for planar assumptions, duplicate evidence, and useful velocity constraints.
- Preparing sensor data, for odometry/IMU frames, coordinate conventions, and covariance.
- State-estimation node reference, for outputs, differential/relative inputs, and local versus map frame roles.
- ROS 2 Jazzy Odometry definition and IMU definition, for the message contracts.