"""SE(3) helpers: compose, invert, conversions rvec/quat/euler/matrix.""" from __future__ import annotations import numpy as np def rvec_to_R(rvec: np.ndarray) -> np.ndarray: import cv2 R, _ = cv2.Rodrigues(np.asarray(rvec, dtype=np.float64).reshape(3, 1)) return R def R_to_rvec(R: np.ndarray) -> np.ndarray: import cv2 rvec, _ = cv2.Rodrigues(np.asarray(R, dtype=np.float64)) return rvec.flatten() def quat_xyzw_to_R(q: np.ndarray) -> np.ndarray: x, y, z, w = [float(v) for v in q] n = x * x + y * y + z * z + w * w if n == 0.0: return np.eye(3) s = 2.0 / n return np.array([ [1 - s * (y * y + z * z), s * (x * y - z * w), s * (x * z + y * w)], [s * (x * y + z * w), 1 - s * (x * x + z * z), s * (y * z - x * w)], [s * (x * z - y * w), s * (y * z + x * w), 1 - s * (x * x + y * y)], ]) def R_to_quat_xyzw(R: np.ndarray) -> np.ndarray: m = np.asarray(R, dtype=np.float64) t = m[0, 0] + m[1, 1] + m[2, 2] if t > 0: s = np.sqrt(t + 1.0) * 2 w = 0.25 * s x = (m[2, 1] - m[1, 2]) / s y = (m[0, 2] - m[2, 0]) / s z = (m[1, 0] - m[0, 1]) / s elif m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]: s = np.sqrt(1.0 + m[0, 0] - m[1, 1] - m[2, 2]) * 2 w = (m[2, 1] - m[1, 2]) / s x = 0.25 * s y = (m[0, 1] + m[1, 0]) / s z = (m[0, 2] + m[2, 0]) / s elif m[1, 1] > m[2, 2]: s = np.sqrt(1.0 + m[1, 1] - m[0, 0] - m[2, 2]) * 2 w = (m[0, 2] - m[2, 0]) / s x = (m[0, 1] + m[1, 0]) / s y = 0.25 * s z = (m[1, 2] + m[2, 1]) / s else: s = np.sqrt(1.0 + m[2, 2] - m[0, 0] - m[1, 1]) * 2 w = (m[1, 0] - m[0, 1]) / s x = (m[0, 2] + m[2, 0]) / s y = (m[1, 2] + m[2, 1]) / s z = 0.25 * s return np.array([x, y, z, w]) def make_T(R: np.ndarray, t: np.ndarray) -> np.ndarray: T = np.eye(4) T[:3, :3] = R T[:3, 3] = np.asarray(t, dtype=np.float64).flatten() return T def invert_T(T: np.ndarray) -> np.ndarray: R = T[:3, :3] t = T[:3, 3] Ti = np.eye(4) Ti[:3, :3] = R.T Ti[:3, 3] = -R.T @ t return Ti def T_from_pos_quat(pos: np.ndarray, quat_xyzw: np.ndarray) -> np.ndarray: return make_T(quat_xyzw_to_R(np.asarray(quat_xyzw)), np.asarray(pos)) def pos_quat_from_T(T: np.ndarray) -> tuple[np.ndarray, np.ndarray]: return T[:3, 3].copy(), R_to_quat_xyzw(T[:3, :3]) def T_from_rvec_tvec(rvec: np.ndarray, tvec: np.ndarray) -> np.ndarray: return make_T(rvec_to_R(rvec), np.asarray(tvec).flatten()) def R_to_euler_xyz(R: np.ndarray) -> np.ndarray: """Extrinsic XYZ euler (roll, pitch, yaw) in radians.""" sy = np.sqrt(R[0, 0] ** 2 + R[1, 0] ** 2) singular = sy < 1e-6 if not singular: x = np.arctan2(R[2, 1], R[2, 2]) y = np.arctan2(-R[2, 0], sy) z = np.arctan2(R[1, 0], R[0, 0]) else: x = np.arctan2(-R[1, 2], R[1, 1]) y = np.arctan2(-R[2, 0], sy) z = 0.0 return np.array([x, y, z]) def transform_points(T: np.ndarray, pts: np.ndarray) -> np.ndarray: """Apply T (4x4) to Nx3 points.""" pts = np.asarray(pts, dtype=np.float64) R = T[:3, :3] t = T[:3, 3] return pts @ R.T + t