RadLoc
[IROS 2026] RadLoc: Radar-based 3-DoF Global Localization via Fast, Robust,
and Lightweight Spatial Descriptor Across Diverse Environmental Scenarios
Hogyun Kim, Jiwon Choi, Jungwoo Lee, Younggun Cho
Spatial AI and Robotics Lab (SPARO), Inha University
RadLoc is a radar global descriptor and the SLAM that runs on it. A scan is reduced to one value per range band, averaged over azimuth:
- 🧭 Rotation invariant and tiny: forty floats describe a place, whatever the heading.
- 🔍 Two-stage search: a k-d tree over the leading range-weighted bands proposes candidates, the full descriptor re-ranks them.
- 🎯 3-DoF registration without an initial guess: loops are registered by phase correlation, so there are no correspondences to find and nothing to initialize.
- 🗺️ One library, three applications: single-session SLAM, multi-session SLAM and multi-robot map merging with Commerge, on ROS 1 and ROS 2.
cpp/ C++17 core (header-only, OpenCV): descriptor, place search, phase-correlation registration
python/radloc/ the same in Python (pip install radloc), checked against the C++
applications/
slam/
common/ ROS-free radar odometry (from Yeti) and pose-graph pieces, shared by both ROS versions
ros1/radloc_ros ROS 1 Noetic: odometry_node + pose_graph_node
ros2/radloc_{odometry,slam,interfaces} ROS 2 Humble: the same nodes
multi-session-slam/ align sessions written by the SLAM (ROS-free tool, colcon or plain CMake)
multi-robot-slam/ Commerge: merge two robots' maps by exchanging a few keyframes
docker/ ROS 1 and ROS 2 images
📦 Prerequisites
- Library: Python 3.9+ (NumPy, SciPy, OpenCV) or a C++17 compiler with OpenCV. Nothing else.
- SLAM: ROS 1 Noetic or ROS 2 Humble, PCL, GTSAM 4.2 — or just Docker, which brings all of it.
- Data: radar scans as polar PNGs (one row per azimuth, Oxford/MulRan layout), e.g. MulRan
KAIST_03/polar.
🛠️ Installation
pip install radloc
# or from a clone
git clone https://github.com/sparolab/RadLoc.git && cd RadLoc && pip install .
C++
cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release && cmake --build cpp/build -j8
ctest --test-dir cpp/build # synthetic checks of descriptor, search and registration
In your project, add_subdirectory(RadLoc/cpp) and link radloc::radloc (header-only).
🚀 Quick start
radloc describe <MulRan>/KAIST_03/polar --out kaist03.npy # one descriptor per scan
radloc loops kaist03.npy --out loops.csv --exclude 100 # loop candidates within the run
import radloc
db = radloc.PlaceDatabase() # coarse k-d tree on 20 bands, top 10 re-ranked
for scan in scans: # polar PNG paths or (range x azimuth) arrays
db.add(radloc.compute_descriptor(scan))
matches = db.query(radloc.compute_descriptor(query), limit=len(db) - 100) # [(index, distance), ...]
#include "radloc/descriptor.hpp"
#include "radloc/phase_corr.hpp"
#include "radloc/retrieval.hpp"
radloc::PlaceDatabase db;
for (const auto& path : scans) db.add(radloc::computeDescriptor(path));
const auto matches = db.query(radloc::computeDescriptor(query_path));
const auto rel = radloc::registerScans(target_points, source_points); // rel.x, rel.y [m], rel.theta [rad]
🗺️ SLAM
Radar odometry (Yeti: Cen features, ORB matching, motion-distorted RANSAC with Doppler correction) publishes the motion, the feature cloud and the RadLoc descriptor of every scan; the pose-graph node closes loops with RadLoc, registers them by phase correlation and optimizes with iSAM2. The run is written as a session (pose graph, descriptors, scans) that the multi-session tool reads back.
MulRan KAIST_03 (6.25 km), 3459 keyframes, 1916 loops. Mean position error (ATE) against MulRan's ground truth after a rigid SE(2) alignment, no scale:
| ATE mean | RMSE | max | |
|---|---|---|---|
| odometry | 71.7 m | 80.4 m | 173.7 m |
| with RadLoc loop closure | 5.4 m | 6.1 m | 12.4 m |
ROS 1 and ROS 2 give the same result (ROS 1: 1918 loops, 5.4 m). On 4 of the 3,460 scan pairs the odometry's feature matches agree on no motion (at most 6 of 80–236 matches); those scans keep the previous motion instead of taking a wrong one.
# ROS 2 Humble
ros2 launch radloc_slam radloc_slam.launch.py sequence_dir:=<MulRan>/KAIST_03 save_directory:=/data/output/KAIST_03/ dataset:=mulran
# ROS 1 Noetic
roslaunch radloc_ros radloc_slam.launch sequence_dir:=<MulRan>/KAIST_03 save_directory:=/data/output/KAIST_03/ dataset:=mulran
dataset sets the radar's range resolution: mulran (0.0596 m per bin, also Boreas, the default) or oxford (0.0432 m per bin, Oxford Radar RobotCar); a wrong value scales the whole trajectory. Both read polar/ from the sequence folder in the Oxford layout (MulRan scans: convert once with applications/slam/tools/mulran_to_oxford.py); the session goes to save_directory (optimized_poses_kitti.txt, odom_poses_kitti.txt, keyframe_times.txt, singlesession_posegraph.g2o, RadLocDescriptors/, Scans/). See applications/slam.
🔗 Multi-session SLAM
LT-SLAM style (LT-mapper), as in Sonar Context and ReFeree: each session keeps its own frame and gets an anchor node, and inter-session RadLoc loops (registered by phase correlation) connect the sessions through their anchors. KAIST_03 cut in two halves (1730 and 1731 scans), each run through the SLAM in its own frame; 896 inter-session loops in about 30 s. The distance from each keyframe of the second session to the nearest keyframe of the first (no ground truth needed), and the second session's error against ground truth once the first is aligned to it (SE(2)):
| nearest keyframe, p50 | within 5 m | error vs ground truth, mean (median) | |
|---|---|---|---|
| before | 29.31 m | 12.9 % | 405.2 m (458.4 m) |
| after | 0.75 m | 77.4 % | 5.7 m (5.6 m) |
Only part of the second half retraces the first, so not every keyframe has a counterpart to land on.
multi_session_node applications/multi-session-slam/config/kaist03_split.yaml # sessions_dir/{01,02} -> save_directory
See applications/multi-session-slam.
🤝 Multi-robot SLAM (Commerge)
Two robots, each mapping on its own, merge their maps by exchanging only a few keyframes: Commerge (IJRR 2026) takes the RadLoc affinity between the robots and picks the keyframes worth sending. On MulRan KAIST_03 split into two robots (robot B in a frame turned by 60°): 27 of 2,344 keyframes exchanged (1.2 %, 2.6 KB of poses), B's frame recovered within 0.05°.
See applications/multi-robot-slam.
🐳 Docker
docker compose -f docker/docker-compose.yml build # ROS 2 Humble image (radloc:humble)
docker compose -f docker/docker-compose.yml run --rm slam # SLAM on SEQUENCE_DIR -> SAVE_DIR
docker compose -f docker/docker-compose.yml run --rm multi_session
docker build -f docker/Dockerfile.ros1 -t radloc:noetic . # ROS 1 Noetic image
Datasets are mounted read-only; see docker/README.md for the paths.
💡 Design notes
The descriptor is unweighted. Range weighting — counting distant bands for more — is applied once, when the coarse search key is built. It belongs to how two places are compared, not to the place itself.
Search is two-stage. A k-d tree over the leading range-weighted bands proposes candidates; the shortlist is re-ranked on the full unweighted descriptor, where the shared offset cancels.
Rotation is disambiguated. The FFT magnitude is centrally symmetric, so the log-polar stage recovers rotation only modulo 180°. Both candidates are de-rotated and the stronger translation peak wins; without this, five rotations in eight beyond ±90° come back flipped by π.
See cpp/README.md and python/README.md for the rest.
Citation
If you use RadLoc in your research, please cite our paper.
See bibtex
@inproceedings{kim2026radloc,
author = {Kim, Hogyun and Choi, Jiwon and Lee, Jungwoo and Cho, Younggun},
title = {RadLoc: Radar-based 3-DoF Global Localization via Fast, Robust, and Lightweight Spatial Descriptor Across Diverse Environmental Scenarios},
booktitle = {IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS)},
year = {2026}
}
Contributing
Issues and pull requests are welcome.
Acknowledgements
The radar odometry is Yeti by Keenan Burnett, with the feature detector of Sarah H. Cen and Paul Newman. RadLoc builds on ReFeree. The multi-session SLAM follows LT-mapper (Giseop Kim and Ayoung Kim, ICRA 2022), whose BetweenFactorWithAnchoring (MIT License) it uses. Related: SOLiD, Commerge, SKiD-SLAM.
License
BSD 3-Clause. See LICENSE.
Metadata
Release files for radloc 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| radloc-2.0.0.tar.gz | 18.5 kB | With provenance | Details for radloc-2.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| radloc-2.0.0-py3-none-any.whl | Python 3 | none | any | Details for radloc-2.0.0-py3-none-any.whl |
Total release size: 33.5 kB
Release files / radloc-2.0.0.tar.gz
| Download URL | radloc-2.0.0.tar.gz |
|---|---|
| Size | 18.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d216e8673932081c32e23c2d2e7f50742026d2b3fd6ac6de0b554168e3c34cb
|
|
BLAKE2b-256 checksum How to use checksums |
614a9684afea0067bcc5794cca970d27ad5d622aae0b2b007c2e36c5f8c3208a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / radloc-2.0.0-py3-none-any.whl
| Download URL | radloc-2.0.0-py3-none-any.whl |
|---|---|
| Size | 14.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d3087614ff60b73081cd2115a420d611bc4b43ac278207e398a04bbc4b8adb8
|
|
BLAKE2b-256 checksum How to use checksums |
b2e867958a4184486ae7161cdc4bdc7a737362a335b3c77ffd96d0c35ab175da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logSecurity for radloc 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for radloc-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.