# lidar_localization_ros2: map-based LiDAR localization for ROS 2

> lidar_localization_ros2 takes a point cloud map and a LiDAR stream and publishes the robot pose in that map, finding the starting pose by itself and falling back to a global search when matching fails. It is localization rather than SLAM, so it needs a map you already have.

**rsasaki0109/lidar_localization_ros2** — 3D LIDAR Localization using pointcloud map in ROS 2 (Not SLAM)

- Repository: https://github.com/rsasaki0109/lidar_localization_ros2
- Stars: 587 · Forks: 133
- Language: Python
- License: BSD-2-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/rsasaki0109-lidar-localization-ros2

## Localization against a map you already have

The repository description is blunt about what this is and is not: 3D LIDAR Localization using pointcloud map in ROS 2, with the word Not SLAM in brackets after it.

The README puts the contract in two sentences. Give it a point cloud map, in .pcd or .ply form, and a LiDAR stream, and 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.

The topic tags give the technical vocabulary: gicp, lidar, localization, ndt, pcl and ros2, which is the register of an engineering package rather than a tutorial. The licence is BSD-2-Clause, ROS 2 Jazzy is the main target and Humble is supported, and the header carries badges for both.

The distinction the package insists on is the important design decision. A SLAM system builds a map and localizes in it at the same time, and its pose is only as good as the map it is building. This package takes the map as given, which means the accuracy of your pose is bounded by the quality and the coverage of a map someone captured earlier, and in exchange the pose does not drift as the robot explores.

## Building takes a colcon workspace and about half an hour

The install path is a standard ROS 2 source workspace, with a helper script that does the bootstrapping.

```bash
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
```

That builds ndt_omp_ros2 and this package, which the README puts at about 30 minutes on an 8-core machine. The NDT implementation is compiled rather than shipped as a wheel, which is where the time goes.

small_gicp is optional, and the README points to a local build document for a manual or no-sudo build. For people with no ROS installation at all, there is a Docker route that checks the whole thing in one command.

```bash
docker build -f docker/Dockerfile -t lidar_localization_ros2 .
docker run --rm lidar_localization_ros2 /ws/src/lidar_localization_ros2/docker/smoke_status.sh
```

The repository is laid out like a serious ROS 2 package rather than a script collection. There is a package.xml, a CMakeLists.txt, and separate directories for include/, src/, launch/, param/, rviz/, scripts/, tools/, test/, docker/, docs/ and experiments/. A dependencies.repos file declares the extra repositories to fetch, and a .clang-tidy configuration is present, which tells you the C++ side is linted.

The Python side is managed with Ruff, and the pyproject.toml is worth a look because the configuration carries its own reasoning. The target version is py310, with a comment explaining that the local overlay tracks ROS 2 Humble on Python 3.10 while Jazzy on 3.12 is also supported. The lint rule set is selected by family, from pycodestyle through pyflakes, import sorting, pyupgrade, bugbear, blind-except, comprehensions, simplify and the ruff-specific rules, with a single ignore for UP031 because the ROS logger uses percent formatting and the rewrite would change how values render.

## Quickstart discovers your topics and finds the first pose

The quick start is three steps, and the middle one does more work than it looks.

Start the LiDAR driver, and odometry if you have it. Then run the quickstart with your map.

The command is ros2 run lidar_localization_ros2 quickstart.py with the --map argument pointing at your map file.
```
   ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py --profile standalone
The check is ros2 run lidar_localization_ros2 check_lidar_localization_bringup.py with the --profile standalone argument.

What the quickstart does on startup is described precisely. It 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.

The initial pose handling is the part that matters for reliability. On the first run it searches the whole map for the robot. Next time it restores the pose saved for the same map. And if the search finds no safe match, it asks you for a 2D Pose Estimate in RViz, by pressing P, clicking the robot and dragging along its heading, instead of guessing.

That last behaviour is a deliberate design choice. A localisation system that silently snaps to a wrong place is worse than one that stops and asks, and the status line exists to make the difference visible.

## The status line tells you when tracking is in trouble

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.

The status line prints messages such as a tracking confirmation and, when matching is lost, a note that no scan has matched the map for four seconds with the instruction to set a 2D Pose Estimate in RViz.

The example lost-tracking line is specific about the cause and the remedy. No scan has matched the map for 4 seconds, and if it does not recover you should set a 2D Pose Estimate in RViz. The README links a troubleshooting document section on reading the status line for what each line means.

The command line options give you a way to change the behaviour without editing configuration files.

The `--dry-run` option shows what it would start, without starting it, which is the safe way to check topic discovery on a new robot. The `--profile mid360` option uses the Livox MID-360 preset. The `--initial-pose X Y Z QX QY QZ QW` option skips the search entirely when you already know where the robot is, which is the difference between a robot that starts in two seconds and one that spends a minute searching the map. The `--no-rviz` option is for a headless robot and `--no-status` turns the status line off. And `--help` lists the bringup options while `--help-all` adds the tuning ones.

## Nav2 mode changes what the localizer publishes

Running with Nav2 is a launch file rather than the quickstart.

The launch is ros2 launch lidar_localization_ros2 nav2_navigation.launch.py with map_yaml set to the path of your 2D map YAML file.

Nav2 also needs a 2D occupancy map and an `odom -> base_link` source, and in this mode the localizer publishes `map -> odom`. That is the standard Nav2 arrangement, with the localizer taking the responsibility that would otherwise belong to a separate localisation component.

The launch file table shows the four ways in.

The `nav2_lidar_localization.launch.py` file is for localization only. The `nav2_navigation.launch.py` file is for localization plus the Nav2 stack. The `mid360_legged_localization.launch.py` file is for a Livox MID-360 on a legged robot. And `lidar_localization.launch.py` is the base launch with every argument exposed.

The legged robot file is the one to copy for a quadruped.

The command passes map_path, cloud_topic and imu_topic as launch arguments, in that order.

A separate document covers a map produced by lidar_slam_ros2 with a MID-360, and the instruction is to start that odometry first with a launch file, then run the quickstart against the map file it produced. That is the practical bridge between the SLAM package and this one, and it is the only documented path from a robot with no map to a robot that localizes in one.

## NDT matching, odometry prediction and a global recovery search

The how it works section is short and describes the loop. Startup finds a first pose, then every scan is matched against the map near the predicted pose, and when matching keeps failing, recovery searches the map again.

The startup flow diagram makes the decision points explicit. An explicit `--initial-pose` skips the search and goes straight to verification. Otherwise the system checks whether a pose was saved for this map, and if so verifies it against the next scans. If there is no saved pose, or verification fails, it runs a global search that scores 2D candidates with NDT. If two views agree, it proceeds to verification. If the search is ambiguous, it asks the operator for a 2D Pose Estimate. Verification either succeeds and tracking begins, or it loops back.

Once tracking, the recovery path is triggered by a reinitialisation request or by an odometry dropout, and it queries the global search with a guarded reset before verifying again.

The benchmark in the header is a single specific run, and it is worth reading carefully rather than as a headline number. A handheld Livox MID-360 was carried about 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, while 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, on the Koide Hard Point Cloud Localization Dataset, outdoor_hard_02b, under CC BY 4.0.

The last detail changes how to read the number. An offline replay started from the ground truth is a measurement of the tracking loop, not of the initialisation, which is the part the same README says is the hardest and asks you to supervise.

## Where map-based localization is the wrong tool

The first case is a robot in a place it has never been. This package does not build maps, and the README says so in the parenthetical. If you have no prior point cloud of the site, the honest answer is a SLAM system, and the repository points at one: lidar_slam_ros2, with its RKO-LIO odometry launch file, produces the map that this package then localizes in. The two are complementary rather than competing, and the README documents the hand-off.

The second case is a site whose map changes faster than you can re-record it. The accuracy of the pose is bounded by the map, so a construction site or a warehouse whose racking moves is a problem that no amount of NDT tuning fixes, because the map is wrong rather than the matching.

The third case is a lightweight script where a full colcon build is overkill. The build takes around half an hour on eight cores and the package is C++ with a Python orchestration layer, which is the right shape for a robot but a heavy one for a utility that needs a sunrise time.

The alternative to weigh is the SLAM-first workflow. The difference in approach is where the map comes from and when the pose becomes trustworthy. A SLAM system produces the map and the pose together, so it works anywhere immediately but its pose drifts with the map and cannot be corrected against a known reference. This package takes the map as ground truth, so the pose is anchored and re-verifiable against it, and in exchange you must have captured that map already and you must keep it current.

## Conclusion

This package suits a team that already has a point cloud map of the site and needs a reliable pose estimate for Nav2, especially on a Livox MID-360 setup where scan matching alone is unreliable. It does not suit a robot that has never seen the site, since it is not a SLAM system, and it is a heavier commitment than a Python script, with a colcon build that takes around half an hour. Verify first that your map is in .pcd or .ply form, that your LiDAR and IMU topics match what quickstart discovers, and that you can supply an odom to base transform, because the Nav2 mode depends on it.

## FAQ

### How do I install lidar_localization_ros2?

Clone the repository into a colcon workspace source directory, run scripts/bootstrap_colcon_workspace.sh --build, and source the workspace setup file. The README puts the build at about 30 minutes on an 8-core machine. Without a ROS installation, the Docker image can be built and checked in one command.

### Does lidar_localization_ros2 need a map, and can it build one?

It needs a prior map in .pcd or .ply form and it does not build maps, the repository description marking it as not SLAM. A map produced by lidar_slam_ros2 with a MID-360 is used in the README's example, starting that package's odometry first and then pointing the quickstart at the map file.

### How does the localizer find the initial pose?

On the first run it searches the whole map, on later runs it restores the pose saved for the same map, and you can skip the search entirely with --initial-pose. If the global search finds no safe match or is ambiguous, it asks for a 2D Pose Estimate in RViz instead of guessing.

### Which ROS 2 distributions are supported?

ROS 2 Jazzy is the main target and Humble is supported. The Ruff configuration notes that the local overlay tracks Humble on Python 3.10 while Jazzy on 3.12 is also supported.

### What accuracy does the README report?

One specific run: a handheld Livox MID-360 carried about 390 m outdoors on a prior map, including stretches where the scans do not match the map and a section outside it, with an error of 0.26 m RMSE for the whole run, measured in an offline replay started from the ground-truth pose on the Koide outdoor_hard_02b dataset.

## Sources

- [Issues](https://github.com/rsasaki0109/lidar_localization_ros2/issues)
- [License: BSD-2-Clause](https://github.com/rsasaki0109/lidar_localization_ros2/blob/main/LICENSE)
- [README](https://github.com/rsasaki0109/lidar_localization_ros2/blob/main/README.md)
- [Releases](https://github.com/rsasaki0109/lidar_localization_ros2/releases)
- [rsasaki0109/lidar_localization_ros2 on GitHub](https://github.com/rsasaki0109/lidar_localization_ros2)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rsasaki0109-lidar-localization-ros2
