FusionCore UKF: fusing ZED IMU and visual odometry for stable mobile robot localization

Hey everyone,

I’ve been building a ROS 2 state estimator called FusionCore and wanted to share how it integrates with ZED cameras for mobile robot setups. @Myzhar pointed me here after I opened an issue (#429) on the ZED ROS2 wrapper.

What FusionCore does?

FusionCore is a 22-state UKF that fuses IMU, wheel odometry, visual odometry, and optional GPS into a single clean odom → base_link estimate. It runs gyro and accelerometer bias estimation in the filter state, adapts its noise covariance automatically, and gates outliers with a chi-squared test per sensor.


How it works with ZED

The ZED node publishes two things FusionCore can consume directly:

  • /zed/zed_node/imu/data → FusionCore IMU input

  • /zed/zed_node/odom → FusionCore secondary odometry (encoder2.topic)

If you also have wheel encoders, those go in as the primary source and ZED visual odometry becomes the secondary. FusionCore outputs /fusion/odom (nav_msgs/Odometry) and broadcasts the odom → base_link TF at 100Hz.


Config for ZED + wheel encoders

fusioncore:
  ros__parameters:
    base_frame: base_link
    odom_frame: odom
    publish_rate: 100.0
    publish.force_2d: true

    imu.has_magnetometer: false
    imu.gyro_noise: 0.005
    imu.accel_noise: 0.1
    imu.frame_id: "zed_imu_link"

    encoder.vel_noise: 0.05
    encoder.yaw_noise: 0.02

    encoder2.topic: "/zed/zed_node/odom"

    outlier_rejection: true
    outlier_threshold_gnss: 16.27
    outlier_threshold_imu: 15.09
    outlier_threshold_enc: 11.34
    outlier_threshold_hdg: 10.83

    adaptive.imu: true
    adaptive.encoder: true
    adaptive.gnss: true

    reference.use_first_fix: false
    reference.x: 0.0
    reference.y: 0.0
    reference.z: 0.0

Starting FusionCore

FusionCore is a ROS 2 lifecycle node: it needs to go through configure → activate before it starts publishing. Two ways to handle this:

If you use Nav2:

ros2 launch fusioncore_ros fusioncore_nav2.launch.py
fusioncore_config:=/path/to/zed_robot.yaml

The launch file handles configure and activate automatically, then starts Nav2 once FusionCore is publishing. Update your Nav2 config to read from /fusion/odom instead of /odometry/filtered.

If you don’t use Nav2:

# Terminal 1 — start the node with remaps
ros2 run fusioncore_ros fusioncore_node --ros-args \
  --params-file /path/to/zed_robot.yaml \
  -r /odom/wheels:=/your/wheel/odom \
  -r /imu/data:=/zed/zed_node/imu/data

# Terminal 2 — trigger lifecycle transitions
ros2 lifecycle set /fusioncore configure
ros2 lifecycle set /fusioncore activate

After activating, /fusion/odom should be publishing and ros2 topic hz /fusion/odom should show ~100Hz.

Full lifecycle documentation: https://manankharwar.github.io/fusioncore/getting-started/


TF architecture

ZED → /zed/zed_node/imu/data  ─┐
ZED → /zed/zed_node/odom      ─┤→ FusionCore → odom → base_link TF
Wheels → /odom/wheels         ─┘              → /fusion/odom

SLAM (rtabmap, slam_toolbox)  → map → odom TF

FusionCore sits underneath your SLAM stack. They don’t conflict.


Adding GPS for outdoor robots

Add a GPS receiver and FusionCore fuses all three: ZED IMU, visual odometry, and GPS… into a global-frame estimate. No UTM projection, no separate navsat_transform node. Add to your config:

    input.gnss_crs: "EPSG:4326"
    output.crs: "EPSG:4978"
    output.convert_to_enu_at_reference: true
    reference.use_first_fix: true

Then remap /gnss/fix to your GPS topic. Full GPS configuration guide: https://manankharwar.github.io/fusioncore/configuration/

Trajectory


Where it helps most with ZED

ZED visual odometry is excellent but can drift briefly during fast rotation, low-texture scenes, or lighting transitions. FusionCore bridges those gaps using IMU dead-reckoning and wheel encoder velocity. The result is an odometry signal that doesn’t stutter when the camera loses tracking momentarily.

Happy to answer questions about specific ZED setups.

Hi @manankharwar
Welcome to the StereoLabs community.

Fusion of odometry data from multiple sensors with a ZED camera is a frequent request in this forum.

Your solution looks promising, and it would be great if other members of our community could test and validate it in different environments and operating conditions.

We look forward to hearing the new results.

1 Like

Thanks @Myzhar! Happy to be here.

If anyone in the community runs into issues getting it configured with their specific ZED setup… different camera models, frame IDs, or sensor combinations…. feel free to post here or open an issue on GitHub. I’ll respond quickly

Your package seems really promising, especially as a solution to the growing pains of integrating robot_localization in modern systems. I am going to be spending this week evaluating and comparing your FusionCore against Wolf, TIER IV’s EagleEye and Robot Localization for Ackermann (car-like) vehicles. I will test on two platforms:

  1. An F1/10 scale car used indoors with a wheel encoder, IMUs (from VESC and RealSense D435i), and either VSLAM or LaserScan as a secondary encoder.
  2. 2. A full-size van with a GPS, Zed 2i, IMU, and optionally wheel encoder or by-wire odometry. This also has a 360 LIDAR but I don’t think that is relevant to this test or your package.

I read through your documentation, and it seems you already have support for modifying the vehicle model to Ackermann, so I will use that.

  1. Any tips for indoor vs. outdoor configurations? For context, the wheel encoder (odom from wheels and steering servo) does not output reliable covariance. We also have odometry from LaserScans and PointClouds (for the van). I am happy to propose improvements and contribute to your repository.
  2. Also, since your package is meant to be used primarily with GPS + IMU + wheel encoder, can the /tf publication be disabled if another node fuses all the odom topics/tfs into one? How does it perform without GPS, i.e., indoor cases? It handles multiple “encoders,” but how about multiple IMUs, or does that require starting up another instance/node of the FusionCore (which isn’t a major issue)?
  3. What is the CPU consumption and expected publication frequency on a Jetson Orin Nano and a high-powered workstation? In your documentation, you state 100 Hz, but the robot_localization package claims north of 100 Hz but struggles to even run at 10 Hz on a Jetson Orin Nano with 4 sensors.