|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file
Package Dependencies
System Dependencies
Dependant Packages
Launch files
Messages
Services
Plugins
Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange
|
lidar_localization_ros2 package from lidar_localization_ros2 repolidar_localization_ros2 |
ROS Distro
|
Package Summary
| Version | 1.10.0 |
| License | BSD2.0 |
| Build type | AMENT_CMAKE |
| Use | RECOMMENDED |
Repository Summary
| Description | |
| Checkout URI | https://github.com/rsasaki0109/lidar_localization_ros2.git |
| VCS Type | git |
| VCS Version | main |
| Last Updated | 2026-11-13 |
| Dev Status | UNKNOWN |
| Released | UNRELEASED |
| Contributing |
Help Wanted (-)
Good First Issues (-) Pull Requests to Review (-) |
Package Description
Maintainers
- ryohei sasaki
Authors
lidar_localization_ros2
Map-based 3D LiDAR localization for ROS 2 and Nav2.
Handheld Livox MID-360, ~390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it. Each scan is matched to the map with NDT; RKO-LIO odometry predicts the motion between scans and carries the pose where matching fails. The error is 0.26 m RMSE for the whole run, in an offline replay started from the ground-truth pose (Koide Hard Point Cloud Localization Dataset outdoor_hard_02b, CC BY 4.0). How this was made · more runs
Give it a point cloud map (.pcd or .ply) and a LiDAR stream; it publishes the
robot pose in the map. It finds the starting pose by itself, uses odometry and IMU when
you have them, and works standalone or as the localization source for Nav2.
Where to start
| You want to | Go to |
|---|---|
| Localize your robot on a map you already have | Install, then Quick start |
| Run it on a Unitree Go2 or another MID-360 robot | Go2 / MID-360 in five minutes |
| Navigate with Nav2 | Nav2 |
| Try it without a robot | Run the public dataset |
| See how accurate it is | Benchmark |
| Fix a bringup that does not work | Troubleshooting |
Install
ROS 2 Jazzy is the main target; Humble is supported.
mkdir -p ~/lidarloc_ws/src
cd ~/lidarloc_ws/src
git clone https://github.com/rsasaki0109/lidar_localization_ros2.git
cd lidar_localization_ros2
scripts/bootstrap_colcon_workspace.sh --build
source ~/lidarloc_ws/install/setup.bash
This builds ndt_omp_ros2 and this
package, about 30 minutes on an 8-core machine.
small_gicp is optional (SMALL_GICP and
SMALL_VGICP backends): on Jazzy, sudo apt install ros-jazzy-small-gicp-vendor
before building is enough. For Humble, a manual or no-sudo build, see
local build.
No ROS install? Build the Docker image and check it in one command:
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
Quick start
- Start the LiDAR driver, and odometry if you have it.
- Run quickstart with your map:
ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
- Check that it is tracking:
ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
Quickstart finds the cloud and IMU topics, /clock, an odom -> base TF and the
LiDAR frame, and prints what it picked on its Discovery: line. Then it starts the
localizer and RViz. On the first run it searches the whole map for the robot; next time
it restores the pose saved for the same map. If the search finds no safe match, it asks
you for 2D Pose Estimate in RViz (press P, click the robot, drag along its
heading) instead of guessing.
While it runs, the terminal prints one line whenever tracking starts, weakens, is lost or comes back, with what to do about it, and RViz shows the same status above the robot:
[localization_status]: Tracking.
[localization_status]: Tracking lost: no scan has matched the map for 4 s. If it does not recover, set 2D Pose Estimate in RViz.
What each line means: reading the status line.
Useful options:
-
--dry-runshows what it would start, without starting it. -
--profile mid360uses the Livox MID-360 preset.
File truncated at 100 lines see the full file
Changelog
Unreleased
1.10.0 - 2026-11-13
Upgrading: no configuration needs changing, and every new setting defaults to the
old behaviour. One behaviour differs for a deployment that runs the reinitialization
supervisor (global_localization_recovery.launch.py): it now ends its episode on an
/initialpose that it did not publish, so a reset by an operator or another node
stops a pending recovery instead of being followed by the supervisor’s own reset.
Added
-
Three settings answer a global localization query in 2 s instead of 19 s on the
Boreas fixture:
g2_ndt_local_map_radius:=0.0,g2_ndt_num_threads:=4andg2_ndt_scan_max_range:=80.0onglobal_localization_recovery.launch.py(node parametersndt_local_map_radius,ndt_num_threadsand the newndt_scan_max_range). The defaults are unchanged.- Radius 0 prepares the whole map’s scoring target once at startup instead of a crop per candidate on every query, which took 13-14 of the 19 s. The node holds about 240 MiB more on Boreas.
- The range cap scores only scan points within it. With every point the true pose’s fitness was 300-770, so no answer was ever published; within 80 m it is 0.05-0.21.
-
Over 16 scans with no prior, with
g2_registration_refine_candidates:=true: 8 right answers, none wrong and 8 not published, where the defaults published none. The map-wide search still misses the right pose for those 8 (docs/global_localization.md). - The fitness score of the default crop path is unchanged, and it is now threaded too.
- In the closed loop they do not recover a 10 s stop on Boreas, 0 of 10: every published answer was right for its scan and 12.6-134 m behind the vehicle when applied. They stay opt-in.
-
run_koide_public_bag.py --stall START_SEC:DURATION_SECfreezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop intosummary.jsonastools/benchmark/soak_replay.pyscores a stall.-
With odometry a 10 s stop recovers. On
outdoor_hard_02b, the first scan is accepted 0.2-1.0 s after a 10 s stop, in a turn and on a straight, seeded from the odometry transform 13-18 m from the last accepted pose. No run requested reinitialization, and no pose was more than 0.54 m off (docs/global_localization.md). - A replay that ends while a process is frozen now resumes it before stopping it.
-
With odometry a 10 s stop recovers. On
Fixed
-
The reinitialization supervisor no longer resets a localizer that is tracking
after someone else’s
/initialpose. It ends its episode on an/initialposeit did not publish (reasonexternal_resetin the event log), as the localizer already treats that reset as authoritative.-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
alias_confirmed. The episode kept querying with no request raised, and 28 s later reset a localizer tracking at 96% inliers to a pose 12.6 m behind it; the track was lost for the rest of the lap. - A request still raised after the operator’s reset starts a new, debounced episode with a fresh attempt count.
-
What happened: on the looped Boreas fixture, a check query answered on a scan
from before an operator’s reset set
-
tools/benchmark/soak_replay.pyscores a fault only on the poses of aligned scans. The pose the localizer republishes on/initialposecounted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores indocs/benchmark.mddo not change: 24 saved runs re-score the same.
Validation
- The release check: two full Koide
outdoor_hard_02breplays at 1x passed onrelease/v1.10.0at8d7183e(mainatf4f5213plus the version bump), built without a build type, with ndt_omp_ros2 at its 0.2.0 release. The localizer executable is byte-identical to 1.9.0’s.- Accuracy: 0.242 m RMSE in both (0.170-0.171 m on the map), against 0.240-0.242 m (0.170 m) for v1.9.0.
- Coverage: 100.0% diagnostic coverage in both.
-
Alignment: 16 ms at the median and 39-41 ms at p95, against 13 and 33-34 ms,
on a host CPU at 2.10 GHz where v1.9.0’s was 2.30 GHz, so the times are not
like-for-like
(docs/koide-online-replay.md,
docs/validation/v1.10.0-koide-20261113).
- Boreas,
param/boreas_ndt_velodyne.yamlon the release build: 0.132 m RMSE over the first 120 s, 0.127 m withuse_imu_preintegration: true, every scan accepted, as on 1.9.0.
1.9.0 - 2026-11-10
Upgrading: 1.9.0 removes ten features that no preset turned on, among them the
twist EKF (see Removed). Their switches stay declared, and configuring one of them
true now fails with a message that names it; their thresholds are no longer
declared, and ROS ignores them. A configuration that left them off is unaffected.
lidar_localization_node now limits glibc to two malloc arenas unless
MALLOC_ARENA_MAX is set; a component loaded into another container needs it set in
that container’s environment.
Changed
-
global_localization_recovery.launch.py start_localization:=falsestarts only the global localization node and the supervisor, beside a localizer already running.tools/benchmark/soak_replay.py --companion COMMANDstarts such a command once the soaked localizer is active, in its ROS domain. On the Boreas fixture, after a 10 s stop, the recovery nodes did not recover the track: their map-wide search missed and each answer took 18-21 s (docs/global_localization.md). -
The node frees a replaced registration target before it builds the next one, and
returns freed memory to the OS. On the Boreas fixture with the preset (80 m crop,
use_imu_preintegration: true):
File truncated at 100 lines see the full file