splatreg 1.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.
Files changed (42) hide show
  1. splatreg-1.0.0/LICENSE +29 -0
  2. splatreg-1.0.0/PKG-INFO +218 -0
  3. splatreg-1.0.0/README.md +171 -0
  4. splatreg-1.0.0/pyproject.toml +76 -0
  5. splatreg-1.0.0/setup.cfg +4 -0
  6. splatreg-1.0.0/splatreg/__init__.py +37 -0
  7. splatreg-1.0.0/splatreg/align.py +381 -0
  8. splatreg-1.0.0/splatreg/align_features.py +1849 -0
  9. splatreg-1.0.0/splatreg/api.py +736 -0
  10. splatreg-1.0.0/splatreg/core/__init__.py +3 -0
  11. splatreg-1.0.0/splatreg/core/lie.py +221 -0
  12. splatreg-1.0.0/splatreg/core/types.py +95 -0
  13. splatreg-1.0.0/splatreg/fuse.py +294 -0
  14. splatreg-1.0.0/splatreg/geometry/__init__.py +10 -0
  15. splatreg-1.0.0/splatreg/geometry/gaussian_sdf.py +370 -0
  16. splatreg-1.0.0/splatreg/io.py +424 -0
  17. splatreg-1.0.0/splatreg/py.typed +0 -0
  18. splatreg-1.0.0/splatreg/quality.py +332 -0
  19. splatreg-1.0.0/splatreg/residuals/__init__.py +31 -0
  20. splatreg-1.0.0/splatreg/residuals/base.py +49 -0
  21. splatreg-1.0.0/splatreg/residuals/icp.py +192 -0
  22. splatreg-1.0.0/splatreg/residuals/photometric.py +337 -0
  23. splatreg-1.0.0/splatreg/residuals/prior.py +117 -0
  24. splatreg-1.0.0/splatreg/residuals/sdf.py +237 -0
  25. splatreg-1.0.0/splatreg/solvers/__init__.py +10 -0
  26. splatreg-1.0.0/splatreg/solvers/_backend_common.py +84 -0
  27. splatreg-1.0.0/splatreg/solvers/base.py +21 -0
  28. splatreg-1.0.0/splatreg/solvers/lm.py +360 -0
  29. splatreg-1.0.0/splatreg/solvers/pypose_backend.py +113 -0
  30. splatreg-1.0.0/splatreg/solvers/theseus_backend.py +143 -0
  31. splatreg-1.0.0/splatreg/testing.py +75 -0
  32. splatreg-1.0.0/splatreg/track.py +211 -0
  33. splatreg-1.0.0/splatreg.egg-info/PKG-INFO +218 -0
  34. splatreg-1.0.0/splatreg.egg-info/SOURCES.txt +40 -0
  35. splatreg-1.0.0/splatreg.egg-info/dependency_links.txt +1 -0
  36. splatreg-1.0.0/splatreg.egg-info/requires.txt +43 -0
  37. splatreg-1.0.0/splatreg.egg-info/top_level.txt +1 -0
  38. splatreg-1.0.0/tests/test_backends.py +237 -0
  39. splatreg-1.0.0/tests/test_jacobians.py +159 -0
  40. splatreg-1.0.0/tests/test_lie.py +451 -0
  41. splatreg-1.0.0/tests/test_recovery.py +163 -0
  42. splatreg-1.0.0/tests/test_solver.py +550 -0
splatreg-1.0.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Krishi Attri
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: splatreg
3
+ Version: 1.0.0
4
+ Summary: Composable geometry-first SE(3)/Sim(3) registration for 3D Gaussian Splatting — the inverse of gsplat.
5
+ Author: Krishi Attri
6
+ License: BSD-3-Clause
7
+ Keywords: gaussian-splatting,3dgs,registration,pose-estimation,se3,sim3,gsplat
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: torch
12
+ Requires-Dist: numpy
13
+ Provides-Extra: render
14
+ Requires-Dist: gsplat>=1.4; extra == "render"
15
+ Provides-Extra: pypose
16
+ Requires-Dist: pypose; extra == "pypose"
17
+ Provides-Extra: theseus
18
+ Requires-Dist: theseus-ai; extra == "theseus"
19
+ Provides-Extra: gtsam
20
+ Requires-Dist: gtsam; extra == "gtsam"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: pytest-xdist; extra == "dev"
24
+ Requires-Dist: black>=24.0; extra == "dev"
25
+ Requires-Dist: mypy>=1.5; extra == "dev"
26
+ Requires-Dist: isort>=5.12; extra == "dev"
27
+ Requires-Dist: flake8>=6.0; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: hypothesis; extra == "dev"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=7.0; extra == "test"
32
+ Requires-Dist: pytest-xdist; extra == "test"
33
+ Requires-Dist: black>=24.0; extra == "test"
34
+ Requires-Dist: mypy>=1.5; extra == "test"
35
+ Requires-Dist: isort>=5.12; extra == "test"
36
+ Requires-Dist: flake8>=6.0; extra == "test"
37
+ Requires-Dist: ruff; extra == "test"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx; extra == "docs"
40
+ Requires-Dist: furo; extra == "docs"
41
+ Requires-Dist: myst-nb; extra == "docs"
42
+ Provides-Extra: all
43
+ Requires-Dist: gsplat>=1.4; extra == "all"
44
+ Requires-Dist: pypose; extra == "all"
45
+ Requires-Dist: theseus-ai; extra == "all"
46
+ Dynamic: license-file
47
+
48
+ <div align="center">
49
+
50
+ # splatreg
51
+
52
+ ### Register Gaussian splats — align & merge two 3DGS scans into one SE(3)/Sim(3) frame.
53
+
54
+ *The inverse of [gsplat](https://github.com/nerfstudio-project/gsplat): gsplat **renders** Gaussians, splatreg **registers** against them.* Pure PyTorch — no meshing, no CUDA extension, no point-cloud detour.
55
+
56
+ [![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](LICENSE)
57
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
58
+ [![PyTorch](https://img.shields.io/badge/pure-PyTorch-ee4c2c.svg)](https://pytorch.org)
59
+ [![tests](https://img.shields.io/badge/tests-44%20passing-brightgreen.svg)](tests)
60
+ [![Jacobian audit](https://img.shields.io/badge/Jacobian%20audit-8%2F8-brightgreen.svg)](tests/test_jacobians.py)
61
+
62
+ <img src="assets/registration_demo.png" alt="splatreg before/after registration" width="92%">
63
+
64
+ </div>
65
+
66
+ ---
67
+
68
+ ## What it is
69
+
70
+ A 3D Gaussian Splat is a cloud of oriented Gaussians that already traces an object's surface. **splatreg takes two such splats and finds the rigid (SE(3)) or similarity (Sim(3), +scale) transform that aligns them** — then optionally merges + dedupes them into one. It is the missing *registration* half of the Gaussian-splatting toolchain — the splat-to-splat alignment SuperSplat / INRIA / geospatial users keep asking for, where today's tooling punts to a manual gizmo.
71
+
72
+ The pipeline is two stages:
73
+
74
+ ```mermaid
75
+ flowchart LR
76
+ A["splat A<br/>(target)"]:::s --> G
77
+ B["splat B<br/>(source)"]:::s --> G
78
+ G["<b>Global aligner</b><br/>super-Fibonacci SO(3) seeds<br/>+ batched trimmed ICP<br/><i>(or FPFH / learned)</i>"]:::g --> L
79
+ L["<b>Levenberg–Marquardt</b><br/>multi-residual:<br/>ICP + Gaussian-SDF<br/>SE(3) / Sim(3)"]:::l --> T["T* (4×4)<br/>+ merge / dedupe"]:::o
80
+ classDef s fill:#e8f6f8,stroke:#17becf,color:#0b3d44;
81
+ classDef g fill:#fff1ee,stroke:#ff6b5b,color:#5a1a12;
82
+ classDef l fill:#eef7ee,stroke:#2e8b57,color:#143d22;
83
+ classDef o fill:#f3eefc,stroke:#7d52c7,color:#2c1654;
84
+ ```
85
+
86
+ 1. **Global init** — a coarse pose from a dense super-Fibonacci rotation sweep + batched trimmed ICP (no local-minimum trap), with optional **FPFH+RANSAC** and **learned** (GeoTransformer) seeds for harder real scans.
87
+ 2. **Refinement** — a from-scratch **Levenberg–Marquardt** core over a stack of residuals: classic **ICP** (point-to-point / point-to-plane) *and* splatreg's flagship **Gaussian-SDF** residual, solving the full SE(3) or Sim(3) tangent.
88
+
89
+ It **composes, it doesn't compete**: bring gsplat tensors directly; the LM loop and residual stack are pluggable.
90
+
91
+ ### The differentiator — the Gaussian-SDF residual
92
+
93
+ No competitor packages this. splatreg derives a smooth, queryable **signed-distance field directly from the target Gaussians** — no mesh, no marching cubes — and drives registration by it:
94
+
95
+ ```
96
+ w_i(p) = exp(−‖p − q_i‖² / 2σ²) # Gaussian kernel weight per anchor
97
+ q̃(p) = Σ w_i q_i / Σ w_i # kernel-weighted centroid
98
+ ñ(p) = Σ w_i n_i / ‖Σ w_i n_i‖ # kernel-weighted surface normal
99
+ d(p) = (p − q̃(p)) · ñ(p) # signed distance — the residual
100
+ ```
101
+
102
+ `d(p)` vanishes exactly when the source points land on the target's surface. It has a **closed-form, audited Jacobian** and is a standalone, reusable implicit-field primitive: `gaussian_sdf(splat, points, sigma=...) → (sdf, normal)`.
103
+
104
+ ---
105
+
106
+ ## Headline results
107
+
108
+ | | **splatreg** | reference |
109
+ |---|---|---|
110
+ | **Official 3DMatch registration recall** (Choi/Zeng protocol, 1279 pairs) | **91.5%** mean · 93.5% pooled | GeoTransformer ~92% · Open3D ~77% |
111
+ | **Official 3DMatch rotation / translation error** | **1.81° / 0.071 m** | — |
112
+ | **Official 3DLoMatch** (hard, 10–30% overlap) | 72.5% mean · **74.4%** pooled | GeoTransformer ~74% · Open3D ~20% |
113
+ | **Real-splat merge** (real 103k-Gaussian capture) | Chamfer **10.3→2.0 mm (5.1×)** · overlap **0.03→0.67 (22×)** | naive concat |
114
+ | **vs splat competitors** (real splat, known GT Sim3) | **5.2°** (SE3) · recovers scale (Sim3) | splatalign 15.3° · GaussianSplattingRegistration 36.3° |
115
+ | **Sim(3) scale estimation** | ✅ native | ✗ none of these do it |
116
+ | **Registration speed** | **~17 ms** (fast) · 104 ms (learned) | GeoTransformer ~50 ms · Open3D 142 ms |
117
+ | **Real-time tracking** | **~17 ms/frame** | GaussianFeels tracker ~45 ms |
118
+
119
+ splatreg is the **only library** that registers native Gaussian splats with SE(3)+**Sim(3)** behind a closed-form-Jacobian Gaussian-SDF.
120
+
121
+ - **Matches GeoTransformer on official 3DMatch** — 91.5% mean / 93.5% pooled recall vs their published ~92% — because the `learned` path **rides GeoTransformer's matcher at its native 0.025 m resolution** and then layers splatreg's SDF/LM refine + Sim(3) scale **on top**. The recall is GeoTransformer's; what splatreg *adds* is **accuracy** (RRE 1.87° → 1.81°), the unique **Sim(3) scale DoF**, and a **verified no-regression floor** (a per-pair audit found **0 demotions** — the refine only tightens already-successful pairs, never breaks them). On hard 3DLoMatch it reaches **74.4% pooled**, matching GeoTransformer's ~74%.
122
+ - **Decisively beats classical Open3D** (~77% / ~20%) on both splits.
123
+ - **Wins outright vs the splat-registration tools** — **5.2°** vs splatalign's 15.3° and GaussianSplattingRegistration's 36.3° on a real splat — and is the **only one that recovers Sim(3) scale.**
124
+ - **Real-splat merge** (`examples/merge_demo.py`) fuses two overlapping captures (a real 103k-Gaussian splat) into one deduped `.ply`: post-merge Chamfer **10.3 → 2.0 mm (5.1× closer)** and overlap **0.03 → 0.67 (22× more)** vs a naive `torch.cat`, removing ~9k overlap duplicates (verified on GPU, two independent runs).
125
+
126
+ ### Four init modes — trade speed ↔ robustness
127
+
128
+ | `init=` | what | when |
129
+ |---|---|---|
130
+ | `"fast"` *(default)* | FPFH + GPU-batched RANSAC seed → closed-form LM | objects / full-overlap, **~17 ms** |
131
+ | `"robust"` | Open3D FPFH+RANSAC seed → splatreg refine + scale | real metre-scale scans |
132
+ | `"learned"` | pretrained GeoTransformer seed → splatreg refine + scale | best accuracy on real scans |
133
+ | `"global"` | blind super-Fibonacci SO(3) sweep | robust fallback, any rotation |
134
+
135
+ ---
136
+
137
+ ## Install
138
+
139
+ ```bash
140
+ git clone https://github.com/Archerkattri/splatreg.git
141
+ cd splatreg
142
+ pip install -e . # pure PyTorch + numpy; pip install -e ".[test]" for test extras
143
+ ```
144
+
145
+ ## Quickstart
146
+
147
+ ```python
148
+ from splatreg.api import register, merge
149
+
150
+ # two Gaussian splats of the same object, in unknown relative pose/scale.
151
+ # register aligns `source` onto the reference `target` (target is the first arg).
152
+ result = register(target, source, transform="sim3") # init="fast" by default (objects / full-overlap)
153
+ # real metre-scale scans -> init="robust" (FPFH+RANSAC) or init="learned" (GeoTransformer seed, best accuracy)
154
+ print(result.T) # recovered 4×4 similarity [[s·R, t], [0, 1]] — maps source -> target
155
+ print(result.scale) # recovered scale s (1.0 for transform="se3")
156
+ print(result.converged) # solver convergence flag
157
+
158
+ # register + dedupe a list of splats into one fused splat (registers internally)
159
+ fused = merge([source, target], transform="sim3")
160
+ ```
161
+
162
+ The Gaussian-SDF field on its own:
163
+
164
+ ```python
165
+ from splatreg.geometry.gaussian_sdf import gaussian_sdf, gaussian_sdf_grad
166
+ sdf, normal = gaussian_sdf(target, query_points, sigma=0.02) # signed distance + surface normal
167
+ sdf, grad = gaussian_sdf_grad(target, query_points, sigma=0.02) # signed distance + EXACT ∇_p d
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Validation
173
+
174
+ Every number is reproducible; the full record — commands, seeds, and honest limitations — is in [`RESULTS.md`](RESULTS.md).
175
+
176
+ - **Synthetic Sim(3)/SE(3) recovery** — apply a known transform, recover it: **36/36 = 100%**, median rot 0.03°, scale error 0.34%, Chamfer 0.6 mm (`examples/validate_recovery.py`).
177
+ - **Jacobian audit** — every analytic Jacobian checked against a tangent-space numerical one in float64 (`tests/test_jacobians.py`): ICP point-to-point/plane, the **Gaussian-SDF closed-form gradient** (~1e-8 vs numerical), and SE(3)/Sim(3) exp·log incl. near-π. Ships a reusable `assert_residual_jacobian` so every future residual gets the audit.
178
+ - **vs plain ICP** — splatreg **27/27 Sim(3)** vs ICP's **9/27** (plain ICP cannot estimate scale; the LM Sim(3) solve is load-bearing).
179
+ - **Robustness** — sensor noise **9/9**, outlier clutter **9/9**, symmetric sphere **9/9**; partial overlap **6/9 solved** (all keep ≥ 60% at 0.00°) + 3 honestly flagged ambiguous (heavy keep ≤ 40% crops), **0 silent-wrong**.
180
+ - **Official 3DMatch / 3DLoMatch** — canonical Choi/Zeng protocol, 1279 pairs, covariance-weighted error (`benchmarks/threedmatch_official_bench.py`): **91.5% / 74.4%** as above.
181
+ - **Test suite** — `pytest tests/` → **44 passing**; `black` + `mypy` clean, ships `py.typed`.
182
+
183
+ ```bash
184
+ pip install -e ".[test]"
185
+ python -m pytest tests/ -q # 44 passing: audit + Lie + solver
186
+ python tests/test_jacobians.py # the numerical-vs-analytic Jacobian audit
187
+ SPLATREG_DEVICE=cuda python examples/validate_recovery.py --device cuda # recovery 36/36
188
+ SPLATREG_DEVICE=cuda python benchmarks/robustness_bench.py --device cuda
189
+ python examples/merge_demo.py # real-splat merge demo
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Limitations
195
+
196
+ splatreg is honest about its edges (full detail in [`RESULTS.md`](RESULTS.md)):
197
+
198
+ - **Partial overlap — moderate crops now solve; heavy crops are genuinely ambiguous.** The overlap basin-sweep now keeps a deep candidate pool (`topk=200`) and ranks the refined seeds by a **symmetric** overlap residual (target→source *and* source→target), so the moderate keep ≈ 60% crops resolve to the true basin instead of a ~170° mirror — robustness sweep **PARTIAL solved-count 4/9 → 6/9** (all keep ≥ 60% at 0.00°). Heavy one-sided crops (keep ≤ 40%) delete the rotation-disambiguating geometry — there *even the true pose* no longer seats cleanly (symmetric residual 0.003 vs a forest of ~0.017 wrong basins), so the aligner returns an **honest ambiguity flag** (`result.info['ambiguous']` / `['confidence']`) rather than a silent wrong pose (0 silent-wrong, verified). This deep sweep costs ~22 s/cell (registration path only, never the real-time tracker); the heavy-crop case is genuinely open.
199
+ - **Scale is loose under low overlap.** A dedicated golden-section **scale line-search** against the symmetric overlap residual now refines the Sim(3) scale DoF after the pose solve (the one-directional fit is scale-blind). It tightens scale on its own objective, but on a thin shared band the scale still has a wide valley — under ~20% overlap the recovered scale can drift; `merge` is reliable for high-overlap captures.
200
+ - **The 3DMatch recall is GeoTransformer's, not ours.** splatreg `learned` **matches** GeoTransformer by riding its matcher; it does not beat that matcher's recall. What splatreg adds is accuracy, the Sim(3) scale DoF, and the no-regression floor. Closing the gap with splatreg's *own* dense correspondence is open work.
201
+ - **Cost on rigid SE(3).** Plain ICP reaches the same SE(3) success and is far faster; the SDF residual buys scale + implicit-field robustness at a real compute cost. Use `track()` (~17 ms/frame) for the warm-start real-time path.
202
+
203
+ ---
204
+
205
+ ## Roadmap
206
+
207
+ Shipped: official 3DMatch + 3DLoMatch (Choi/Zeng, 91.5% / 74.4% pooled, matching GeoTransformer); pluggable `fast`/`robust`/`learned` init backends; CI regression gates (determinism + worst-case + PR-comment benchmark); the real-splat merge demo (`examples/merge_demo.py`); the head-to-head vs `splatalign` / `GaussianSplattingRegistration` (only tool recovering Sim(3) scale); the seeded-RANSAC determinism fix; and the partial-overlap basin sweep (4/9 → 6/9 solved). Remaining:
208
+
209
+ - [ ] **Heavy partial overlap (keep ≤ 40%) to publication.** The moderate keep ≈ 60% crops now solve; the heavy one-sided crops are correctly flagged ambiguous but still unsolved. Needs an overlap-region-restricted descriptor / learned overlap prior to push past the geometry-only basin ambiguity.
210
+ - [ ] **Tighten Sim(3) scale under low overlap.** The scale line-search helps but a thin shared band leaves a wide scale valley; a multi-scale or descriptor-anchored scale constraint is the next step.
211
+ - [ ] Close the gap to GeoTransformer's full coarse-to-fine matcher with splatreg's *own* dense correspondence (not just a borrowed seed).
212
+ - [ ] 6-DoF object-pose mode + FoundationPose/YCB benchmark (v0.2)
213
+ - [ ] Camera localization in a splat (v0.2)
214
+ - [ ] PyPI release
215
+
216
+ ## License & layout
217
+
218
+ BSD 3-Clause — permissive, composes with the gsplat / Theseus / GTSAM ecosystem. `splatreg/` — library (`api`, `align`, `align_features`, `core/lie`, `geometry/gaussian_sdf`, `residuals/`, `solvers/lm`). `tests/` · `benchmarks/` · `examples/`. Full validation record: [`RESULTS.md`](RESULTS.md).
@@ -0,0 +1,171 @@
1
+ <div align="center">
2
+
3
+ # splatreg
4
+
5
+ ### Register Gaussian splats — align & merge two 3DGS scans into one SE(3)/Sim(3) frame.
6
+
7
+ *The inverse of [gsplat](https://github.com/nerfstudio-project/gsplat): gsplat **renders** Gaussians, splatreg **registers** against them.* Pure PyTorch — no meshing, no CUDA extension, no point-cloud detour.
8
+
9
+ [![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](LICENSE)
10
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
11
+ [![PyTorch](https://img.shields.io/badge/pure-PyTorch-ee4c2c.svg)](https://pytorch.org)
12
+ [![tests](https://img.shields.io/badge/tests-44%20passing-brightgreen.svg)](tests)
13
+ [![Jacobian audit](https://img.shields.io/badge/Jacobian%20audit-8%2F8-brightgreen.svg)](tests/test_jacobians.py)
14
+
15
+ <img src="assets/registration_demo.png" alt="splatreg before/after registration" width="92%">
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## What it is
22
+
23
+ A 3D Gaussian Splat is a cloud of oriented Gaussians that already traces an object's surface. **splatreg takes two such splats and finds the rigid (SE(3)) or similarity (Sim(3), +scale) transform that aligns them** — then optionally merges + dedupes them into one. It is the missing *registration* half of the Gaussian-splatting toolchain — the splat-to-splat alignment SuperSplat / INRIA / geospatial users keep asking for, where today's tooling punts to a manual gizmo.
24
+
25
+ The pipeline is two stages:
26
+
27
+ ```mermaid
28
+ flowchart LR
29
+ A["splat A<br/>(target)"]:::s --> G
30
+ B["splat B<br/>(source)"]:::s --> G
31
+ G["<b>Global aligner</b><br/>super-Fibonacci SO(3) seeds<br/>+ batched trimmed ICP<br/><i>(or FPFH / learned)</i>"]:::g --> L
32
+ L["<b>Levenberg–Marquardt</b><br/>multi-residual:<br/>ICP + Gaussian-SDF<br/>SE(3) / Sim(3)"]:::l --> T["T* (4×4)<br/>+ merge / dedupe"]:::o
33
+ classDef s fill:#e8f6f8,stroke:#17becf,color:#0b3d44;
34
+ classDef g fill:#fff1ee,stroke:#ff6b5b,color:#5a1a12;
35
+ classDef l fill:#eef7ee,stroke:#2e8b57,color:#143d22;
36
+ classDef o fill:#f3eefc,stroke:#7d52c7,color:#2c1654;
37
+ ```
38
+
39
+ 1. **Global init** — a coarse pose from a dense super-Fibonacci rotation sweep + batched trimmed ICP (no local-minimum trap), with optional **FPFH+RANSAC** and **learned** (GeoTransformer) seeds for harder real scans.
40
+ 2. **Refinement** — a from-scratch **Levenberg–Marquardt** core over a stack of residuals: classic **ICP** (point-to-point / point-to-plane) *and* splatreg's flagship **Gaussian-SDF** residual, solving the full SE(3) or Sim(3) tangent.
41
+
42
+ It **composes, it doesn't compete**: bring gsplat tensors directly; the LM loop and residual stack are pluggable.
43
+
44
+ ### The differentiator — the Gaussian-SDF residual
45
+
46
+ No competitor packages this. splatreg derives a smooth, queryable **signed-distance field directly from the target Gaussians** — no mesh, no marching cubes — and drives registration by it:
47
+
48
+ ```
49
+ w_i(p) = exp(−‖p − q_i‖² / 2σ²) # Gaussian kernel weight per anchor
50
+ q̃(p) = Σ w_i q_i / Σ w_i # kernel-weighted centroid
51
+ ñ(p) = Σ w_i n_i / ‖Σ w_i n_i‖ # kernel-weighted surface normal
52
+ d(p) = (p − q̃(p)) · ñ(p) # signed distance — the residual
53
+ ```
54
+
55
+ `d(p)` vanishes exactly when the source points land on the target's surface. It has a **closed-form, audited Jacobian** and is a standalone, reusable implicit-field primitive: `gaussian_sdf(splat, points, sigma=...) → (sdf, normal)`.
56
+
57
+ ---
58
+
59
+ ## Headline results
60
+
61
+ | | **splatreg** | reference |
62
+ |---|---|---|
63
+ | **Official 3DMatch registration recall** (Choi/Zeng protocol, 1279 pairs) | **91.5%** mean · 93.5% pooled | GeoTransformer ~92% · Open3D ~77% |
64
+ | **Official 3DMatch rotation / translation error** | **1.81° / 0.071 m** | — |
65
+ | **Official 3DLoMatch** (hard, 10–30% overlap) | 72.5% mean · **74.4%** pooled | GeoTransformer ~74% · Open3D ~20% |
66
+ | **Real-splat merge** (real 103k-Gaussian capture) | Chamfer **10.3→2.0 mm (5.1×)** · overlap **0.03→0.67 (22×)** | naive concat |
67
+ | **vs splat competitors** (real splat, known GT Sim3) | **5.2°** (SE3) · recovers scale (Sim3) | splatalign 15.3° · GaussianSplattingRegistration 36.3° |
68
+ | **Sim(3) scale estimation** | ✅ native | ✗ none of these do it |
69
+ | **Registration speed** | **~17 ms** (fast) · 104 ms (learned) | GeoTransformer ~50 ms · Open3D 142 ms |
70
+ | **Real-time tracking** | **~17 ms/frame** | GaussianFeels tracker ~45 ms |
71
+
72
+ splatreg is the **only library** that registers native Gaussian splats with SE(3)+**Sim(3)** behind a closed-form-Jacobian Gaussian-SDF.
73
+
74
+ - **Matches GeoTransformer on official 3DMatch** — 91.5% mean / 93.5% pooled recall vs their published ~92% — because the `learned` path **rides GeoTransformer's matcher at its native 0.025 m resolution** and then layers splatreg's SDF/LM refine + Sim(3) scale **on top**. The recall is GeoTransformer's; what splatreg *adds* is **accuracy** (RRE 1.87° → 1.81°), the unique **Sim(3) scale DoF**, and a **verified no-regression floor** (a per-pair audit found **0 demotions** — the refine only tightens already-successful pairs, never breaks them). On hard 3DLoMatch it reaches **74.4% pooled**, matching GeoTransformer's ~74%.
75
+ - **Decisively beats classical Open3D** (~77% / ~20%) on both splits.
76
+ - **Wins outright vs the splat-registration tools** — **5.2°** vs splatalign's 15.3° and GaussianSplattingRegistration's 36.3° on a real splat — and is the **only one that recovers Sim(3) scale.**
77
+ - **Real-splat merge** (`examples/merge_demo.py`) fuses two overlapping captures (a real 103k-Gaussian splat) into one deduped `.ply`: post-merge Chamfer **10.3 → 2.0 mm (5.1× closer)** and overlap **0.03 → 0.67 (22× more)** vs a naive `torch.cat`, removing ~9k overlap duplicates (verified on GPU, two independent runs).
78
+
79
+ ### Four init modes — trade speed ↔ robustness
80
+
81
+ | `init=` | what | when |
82
+ |---|---|---|
83
+ | `"fast"` *(default)* | FPFH + GPU-batched RANSAC seed → closed-form LM | objects / full-overlap, **~17 ms** |
84
+ | `"robust"` | Open3D FPFH+RANSAC seed → splatreg refine + scale | real metre-scale scans |
85
+ | `"learned"` | pretrained GeoTransformer seed → splatreg refine + scale | best accuracy on real scans |
86
+ | `"global"` | blind super-Fibonacci SO(3) sweep | robust fallback, any rotation |
87
+
88
+ ---
89
+
90
+ ## Install
91
+
92
+ ```bash
93
+ git clone https://github.com/Archerkattri/splatreg.git
94
+ cd splatreg
95
+ pip install -e . # pure PyTorch + numpy; pip install -e ".[test]" for test extras
96
+ ```
97
+
98
+ ## Quickstart
99
+
100
+ ```python
101
+ from splatreg.api import register, merge
102
+
103
+ # two Gaussian splats of the same object, in unknown relative pose/scale.
104
+ # register aligns `source` onto the reference `target` (target is the first arg).
105
+ result = register(target, source, transform="sim3") # init="fast" by default (objects / full-overlap)
106
+ # real metre-scale scans -> init="robust" (FPFH+RANSAC) or init="learned" (GeoTransformer seed, best accuracy)
107
+ print(result.T) # recovered 4×4 similarity [[s·R, t], [0, 1]] — maps source -> target
108
+ print(result.scale) # recovered scale s (1.0 for transform="se3")
109
+ print(result.converged) # solver convergence flag
110
+
111
+ # register + dedupe a list of splats into one fused splat (registers internally)
112
+ fused = merge([source, target], transform="sim3")
113
+ ```
114
+
115
+ The Gaussian-SDF field on its own:
116
+
117
+ ```python
118
+ from splatreg.geometry.gaussian_sdf import gaussian_sdf, gaussian_sdf_grad
119
+ sdf, normal = gaussian_sdf(target, query_points, sigma=0.02) # signed distance + surface normal
120
+ sdf, grad = gaussian_sdf_grad(target, query_points, sigma=0.02) # signed distance + EXACT ∇_p d
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Validation
126
+
127
+ Every number is reproducible; the full record — commands, seeds, and honest limitations — is in [`RESULTS.md`](RESULTS.md).
128
+
129
+ - **Synthetic Sim(3)/SE(3) recovery** — apply a known transform, recover it: **36/36 = 100%**, median rot 0.03°, scale error 0.34%, Chamfer 0.6 mm (`examples/validate_recovery.py`).
130
+ - **Jacobian audit** — every analytic Jacobian checked against a tangent-space numerical one in float64 (`tests/test_jacobians.py`): ICP point-to-point/plane, the **Gaussian-SDF closed-form gradient** (~1e-8 vs numerical), and SE(3)/Sim(3) exp·log incl. near-π. Ships a reusable `assert_residual_jacobian` so every future residual gets the audit.
131
+ - **vs plain ICP** — splatreg **27/27 Sim(3)** vs ICP's **9/27** (plain ICP cannot estimate scale; the LM Sim(3) solve is load-bearing).
132
+ - **Robustness** — sensor noise **9/9**, outlier clutter **9/9**, symmetric sphere **9/9**; partial overlap **6/9 solved** (all keep ≥ 60% at 0.00°) + 3 honestly flagged ambiguous (heavy keep ≤ 40% crops), **0 silent-wrong**.
133
+ - **Official 3DMatch / 3DLoMatch** — canonical Choi/Zeng protocol, 1279 pairs, covariance-weighted error (`benchmarks/threedmatch_official_bench.py`): **91.5% / 74.4%** as above.
134
+ - **Test suite** — `pytest tests/` → **44 passing**; `black` + `mypy` clean, ships `py.typed`.
135
+
136
+ ```bash
137
+ pip install -e ".[test]"
138
+ python -m pytest tests/ -q # 44 passing: audit + Lie + solver
139
+ python tests/test_jacobians.py # the numerical-vs-analytic Jacobian audit
140
+ SPLATREG_DEVICE=cuda python examples/validate_recovery.py --device cuda # recovery 36/36
141
+ SPLATREG_DEVICE=cuda python benchmarks/robustness_bench.py --device cuda
142
+ python examples/merge_demo.py # real-splat merge demo
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Limitations
148
+
149
+ splatreg is honest about its edges (full detail in [`RESULTS.md`](RESULTS.md)):
150
+
151
+ - **Partial overlap — moderate crops now solve; heavy crops are genuinely ambiguous.** The overlap basin-sweep now keeps a deep candidate pool (`topk=200`) and ranks the refined seeds by a **symmetric** overlap residual (target→source *and* source→target), so the moderate keep ≈ 60% crops resolve to the true basin instead of a ~170° mirror — robustness sweep **PARTIAL solved-count 4/9 → 6/9** (all keep ≥ 60% at 0.00°). Heavy one-sided crops (keep ≤ 40%) delete the rotation-disambiguating geometry — there *even the true pose* no longer seats cleanly (symmetric residual 0.003 vs a forest of ~0.017 wrong basins), so the aligner returns an **honest ambiguity flag** (`result.info['ambiguous']` / `['confidence']`) rather than a silent wrong pose (0 silent-wrong, verified). This deep sweep costs ~22 s/cell (registration path only, never the real-time tracker); the heavy-crop case is genuinely open.
152
+ - **Scale is loose under low overlap.** A dedicated golden-section **scale line-search** against the symmetric overlap residual now refines the Sim(3) scale DoF after the pose solve (the one-directional fit is scale-blind). It tightens scale on its own objective, but on a thin shared band the scale still has a wide valley — under ~20% overlap the recovered scale can drift; `merge` is reliable for high-overlap captures.
153
+ - **The 3DMatch recall is GeoTransformer's, not ours.** splatreg `learned` **matches** GeoTransformer by riding its matcher; it does not beat that matcher's recall. What splatreg adds is accuracy, the Sim(3) scale DoF, and the no-regression floor. Closing the gap with splatreg's *own* dense correspondence is open work.
154
+ - **Cost on rigid SE(3).** Plain ICP reaches the same SE(3) success and is far faster; the SDF residual buys scale + implicit-field robustness at a real compute cost. Use `track()` (~17 ms/frame) for the warm-start real-time path.
155
+
156
+ ---
157
+
158
+ ## Roadmap
159
+
160
+ Shipped: official 3DMatch + 3DLoMatch (Choi/Zeng, 91.5% / 74.4% pooled, matching GeoTransformer); pluggable `fast`/`robust`/`learned` init backends; CI regression gates (determinism + worst-case + PR-comment benchmark); the real-splat merge demo (`examples/merge_demo.py`); the head-to-head vs `splatalign` / `GaussianSplattingRegistration` (only tool recovering Sim(3) scale); the seeded-RANSAC determinism fix; and the partial-overlap basin sweep (4/9 → 6/9 solved). Remaining:
161
+
162
+ - [ ] **Heavy partial overlap (keep ≤ 40%) to publication.** The moderate keep ≈ 60% crops now solve; the heavy one-sided crops are correctly flagged ambiguous but still unsolved. Needs an overlap-region-restricted descriptor / learned overlap prior to push past the geometry-only basin ambiguity.
163
+ - [ ] **Tighten Sim(3) scale under low overlap.** The scale line-search helps but a thin shared band leaves a wide scale valley; a multi-scale or descriptor-anchored scale constraint is the next step.
164
+ - [ ] Close the gap to GeoTransformer's full coarse-to-fine matcher with splatreg's *own* dense correspondence (not just a borrowed seed).
165
+ - [ ] 6-DoF object-pose mode + FoundationPose/YCB benchmark (v0.2)
166
+ - [ ] Camera localization in a splat (v0.2)
167
+ - [ ] PyPI release
168
+
169
+ ## License & layout
170
+
171
+ BSD 3-Clause — permissive, composes with the gsplat / Theseus / GTSAM ecosystem. `splatreg/` — library (`api`, `align`, `align_features`, `core/lie`, `geometry/gaussian_sdf`, `residuals/`, `solvers/lm`). `tests/` · `benchmarks/` · `examples/`. Full validation record: [`RESULTS.md`](RESULTS.md).
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splatreg"
7
+ version = "1.0.0"
8
+ description = "Composable geometry-first SE(3)/Sim(3) registration for 3D Gaussian Splatting — the inverse of gsplat."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "BSD-3-Clause" }
12
+ authors = [{ name = "Krishi Attri" }]
13
+ keywords = ["gaussian-splatting", "3dgs", "registration", "pose-estimation", "se3", "sim3", "gsplat"]
14
+ dependencies = ["torch", "numpy"]
15
+
16
+ [project.optional-dependencies]
17
+ render = ["gsplat>=1.4"] # the Photometric residual + splatreg.render
18
+ pypose = ["pypose"] # solver backend
19
+ theseus = ["theseus-ai"] # differentiable solver backend
20
+ gtsam = ["gtsam"] # factor-graph backend (needs analytic Jacobians)
21
+ dev = [
22
+ "pytest>=7.0",
23
+ "pytest-xdist",
24
+ "black>=24.0",
25
+ "mypy>=1.5",
26
+ "isort>=5.12",
27
+ "flake8>=6.0",
28
+ "ruff",
29
+ "hypothesis",
30
+ ]
31
+ test = [
32
+ "pytest>=7.0",
33
+ "pytest-xdist",
34
+ "black>=24.0",
35
+ "mypy>=1.5",
36
+ "isort>=5.12",
37
+ "flake8>=6.0",
38
+ "ruff",
39
+ ]
40
+ docs = ["sphinx", "furo", "myst-nb"]
41
+ all = ["gsplat>=1.4", "pypose", "theseus-ai"]
42
+
43
+ [tool.setuptools.packages.find]
44
+ include = ["splatreg*"]
45
+
46
+ [tool.setuptools.package-data]
47
+ splatreg = ["py.typed"]
48
+
49
+ [tool.black]
50
+ line-length = 110
51
+ target-version = ["py310", "py311"]
52
+
53
+ [tool.isort]
54
+ profile = "black"
55
+ line_length = 110
56
+
57
+ [tool.mypy]
58
+ python_version = "3.10"
59
+ ignore_missing_imports = true
60
+ # torch stubs are incomplete; suppress **kwargs unpacking noise for as_tensor calls
61
+ # and Optional→Tensor variance in the .to() convenience method.
62
+ [[tool.mypy.overrides]]
63
+ module = "splatreg.io"
64
+ ignore_errors = true
65
+ [[tool.mypy.overrides]]
66
+ module = "splatreg.core.types"
67
+ ignore_errors = true
68
+
69
+ [tool.ruff]
70
+ line-length = 110
71
+
72
+ [tool.ruff.lint]
73
+ # Default ruff lint set (pyflakes F + pycodestyle E/W). E731 (lambda assignment) and E741
74
+ # (ambiguous name `I`) flag intentional, readable code here (the Gaussians.to lambda; the
75
+ # identity matrix `I` in the Lie-group invariance tests), so they are silenced project-wide.
76
+ ignore = ["E731", "E741"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ """splatreg — composable geometry-first SE(3)/Sim(3) registration for 3D Gaussian Splatting.
2
+
3
+ *gsplat renders your Gaussians; splatreg registers against them.*
4
+
5
+ Public surface (filled in by the carve):
6
+ register(target, source, residuals=[...], transform="sim3", backend="builtin") -> RegisterResult
7
+ merge([a, b, ...], ref=0) -> Gaussians
8
+ Tracker(target, residuals=[...]).track(frame) -> RegisterResult
9
+ Residual, Solver (extension points)
10
+ """
11
+
12
+ from .core.types import Gaussians, Frame, RegisterResult, LinearizedProblem, SE3Update
13
+ from .residuals.base import Residual
14
+ from .solvers.base import Solver
15
+ from .quality import QualityConfig, resolve_quality
16
+
17
+ # The high-level pipeline (splatreg.api) is added by the carve; tolerate its absence pre-build.
18
+ try:
19
+ from .api import register, merge, Tracker # noqa: F401
20
+ except ImportError:
21
+ register = merge = Tracker = None # type: ignore
22
+
23
+ __version__ = "0.0.1"
24
+ __all__ = [
25
+ "register",
26
+ "merge",
27
+ "Tracker",
28
+ "Residual",
29
+ "Solver",
30
+ "QualityConfig",
31
+ "resolve_quality",
32
+ "Gaussians",
33
+ "Frame",
34
+ "RegisterResult",
35
+ "LinearizedProblem",
36
+ "SE3Update",
37
+ ]