Skip to content

Aligning two scans

Registering two overlapping point clouds is a two-stage problem: a coarse alignment when correspondences are already known (or can be guessed), and a refinement that alternates finding nearest-point correspondences with re-solving for the best rigid transform. registration.procrustes does the first, and registration.icp does the second, taking the first's result as its starting point.

import numpy as np
import warp as wp

import triwarp as tw

vertices, faces = tw.creation.icosphere(subdivisions=3)
points_a, _ = tw.sample.sample_surface(vertices, faces, 2_000, seed=1)

# Simulate a second, misaligned scan of the same surface: rotate, translate, and jitter it.
rng = np.random.default_rng(0)
theta = 0.4
rotation = np.array(
    [[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]],
    dtype=np.float32,
)
points_b_np = points_a.numpy() @ rotation.T + np.array([0.3, -0.1, 0.05], dtype=np.float32)
points_b_np += rng.normal(scale=0.01, size=points_b_np.shape).astype(np.float32)
points_b = wp.array(points_b_np, dtype=wp.vec3, device=vertices.device)

# Coarse alignment: procrustes needs points_b and points_a in correspondence already (same
# sampling, here), and solves for the similarity transform that best maps one onto the other.
transforms, aligned, cost = tw.registration.procrustes(points_b, points_a)
print("procrustes residual:", cost)  # ~3e-4 (the jitter's own variance)

# Refinement against the original surface: icp re-derives correspondences by nearest point on
# the target mesh every iteration, starting from procrustes's transform.
transforms, aligned, rmse = tw.registration.icp(
    points_b, vertices, faces, initial=transforms
)
print("icp rmse:", rmse)  # ~1e-4 -- a real, if small, improvement over the coarse fit

When there's no known correspondence

The example above cheats a little: points_a and points_b are already index-aligned (points_b is a transform of points_a), so procrustes can solve directly. A real pair of independent scans has no such correspondence, and procrustes alone isn't the right tool — it needs matched pairs. In that case, skip straight to registration.icp with a rough initial guess (identity, or a coarse alignment from any prior knowledge of the scan setup); each iteration finds its own nearest-point correspondences and only needs the previous iteration's transform to be in the right neighbourhood, not exact.

Point-to-plane for faster convergence on flat regions

registration.icp_point_to_plane minimizes the point-to-plane distance (using the target's normals) rather than point-to-point, which converges faster on largely flat surfaces — the same trade Open3D's TransformationEstimationPointToPlane makes over PointToPoint. It takes the same arguments as icp plus the target's normals.