No version for distro humble showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro jazzy showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro kilted showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro lyrical showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro rolling showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro galactic showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro iron showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro melodic showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange

No version for distro noetic showing github. Known supported distros are highlighted in the buttons above.
Package symbol

lidar_localization_ros2 package from lidar_localization_ros2 repo

lidar_localization_ros2

ROS Distro
github

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

ROS 2 LiDAR localization with Nav2 integration, replay helpers, and benchmarking tools.

Maintainers

  • ryohei sasaki

Authors

No additional authors.

lidar_localization_ros2

Map-based 3D LiDAR localization for ROS 2 and Nav2.

build ROS 2 Jazzy ROS 2 Humble License BSD 2 Clause

Handheld outdoor run tracked on a prior point-cloud map; the estimate follows the ground truth for the whole run

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

  1. Start the LiDAR driver, and odometry if you have it.
  2. Run quickstart with your map:
   ros2 run lidar_localization_ros2 quickstart.py --map /absolute/path/to/map.pcd
   
  1. 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-run shows what it would start, without starting it.
  • --profile mid360 uses the Livox MID-360 preset.

File truncated at 100 lines see the full file

CHANGELOG

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:=4 and g2_ndt_scan_max_range:=80.0 on global_localization_recovery.launch.py (node parameters ndt_local_map_radius, ndt_num_threads and the new ndt_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_SEC freezes only the localizer for DURATION_SEC, while RKO-LIO and the player run, and scores the stop into summary.json as tools/benchmark/soak_replay.py scores 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.

Fixed

  • The reinitialization supervisor no longer resets a localizer that is tracking after someone else’s /initialpose. It ends its episode on an /initialpose it did not publish (reason external_reset in 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.
  • tools/benchmark/soak_replay.py scores a fault only on the poses of aligned scans. The pose the localizer republishes on /initialpose counted as tracking, so five stops after which no scan was accepted scored as recovered in 21-50 s. The fault scores in docs/benchmark.md do not change: 24 saved runs re-score the same.

Validation

  • The release check: two full Koide outdoor_hard_02b replays at 1x passed on release/v1.10.0 at 8d7183e (main at f4f5213 plus 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.yaml on the release build: 0.132 m RMSE over the first 120 s, 0.127 m with use_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:=false starts only the global localization node and the supervisor, beside a localizer already running. tools/benchmark/soak_replay.py --companion COMMAND starts 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

Launch files

No launch files found

Messages

No message files found.

Services

No service files found

Plugins

No plugins found.

Recent questions tagged lidar_localization_ros2 at Robotics Stack Exchange