Tools for offline evaluation of a SLAM system (GLIM) against Qualisys motion-capture ground truth, on ROS2 bags (.db3 / .mcap). It contains two independent pipelines meant to be used in sequence:
slam_eval_pipeline.py— trajectory accuracy (ATE/RPE) via evo.fido_eval.py— evaluation of the FIDO dynamic object rejection module: checks whether lidar points belonging to the moving person are correctly classified as "dynamic" by the SLAM.
pip install rosbags evoVersions used in this project: rosbags==0.11.2, evo==1.36.4, Python 3.12.
Both scripts read mocap_msgs/RigidBodies / mocap4r2_msgs/RigidBody messages (defined in msg/), registering them at runtime in the rosbags typestore, so the ROS2 mocap packages don't need to be installed — only the bags are needed.
| Path | Content |
|---|---|
slam_eval_pipeline.py |
ATE/RPE pipeline for SLAM trajectory vs Qualisys |
fido_eval.py |
FIDO evaluation (TP/FP/FN/TN on dynamic/static points) |
launcher_slam |
Example command for slam_eval_pipeline.py |
launcher_fido |
(empty — to be filled with an example command for fido_eval.py) |
msg/ |
Reference .msg definitions for the mocap4r2 types |
bags/, filter_bags/ |
Input ROS2 bags (raw/filtered data, not versioned) |
results/, results_new/ |
Output of slam_eval_pipeline.py (one per bag/run, not versioned) |
fido_results/ |
Output of fido_eval.py: CSV, logs, RViz marker bags (not versioned) |
Data folders (
bags/,filter_bags/,results*/,fido_results/) are in.gitignorebecause they contain large files generated locally.
Extracts the SLAM and Qualisys trajectories in TUM format, aligns them (Umeyama), and computes:
- ATE (Absolute Trajectory Error) via
evo_ape - RPE (Relative Pose Error) via
evo_rpe
The Umeyama alignment computed by evo_ape (saved in ape_result.zip) is a prerequisite for fido_eval.py.
python3 slam_eval_pipeline.py \
--slam_bag ./filter_bags/1912_none/1912_none_0.mcap \
--slam_topic /glim_ros/odom_corrected \
--gt_bag /path/to/qualisys_bag/qualisys_0.db3 \
--gt_topic /rigid_bodies \
--rigid_body_name JO \
--slam_ros_version jazzy \
--gt_ros_version humble \
--t_max_diff 0.05 \
--output_dir ./results| Flag | Description | Default |
|---|---|---|
--slam_bag |
Bag containing the SLAM odometry (.mcap/.db3) |
required |
--slam_topic |
SLAM odometry topic (e.g. /glim_ros/odom_corrected) |
required |
--gt_bag |
Qualisys bag with the rigid bodies | required |
--gt_topic |
RigidBodies topic |
required |
--rigid_body_name |
Name of the rigid body to use as ground truth (e.g. the robot) | None |
--slam_ros_version / --gt_ros_version |
ROS2 version of the respective bags (humble, iron, jazzy, foxy) |
humble / jazzy |
--t_offset |
Time offset (s) to add to the SLAM timestamps, if the two bags aren't synchronized | 0.0 |
--t_max_diff |
Maximum time difference (s) to match SLAM/GT poses to the same instant | 0.01 |
--t_start / --t_end |
Seconds to trim from the start/end of the overlapping trajectory | 0.0 |
--skip_rpe |
Skip the RPE computation (ATE only) | False |
--output_dir |
Output directory | ./results |
If --t_offset is 0.0 and the initial timestamps of SLAM and GT differ by more than 0.5s, the script prints a warning with the suggested offset.
For each run, a subfolder <output_dir>/<slam_bag_name>[_t<N>s][_e<N>s]/ is created with:
gt.txt,slam.txt— trajectories in TUM formatape_result.zip,ape_plot_map.png— ATE results (also containsalignment_transformation_sim3.npy, used byfido_eval.py)rpe_result.zip,rpe_plot_map.png— RPE results (unless--skip_rpe)
For each synchronized lidar frame:
- Reads the person's head position from Qualisys.
- Transforms it into the lidar frame using the GLIM odometry and the Umeyama alignment computed in step 1 (or, if
--robot_body_nameis given, using the robot's per-frame pose instead of the static alignment — more robust to drift). - Builds a vertical cylinder (head → feet) around the person in the lidar frame.
- Classifies the points from the "dynamic" and "static" topics against the cylinder → TP/FP/FN/TN.
- Computes precision, recall, F1 per frame and aggregated over the whole sequence.
Requires the output of slam_eval_pipeline.py (ape_result.zip) as input, from which it reads the Umeyama alignment matrix.
python3 fido_eval.py \
--slam_bag ./filter_bags/1912_none/1912_none_0.mcap \
--gt_bag /path/to/qualisys_bag/qualisys_0.db3 \
--output_bag ./filter_bags/1912_none/1912_none_0.mcap \
--evo_result ./results/1912_none_0/ape_result.zip \
--rigid_body_name JO \
--slam_ros_version jazzy \
--gt_ros_version humble \
--output_ros_version jazzy \
--person_height 1.75 \
--cylinder_radius 0.4 \
--output_csv ./fido_results/fido_eval_1912.csv--output_bag can be the same as --slam_bag if the dynamic/static topics, the odometry, and the ground truth are all in the same bag.
| Flag | Description | Default |
|---|---|---|
--slam_bag |
Bag containing /glim_ros/odom_corrected (SLAM odometry) |
required |
--gt_bag |
Qualisys bag with the rigid bodies | required |
--output_bag |
GLIM bag with the dynamic/static pointclouds (can = --slam_bag) |
required |
--evo_result |
ape_result.zip generated by slam_eval_pipeline.py, contains the Umeyama matrix |
required |
--rigid_body_name |
Qualisys rigid body name of the person | JO |
--robot_body_name |
Qualisys rigid body name of the robot; if given, uses per-frame transformation instead of the static Umeyama | None |
--slam_topic / --gt_topic |
Odometry / ground-truth topics | /glim_ros/odom_corrected / /rigid_bodies |
--dynamic_topic / --static_topic |
Pointcloud topics classified as dynamic/static | /glim_ros/velodyne_points_dynamic / /glim_ros/velodyne_points_static |
--slam_ros_version / --gt_ros_version / --output_ros_version |
ROS2 versions of the respective bags | jazzy / humble / jazzy |
--person_height |
Approximate person height (m), defines the cylinder from feet to head | 1.75 |
--cylinder_radius |
Cylinder radius (m) | 0.4 |
--t_max_diff |
Maximum time difference (s) to synchronize odom/GT/pointcloud | 0.05 |
--t_offset_gt |
Offset (s) to add to the Qualisys timestamps, if the cylinder appears lagging/leading the points | 0.0 |
--T_lidar_imu |
Lidar↔IMU extrinsic as 16 floats (row-major 4×4 matrix); omit if the odometry is already in the lidar frame | None (identity) |
--output_csv |
Output CSV path | ./fido_eval.csv |
--rviz_bag |
If given, generates an .mcap bag with the cylinder marker + pointclouds for visual inspection in RViz2 |
None |
--marker_frame |
frame_id of the RViz marker (map = GLIM world, or velodyne/lidar/base_link) |
map |
--marker_topic |
Topic on which to publish the marker in the rviz_bag |
/fido_eval/cylinder |
- CSV (
--output_csv) with one row per frame:t, TP, FP, FN, TN, precision, recall, f1, n_dyn, n_sta, head_x/y/z_lidar. - Aggregated summary printed to screen (total TP/FP/FN/TN, global precision/recall/F1, and per-frame mean±σ).
- (optional)
--rviz_bag:.mcapbag with the cylinder marker and the dynamic/static pointclouds remapped to/fido_eval/points_dynamicand/fido_eval/points_static, useful for visually validating the alignment in RViz2.
- Record a SLAM (GLIM) bag and a Qualisys bag for the same experiment (see [[project_experiment_setup]] for the setup: one person walking around the room for the whole duration of the bag).
- Run
slam_eval_pipeline.pyto get ATE/RPE and the Umeyama alignment matrix (ape_result.zip). - Run
fido_eval.py, passing theape_result.zipfrom step 2, to evaluate the dynamic/static classification on the person. - (Optional) Generate the
--rviz_bagand inspect it in RViz2 to visually verify that the cylinder correctly follows the person.
- ROS2 topics used in this project: Qualisys →
/rigid_bodies(RigidBodies), SLAM →/glim_ros/odom_corrected(Odometry). - If the SLAM and Qualisys bags come from different ROS2 versions (e.g. Jazzy vs Humble), set
--slam_ros_version/--gt_ros_version(and--output_ros_versionforfido_eval.py) correctly: CDR typestores differ between versions. - If the script reports many frames skipped due to failed synchronization (
skipped_odom/skipped_gt), increase--t_max_diffor check that the bags were recorded in the same time window (see the time-offset warning).