- Background Knowledge
Procrustes analysis (in this rigid case) is about finding the best-fit rigid transform (rotation + translation, no scaling or shearing) that aligns one set of points to another in a least squares sense. You are given corresponding points {pi} and {qi}, and you want the rotation matrix R (orthonormal, RTR=I, det(R)=1) and translation vector t that minimize
i∑∥Rpi+t−qi∥2.
This is a classic problem in computer vision and shape analysis, used in tasks like shape matching, pose estimation, and image alignment.
The key idea is to decouple translation from rotation: if you subtract the centroid from each point set, you remove translation, leaving only rotation to be estimated. The optimal rotation between two centered point sets can be found via the Singular Value Decomposition (SVD) of a covariance (cross-correlation) matrix built from the two sets. This gives a closed-form, globally optimal solution (for the least squares criterion).
- Algorithm / Approach
The general pattern:
- Step 1: Center each point set by subtracting its centroid.
- Step 2: Compute covariance between centered source and target points.
- Step 3: Use SVD of this covariance to extract the optimal rotation.
- Step 4: Recover translation from the centroids and the found rotation.
- Optionally, enforce proper rotation (det(R)=1) to avoid reflections.
This is a one-shot, non-iterative algorithm: no gradient descent needed, just linear algebra (means + SVD).
- Step-by-Step Strategy
Assume you have N corresponding points in d dimensions:
- Source: P \in \mathbb{R}^{N \times d}(rowsarep_i^T$)
- Target: Q∈RN×d (rows are qiT)
- Compute centroids
mu_P = P.mean(axis=0) # shape (d,)
mu_Q = Q.mean(axis=0) # shape (d,)
- Center the point sets
P_centered = P - mu_P
Q_centered = Q - mu_Q
- Compute covariance matrix
- Typically: H = P_{\text{centered}}^T Q_{\text{centered}} (shape d×d).
H = P_centered.T @ Q_centered
- SVD of covariance
- Compute H=USVT.
U, S, Vt = np.linalg.svd(H)
- Compute rotation
- Naively: R=VUT (or R=Vt.T@U.T in code).
- Then optionally fix for improper rotation (reflection) if det(R)<0.
R = Vt.T @ U.T
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
- Compute translation
- Using centroids: t=μQ−RμP.
t = mu_Q - R @ mu_P
- Transform source points (if needed)
P_aligned = (R @ P.T).T + t # shape (N, d)
- Common Pitfalls
- Mismatched correspondence: The method assumes point pi corresponds to qi. Wrong ordering = wrong alignment.
- Shape and orientation of matrices:
- Be consistent whether points are rows or columns.
- Make sure H is d×d, not N×N.
- Reflections:
- SVD can produce a rotation with det(R)=−1 (a reflection). For a pure rigid transform, enforce det(R)=1 by flipping the sign of the last singular vector.
- Degenerate cases:
- If all points are collinear or identical, H becomes rank-deficient and the rotation may not be unique.
- Numerical precision:
- For very large or very small coordinate values, centering and SVD can suffer from numeric issues; using double precision and standard libraries usually suffices.
- Time & Space Complexity
Let N be the number of points and d the dimension (usually d=2 or 3).
-
Time complexity
-
Computing centroids: O(Nd)
-
Centering: O(Nd)
-
Covariance matrix H: O(Nd2)
-
SVD on d×d matrix: O(d3)
-
Overall: O(Nd2), which is linear in N for fixed d.
-
Space complexity
-
Storing P,Q: O(Nd)
-
Centered copies and H: also O(Nd) + O(d2)
-
Overall: O(Nd) extra space (or in-place with some care).