feat(aruco): implement core alignment utilities for ground plane alignment
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
- Alignment is applied via pre-multiplication to the 4x4 pose matrix, consistent with global frame rotation.
|
||||
- Chose to raise ValueError for degenerate cases (collinear corners) in compute_face_normal.
|
||||
@@ -0,0 +1,2 @@
|
||||
- Implemented core alignment utilities in aruco/alignment.py.
|
||||
- Used Rodrigues' rotation formula for vector alignment with explicit handling for parallel and anti-parallel cases.
|
||||
@@ -0,0 +1,99 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def compute_face_normal(corners: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Compute the normal vector of a face defined by its corners.
|
||||
Assumes corners are in order (e.g., clockwise or counter-clockwise).
|
||||
|
||||
Args:
|
||||
corners: (N, 3) array of corner coordinates.
|
||||
|
||||
Returns:
|
||||
(3,) normalized normal vector.
|
||||
"""
|
||||
if corners.shape[0] < 3:
|
||||
raise ValueError("At least 3 corners are required to compute a normal.")
|
||||
|
||||
# Use the cross product of two edges
|
||||
v1 = corners[1] - corners[0]
|
||||
v2 = corners[2] - corners[0]
|
||||
|
||||
normal = np.cross(v1, v2)
|
||||
norm = np.linalg.norm(normal)
|
||||
|
||||
if norm < 1e-10:
|
||||
raise ValueError("Corners are collinear or degenerate; cannot compute normal.")
|
||||
|
||||
return normal / norm
|
||||
|
||||
|
||||
def rotation_align_vectors(from_vec: np.ndarray, to_vec: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Compute the 3x3 rotation matrix that aligns from_vec to to_vec.
|
||||
|
||||
Args:
|
||||
from_vec: (3,) source vector.
|
||||
to_vec: (3,) target vector.
|
||||
|
||||
Returns:
|
||||
(3, 3) rotation matrix.
|
||||
"""
|
||||
# Normalize inputs
|
||||
a = from_vec / np.linalg.norm(from_vec)
|
||||
b = to_vec / np.linalg.norm(to_vec)
|
||||
|
||||
v = np.cross(a, b)
|
||||
c = np.dot(a, b)
|
||||
s = np.linalg.norm(v)
|
||||
|
||||
# Handle parallel case
|
||||
if s < 1e-10:
|
||||
if c > 0:
|
||||
return np.eye(3)
|
||||
else:
|
||||
# Anti-parallel case: 180 degree rotation around an orthogonal axis
|
||||
# Find an orthogonal axis
|
||||
if abs(a[0]) < 0.9:
|
||||
ortho = np.array([1, 0, 0])
|
||||
else:
|
||||
ortho = np.array([0, 1, 0])
|
||||
|
||||
axis = np.cross(a, ortho)
|
||||
axis /= np.linalg.norm(axis)
|
||||
|
||||
# Rodrigues formula for 180 degrees
|
||||
K = np.array(
|
||||
[[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]]
|
||||
)
|
||||
return np.eye(3) + 2 * (K @ K)
|
||||
|
||||
# General case using Rodrigues' rotation formula
|
||||
# R = I + [v]_x + [v]_x^2 * (1-c)/s^2
|
||||
vx = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
|
||||
|
||||
R = np.eye(3) + vx + (vx @ vx) * ((1 - c) / (s**2))
|
||||
return R
|
||||
|
||||
|
||||
def apply_alignment_to_pose(T: np.ndarray, R_align: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Apply an alignment rotation to a 4x4 pose matrix.
|
||||
The alignment is applied in the global frame (pre-multiplication of rotation).
|
||||
|
||||
Args:
|
||||
T: (4, 4) homogeneous transformation matrix.
|
||||
R_align: (3, 3) alignment rotation matrix.
|
||||
|
||||
Returns:
|
||||
(4, 4) aligned transformation matrix.
|
||||
"""
|
||||
if T.shape != (4, 4):
|
||||
raise ValueError(f"Expected 4x4 matrix, got {T.shape}")
|
||||
if R_align.shape != (3, 3):
|
||||
raise ValueError(f"Expected 3x3 matrix, got {R_align.shape}")
|
||||
|
||||
T_align = np.eye(4)
|
||||
T_align[:3, :3] = R_align
|
||||
|
||||
return T_align @ T
|
||||
Reference in New Issue
Block a user