the inverse of gsplat · SE(3) / Sim(3)
splatreg▍
Register Gaussian splats: align and merge 3DGS scans into one frame.
gsplat renders your Gaussians; splatreg registers them. Two 3DGS scans of the same scene go in, one SE(3) or Sim(3) transform comes out, and optionally one fused, deduped splat. Pure PyTorch — no meshing, no CUDA extension, no point-cloud detour.
splatreg is the missing registration half of the Gaussian-splatting toolchain: the splat-to-splat alignment that SuperSplat / INRIA / geospatial users keep asking for, where today's tooling punts to a manual gizmo. It works with anything that speaks the standard 3DGS PLY (gsplat, Nerfstudio/splatfacto, INRIA, SuperSplat) or hands over means/covariance tensors.
Headline result — a zero-shot seed that holds up on real data¶
gt.log pair set (8/8 scenes, n=1619: 0.962 vs 0.630); the low-overlap bars are the official 3DLoMatch gt.log set (n=1781: 0.777 vs 0.122). Both seeds share the lighter feature_align refine, so these isolate the seed rather than report full-pipeline absolute numbers. See Init modes.What you get that no other splat registrar ships¶
-
Provably correct SH rotation
Higher-order spherical-harmonic bands (
f_rest) are mixed by the real-basis Wigner-D matrix, so glossy highlights turn with the splat. Test-locked to ~2.4e-15 in float64. -
Align without merging
apply_transform()/splatreg alignbakes the recovered pose into the source and writes it as its own PLY — both scans stay separate files, now in one frame. -
Sim(3) scale recovery
Native scale estimation, which none of the competing splat tools attempt at all — plus photometric refinement (exposure compensation + coarse-to-fine ladder) for the poses geometry cannot see.
-
Honest diagnostics
Pose covariance on every builtin-LM solve for pose-graph weighting (
Nonewhen singular, never faked), and ambiguous overlaps are flagged — never silently wrong-posed.
D(R)⁻¹·D(R)·f = f to ~2e-16 in float64. See PLY interop.30 seconds, end to end¶
pip install splatreg
splatreg align scan_a.ply scan_b.ply -o b_aligned.ply # register + write aligned PLY
splatreg merge scan_a.ply scan_b.ply -o fused.ply # register + fuse + dedupe
or in Python:
from splatreg import register, merge, apply_transform
from splatreg.io import load_ply, save_ply
a = load_ply("scan_a.ply") # target (stays fixed)
b = load_ply("scan_b.ply") # source (gets aligned)
result = register(a, b, transform="sim3") # init="fast" by default, ~17 ms
print(result.T) # 4x4 similarity [[s*R, t], [0, 1]], maps source -> target
print(result.scale) # recovered scale (1.0 for transform="se3")
fused = merge([a, b]) # register + concat + dedupe the overlap
save_ply(fused, "fused.ply") # opens in SuperSplat / any 3DGS viewer
# or keep the scans separate, just registered into one frame:
save_ply(apply_transform(b, result.T, result.scale), "b_aligned.ply")
merge on two real overlapping 3DMatch scans (7-scenes-redkitchen, ~19k points each): register (SE(3), recovered to 0.58° / 17 mm against the 3DMatch ground truth; the seam gap closes 101 → 18 mm, overlap 0.27 → 0.82), then fuse + voxel-dedupe the double-covered seam (38,059 → 23,564 Gaussians). Every number is measured on this run. Regenerate: examples/make_merge_fusion_gif.py.Capability matrix¶
Honest comparison against the tools people actually use for this job. The accuracy row is measured head-to-head on a real splat with known ground truth; editor columns reflect their design (manual transforms, not registration).
| splatreg | splatalign | GaussianSplattingRegistration | SuperSplat / SplatTransform | |
|---|---|---|---|---|
| Automatic splat-to-splat registration | yes (6 init modes) | ICP from identity | Open3D RANSAC+ICP | no (manual gizmo) |
| Measured rotation error, real splat + GT | 5.2° | 15.3° | 36.3° | n/a |
| Sim(3) scale recovery | yes, native | no (SE(3) only) | no (SE(3) only) | manual |
SH (f_rest) rotated with the splat |
yes, test-locked | no | no | no |
| Merge + overlap dedupe | yes | no | no dedupe | concat only |
| Photometric refine (exposure comp + ladder) | yes | no | no | no |
| Pose covariance for pose graphs | yes | no | no | n/a |
| Honest ambiguity flag (never silent-wrong) | yes | no | no | n/a |
| Zero-shot learned seed (BUFFER-X) | yes | no | no | no |
How it works¶
- Global init: a coarse pose from a dense super-Fibonacci rotation sweep + batched
trimmed ICP (no local-minimum trap), with FPFH+RANSAC (
init="robust"), learned GeoTransformer (init="learned"), zero-shot BUFFER-X (init="bufferx"), and MAC maximal-clique (init="mac") seeds for harder real scans. See Init modes. - Refinement: a from-scratch Levenberg-Marquardt core over ICP (point-to-point / point-to-plane) and splatreg's flagship Gaussian-SDF residual (a smooth signed distance field derived directly from the target Gaussians, with a closed-form, audited Jacobian), solving the full SE(3) or Sim(3) tangent and exposing the pose information/covariance at the optimum.
More headline numbers¶
| splatreg | reference | |
|---|---|---|
| Real-splat merge (103k Gaussians) | Chamfer 10.3 → 2.0 mm (5.1×), overlap 0.03 → 0.67 (22×) | naive concat |
Official 3DMatch recall (learned seed) |
91.5% mean, 93.5% pooled | GeoTransformer ~92%, Open3D ~77% |
| Official 3DLoMatch (hard, 10–30% overlap) | 72.5% mean, 74.4% pooled | GeoTransformer ~74%, Open3D ~20% |
| Photometric refine (real rasterizer) | 5°/7 mm → 0.36°/0.5 mm | geometric alone worsens the symmetric case |
| Registration speed | ~17 ms (fast) | Open3D 142 ms |
Full record with reproduce commands: Benchmarks.
Honest edges — the repo's signature
Limitations¶
splatreg states where it stops working, in the docs and in the diagnostics:
- Heavy overlap loss (keep ≤ 40%) is genuinely ambiguous. The rotation-disambiguating
geometry is physically absent; even the true pose does not seat cleanly. The aligner flags
these (
result.info["ambiguous"]/["confidence"]) and never silently wrong-poses.mergeandtrackare built for high-overlap captures. - Scale is unobservable under thin overlap. Under ~20% shared geometry the Sim(3) scale valley is flat; no algorithm recovers what the geometry does not carry.
- Cost on rigid SE(3). Plain ICP reaches the same SE(3) success 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.
Full detail, including the failure analyses, is in
RESULTS.md.
Where next¶
- Quickstart: install + the core workflows in Python.
- CLI guide:
splatreg align / merge / infofrom the shell. - Init modes: speed vs robustness —
fast,robust,learned, the zero-shotbufferxseed,mac,global— with the honest measured 3DMatch/3DLoMatch verdicts. - Photometric refinement: the opt-in stage for poses geometry can't see (symmetry / texture-only DoF), with the measured when-and-why table, per-pair exposure compensation (default ON), and the coarse-to-fine render ladder.
- PLY interop: splatfacto / INRIA / SuperSplat round-trip, and what happens to spherical harmonics under a recovered rotation (higher-order SH bands are Wigner-rotated with the splat; Ivanic-Ruedenberg, test-locked math).
- Benchmarks: every number with its reproduce command.
- API reference: every public function, autodoc'd.