Use FAST-LIO2
1 Overview
FAST-LIO2 is a tightly coupled LiDAR-Inertial Odometry algorithm. It fuses LiDAR point clouds and IMU data to output odometry, trajectory, and point cloud maps in real time. Hesai has adapted FAST-LIO2 for JT16 / JT128, supporting mapping in ROS 1 and ROS 2 through standard point cloud and IMU messages.
2 Prerequisites
Before you start, make sure you have:
- An Ubuntu + ROS environment: ROS 1 (Melodic or Noetic recommended) or ROS 2 (Humble).
- The Hesai point cloud SDK
HesaiLidar_SDK_2.0installed and built (underlying dependency of the ROS driver). - The Hesai ROS driver
HesaiLidar_ROS_2.0installed and built, publishing point cloud and IMU normally. - The FAST-LIO2 adaptation code from
FAST_LIO_Hesaiobtained and built (the "Quick Start" below walks you through this). - For a live LiDAR connection, the LiDAR, host NIC, and driver configuration on the same subnet.
2.1 Version Support
| Version | Recommended environment / branch | Build tool | Launch |
|---|---|---|---|
| ROS 1 | Ubuntu 18.04 / 20.04, Melodic / Noetic · main | catkin_make | roslaunch |
| ROS 2 | Ubuntu 22.04, ROS 2 Humble · ROS2 | colcon build | ros2 launch |
2.2 Input Data Requirements
The default configuration reads these topics:
| Data | ROS 1 message type | ROS 2 message type | Default topic |
|---|---|---|---|
| Point cloud | sensor_msgs/PointCloud2 | sensor_msgs/msg/PointCloud2 | /lidar_points |
| IMU | sensor_msgs/Imu | sensor_msgs/msg/Imu | /lidar_imu |
The point cloud must include per-point timestamp and ring fields:
| Field | Type | Description |
|---|---|---|
x | float32 | Point X coordinate |
y | float32 | Point Y coordinate |
z | float32 | Point Z coordinate |
intensity | float32 | Reflectivity |
ring | uint16 | Laser ring ID |
timestamp | float64 | Per-point relative timestamp |
The per-point timestamp field name must be timestamp. If your driver or rosbag uses a different field name, convert it to the format above first.
2.3 Verify the Data Is Ready
Before starting FAST-LIO2, run check_input.py to validate the full input pipeline in one step (topic existence, point cloud fields, timestamp monotonicity and unit, frame drops, ring range, IMU frequency, gyro unit, LiDAR/IMU time sync):
# ROS 2 (driver or rosbag already publishing)
ros2 run fast_lio check_input.py --model jt128
# ROS 1
rosrun fast_lio check_input.py --model jt16
To cross-check against your configured preprocess.timestamp_unit, append --timestamp-unit <value> (0=s, 1=ms, 2=us, 3=ns). Proceed to start FAST-LIO2 only after all checks pass.
3 Quick Start
The steps below cover the full procedure from zero to a working map. Dependency details, common parameter configuration, and troubleshooting are covered in the following sections.
3.1 ROS 1 (Noetic) — JT16 / JT128
Step 1 — Install dependencies
sudo apt update
sudo apt install -y \
ros-noetic-pcl-ros \
ros-noetic-eigen-conversions \
libeigen3-dev \
libpcl-dev
Step 2 — Clone and build
mkdir -p ~/fast_lio_ws/src
cd ~/fast_lio_ws/src
git clone git@github.com:HesaiTechnology-Spatial-Perception/FAST_LIO_Hesai.git
cd FAST_LIO_Hesai
git submodule update --init --recursive
cd ~/fast_lio_ws
source /opt/ros/noetic/setup.bash
catkin_make
source devel/setup.bash
Step 3 — Start the Hesai ROS driver
Start the Hesai ROS driver so that it publishes /lidar_points and /lidar_imu.
ROS 1:
cd ~/hesai_ros_ws
source devel/setup.bash
roslaunch hesai_ros_driver start.launch
For a live LiDAR connection, enable the real-time data source and ROS topic output in the driver configuration:
lidar:
- driver:
source_type: 1
lidar_udp_type:
device_ip_address: 192.168.1.201
udp_port: 2368
ptc_port: 9347
ros:
ros_send_point_cloud_topic: /lidar_points
ros_send_imu_topic: /lidar_imu
send_point_cloud_ros: true
send_imu_ros: true
Adjust device_ip_address, udp_port, and ptc_port for your network.
Step 4 — Launch FAST-LIO2
cd ~/fast_lio_ws
source /opt/ros/noetic/setup.bash
source devel/setup.bash
roslaunch fast_lio mapping_jt16.launch # JT16
# roslaunch fast_lio mapping_jt128.launch # JT128
3.2 ROS 2 (Humble) — JT16 / JT128
Step 1 — Install dependencies
sudo apt update
sudo apt install -y \
ros-humble-pcl-ros \
ros-humble-pcl-conversions \
ros-humble-tf2-ros \
libeigen3-dev \
libpcl-dev
Step 2 — Clone and build
mkdir -p ~/fast_lio2_ws/src
cd ~/fast_lio2_ws/src
git clone -b ROS2 git@github.com:HesaiTechnology-Spatial-Perception/FAST_LIO_Hesai.git
cd FAST_LIO_Hesai
git submodule update --init --recursive
cd ~/fast_lio2_ws
source /opt/ros/humble/setup.bash
colcon build --packages-select fast_lio --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
ros2 pkg prefix fast_lio # confirm the build registered the package
Step 3 — Start the Hesai ROS 2 driver
Start the Hesai ROS 2 driver so that it publishes /lidar_points and /lidar_imu.
ROS 2:
cd ~/hesai_ros2_ws
source install/setup.bash
ros2 launch hesai_ros_driver start.launch.py
Step 4 — Launch FAST-LIO2
cd ~/fast_lio2_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 launch fast_lio mapping_jt16.launch.py # JT16
# ros2 launch fast_lio mapping_jt128.launch.py # JT128
Step 5 — View the result
After launching, set Fixed Frame to camera_init in RViz and subscribe to /cloud_registered and /path to see the real-time point cloud and trajectory. See View Mapping Results for the full output list and accumulated map configuration.
Below are mapping examples of JT16 running FAST-LIO2:
4 Advanced Application methods
4.1 Launch Options
Replace jt16 with jt128 in the commands to launch JT128. To run without RViz, append rviz:=false:
# ROS 1
roslaunch fast_lio mapping_jt128.launch rviz:=false
# ROS 2
ros2 launch fast_lio mapping_jt128.launch.py rviz:=false
4.2 Use a Rosbag
ROS 1:
roslaunch fast_lio mapping_jt16.launch
rosbag play your_data.bag
ROS 2:
ros2 launch fast_lio mapping_jt16.launch.py
ros2 bag play /path/to/your_bag
If the rosbag uses different topic names, update the topics in the configuration file:
common:
lid_topic: "/your_points_topic"
imu_topic: "/your_imu_topic"
If the rosbag stores Hesai raw packets, parse them with the Hesai driver first and publish /lidar_points and /lidar_imu before running FAST-LIO2.
4.3 Convert a PCAP to rosbag
For PCAP files recorded with PandarView, convert them into a FAST-LIO2-ready rosbag first. The pipeline is:
input.pcap
↓ Hesai ROS driver (source_type: 2, PCAP parsing mode)
/lidar_points + /lidar_imu
↓ rosbag record / ros2 bag record
output.bag (ROS 1) or output/ directory (ROS 2)
↓ FAST-LIO2
The repository provides wrapper scripts under tools/pcap_to_rosbag/ that automate the whole flow — generate the PCAP driver config, start parsing, wait for topics, record, detect end of playback, and validate the bag — no manual driver config editing needed.
4.3.1 Run FAST-LIO2 from a PCAP
Step 1 — Convert PCAP to rosbag
ROS 1 (output: a .bag file):
bash tools/pcap_to_rosbag/pcap_to_rosbag_ros1.sh \
--model jt128 \
--pcap /data/input.pcap \
--correction /data/correction.csv \
--firetime /data/firetime.csv \
--output /data/output.bag \
--driver-ws ~/hesai_ros_ws
ROS 2 (output: a directory in rosbag2 format):
bash tools/pcap_to_rosbag/pcap_to_rosbag_ros2.sh \
--model jt128 \
--pcap /data/input.pcap \
--correction /data/correction.csv \
--firetime /data/firetime.csv \
--output /data/output \
--driver-ws ~/hesai_ros2_ws
The script prints the bag path and the suggested next-step commands when it finishes.
Step 2 — Validate the bag input (optional but recommended)
Play the bag and run the check tool at the same time to confirm fields, IMU frequency, and time sync before proceeding:
# Terminal 1
rosbag play /data/output.bag # ROS 1
# ros2 bag play /data/output # ROS 2
# Terminal 2
rosrun fast_lio check_input.py --model jt128 # ROS 1
# ros2 run fast_lio check_input.py --model jt128 # ROS 2
Step 3 — Launch FAST-LIO2, play the bag, and view results in RViz
Follow the Use a Rosbag section to launch FAST-LIO2 and play the bag. RViz opens automatically with the launch file — set Fixed Frame to camera_init and subscribe to /cloud_registered and /path to see the real-time mapping result.
Prerequisites: the PCAP must be parseable by the Hesai driver, contain point cloud and IMU data, and the point cloud must include
ringandtimestampfields. Driver config field names such asfiretime_file_pathmay vary across driver versions. The scripts require Python 3 with PyYAML (pip3 install pyyaml).
4.4 Input and Config Check Tools
To reduce mapping failures caused by wrong input pipelines or misconfiguration, the repository provides three check scripts under tools/.
check_input.py — runtime check (use it while the driver or a rosbag is publishing). It diagnoses topic existence, point cloud fields, timestamp monotonicity and unit, frame drops, ring range, IMU frequency, gyro unit (deg/rad), and LiDAR/IMU time synchronization:
# ROS 2
ros2 run fast_lio check_input.py --model jt128
# ROS 1
rosrun fast_lio check_input.py --model jt16
check_config.py — static yaml config check (no ROS required). It validates that lidar_type / scan_line match the model and ROS version, plus timestamp_unit, imu_gyr_unit, blind, and the extrinsic matrix:
python3 tools/check_config.py --config config/jt128.yaml --model jt128 --ros 2
python3 tools/check_config.py --config config/jt16.yaml --model jt16 --ros 1
check_map.py — analyzes mapping output quality and suggests likely causes. It measures surface thickness (ghosting), point density, and trajectory drift, and points to probable causes (extrinsics / time sync / imu_gyr_unit / point_filter_num):
# Offline: analyze a saved PCD (recommended)
python3 tools/check_map.py --pcd PCD/fast_lio2_jt_map.pcd
# Live: requires publish.map_en enabled
ros2 run fast_lio check_map.py --map-topic /Laser_map --odom-topic /Odometry
See tools/README.md for all options.
4.5 AI Agent Skill
Skill files are provided under tools/agent/ (run_fastlio2_ros1.md / run_fastlio2_ros2.md). Two ways to use them:
- Auto-load by agent: add the relevant file to the agent's context and the agent follows the skill flow automatically.
- Explicit command: invoke
/run-fastlio2-hesai-ros1or/run-fastlio2-hesai-ros2directly.
4.6 View Mapping Results
In RViz, set Fixed Frame to camera_init and focus on these outputs:
| Output | Topic | Description |
|---|---|---|
| Odometry | /Odometry | FAST-LIO2 estimated pose |
| Trajectory | /path | Accumulated trajectory |
| Registered point cloud | /cloud_registered | Current frame transformed into the world coordinate frame |
| Body-frame point cloud | /cloud_registered_body | Current frame in the body frame |
| Effective feature points | /cloud_effected | Points used for matching |
| Accumulated map | /Laser_map | Optional accumulated point cloud map |
| Transform | /tf | TF from camera_init to body |
The default configuration is mainly used to view /cloud_registered and /path in real time.
ROS 2 JT16 / JT128 default differences (designed around point cloud load): JT16 has 16 lines and fewer points per frame, so accumulated-map publishing, effective-feature publishing, and PCD saving are enabled by default for an out-of-the-box full mapping view in RViz. JT128 has 128 lines, roughly 8x the points per frame, so the three options above are disabled by default to reduce CPU, memory, and RViz rendering load; enable them in yaml when needed. The ROS 1 yaml of both models does not enable accumulated-map publishing (publish_map is commented out in the source).
To view the full accumulated map in RViz, only the ROS 2 version supports controlling it directly in yaml:
publish:
map_en: true # Periodically publish /Laser_map
effect_map_en: true # Periodically publish /cloud_effected
The ROS 1 version currently has no publish.map_en / publish.effect_map_en parameters. In the source code, the publish_map(...) call is commented out by default; the /Laser_map topic is advertised but does not publish data by default. To view the full accumulated map in ROS 1, you can:
- View the offline PCD result directly (see Save a PCD Map).
- Or uncomment the
publish_map(pubLaserCloudMap);call insrc/laserMapping.cppand rebuild.
Either way, /Laser_map accumulates points over time, and a large number of points increases CPU, memory, and RViz rendering load.
4.7 Save a PCD Map
4.7.1 ROS 1
The ROS 1 version supports enabling PCD saving in yaml. The node can write the file when it exits (or at a configured frame interval), and it also provides the /map_save service for active saving during runtime:
map_file_path: "PCD/fast_lio2_jt_map.pcd" # Optional. Defaults to PCD/scans.pcd if unset.
pcd_save:
pcd_save_en: true
interval: -1 # -1 saves once when the node exits; >0 saves every N frames
Active saving during runtime:
rosservice call /map_save "{}"
Actual output paths:
interval | Output path |
|---|---|
-1 (default) | The path specified by map_file_path; if unset, <package path>/PCD/scans.pcd |
> 0 | <package path>/PCD/scans_<index>.pcd (one file every interval frames) |
map_file_pathsupports absolute and relative paths. Relative paths are joined with the FAST-LIO2 package root. Interval-based saving still uses thePCD/scans_<index>.pcdnaming.
To trigger saving on exit, stop the FAST-LIO2 node with Ctrl+C. To save immediately while the node is running, call /map_save.
4.7.2 ROS 2
The ROS 2 version also supports yaml-based saving on exit and the /map_save service for active saving during runtime. The jt16.yaml enables pcd_save_en by default and sets map_file_path to PCD/fast_lio2_lidar_only_map.pcd; the jt128.yaml disables PCD saving by default, so enable it as shown below when needed:
map_file_path: "PCD/fast_lio2_lidar_only_map.pcd"
pcd_save:
pcd_save_en: true
interval: -1
ros2 service call /map_save std_srvs/srv/Trigger '{}'
After the service returns success=True, the map is written to the path specified by map_file_path.
5 Common Parameters
| Parameter | JT16 | JT128 |
|---|---|---|
common.lid_topicpoint cloud topic | /lidar_points | /lidar_points |
common.imu_topicIMU topic | /lidar_imu | /lidar_imu |
common.imu_gyr_unitangular velocity unit | deg | deg |
preprocess.lidar_typetype enum¹ | ROS 1: 5, ROS 2: 1 | ROS 1: 6, ROS 2: 2 |
preprocess.scan_linenumber of scan lines | 16 | 128 |
preprocess.timestamp_unittimestamp unit² | 0 | 0 |
preprocess.blindblind zone distance (m) | 1.0 | 0.5 |
mapping.fov_degreefield of view (degrees) | 90.0 | 360.0 |
mapping.det_rangedetection range (m) | 200.0 | 200.0 |
publish.map_enpublish accumulated map³ | ROS 2: true | ROS 2: false |
publish.effect_map_enpublish effective points³ | ROS 2: true | ROS 2: false |
pcd_save.pcd_save_ensave PCD file | ROS 1: false, ROS 2: true | false |
map_file_pathPCD output path⁴ | ROS 2: PCD/fast_lio2_lidar_only_map.pcdROS 1: unset | Unset |
Notes:
- ¹ ROS 1 has JT16=5, JT128=6; ROS 2 has JT16=1, JT128=2
- ² 0=seconds, 1=milliseconds, 2=microseconds, 3=nanoseconds
- ³ ROS 2 only; ROS 1 does not support this parameter
- ⁴ Supports relative paths (relative to package root); defaults to
PCD/scans.pcdif unset
On ROS 2,
publish.map_en,publish.effect_map_en, andpcd_save.pcd_save_endefault totruefor JT16 andfalsefor JT128. Together withpoint_filter_num(1for JT16,6for JT128), they follow the same "lightweight when points are few, resource-saving when points are many" default strategy.
5.1 Extrinsic Configuration
In FAST-LIO2, extrinsics describe the LiDAR pose relative to the IMU coordinate frame. If accurate extrinsics are available, disable online extrinsic estimation:
mapping:
extrinsic_est_en: false
extrinsic_T: [0.0, 0.0, 0.0]
extrinsic_R: [1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0]
If extrinsics are unknown, keep extrinsic_est_en: true first. However, insufficient motion excitation or unstable time synchronization may cause online estimation to produce trajectory drift.
6 FAQ
6.1 FAST-LIO2 Has No Output
Run check_input.py while the driver or rosbag is publishing — it diagnoses topic existence, publish frequency, field completeness, and config consistency in one pass:
ros2 run fast_lio check_input.py --model jt128 # ROS 2
rosrun fast_lio check_input.py --model jt16 # ROS 1
The script outputs a clear PASS / FAIL / WARN per check with a fix direction: missing topic → driver not started or misconfigured; frequency 0 → driver not publishing; field or config mismatch → pointed out directly.
6.2 IMU and LiDAR Are Reported as Unsynchronized
Run check_input.py — check 9 (LiDAR/IMU time sync) automatically compares header timestamps: >1000 ms offset is reported as FAIL (different clock sources), 100–1000 ms as WARN (consider common.time_offset_lidar_to_imu). Items that require manual confirmation:
- Whether rosbag playback uses
--clockso that timestamps are replayed correctly. - Whether
common.time_offset_lidar_to_imuneeds a fixed time offset.
6.3 Trajectory Diverges or the Map Has Ghosting
Run the two check tools first to rule out automatically detectable causes:
# 1. Check input data (gyro unit, timestamp field and unit, ring range, frame drops)
ros2 run fast_lio check_input.py --model jt128 --timestamp-unit 0
# 2. Check yaml config (lidar_type, scan_line, imu_gyr_unit, extrinsic_R validity)
python3 tools/check_config.py --config config/jt128.yaml --model jt128 --ros 2
If both tools report no errors, investigate the remaining items that cannot be detected automatically:
- LiDAR-IMU extrinsics are incorrect (recalibration required).
- The static initialization time is too short, or motion during initialization is too large.
6.4 Cannot See the Full Map in RViz
- ROS 1: There is currently no
publish.map_enparameter, and thepublish_map(...)call is commented out by default in the source. View the mapping result offline through the saved PCD map (see "Save a PCD Map" above), or uncomment the source call and rebuild. - ROS 2 (JT16): Few points;
publish.map_enis enabled by default, so you can view/Laser_mapdirectly in RViz. - ROS 2 (JT128): Many points; disabled by default to reduce load. Set
publish.map_en: truein yaml to view/Laser_map.
6.5 Structure Divergence or Point Cloud Misalignment During Mapping
JT128 generates a large amount of point cloud data. When the computing platform lacks sufficient processing capability, issues such as local map divergence, point cloud misalignment, and reduced mapping quality may occur.
After confirming that IMU and point cloud data are continuous and free from significant frame loss or timestamp abnormalities, try increasing point_filter_num above its default (6 for JT128) to verify:
preprocess:
point_filter_num: 8
Find it useful?