Manual Loop Closure Tools
Offline manual loop closure editing and optimization tools for LiDAR mapping pose graphs. 用于激光雷达建图位姿图的离线手动闭环编辑与优化工具。
Authors: Xiangcheng HU, Jin Wu and Xieyuanli Chen Contact: xhubd@connect.ust.hk The project is written primarily in C++, distributed under the GNU General Public License v3.0 license, first published in 2026. Key topics include: gtsam, lidar-mapping, lidar-odometry, lidar-slam, loop-closure.
Manual Loop Closure Tools
<p align="center"> <a href="README.md"><strong>English</strong></a> | <a href="README.zh.md">中文</a> </p> <p align="center"> <img src="assets/hero.svg" alt="Manual Loop Closure Tools hero" width="100%" /> </p> <p align="center"> <a href="https://github.com/JokerJohn/Mannual-Loop-Closure-Tools/actions/workflows/ci.yml"> <img src="https://github.com/JokerJohn/Mannual-Loop-Closure-Tools/actions/workflows/ci.yml/badge.svg" alt="CI" /> </a> <img src="https://img.shields.io/badge/python-3.10-blue.svg" alt="Python 3.10" /> <img src="https://img.shields.io/badge/optimizer-Python-2E8B57.svg" alt="Python optimizer" /> <a href="https://youtu.be/lemd4XfPSYY"> <img src="https://img.shields.io/badge/YouTube-demo-FF0000?logo=youtube&logoColor=white" alt="YouTube video" /> </a> <a href="https://www.bilibili.com/video/BV1B6R4BGEvo/"> <img src="https://img.shields.io/badge/Bilibili-demo-00A1D6?logo=bilibili&logoColor=white" alt="Bilibili video" /> </a> <img src="https://img.shields.io/badge/license-GPLv3-blue.svg" alt="GNU GPL v3.0 License" /> </p> <p align="center"> <strong>Authors</strong>: <a href="https://github.com/JokerJohn">Xiangcheng HU</a>, <a href="https://github.com/zarathustr">Jin Wu</a> and <a href="https://github.com/Chen-Xieyuanli">Xieyuanli Chen</a><br/> <strong>Contact</strong>: <a href="mailto:xhubd@connect.ust.hk">xhubd@connect.ust.hk</a> </p>Offline manual loop-closure editing and optimization tools for LiDAR mapping pose graphs.
Watch the Tutorial
<p align="center"> <a href="https://youtu.be/lemd4XfPSYY"> <img src="https://img.youtube.com/vi/lemd4XfPSYY/hqdefault.jpg" alt="Manual Loop Closure Tools YouTube demo" width="80%" /> </a> </p>Overview
This repository packages the manual loop-closure workflow into a standalone project with:
- a PyQt GUI for trajectory inspection and point-cloud-assisted loop editing
- a Python-first offline optimizer backend for exporting new pose graphs and maps
- helper scripts for virtual environments, Python GTSAM checks, legacy backend build, environment checks, and screenshot generation
It is designed for mapping results that already contain:
pose_graph.g2ooptimized_poses_tum.txtkey_point_frame/*.pcd
Screenshot
<p align="center"> <img src="assets/screenshots/edge-selected.png" alt="Edge selected screenshot" width="82%" /> </p>Workflow
<p align="center"> <img src="assets/workflow.png" alt="Workflow diagram" width="100%" /> </p>The GUI lets you inspect trajectories, select node pairs or existing loop edges, preview target/source point clouds, run GICP, add or replace loop constraints, manage a working graph session, and export a new optimized map.
Feature Demos
Add Loop
<p align="center"> <img src="assets/add_loopsx3.gif" alt="Add loop demo" width="82%" /> </p>Add a new manual loop after validating a GICP result.
Replace Loop
<p align="center"> <img src="assets/replace_loopx3.gif" alt="Replace loop demo" width="82%" /> </p>Replace an existing loop edge with a better manual registration result.
Disable Loop
<p align="center"> <img src="assets/disable_loop.gif" alt="Disable loop demo" width="82%" /> </p>Temporarily disable an existing loop edge before re-optimization.
Key Features
| Feature | Description |
|---|---|
| Embedded PyQt + Open3D viewer | Inspect trajectories and point clouds in one workflow |
Working / Original trajectory comparison | Compare the edited graph against the baseline graph |
| Manual edge add, replace, disable, restore | Control loop constraints explicitly in a working session |
| Interactive source alignment in point-cloud view | Refine source initialization before GICP |
| Auto yaw sweep for ground robots | Search yaw seeds before final registration |
Offline export of g2o, TUM, map, and trajectory PCD | Produce clean optimized outputs after validation |
| Session-based graph editing with undo and change tracking | Keep a controlled workflow while revising loop edges |
Quick Start
Fastest path: Docker
bashcd ~/my_git/Mannual-Loop-Closure-Tools make docker-build xhost +local:docker docker run --rm -it \ --net=host \ -e DISPLAY=$DISPLAY \ -e QT_X11_NO_MITSHM=1 \ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \ -v /path/to/mapping_session:/data/session \ manual-loop-closure-tools:latest \ python launch_gui.py --session-root /data/session
Docker FAQ for first-time users:
- Need a display permission first:
xhost +local:docker - Mount your host session to
/data/session - Host outputs stay under
manual_loop_projects/,manual_loop_runs/, andmanual_loop_exports/ - Headless usage is supported through the Python optimizer CLI
- Full details: docs/DOCKER.md
Recommended local path: requirements.txt + .venv
bashcd ~/my_git/Mannual-Loop-Closure-Tools make venv source .venv/bin/activate make gtsam-python python launch_gui.py --session-root /path/to/mapping_session
For normal use, you can stop here. ROS, catkin, and the legacy C++ optimizer are optional.
The Python GTSAM 4.3 wrapper now has a repository helper path:
make gtsam-python- or
bash scripts/install_gtsam_python.sh - details: docs/INSTALL_GTSAM_PYTHON.md
You can also point directly to a g2o file:
bashpython launch_gui.py --g2o /path/to/pose_graph.g2o
Alternative path: conda
bashcd ~/my_git/Mannual-Loop-Closure-Tools conda env create -f environment.yml conda activate manual-loop-closure python launch_gui.py --session-root /path/to/mapping_session
Python-First Optimizer Path
After reviewing the pose-graph optimization path against the current C++ implementation, this repository now treats the Python backend as the normal runtime path for the validated manual loop-closure workflow.
The legacy C++ optimizer is retained only as:
- a developer fallback
- a parity reference
- a regression benchmark path
It is no longer required for normal installation or GUI usage.
If you still want the legacy backend locally:
bashcd ~/my_git/Mannual-Loop-Closure-Tools make backend
By default, the GUI now exposes only the Python-first path. The legacy C++ selector stays hidden unless a developer explicitly enables it.
Key GUI Modes and Parameters
The most important runtime controls now behave as follows:
Optimizemode inAdvancedFast ISAM2is the default mode for interactive graph editing.Accurate LMremains available as the batch-style parity/reference solve.
TgtVoxelinRegistration- default:
0.1 m - affects the target submap density used for preview and GICP.
- default:
MapVoxelinAdvanced- default:
0.1 m - affects only the final exported global map rebuild during
Export.
- default:
Practical guidance:
- use
Fast ISAM2while adding, replacing, disabling, or restoring loops repeatedly - use
Accurate LMwhen you want a direct batch-solve reference before export or parity checks - after
AddorReplace, the current GICP candidate is consumed and the button is disabled again - if you adjust the seed, delta, or target-map settings, rerun GICP before accepting another graph change
Test Data
You can download a sample mapping session for quick validation here:
Tested Environment
The current repository content was tested with the following dependency versions on Ubuntu 20.04. Python-only usage is the primary path; the ROS/catkin backend is kept as a fallback.
| Ubuntu | ROS | Python | catkin_tools | CMake | GCC / G++ |
|---|---|---|---|---|---|
| 20.04 | Noetic (fallback) | 3.10.16 | 0.9.4 | 3.25.0 | 9.4.0 |
| Open3D | PyQt5 | Qt | NumPy | SciPy | Matplotlib | OpenCV | PCL | GeographicLib | GTSAM |
|---|---|---|---|---|---|---|---|---|---|
| 0.19.0 | 5.15.10 | 5.15.2 | 1.24.4 | 1.14.1 | 3.10.8 | 4.2.0 | 1.10.0 | 1.50.1 | 4.3.0 |
Output Artifacts
The tool now separates edit history, optimization outputs, and final export manifests:
| Location | Purpose | Main files |
|---|---|---|
manual_loop_projects/<project_id>/ | Persistent edit project state for resume and review | project_state.json, execution.log, operations.jsonl |
manual_loop_runs/<run_id>/ | One optimization run output | edited_input_pose_graph.g2o, manual_loop_constraints.csv, pose_graph.g2o, optimized_poses_tum.txt, pose_graph.png, manual_loop_report.json, run_context.json |
manual_loop_exports/<export_id>/ | Lightweight final-export pointer without duplicating the full run directory | export_manifest.json, selected_run.txt, run symlink |
Resume behavior:
Load Sessionresumes the latest edit project for the selected session root.Resume Projectrestores a specific historical project by selecting itsproject_state.json.Optimizeupdates the working graph and optimized TUM immediately, but defers full map rebuilding.Exportbuildsglobal_map_manual_imu.pcdandtrajectory.pcdon demand before writing the final manifest.- Export no longer copies the full optimized run again; it records a manifest that points to the selected run.
Python vs C++ Parity Validation
The Python backend was validated against the legacy C++ optimizer on multiple real sessions. The GUI now defaults to Python and only falls back to C++ when Python optimization fails and a legacy binary is available.
| Session | Constraints | Python time [s] | C++ time [s] | TUM max t err [m] | TUM max r err [rad] | g2o max t err [m] | g2o max r err [rad] | Map points Py / C++ |
|---|---|---|---|---|---|---|---|---|
office_fs_fastlio_saved | 1 | 10.193 | 2.096 | 5.20e-09 | 3.11e-06 | 6.85e-05 | 3.27e-06 | 4,850,749 / 4,850,749 |
floor34-1_fs_fastlio_saved | 1 | 16.335 | 3.827 | 1.00e-09 | 3.56e-06 | 6.83e-05 | 4.50e-06 | 13,771,605 / 13,771,605 |
dr_tunnel_2026_01_24_145439 | 0 | 12.112 | 2.909 | 2.49e-08 | 2.87e-09 | 4.99e-04 | 1.05e-06 | 2,139,789 / 2,139,789 |
Notes:
optimized_poses_tum.txtis already numerically aligned at the1e-9 mto1e-8 mtranslation level and1e-6 radrotation level.- The remaining
pose_graph.g2odifference mainly comes from export text precision and quaternion sign-equivalent representations, not from optimizer mismatch.
Repository Layout
textMannual-Loop-Closure-Tools/ ├── README.md ├── README.zh.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Dockerfile ├── requirements.txt ├── environment.yml ├── launch_gui.py ├── assets/ ├── docs/ ├── gui/ ├── backend/ │ └── catkin_ws/ │ └── src/ │ ├── CMakeLists.txt │ └── manual_loop_closure_backend/ ├── scripts/ └── wiki/
Documentation
- 中文 README
- Installation Guide
- Python GTSAM 4.3
- Docker Guide
- Tool Manual
- GitHub Wiki
- Contributing
- Changelog
Developer Utilities
bashmake help make gtsam-python make check make env-check make optimizer-help make docker-build make backend make assets SESSION_ROOT=/path/to/session
Open-Source Notes
This repository focuses on the standalone manual-loop-closure workflow only. It does not include the full online mapping stack.
This project is derived from and complements the broader MS-Mapping research and codebase:
- Project URL: https://github.com/JokerJohn/MS-Mapping
If you use this repository in academic work, please also cite the MS-Mapping paper:
bibtex@misc{hu2024msmapping, title={MS-Mapping: An Uncertainty-Aware Large-Scale Multi-Session LiDAR Mapping System}, author={Xiangcheng Hu, Jin Wu, Jianhao Jiao, Binqian Jiang, Wei Zhang, Wenshuo Wang and Ping Tan}, year={2024}, eprint={2408.03723}, archivePrefix={arXiv}, primaryClass={cs.RO}, url={https://arxiv.org/abs/2408.03723}, }
License
This standalone repository is released under the GNU General Public License v3.0 (GPLv3).
Contributors
Showing top 2 contributors by commit count.
