Skip to content

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.

$ pip install splatreg
0.962
BUFFER-X zero-shot seed recall, official 3DMatch (8/8 scenes, n=1619)
5.2°
rotation error on a real splat, vs 15.3° / 36.3° for splat tools
native
Sim(3) scale recovery — no other splat registrar does it
~2.4e-15
SH-under-rotation error (float64), test-locked

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

BUFFER-X zero-shot seed vs classical FPFH seed: registration recall on 3DMatch and the low-overlap 3DLoMatch regime
Registration recall for the zero-shot BUFFER-X seed (ICCV 2025) against the classical robust FPFH seed, both pushed through the identical splatreg refine so the bars isolate the seed. 3DMatch is the complete official 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 align bakes 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 (None when singular, never faked), and ambiguous overlaps are flagged — never silently wrong-posed.

A view-dependent-coloured Gaussian sphere rotated 90 degrees three ways and rendered by gsplat: naive rotation (wrong colour), splatreg Wigner-D (correct), and an independent ground truth
Provably correct SH rotation, rendered through gsplat. A view-dependent-coloured splat rotated 90°: the naive rotation leaves the higher-order SH in the old, world-fixed frame (13–15 dB vs an independent ground truth), while splatreg's real-basis Wigner-D render is pixel-identical to it. Coefficient round-trip 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")
Three-stage animation of merging two real overlapping 3DMatch scans: misaligned, registered by SE(3), then fused with the overlap deduped
The 3-line 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

splatreg pipeline: two 3DGS splats, six coarse-init seeds (fast/robust/learned/bufferx/mac/global), a multi-residual Levenberg-Marquardt core (ICP + Gaussian-SDF, SE(3)/Sim(3)), outputs of the transform plus recovered scale and pose covariance, feeding the merge / align / track / pose-graph consumers
Two splats → one of six coarse-init seeds → the multi-residual Levenberg–Marquardt core (ICP + the flagship Gaussian-SDF, SE(3)/Sim(3)) → the transform, recovered scale, and pose covariance → the merge / align / track / pose-graph consumers.
  1. 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.
  2. 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.

Photometric refinement converging: a colour splat knocked 9 degrees out of alignment locks onto the target through the gsplat rasterizer, with rotation and translation error ticking down to zero
The opt-in photometric refine for the pose geometry can't see. A colour splat knocked 9° / 151 mm out of alignment is polished by the PhotoReg-style splat-vs-splat photometric LM (renders source vs target through gsplat, no real images) down to 0.04° / 0.04 mm — a real per-iteration trajectory. See Photometric refinement.

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. merge and track are 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 / info from the shell.
  • Init modes: speed vs robustness — fast, robust, learned, the zero-shot bufferx seed, 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.