commerge 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
commerge-2.0.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, SPARO LAB
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: commerge
3
+ Version: 2.0.0
4
+ Summary: Commerge: communication-efficient multi-robot map merging by selecting a few scans
5
+ Author-email: Hogyun Kim <hg.kim@inha.edu>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/sparolab/Commerge
8
+ Project-URL: Documentation, https://github.com/sparolab/Commerge/blob/main/docs/GUIDE.md
9
+ Project-URL: Project page, https://sparolab.github.io/research/commerge/
10
+ Project-URL: Paper, https://arxiv.org/abs/2606.25386
11
+ Project-URL: Issues, https://github.com/sparolab/Commerge/issues
12
+ Keywords: map merging,multi-robot,multi-robot slam,lidar,slam,place recognition,point cloud,registration,communication-efficient,vertex cover,maximum clique,gurobi,highs,solid,kiss-matcher,robotics
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy
25
+ Requires-Dist: scipy>=1.9
26
+ Requires-Dist: matplotlib
27
+ Requires-Dist: kiss-matcher>=1.0.2; python_version < "3.13"
28
+ Requires-Dist: small-gicp>=1.0
29
+ Provides-Extra: gurobi
30
+ Requires-Dist: gurobipy; extra == "gurobi"
31
+ Provides-Extra: solid
32
+ Requires-Dist: solid-descriptor; extra == "solid"
33
+ Provides-Extra: compare
34
+ Requires-Dist: scikit-learn; extra == "compare"
35
+ Dynamic: license-file
36
+
37
+ <div align="center">
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/logo_dark.svg" />
40
+ <img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/logo_light.svg" alt="Commerge: Communication-Efficient Map Merging" width="560" />
41
+ </picture>
42
+ <br />
43
+ <a href="https://github.com/sparolab/Commerge/tree/main/commerge"><img src="https://img.shields.io/badge/Python-3670A0?logo=python&logoColor=ffdd54" /></a>
44
+ <a href="https://pypi.org/project/commerge/"><img src="https://img.shields.io/pypi/v/commerge?logo=pypi&logoColor=white&label=PyPI" /></a>
45
+ <a href="#prerequisites"><img src="https://img.shields.io/badge/Gurobi-EE3524" /></a>
46
+ <a href="#prerequisites"><img src="https://img.shields.io/badge/HiGHS-license--free-2E7D32" /></a>
47
+ <a href="https://github.com/sparolab/SOLiD"><img src="https://img.shields.io/badge/SOLiD-descriptor-3F51B5" /></a>
48
+ <a href="#installation"><img src="https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=white" /></a>
49
+ <a href="#installation"><img src="https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white" /></a>
50
+ <a href="#docker"><img src="https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white" /></a>
51
+ <a href="https://github.com/sparolab/Commerge/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-BSD--3--Clause-green" /></a>
52
+ <a href="https://github.com/sparolab/Commerge/actions/workflows/ci.yml"><img src="https://github.com/sparolab/Commerge/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" /></a>
53
+ <br />
54
+ <a href="https://sparolab.github.io/research/commerge/"><img src="https://img.shields.io/badge/Project-Page-6f42c1" /></a>
55
+ <a href="https://arxiv.org/abs/2606.25386"><img src="https://img.shields.io/badge/arXiv-b33737?logo=arXiv" /></a>
56
+ <a href="https://sparolab.github.io/research/commerge/"><img src="https://img.shields.io/badge/IJRR-2026-1f6feb" /></a>
57
+ <a href="https://youtu.be/LV_8_jZbHV8"><img src="https://img.shields.io/badge/YouTube-FF0000?logo=youtube&logoColor=white" /></a>
58
+ <a href="#robot-run-with-your-agent"><img src="https://img.shields.io/badge/Claude%20Code-ready-D97757?logo=claude&logoColor=white" alt="Claude Code ready" /></a>
59
+ <a href="#robot-run-with-your-agent"><img src="https://img.shields.io/badge/Codex-ready-000000?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMS41IiB5PSIzIiB3aWR0aD0iMjEiIGhlaWdodD0iMTgiIHJ4PSIzIiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNiA5bDMuNSAzTDYgMTUiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEyIDE1LjVoNiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=" alt="Codex ready" /></a>
60
+ <br />
61
+ <a href="https://pypi.org/project/commerge/"><img src="https://readme-typing-svg.demolab.com?background=0D1117&color=22C55E&font=Fira+Code&size=14&duration=2500&pause=800&center=true&vCenter=true&width=230&height=24&lines=%24+pip+install+commerge" alt="pip install commerge" /></a>
62
+ <br />
63
+ <img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/commerge_demo.gif" alt="Commerge on KITTI 00 split into two robots: the affinity of their SOLiD descriptors goes through step (A) sequential matching, step (B) balanced minimum vertex cover and step (C) maximum edge-weighted clique; the most consistent region's scans (magenta) are the only ones exchanged, and the transform registered from them merges the two maps." width="95%" />
64
+ <p>
65
+ <strong>[IJRR 2026] Commerge: Communication-Efficient, Robust, and Fast LiDAR Map Merging Framework<br />
66
+ for Multi-Robot Coordination in Resource-Constrained Scenarios</strong>
67
+ </p>
68
+ </div>
69
+
70
+ ______________________________________________________________________
71
+
72
+ Robots that map on their own must exchange scans to merge their maps, and over a weak link that exchange is the bottleneck. Commerge picks the few scans that carry the overlap and sends only those, in three steps:
73
+
74
+ - :robot: **Before the steps, each robot runs its own SLAM with [SOLiD](https://github.com/sparolab/SOLiD)**: keyframe scans, poses and SOLiD descriptors, saved as a session.
75
+ - :link: **Step (A) Sequential matching**: regions of the inter-robot affinity matrix where both robots saw the same places in sequence.
76
+ - :balance_scale: **Step (B) Balanced minimum vertex cover**: the fewest keyframes that still touch every candidate match, shared fairly between the robots.
77
+ - :gem: **Step (C) Maximum edge-weighted clique**: among those, only the pairs that agree with each other geometrically.
78
+ - :handshake: **Exchange and merge**: the robots exchange the scans of the most consistent region only; their submaps are registered ([KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher) + GICP) and the transform merges the whole maps.
79
+ - :package: **MB, not GB**: up to 99.98 % less data exchanged (7,000 MB → 1.3 MB on HeLiPR) with comparable alignment, also on a Jetson Nano.
80
+
81
+ <p align="center"><img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/summary.png" alt="Data exchanged after each step on HeLiPR: 7,000 MB for the whole map, 60 MB after step (A), 26 MB after step (B) and 1.3 MB after step (C)" width="95%" /></p>
82
+
83
+ ______________________________________________________________________
84
+
85
+ ## :package: Prerequisites
86
+
87
+ - **Python 3.9+**. `pip install commerge` brings NumPy, SciPy, Matplotlib and the registration ([KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher), wheels for Python 3.9-3.12, and [small_gicp](https://github.com/koide3/small_gicp)).
88
+ - **Solver: [Gurobi](https://www.gurobi.com/academia/academic-program-and-licenses/), free for academic use**, as in the paper (`pip install gurobipy`, then `grbgetkey <key>`). **No license? HiGHS** ships with SciPy and returns the same selection, only slower on the largest regions (`--solver highs`; `auto`, the default, picks Gurobi when it is licensed).
89
+ - **One session per robot** from [SOLiD](https://github.com/sparolab/SOLiD) (`thirdparty/SOLiD`): keyframe scans, poses and SOLiD descriptors.
90
+
91
+ ## :hammer_and_wrench: Installation
92
+
93
+ ```bash
94
+ pip install commerge # + "commerge[gurobi]" for gurobipy, "commerge[compare]" for the AutoMerge comparison
95
+ # or from a clone, with SOLiD and KISS-Matcher as submodules:
96
+ git clone --recursive https://github.com/sparolab/Commerge.git && cd Commerge && pip install .
97
+ # KISS-Matcher from the submodule instead of the wheel: pip install thirdparty/KISS-Matcher/python
98
+ commerge --version
99
+ ```
100
+
101
+ Ubuntu and macOS (Python only, no ROS needed for Commerge itself).
102
+
103
+ ### Docker
104
+
105
+ ```bash
106
+ docker pull ghcr.io/sparolab/commerge # or: docker build -f docker/Dockerfile -t ghcr.io/sparolab/commerge .
107
+ docker run --rm -v $HOME/robot_a:/a -v $HOME/robot_b:/b -v $PWD/merged:/out ghcr.io/sparolab/commerge \
108
+ commerge /a /b --threshold 0.96 --min-area 2000 --out /out --plot
109
+ # Gurobi: add -v $HOME/gurobi.lic:/opt/gurobi/gurobi.lic (otherwise HiGHS)
110
+ ```
111
+
112
+ ## :rocket: Run
113
+
114
+ **1. One session per robot** with SOLiD, with any odometry it supports ([FAST-LIO, KISS-ICP, GeoRIO, …](https://github.com/sparolab/SOLiD/tree/main/slam)) or without ROS:
115
+
116
+ ```bash
117
+ cd thirdparty/SOLiD
118
+ roslaunch solid_ros run.launch odometry:=fast_lio save_dir:=$HOME/robot_a # ROS 1, while the bag of robot A plays
119
+ ros2 launch solid_ros run.launch.py odometry:=kiss_icp save_dir:=$HOME/robot_b # ROS 2
120
+ python slam/common/mac/solid_loops.py --kitti <sequence> --config slam/config/kitti.yaml --out $HOME/robot_a # no ROS
121
+ ```
122
+
123
+ **2. Select the scans to exchange, register them and merge:**
124
+
125
+ ```bash
126
+ commerge $HOME/robot_a $HOME/robot_b --threshold 0.96 --min-area 2000 --out merged/ --plot [--gt-a gt_a.txt --gt-b gt_b.txt]
127
+ ```
128
+
129
+ ```
130
+ [commerge] kitti00_a: 1011 keyframes | kitti00_b: 1701 keyframes
131
+ [commerge] steps (A)+(B)+(C), solver gurobi: (A) 4 region(s) (threshold 0.96, min area 2000) in 0.021 s
132
+ [commerge] region 2: A 250-381 x B 899-1027: (A) 1077 candidate pairs -> (B) 66+66 keyframes -> (C) 24 pairs = 14 A + 17 B scans, (B) 0.34 s, (C) 0.13 s
133
+ ...
134
+ [commerge] registered B to A: 540 KISS-Matcher inliers, GICP in 1.3 s; vs ground truth: 0.35 m, 0.06 deg (B keyframes 0.15 m off on average)
135
+ [commerge] exchanged 14 A + 17 B scans (1.1 % of 2712, 4.6 MB) from region(s) [2] of 4 -> merged/
136
+ ```
137
+
138
+ All outputs, options and the Python API: [docs/GUIDE.md](https://github.com/sparolab/Commerge/blob/main/docs/GUIDE.md).
139
+
140
+ ## :bar_chart: Compare
141
+
142
+ `commerge-compare` runs Commerge, its ablations and AutoMerge on the same sessions. On KITTI 00 split into two robots, Commerge sends **31 of 2,712 scans (4.6 MB)** and merges the maps within **0.35 m**; AutoMerge's region sends 156 scans and merges within 1.05 m. Details and the ablations: [docs/GUIDE.md](https://github.com/sparolab/Commerge/blob/main/docs/GUIDE.md#compare-commerge-compare).
143
+
144
+ ______________________________________________________________________
145
+
146
+ ## :robot: Run with your agent
147
+
148
+ Open [Claude Code](https://claude.com/claude-code) or [Codex](https://openai.com/codex/) in an empty folder and paste:
149
+
150
+ ```text
151
+ Clone https://github.com/sparolab/Commerge, read its AGENTS.md, and follow it to install Commerge and merge KITTI 00 split into two robots on this machine. Ask me what you need along the way.
152
+ ```
153
+
154
+ The agent follows [`AGENTS.md`](https://github.com/sparolab/Commerge/blob/main/AGENTS.md): it installs Commerge with Gurobi or HiGHS, makes one SOLiD session per robot, selects and exchanges the scans, merges the maps, and checks the result.
155
+
156
+ ______________________________________________________________________
157
+
158
+ ## :warning: Limitation: heterogeneous LiDARs
159
+
160
+ Commerge selects scans from the affinity of the two robots' descriptors. With the same LiDAR on both robots, SOLiD gives an affinity whose overlapping regions stand out. With different LiDARs (a spinning Ouster and a Livox Avia with a 70° field of view, for instance), SOLiD describes the same place differently and step (A) finds hundreds of regions with no structure. A descriptor made for heterogeneous LiDARs, such as [InLiER](https://github.com/LTU-RAI/InLiER), passed with `--affinity` (and `--enhance`), brings the right region back; registering the few scans exchanged across such different fields of view is still unreliable (`--registration grid` is experimental). Contributions are welcome.
161
+
162
+ ______________________________________________________________________
163
+
164
+ ## Citation
165
+
166
+ If you use Commerge in your research, please cite our [paper](https://arxiv.org/abs/2606.25386).
167
+
168
+ <details>
169
+ <summary><strong>See bibtex</strong></summary>
170
+
171
+ ```bibtex
172
+ @article{kim2026commerge,
173
+ author = {Kim, Hogyun and Choi, Jiwon and Kim, Juwon and Yang, Geonmo and Jeong, Seokhwan and Lim, Hyungtae and Cho, Younggun},
174
+ title = {Commerge: Communication-Efficient, Robust, and Fast LiDAR Map Merging Framework for Multi-Robot Coordination in Resource-Constrained Scenarios},
175
+ journal = {International Journal of Robotics Research (IJRR)},
176
+ year = {2026}
177
+ }
178
+ ```
179
+
180
+ </details>
181
+
182
+ ## Contributing
183
+
184
+ Issues and pull requests are welcome.
185
+
186
+ <a href="https://github.com/sparolab/Commerge/graphs/contributors">
187
+ <img src="https://contrib.rocks/image?repo=sparolab/Commerge" />
188
+ </a>
189
+
190
+ ## Acknowledgements
191
+
192
+ Commerge uses [SOLiD](https://github.com/sparolab/SOLiD) for inter-robot place recognition. The comparison re-implements the region selection of [AutoMerge](https://github.com/MetaSLAM/AutoMerge_Server), which builds on [SeqSLAM](https://doi.org/10.1109/ICRA.2012.6224623). Related research: [KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher), [SKiD-SLAM](https://github.com/sparolab/SKiD-SLAM).
193
+
194
+ ## Special Thanks
195
+
196
+ We thank [Minwoo Jung](https://github.com/minwoo0611) for the [HeLiPR](https://sites.google.com/view/heliprdataset) dataset, and [Nikolaos Stathoulopoulos](https://github.com/nstathou) for [InLiER](https://github.com/LTU-RAI/InLiER), learning-free heterogeneous LiDAR place recognition.
@@ -0,0 +1,160 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/logo_dark.svg" />
4
+ <img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/logo_light.svg" alt="Commerge: Communication-Efficient Map Merging" width="560" />
5
+ </picture>
6
+ <br />
7
+ <a href="https://github.com/sparolab/Commerge/tree/main/commerge"><img src="https://img.shields.io/badge/Python-3670A0?logo=python&logoColor=ffdd54" /></a>
8
+ <a href="https://pypi.org/project/commerge/"><img src="https://img.shields.io/pypi/v/commerge?logo=pypi&logoColor=white&label=PyPI" /></a>
9
+ <a href="#prerequisites"><img src="https://img.shields.io/badge/Gurobi-EE3524" /></a>
10
+ <a href="#prerequisites"><img src="https://img.shields.io/badge/HiGHS-license--free-2E7D32" /></a>
11
+ <a href="https://github.com/sparolab/SOLiD"><img src="https://img.shields.io/badge/SOLiD-descriptor-3F51B5" /></a>
12
+ <a href="#installation"><img src="https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=white" /></a>
13
+ <a href="#installation"><img src="https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white" /></a>
14
+ <a href="#docker"><img src="https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white" /></a>
15
+ <a href="https://github.com/sparolab/Commerge/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-BSD--3--Clause-green" /></a>
16
+ <a href="https://github.com/sparolab/Commerge/actions/workflows/ci.yml"><img src="https://github.com/sparolab/Commerge/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" /></a>
17
+ <br />
18
+ <a href="https://sparolab.github.io/research/commerge/"><img src="https://img.shields.io/badge/Project-Page-6f42c1" /></a>
19
+ <a href="https://arxiv.org/abs/2606.25386"><img src="https://img.shields.io/badge/arXiv-b33737?logo=arXiv" /></a>
20
+ <a href="https://sparolab.github.io/research/commerge/"><img src="https://img.shields.io/badge/IJRR-2026-1f6feb" /></a>
21
+ <a href="https://youtu.be/LV_8_jZbHV8"><img src="https://img.shields.io/badge/YouTube-FF0000?logo=youtube&logoColor=white" /></a>
22
+ <a href="#robot-run-with-your-agent"><img src="https://img.shields.io/badge/Claude%20Code-ready-D97757?logo=claude&logoColor=white" alt="Claude Code ready" /></a>
23
+ <a href="#robot-run-with-your-agent"><img src="https://img.shields.io/badge/Codex-ready-000000?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMS41IiB5PSIzIiB3aWR0aD0iMjEiIGhlaWdodD0iMTgiIHJ4PSIzIiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNiA5bDMuNSAzTDYgMTUiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTEyIDE1LjVoNiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=" alt="Codex ready" /></a>
24
+ <br />
25
+ <a href="https://pypi.org/project/commerge/"><img src="https://readme-typing-svg.demolab.com?background=0D1117&color=22C55E&font=Fira+Code&size=14&duration=2500&pause=800&center=true&vCenter=true&width=230&height=24&lines=%24+pip+install+commerge" alt="pip install commerge" /></a>
26
+ <br />
27
+ <img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/commerge_demo.gif" alt="Commerge on KITTI 00 split into two robots: the affinity of their SOLiD descriptors goes through step (A) sequential matching, step (B) balanced minimum vertex cover and step (C) maximum edge-weighted clique; the most consistent region's scans (magenta) are the only ones exchanged, and the transform registered from them merges the two maps." width="95%" />
28
+ <p>
29
+ <strong>[IJRR 2026] Commerge: Communication-Efficient, Robust, and Fast LiDAR Map Merging Framework<br />
30
+ for Multi-Robot Coordination in Resource-Constrained Scenarios</strong>
31
+ </p>
32
+ </div>
33
+
34
+ ______________________________________________________________________
35
+
36
+ Robots that map on their own must exchange scans to merge their maps, and over a weak link that exchange is the bottleneck. Commerge picks the few scans that carry the overlap and sends only those, in three steps:
37
+
38
+ - :robot: **Before the steps, each robot runs its own SLAM with [SOLiD](https://github.com/sparolab/SOLiD)**: keyframe scans, poses and SOLiD descriptors, saved as a session.
39
+ - :link: **Step (A) Sequential matching**: regions of the inter-robot affinity matrix where both robots saw the same places in sequence.
40
+ - :balance_scale: **Step (B) Balanced minimum vertex cover**: the fewest keyframes that still touch every candidate match, shared fairly between the robots.
41
+ - :gem: **Step (C) Maximum edge-weighted clique**: among those, only the pairs that agree with each other geometrically.
42
+ - :handshake: **Exchange and merge**: the robots exchange the scans of the most consistent region only; their submaps are registered ([KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher) + GICP) and the transform merges the whole maps.
43
+ - :package: **MB, not GB**: up to 99.98 % less data exchanged (7,000 MB → 1.3 MB on HeLiPR) with comparable alignment, also on a Jetson Nano.
44
+
45
+ <p align="center"><img src="https://raw.githubusercontent.com/sparolab/Commerge/main/assets/summary.png" alt="Data exchanged after each step on HeLiPR: 7,000 MB for the whole map, 60 MB after step (A), 26 MB after step (B) and 1.3 MB after step (C)" width="95%" /></p>
46
+
47
+ ______________________________________________________________________
48
+
49
+ ## :package: Prerequisites
50
+
51
+ - **Python 3.9+**. `pip install commerge` brings NumPy, SciPy, Matplotlib and the registration ([KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher), wheels for Python 3.9-3.12, and [small_gicp](https://github.com/koide3/small_gicp)).
52
+ - **Solver: [Gurobi](https://www.gurobi.com/academia/academic-program-and-licenses/), free for academic use**, as in the paper (`pip install gurobipy`, then `grbgetkey <key>`). **No license? HiGHS** ships with SciPy and returns the same selection, only slower on the largest regions (`--solver highs`; `auto`, the default, picks Gurobi when it is licensed).
53
+ - **One session per robot** from [SOLiD](https://github.com/sparolab/SOLiD) (`thirdparty/SOLiD`): keyframe scans, poses and SOLiD descriptors.
54
+
55
+ ## :hammer_and_wrench: Installation
56
+
57
+ ```bash
58
+ pip install commerge # + "commerge[gurobi]" for gurobipy, "commerge[compare]" for the AutoMerge comparison
59
+ # or from a clone, with SOLiD and KISS-Matcher as submodules:
60
+ git clone --recursive https://github.com/sparolab/Commerge.git && cd Commerge && pip install .
61
+ # KISS-Matcher from the submodule instead of the wheel: pip install thirdparty/KISS-Matcher/python
62
+ commerge --version
63
+ ```
64
+
65
+ Ubuntu and macOS (Python only, no ROS needed for Commerge itself).
66
+
67
+ ### Docker
68
+
69
+ ```bash
70
+ docker pull ghcr.io/sparolab/commerge # or: docker build -f docker/Dockerfile -t ghcr.io/sparolab/commerge .
71
+ docker run --rm -v $HOME/robot_a:/a -v $HOME/robot_b:/b -v $PWD/merged:/out ghcr.io/sparolab/commerge \
72
+ commerge /a /b --threshold 0.96 --min-area 2000 --out /out --plot
73
+ # Gurobi: add -v $HOME/gurobi.lic:/opt/gurobi/gurobi.lic (otherwise HiGHS)
74
+ ```
75
+
76
+ ## :rocket: Run
77
+
78
+ **1. One session per robot** with SOLiD, with any odometry it supports ([FAST-LIO, KISS-ICP, GeoRIO, …](https://github.com/sparolab/SOLiD/tree/main/slam)) or without ROS:
79
+
80
+ ```bash
81
+ cd thirdparty/SOLiD
82
+ roslaunch solid_ros run.launch odometry:=fast_lio save_dir:=$HOME/robot_a # ROS 1, while the bag of robot A plays
83
+ ros2 launch solid_ros run.launch.py odometry:=kiss_icp save_dir:=$HOME/robot_b # ROS 2
84
+ python slam/common/mac/solid_loops.py --kitti <sequence> --config slam/config/kitti.yaml --out $HOME/robot_a # no ROS
85
+ ```
86
+
87
+ **2. Select the scans to exchange, register them and merge:**
88
+
89
+ ```bash
90
+ commerge $HOME/robot_a $HOME/robot_b --threshold 0.96 --min-area 2000 --out merged/ --plot [--gt-a gt_a.txt --gt-b gt_b.txt]
91
+ ```
92
+
93
+ ```
94
+ [commerge] kitti00_a: 1011 keyframes | kitti00_b: 1701 keyframes
95
+ [commerge] steps (A)+(B)+(C), solver gurobi: (A) 4 region(s) (threshold 0.96, min area 2000) in 0.021 s
96
+ [commerge] region 2: A 250-381 x B 899-1027: (A) 1077 candidate pairs -> (B) 66+66 keyframes -> (C) 24 pairs = 14 A + 17 B scans, (B) 0.34 s, (C) 0.13 s
97
+ ...
98
+ [commerge] registered B to A: 540 KISS-Matcher inliers, GICP in 1.3 s; vs ground truth: 0.35 m, 0.06 deg (B keyframes 0.15 m off on average)
99
+ [commerge] exchanged 14 A + 17 B scans (1.1 % of 2712, 4.6 MB) from region(s) [2] of 4 -> merged/
100
+ ```
101
+
102
+ All outputs, options and the Python API: [docs/GUIDE.md](https://github.com/sparolab/Commerge/blob/main/docs/GUIDE.md).
103
+
104
+ ## :bar_chart: Compare
105
+
106
+ `commerge-compare` runs Commerge, its ablations and AutoMerge on the same sessions. On KITTI 00 split into two robots, Commerge sends **31 of 2,712 scans (4.6 MB)** and merges the maps within **0.35 m**; AutoMerge's region sends 156 scans and merges within 1.05 m. Details and the ablations: [docs/GUIDE.md](https://github.com/sparolab/Commerge/blob/main/docs/GUIDE.md#compare-commerge-compare).
107
+
108
+ ______________________________________________________________________
109
+
110
+ ## :robot: Run with your agent
111
+
112
+ Open [Claude Code](https://claude.com/claude-code) or [Codex](https://openai.com/codex/) in an empty folder and paste:
113
+
114
+ ```text
115
+ Clone https://github.com/sparolab/Commerge, read its AGENTS.md, and follow it to install Commerge and merge KITTI 00 split into two robots on this machine. Ask me what you need along the way.
116
+ ```
117
+
118
+ The agent follows [`AGENTS.md`](https://github.com/sparolab/Commerge/blob/main/AGENTS.md): it installs Commerge with Gurobi or HiGHS, makes one SOLiD session per robot, selects and exchanges the scans, merges the maps, and checks the result.
119
+
120
+ ______________________________________________________________________
121
+
122
+ ## :warning: Limitation: heterogeneous LiDARs
123
+
124
+ Commerge selects scans from the affinity of the two robots' descriptors. With the same LiDAR on both robots, SOLiD gives an affinity whose overlapping regions stand out. With different LiDARs (a spinning Ouster and a Livox Avia with a 70° field of view, for instance), SOLiD describes the same place differently and step (A) finds hundreds of regions with no structure. A descriptor made for heterogeneous LiDARs, such as [InLiER](https://github.com/LTU-RAI/InLiER), passed with `--affinity` (and `--enhance`), brings the right region back; registering the few scans exchanged across such different fields of view is still unreliable (`--registration grid` is experimental). Contributions are welcome.
125
+
126
+ ______________________________________________________________________
127
+
128
+ ## Citation
129
+
130
+ If you use Commerge in your research, please cite our [paper](https://arxiv.org/abs/2606.25386).
131
+
132
+ <details>
133
+ <summary><strong>See bibtex</strong></summary>
134
+
135
+ ```bibtex
136
+ @article{kim2026commerge,
137
+ author = {Kim, Hogyun and Choi, Jiwon and Kim, Juwon and Yang, Geonmo and Jeong, Seokhwan and Lim, Hyungtae and Cho, Younggun},
138
+ title = {Commerge: Communication-Efficient, Robust, and Fast LiDAR Map Merging Framework for Multi-Robot Coordination in Resource-Constrained Scenarios},
139
+ journal = {International Journal of Robotics Research (IJRR)},
140
+ year = {2026}
141
+ }
142
+ ```
143
+
144
+ </details>
145
+
146
+ ## Contributing
147
+
148
+ Issues and pull requests are welcome.
149
+
150
+ <a href="https://github.com/sparolab/Commerge/graphs/contributors">
151
+ <img src="https://contrib.rocks/image?repo=sparolab/Commerge" />
152
+ </a>
153
+
154
+ ## Acknowledgements
155
+
156
+ Commerge uses [SOLiD](https://github.com/sparolab/SOLiD) for inter-robot place recognition. The comparison re-implements the region selection of [AutoMerge](https://github.com/MetaSLAM/AutoMerge_Server), which builds on [SeqSLAM](https://doi.org/10.1109/ICRA.2012.6224623). Related research: [KISS-Matcher](https://github.com/MIT-SPARK/KISS-Matcher), [SKiD-SLAM](https://github.com/sparolab/SKiD-SLAM).
157
+
158
+ ## Special Thanks
159
+
160
+ We thank [Minwoo Jung](https://github.com/minwoo0611) for the [HeLiPR](https://sites.google.com/view/heliprdataset) dataset, and [Nikolaos Stathoulopoulos](https://github.com/nstathou) for [InLiER](https://github.com/LTU-RAI/InLiER), learning-free heterogeneous LiDAR place recognition.
@@ -0,0 +1,6 @@
1
+ """Commerge: communication-efficient multi-robot LiDAR map merging."""
2
+ __version__ = "2.0.0"
3
+
4
+ from .selection import STEPS, Selection, cosine_affinity, enhance_affinity, extract_regions, select # noqa: E402,F401
5
+ from .session import Session, read_pcd, write_pcd # noqa: E402,F401
6
+ from .merge import POLICIES, alignment_error, choose_regions, register, register_exchanged, register_grid # noqa: E402,F401
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
File without changes
@@ -0,0 +1,157 @@
1
+ """AutoMerge-style region selection (our re-implementation, for comparison).
2
+
3
+ AutoMerge (P. Yin et al., "AutoMerge: A Framework for Map Assembling and Smoothing in City-Scale
4
+ Environments", IEEE T-RO 2023) finds overlapping sequences with SeqSLAM. This module follows that
5
+ idea on the same affinity matrix Commerge uses:
6
+
7
+ 1. local contrast enhancement of the difference matrix D = 1 - affinity (SeqSLAM);
8
+ 2. sequence matching along constant-velocity trajectories, with a ratio test;
9
+ 3. K-means on the good matches, a line RANSAC per cluster;
10
+ 4. each cluster's padded bounding box is a region; the best region is exchanged whole.
11
+
12
+ It is not the authors' code. All scans of the chosen region are sent, which is what Commerge's
13
+ selection is compared against.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from dataclasses import dataclass
19
+ from typing import List, Tuple
20
+
21
+ import numpy as np
22
+
23
+ from ..selection import Region, Selection
24
+
25
+
26
+ @dataclass
27
+ class AutoMergeParams:
28
+ min_area: int = 20000
29
+ contrast_r: int = 50 # rows of the local contrast window
30
+ ds: int = 50 # sequence length
31
+ vmin: float = 0.8 # trajectory slopes searched
32
+ vmax: float = 1.2
33
+ r_window: int = 10 # window excluded around the best match for the ratio test
34
+ ratio: float = 0.8 # keep matches with best / second-best below this
35
+ n_clusters: int = 5
36
+ ransac_iters: int = 100
37
+ padding: int = 20 # region padding (keyframes)
38
+ best_only: bool = True # exchange only the best region (as AutoMerge does)
39
+ seed: int = 0
40
+
41
+
42
+ def enhance_contrast(D: np.ndarray, r: int) -> np.ndarray:
43
+ """SeqSLAM local contrast enhancement along the rows (reference keyframes)."""
44
+ DD = np.empty_like(D, dtype=float)
45
+ n = D.shape[0]
46
+ for i in range(n):
47
+ a, b = max(0, i - r // 2), min(n, i + r // 2 + 1)
48
+ v = D[a:b]
49
+ std = v.std(axis=0, ddof=1) if b - a > 1 else np.ones(D.shape[1])
50
+ std[std == 0] = 1.0
51
+ DD[i] = (D[i] - v.mean(axis=0)) / std
52
+ return DD - DD.min()
53
+
54
+
55
+ def sequence_matches(DD: np.ndarray, p: AutoMergeParams) -> np.ndarray:
56
+ """(Nb, 2): for each query column, the best reference row and the ratio to the second best."""
57
+ ds = p.ds + (p.ds % 2)
58
+ rows, cols = DD.shape
59
+ matches = np.full((cols, 2), np.nan)
60
+ move = np.arange(int(p.vmin * ds), int(p.vmax * ds) + 1)
61
+ v = move / ds
62
+ steps = np.floor(np.arange(ds + 1)[None, :] * v[:, None]).astype(int) # (V, ds+1)
63
+ padded = np.vstack([DD, np.full((1, cols), np.inf)]) # out-of-bounds row
64
+ starts = np.arange(rows)
65
+ for n in range(ds // 2, cols - ds // 2):
66
+ x = np.arange(n - ds // 2, n + ds // 2 + 1) # ds+1 query columns
67
+ y = np.minimum(starts[:, None, None] + steps[None], rows) # (S, V, ds+1)
68
+ scores = padded[y, x[None, None, :]].sum(axis=2).min(axis=1) # (S,)
69
+ if not np.isfinite(scores).any():
70
+ continue
71
+ k = int(np.argmin(scores))
72
+ mask = np.ones(rows, bool)
73
+ mask[max(0, k - p.r_window // 2): min(rows, k + p.r_window // 2)] = False
74
+ second = scores[mask].min() if mask.any() else np.inf
75
+ matches[n] = (k + ds // 2, scores[k] / second if second > 0 else 1.0)
76
+ return matches
77
+
78
+
79
+ def _kmeans(points: np.ndarray, k: int, seed: int) -> np.ndarray:
80
+ try:
81
+ from sklearn.cluster import KMeans
82
+
83
+ return KMeans(n_clusters=k, random_state=seed, n_init=10).fit_predict(points)
84
+ except ImportError:
85
+ from scipy.cluster.vq import kmeans2
86
+
87
+ return kmeans2(points.astype(float), k, seed=seed, minit="++")[1]
88
+
89
+
90
+ def _ransac_line(pts: np.ndarray, iters: int, rng: np.random.Generator) -> np.ndarray:
91
+ if len(pts) < 4:
92
+ return np.arange(len(pts))
93
+ best = np.arange(0)
94
+ tol = max(10.0, 0.1 * len(pts))
95
+ for _ in range(iters):
96
+ i, j = rng.choice(len(pts), 2, replace=False)
97
+ if abs(pts[j, 0] - pts[i, 0]) < 1e-6:
98
+ continue
99
+ a = (pts[j, 1] - pts[i, 1]) / (pts[j, 0] - pts[i, 0])
100
+ b = pts[i, 1] - a * pts[i, 0]
101
+ inl = np.nonzero(np.abs(pts[:, 1] - (a * pts[:, 0] + b)) < tol)[0]
102
+ if len(inl) > len(best):
103
+ best = inl
104
+ return best if len(best) else np.arange(len(pts))
105
+
106
+
107
+ def automerge_regions(affinity: np.ndarray, p: AutoMergeParams = AutoMergeParams()
108
+ ) -> List[Tuple[Tuple[int, int, int, int], float]]:
109
+ """Regions (a0, a1, b0, b1) with their mean match ratio, best first."""
110
+ DD = enhance_contrast(np.clip(1.0 - affinity, 0, 1), p.contrast_r)
111
+ m = sequence_matches(DD, p)
112
+ good = [(int(m[q, 0]), q, m[q, 1]) for q in range(len(m)) if np.isfinite(m[q, 0]) and m[q, 1] < p.ratio]
113
+ if not good:
114
+ return []
115
+ pts = np.array([(a, b) for a, b, _ in good], dtype=float)
116
+ k = max(2, min(p.n_clusters, len(good) // 10))
117
+ labels = _kmeans(pts, k, p.seed) if len(good) >= k else np.zeros(len(good), int)
118
+ rng = np.random.default_rng(p.seed)
119
+ regions = []
120
+ for c in range(labels.max() + 1):
121
+ idx = np.nonzero(labels == c)[0]
122
+ if len(idx) <= 2:
123
+ continue
124
+ idx = idx[_ransac_line(pts[idx], p.ransac_iters, rng)]
125
+ if len(idx) < 3:
126
+ continue
127
+ a0, a1 = pts[idx, 0].min(), pts[idx, 0].max()
128
+ b0, b1 = pts[idx, 1].min(), pts[idx, 1].max()
129
+ box = (int(max(0, a0 - p.padding)), int(min(affinity.shape[0], a1 + p.padding)),
130
+ int(max(0, b0 - p.padding)), int(min(affinity.shape[1], b1 + p.padding)))
131
+ if (box[1] - box[0]) * (box[3] - box[2]) >= p.min_area:
132
+ regions.append((box, float(np.mean([good[i][2] for i in idx]))))
133
+
134
+ def score(r): # as in our comparison: similar sequences and large overlap first
135
+ (a0, a1, b0, b1), ratio = r
136
+ sim = float(affinity[a0:a1, b0:b1].mean())
137
+ return 0.7 * sim + 0.3 * min((a1 - a0) * (b1 - b0) / 50000.0, 1.0)
138
+
139
+ return sorted(regions, key=score, reverse=True)
140
+
141
+
142
+ def automerge_select(affinity: np.ndarray, p: AutoMergeParams = AutoMergeParams(), log=print) -> Selection:
143
+ """The chosen region(s), every keyframe of which is exchanged."""
144
+ t = time.perf_counter()
145
+ regions = automerge_regions(affinity, p)
146
+ if p.best_only:
147
+ regions = regions[:1]
148
+ out = []
149
+ for (a0, a1, b0, b1), _ in regions:
150
+ r = Region((a0, a1, b0, b1))
151
+ r.scans_a, r.scans_b = list(range(a0, a1)), list(range(b0, b1))
152
+ r.candidates = int((affinity[a0:a1, b0:b1] > 0).sum())
153
+ r.seconds["automerge"] = time.perf_counter() - t
154
+ out.append(r)
155
+ log(f"[automerge] {len(out)} region(s), {sum(len(r.keyframes_a) + len(r.keyframes_b) for r in out)} scans "
156
+ f"in {time.perf_counter() - t:.2f} s")
157
+ return Selection(out, "", "-")