Main features:
- Robust constant-velocity motion model for translation prediction, improving resilience to IMU noise and sensor dropouts. Works with ouster's IMU out-of-the-box.
- REP-105 compliant frame hierarchy with layered separation between local SLAM (
mapFrameLocal) and global georeferencing (mapFrameEnu), enabling clean extension without degrading smoothness. - Floating-anchor GPS fusion with improved timestamp-based keyframe association (0.30s hard gating), GPS constraint visualization in RViz, and configurable processing delays to handle sensor sync issues.
- Add
--force_initial_gpsmode for manual GPS datum bootstrap before the first real fix, allowing ENU outputs to start immediately. - Publish fused GPS/SLAM poses in both ENU and NED frames for compatibility with different navigation systems.
- Keep publishing the estimated lla coordinates of the current pose after GPS disappears.
- Publish the gps origin and more useful informaiton related to GPS fusion.
- Add
- Rolling local map optimization using spatial hash grids for stable CPU load during loop closure and map rebuilds (triggerable on loop closure and GPS jumps).
- NED frame outputs alongside ENU for navigation system compatibility, including full pose/odometry split between LiDAR and base-link frames.
- Dense trajectory exports (CSV format) preserving full raw GPS and odometry histories for offline geo-referencing and post-run analysis.
- Satellite visualization tool (
visualize_saved_map_satellite.py) rendering saved maps over satellite imagery as interactive HTML overlays. - Diagnostics infrastructure with per-stage time-slicing statistics and per-cycle event logging for systematic performance analysis.
- Structured map metadata output with geo-referenced clouds in both local and ENU frames, timestamped trajectories, and automatic orbit path persistence.
This repository provides a ROS 2 Jazzy port of LIORF/LIO-SAM style lidar-inertial odometry and mapping.
This guide is simplified for Ubuntu 24.04 and uses Zenoh (rmw_zenoh_cpp) as the default ROS middleware.
- Ubuntu 24.04
- ROS 2 Jazzy installed (
ros-baseordesktop) - Build tools:
colcon,cmake,gcc/g++
If ROS 2 Jazzy is not installed yet, follow the official ROS 2 Jazzy install first.
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
git \
python3-colcon-common-extensions \
python3-rosdep \
libpcl-dev \
libopencv-dev \
libyaml-cpp-dev \
libgeographiclib-dev \
libtbb-dev \
libboost-all-dev \
ros-jazzy-rmw-zenoh-cppInitialize rosdep once (if needed):
sudo rosdep init || true
rosdep updateThe old
apt/PPA instructions are intentionally removed. Build from source for reproducibility on 24.04.
mkdir -p ~/repos
cd ~/repos
git clone https://github.com/borglab/gtsam.git
cd gtsam
git checkout 4.2.0
# Optional: faster build
sed -i 's/add_subdirectory(examples)/# add_subdirectory(examples)/g' CMakeLists.txt
sed -i 's/add_subdirectory(tests)/# add_subdirectory(tests)/g' CMakeLists.txt
mkdir -p build
cd build
cmake .. \
-DGTSAM_WITH_TBB=OFF \
-DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF \
-DGTSAM_USE_SYSTEM_EIGEN=ON \
-DGTSAM_BUILD_UNSTABLE=ON \
-DCMAKE_BUILD_TYPE=Release
make -j"$(nproc)"
sudo make install
sudo ldconfigmkdir -p ~/liorf-ros2/src
cd ~/liorf-ros2/src
git clone <YOUR_FORK_OR_THIS_REPO_URL> liorf
cd ..
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --cmake-args -DCMAKE_BUILD_TYPE=ReleaseAdd to your shell config (~/.bashrc):
source /opt/ros/jazzy/setup.bash
export ROS_DOMAIN_ID=0
export RMW_IMPLEMENTATION=rmw_zenoh_cppApply it:
source ~/.bashrcQuick check:
echo $RMW_IMPLEMENTATION
# expected: rmw_zenoh_cppcd ~/liorf-ros2
source install/setup.bash
ros2 launch liorf run_lio_sam_default.launch.pyIn another terminal (same environment), play a bag:
cd ~/liorf-ros2
source install/setup.bash
ros2 bag play <path_to_ros2_bag>For bagfile replay (especially when collecting diagnostics/telemetry), prefer playing with ROS time:
ros2 bag play <path_to_ros2_bag> --clockAnd set use_sim_time to true either:
- as a launch argument (recommended for ad-hoc runs), e.g.
use_sim_time:=true, or - in your parameter YAML file.
Launch argument behavior is override-only: if use_sim_time is not specified in launch, YAML parameter values remain unchanged.
Diagnostics logging also maintains a stable symlink:
~/.ros/liorf_logs/latest-> newestrun_YYYYMMDD_HHMMSSdirectory
After a run, plot timing_stats.csv from diagnostics logs:
python3 scripts/plot_time_slicing_stats.pyBy default, the script reads the latest run (via ~/.ros/liorf_logs/latest when available), saves the PNG plot, and displays it.
Useful options:
# specific run directory or CSV
python3 scripts/plot_time_slicing_stats.py --input ~/.ros/liorf_logs/run_YYYYMMDD_HHMMSS
# smoothing + custom output
python3 scripts/plot_time_slicing_stats.py --window 10 --output /tmp/timing_plot.pngLIORF follows ROS frame guidance from REP-105 (map/odom/base_link) and uses a layered variant so local smooth odometry and global georeferencing remain cleanly separated.
----------------- gps-enabled frames
ECEFframe (e.g., "earth")
└── mapFrameEnu (e.g., "map_enu")
├── mapFrameNed (e.g., "map_ned")
------------------ ¡always-active local frames!
└── mapFrameLocal (e.g., "map")
└── odometryFrame (e.g., "odom")
├── "lidar_link" (compatibility branch)
└── baselinkFrame (e.g., "os_sensor")
---------------- if lidarFrame != baselinkFrame:
└── ... TF chain ...
└── lidarFrame (e.g., "os_lidar")
mapFrameLocal: local SLAM optimization frame (always present; identity-aligned tomapFrameEnuuntil GPS anchor estimation is available).odometryFrame: compatibility/output frame used by odometry and map products consumed by downstream tools.baselinkFrame: robot body frame.lidarFrame: lidar sensor frame.
mapFrameEnu: ENU frame anchored at the first accepted GNSS datum.ECEFframe: Earth-centered global frame.
- Global/georeferencing layer:
ECEFframe -> mapFrameEnu -> mapFrameLocal
- Local motion/robot layer:
mapFrameLocal -> odometryFrame -> baselinkFrame- Alternative compatibility branch (always published):
odometryFrame -> "lidar_link"
odometryFrame -> "lidar_link"is hardcoded and always published as a compatibility branch.odometryFrame -> "lidar_link"andodometryFrame -> baselinkFramenow come from the smooth incremental LiDAR odometry state, so loop closures and GPS updates are absorbed upstream inmapFrameLocal -> odometryFrameinstead of causing odom-side TF jumps.- When
lidarFrame != baselinkFrame, base-link poses are derived by composing the LiDAR pose with the looked-uplidarFrame -> baselinkFrametransform. - For now,
mapFrameLocalandodometryFrameare practically the same in nominal operation (their relative transform is initialized as identity and usually remains near identity). - Relative to REP-105 terminology, this implementation intentionally splits the usual “map/odom” behavior into
mapFrameLocalandodometryFrameso future loop-closure/global alignment corrections can be represented upstream without degrading odometry smoothness; georeferencing-related jumps are isolated at themapFrameEnu -> mapFrameLocaljoint.
- Legacy LiDAR topics kept for compatibility:
liorf/mapping/odometry: graph-optimized LiDAR pose inmapFrameLocal -> "lidar_link"liorf/mapping/odometry_incremental: smooth LiDAR pose inodometryFrame -> "lidar_link"
- New base-link topics:
liorf/mapping/baselink_odometry: graph-optimized base-link pose inmapFrameLocal -> baselinkFrameliorf/mapping/baselink_odometry_incremental: smooth base-link pose inodometryFrame -> baselinkFrame
See ARCHITECTURE.md for full transform chain, TF ownership, and publication behavior details.
This repository uses a Floating Anchor GPS fusion strategy:
- The local SLAM trajectory remains in its native
mapFrameLocalframe (default:map_local). - GPS does not hard-snap local key poses to global ENU.
- A separate global-to-local transform
$T_{G_L}$ (published asmapFrameEnu -> mapFrameLocal) is optimized in the graph.
- GNSS input topic type:
sensor_msgs/msg/NavSatFix - Configure
gpsTopicin your selected YAML under config
To start ENU-related publishing before the first real GNSS fix arrives, enable manual datum bootstrap:
force_initial_gps: true
manual_gps_origin: [lat_deg, lon_deg, alt_m]
manual_global_heading: 0.0Parameter meaning:
force_initial_gps: enables manual origin/heading bootstrap at node startup.manual_gps_origin: datum origin in geodetic coordinates[latitude, longitude, altitude].manual_global_heading: initial global heading in degrees (converted internally to ENU yaw).
Behavior:
liorf/gps_originand ENU/NED GPS-derived outputs can start before the first sensor GNSS message.- On the first accepted real GPS factor, the floating-anchor prior is inserted once and translation is aligned, while manual yaw is preserved.
If this appears inactive at runtime, verify you are launching the build that contains your updated YAML and code:
- for source workspace runs: rebuild and
source install/setup.bashin that workspace beforeros2 launch. - for installed package runs: confirm the installed YAML under
install/liorf/share/liorf/config/has the same parameter values.
Example:
gpsTopic: "gps/fix"liorf/mapping/gps_odom(nav_msgs/msg/Odometry): local Cartesian projection of NavSatFix.liorf/gps_origin(sensor_msgs/msg/NavSatFix): captured datum origin used for local projection.- TF
mapFrameEnu -> mapFrameLocal: optimized global offset/rotation from floating-anchor fusion. liorf/enu_to_local_offset(geometry_msgs/msg/PoseWithCovarianceStamped): same ENU-to-local offset as the TF, including covariance when available.liorf/mapping/lidar_gps_enu_poseandliorf/mapping/lidar_gps_ned_pose(geometry_msgs/msg/PoseStamped): fused LiDAR pose in ENU/NED frames.liorf/mapping/baselink_gps_enu_odometryandliorf/mapping/baselink_gps_ned_odometry(nav_msgs/msg/Odometry): fused base-link pose in ENU/NED frames.
You can trigger map export via ROS service directly or with the helper script:
# default destination/resolution
./scripts/save_map.sh
# set voxel resolution
./scripts/save_map.sh -r 0.2
# HOME-relative destination
./scripts/save_map.sh -d Downloads/my_map
# absolute destination
./scripts/save_map.sh -a /tmp/liorf_mapYou can also trigger map export via launch arguments:
# default values (resolution=0.0, destination='')
ros2 launch liorf save_map.launch.py
# custom resolution
ros2 launch liorf save_map.launch.py resolution:=0.2
# custom destination
ros2 launch liorf save_map.launch.py destination:=/tmp/liorf_map
# custom wait timeout for service availability
ros2 launch liorf save_map.launch.py wait_timeout_sec:=60.0When calling liorf/save_map, georeference metadata is saved to:
goereference.yaml
It includes:
gps_origin_enu(latitude/longitude/altitude when available)T_enu_local(x,y,z,roll,pitch,yaw)
Additional export summary is saved to:
save_summary.yaml
It includes ROS save time, keyframe count, dense trajectory points saved, raw GPS points saved, and map point counters.
Save response also reports useful export stats:
save_directory: resolved absolute save pathenu_map_saved: whether ENU artifacts were exportedkeyframes_used: number of keyframes used for map constructionsurf_points_local/surf_points_enu: surf map point countsfull_points_local/full_points_enu: full map point countstrajectory_points_saved: dense trajectory points written totrajectories/trajectory_dense_local.csvgps_points_saved: raw GPS points written totrajectories/gps_raw_geodetic.csvmessage: status details (success or skip reason)
Saved artifacts are organized as:
- root:
goereference.yaml,save_summary.yaml,README.md maps/:FullMap_*.pcd,SurfaceMap_*.pcdtrajectories/: trajectory PCDs + CSV exports (gps_raw_geodetic.csv,trajectory_keyframes_local.csv,trajectory_dense_local.csv)
After successful map save, the node writes the absolute path to:
~/.liorf_last_saved_map_path
You can visualize saved map outputs over satellite imagery with:
python3 scripts/visualize_saved_map_satellite.py --map-dir <saved_map_directory>If --map-dir is omitted, the script resolves map directory in this order:
~/.liorf_last_saved_map_path(written byliorf/save_mapon successful save)- default
~/Downloads/LOAM - fail with an error message
Optional controls:
--output <html_path>: output HTML file path--max-surf-points <N>: limit sampled surf points on overlay heatmap--max-traj-points <N>: limit sampled trajectory points
The script reads goereference.yaml (with compatibility fallback to older metadata filenames) and map PCD outputs (maps/SurfaceMap_ENU.pcd preferred, or local maps transformed by T_enu_local) and produces an interactive HTML map with satellite basemap and trajectory/surf overlays.
On success, it prints both the generated HTML path and a file://... URI that can be opened from terminal links.
Zenoh-based Docker setup is documented in docker/README.md.
Thanks to the original projects and datasets: