Coordinate Transformations¶
Overview¶
Coordinate transformations are fundamental to robotics, enabling representation of positions and orientations in different reference frames.
Rotation Representations¶
Rotation Matrix (3x3)¶
Orthogonal matrix \(R\) where \(R^T R = I\) and \(\det(R) = 1\)
Interactive visualization — Watch how a 2D rotation matrix rotates a vector:

Properties: - 9 parameters, 6 constraints → 3 DOF - Intuitive for composition - Memory intensive for transmission
Rotation Matrix Construction¶
2D Rotation¶
Mathematical Formulation:
A 2D rotation matrix \(R_z(\theta)\) rotates a vector in the XY plane by angle \(\theta\):
Code Implementation:
import numpy as np
def rot2d(theta: float) -> np.ndarray:
"""
Construct a 2D rotation matrix for rotation by angle theta.
Args:
theta: Rotation angle in radians
Returns:
2x2 rotation matrix
"""
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, -s],
[s, c]])
Code Breakdown: - Line 9-10: Compute cosine and sine of theta once for efficiency - Line 11-12: Construct the 2x2 rotation matrix using the standard formula
Usage Example:
# Rotate a vector by 45 degrees
theta = np.pi / 4
R = rot2d(theta)
# Apply rotation to vector [1, 0]
v = np.array([1.0, 0.0])
v_rotated = R @ v
print(f"Rotated vector: {v_rotated}") # [0.707, 0.707]
3D Rotation Matrices¶
Mathematical Formulation:
3D rotations about principal axes:
Code Implementation:
def rot_x(theta: float) -> np.ndarray:
"""
Construct 3D rotation matrix about the X-axis.
Args:
theta: Rotation angle in radians (right-hand rule)
Returns:
3x3 rotation matrix
"""
c, s = np.cos(theta), np.sin(theta)
return np.array([[1, 0, 0],
[0, c, -s],
[0, s, c]])
def rot_y(theta: float) -> np.ndarray:
"""
Construct 3D rotation matrix about the Y-axis.
Args:
theta: Rotation angle in radians (right-hand rule)
Returns:
3x3 rotation matrix
"""
c, s = np.cos(theta), np.sin(theta)
return np.array([[ c, 0, s],
[ 0, 1, 0],
[-s, 0, c]])
def rot_z(theta: float) -> np.ndarray:
"""
Construct 3D rotation matrix about the Z-axis.
Args:
theta: Rotation angle in radians (right-hand rule)
Returns:
3x3 rotation matrix
"""
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, -s, 0],
[s, c, 0],
[0, 0, 1]])
Code Breakdown:
- Each function follows the same pattern: compute cos/sin once
- rot_x: Rotation in the YZ plane (leaves X unchanged)
- rot_y: Rotation in the XZ plane (leaves Y unchanged)
- rot_z: Rotation in the XY plane (leaves Z unchanged)
- Right-hand rule: positive rotation is counterclockwise when looking along the positive axis
Usage Example:
# Compose rotations: rotate 90° about X, then 45° about Z
R_x_90 = rot_x(np.pi / 2)
R_z_45 = rot_z(np.pi / 4)
R_combined = R_z_45 @ R_x_90 # Matrix multiplication (right-to-left)
Euler Angles¶
Sequential rotations about principal axes.
ZYZ Convention¶
Mathematical Formulation:
The ZYZ Euler angles representation composes three rotations:
Expanding the full matrix:
Gimbal Lock Condition:
When \(\beta = \pm 90°\) (i.e., \(\beta = \pm \frac{\pi}{2}\)), the first and last rotations become about the same axis, losing one degree of freedom. This is called gimbal lock.
Code Implementation:
import numpy as np
def euler_to_rotation(alpha: float, beta: float, gamma: float) -> np.ndarray:
"""
Convert ZYZ Euler angles to rotation matrix.
Args:
alpha: First rotation about Z-axis (radians)
beta: Rotation about Y-axis (radians)
gamma: Final rotation about Z-axis (radians)
Returns:
3x3 rotation matrix R = R_z(alpha) @ R_y(beta) @ R_z(gamma)
Warning:
Gimbal lock occurs when beta = +-pi/2, causing numerical instability.
"""
# Check for gimbal lock condition
if np.isclose(abs(beta), np.pi / 2):
print("Warning: Gimbal lock! alpha and gamma become coupled.")
R_z_alpha = rot_z(alpha)
R_y_beta = rot_y(beta)
R_z_gamma = rot_z(gamma)
# Composition: right-to-left application
return R_z_alpha @ R_y_beta @ R_z_gamma
def rotation_to_euler(R: np.ndarray) -> tuple[float, float, float]:
"""
Extract ZYZ Euler angles from a rotation matrix.
Args:
R: 3x3 rotation matrix
Returns:
Tuple (alpha, beta, gamma) in radians
Raises:
ValueError: If gimbal lock is detected (cos(beta) ≈ 0)
"""
# Check gimbal lock: when cos(beta) ≈ 0
if np.isclose(R[2, 0], 0) and np.isclose(R[0, 2], 0):
raise ValueError("Gimbal lock detected: beta = +-90°. "
"alpha and gamma are not uniquely determined.")
# Extract angles from rotation matrix
beta = np.arctan2(np.sqrt(R[0, 2]**2 + R[1, 2]**2), R[2, 2])
alpha = np.arctan2(R[1, 2], R[0, 2])
gamma = np.arctan2(R[2, 1], -R[2, 0])
return alpha, beta, gamma
Code Breakdown: - Lines 17-18: Gimbal lock check warns when beta approaches ±90° - Line 21-23: Individual rotation matrices for each Euler angle - Line 26: Composition order follows the right-to-left convention (gamma → beta → alpha) - Line 43: Gimbal lock detection based on matrix structure - Lines 47-50: Angle extraction using atan2 for numerical stability
Usage Example:
# Convert Euler angles to rotation matrix
alpha, beta, gamma = np.pi/4, np.pi/3, np.pi/6
R = euler_to_rotation(alpha, beta, gamma)
# Round-trip conversion
alpha2, beta2, gamma2 = rotation_to_euler(R)
print(f"Original: {(alpha, beta, gamma)}")
print(f"Recovered: {(alpha2, beta2, gamma2)}")
Axis-Angle (Exponential Coordinates)¶
Mathematical Formulation:
Any rotation can be represented as a rotation of angle \(\theta\) about axis \(\omega\):
Where \([\omega]_\times\) is the skew-symmetric matrix:
The exponential map (Rodrigues' formula):
Code Implementation:
import numpy as np
def skew_symmetric(omega: np.ndarray) -> np.ndarray:
"""
Convert 3D rotation axis vector to skew-symmetric matrix.
Args:
omega: Rotation axis vector [omega_x, omega_y, omega_z] (unit or scaled)
Returns:
3x3 skew-symmetric matrix [omega]_x
The skew-symmetric matrix satisfies: [omega]_x @ v = omega × v
"""
wx, wy, wz = omega
return np.array([[ 0, -wz, wy],
[ wz, 0, -wx],
[-wy, wx, 0]])
def exp_map_axis_angle(omega: np.ndarray, theta: float) -> np.ndarray:
"""
Exponential map: convert axis-angle to rotation matrix.
Implements Rodrigues' rotation formula.
Args:
omega: Rotation axis (unit vector for pure rotation)
theta: Rotation angle in radians
Returns:
3x3 rotation matrix R = exp([omega]_x * theta)
Note:
If omega is not a unit vector, theta should be 0 (omega contains angle).
"""
omega = np.asarray(omega).flatten()
omega_hat = omega / np.linalg.norm(omega) # Ensure unit axis
# Compute skew-symmetric matrix
omega_x = skew_symmetric(omega_hat)
# Rodrigues' formula: R = I + sin(θ)K + (1-cos(θ))K²
R = (np.eye(3)
+ np.sin(theta) * omega_x
+ (1 - np.cos(theta)) * omega_x @ omega_x)
return R
def log_map_axis_angle(R: np.ndarray) -> tuple[np.ndarray, float]:
"""
Logarithmic map: convert rotation matrix to axis-angle.
Args:
R: 3x3 rotation matrix
Returns:
Tuple of (omega, theta) where omega is the unit rotation axis
and theta is the rotation angle in radians
Note:
Returns (omega, 0) for identity rotation (theta = 0).
"""
# Ensure R is a valid rotation matrix
R = np.asarray(R)
# Compute theta from trace: trace(R) = 1 + 2*cos(theta)
cos_theta = (np.trace(R) - 1) / 2
# Clamp to handle numerical errors
cos_theta = np.clip(cos_theta, -1.0, 1.0)
theta = np.arccos(cos_theta)
# Handle identity rotation (theta ≈ 0)
if np.isclose(theta, 0):
return np.array([1.0, 0.0, 0.0]), 0.0
# Extract rotation axis from skew-symmetric part
# R - R^T = 2*sin(theta)*[omega]_x
omega_x = (R - R.T) / (2 * np.sin(theta))
# Reconstruct omega from skew-symmetric matrix
omega = np.array([-omega_x[1, 2], omega_x[0, 2], -omega_x[0, 1]])
return omega / np.linalg.norm(omega), theta
Code Breakdown: - Lines 17-20: Skew-symmetric matrix construction from 3D vector - Line 30-33: Rodrigues' formula computes rotation matrix from axis-angle - Line 44-48: Normalizes omega to unit vector for consistent behavior - Lines 67-72: Extracts theta from matrix trace: \(\theta = \arccos(\frac{trace(R)-1}{2})\) - Lines 75-78: Handles the identity rotation case to avoid division by zero - Lines 83-86: Reconstructs rotation axis from the skew-symmetric component
Usage Example:
# Create rotation: 90° about Z-axis
omega = np.array([0, 0, 1])
theta = np.pi / 2
R = exp_map_axis_angle(omega, theta)
# Recover axis-angle from rotation matrix
omega_rec, theta_rec = log_map_axis_angle(R)
print(f"Axis: {omega_rec}, Angle: {theta_rec:.4f} rad ({np.degrees(theta_rec):.1f}°)")
Quaternions¶
Mathematical Formulation:
A unit quaternion represents 3D rotation using 4 parameters:
Where \(\omega = [\omega_x, \omega_y, \omega_z]\) is the unit rotation axis and \(\theta\) is the rotation angle.
Quaternion Multiplication:
Code Implementation:
import numpy as np
class Quaternion:
"""
Unit quaternion for representing 3D rotations.
Quaternion q = [w, x, y, z] where:
- w is the scalar (real) part
- [x, y, z] is the vector (imaginary) part
"""
def __init__(self, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0):
"""
Initialize quaternion.
Args:
w: Scalar component (cos(theta/2))
x, y, z: Vector components (sin(theta/2) * axis)
"""
self.w, self.x, self.y, self.z = w, x, y, z
@classmethod
def from_axis_angle(cls, omega: np.ndarray, theta: float) -> 'Quaternion':
"""
Create quaternion from axis-angle representation.
Args:
omega: Unit rotation axis [wx, wy, wz]
theta: Rotation angle in radians
Returns:
Quaternion instance
"""
omega = np.asarray(omega) / np.linalg.norm(omega)
half_angle = theta / 2
return cls(np.cos(half_angle),
*(np.sin(half_angle) * omega))
@classmethod
def from_rotation_matrix(cls, R: np.ndarray) -> 'Quaternion':
"""
Convert rotation matrix to quaternion.
Uses Shepperd's method for numerical stability.
"""
R = np.asarray(R)
trace = np.trace(R)
if trace > 0:
s = np.sqrt(trace + 1.0) * 2 # s = 4*w
w = 0.25 * s
x = (R[2, 1] - R[1, 2]) / s
y = (R[0, 2] - R[2, 0]) / s
z = (R[1, 0] - R[0, 1]) / s
elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
s = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2
w = (R[2, 1] - R[1, 2]) / s
x = 0.25 * s
y = (R[0, 1] + R[1, 0]) / s
z = (R[0, 2] + R[2, 0]) / s
elif R[1, 1] > R[2, 2]:
s = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2
w = (R[0, 2] - R[2, 0]) / s
x = (R[0, 1] + R[1, 0]) / s
y = 0.25 * s
z = (R[1, 2] + R[2, 1]) / s
else:
s = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2
w = (R[1, 0] - R[0, 1]) / s
x = (R[0, 2] + R[2, 0]) / s
y = (R[1, 2] + R[2, 1]) / s
z = 0.25 * s
return cls(w, x, y, z)
def norm(self) -> float:
"""Return quaternion norm (should be 1 for unit quaternions)."""
return np.sqrt(self.w**2 + self.x**2 + self.y**2 + self.z**2)
def conjugate(self) -> 'Quaternion':
"""Return quaternion conjugate (negate vector part)."""
return Quaternion(self.w, -self.x, -self.y, -self.z)
def __mul__(self, other: 'Quaternion') -> 'Quaternion':
"""
Quaternion multiplication: self ⊗ other.
Returns new quaternion representing composition of rotations.
"""
if isinstance(other, Quaternion):
return Quaternion(
self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z,
self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y,
self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x,
self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w
)
raise TypeError("Can only multiply Quaternion by Quaternion")
def to_rotation_matrix(self) -> np.ndarray:
"""Convert quaternion to rotation matrix."""
w, x, y, z = self.w, self.x, self.y, self.z
return np.array([
[1 - 2*(y*y + z*z), 2*(x*y - z*w), 2*(x*z + y*w)],
[ 2*(x*y + z*w), 1 - 2*(x*x + z*z), 2*(y*z - x*w)],
[ 2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x*x + y*y)]
])
@staticmethod
def slerp(q1: 'Quaternion', q2: 'Quaternion', t: float) -> 'Quaternion':
"""
Spherical linear interpolation between two quaternions.
Args:
q1: Start quaternion
q2: End quaternion
t: Interpolation parameter in [0, 1]
Returns:
Interpolated quaternion
Note:
Takes shortest path (handles q1 ≈ -q2 case).
"""
# Ensure shortest path interpolation
q2_adjusted = q2
dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z
if dot < 0:
q2_adjusted = Quaternion(-q2.w, -q2.x, -q2.y, -q2.z)
dot = -dot
# Handle near-identity case with linear interpolation
if dot > 0.9995:
result = Quaternion(
q1.w + t * (q2_adjusted.w - q1.w),
q1.x + t * (q2_adjusted.x - q1.x),
q1.y + t * (q2_adjusted.y - q1.y),
q1.z + t * (q2_adjusted.z - q1.z)
)
return result
# Spherical interpolation
theta_0 = np.arccos(dot)
theta = theta_0 * t
sin_theta = np.sin(theta)
sin_theta_0 = np.sin(theta_0)
s0 = np.cos(theta) - dot * sin_theta / sin_theta_0
s1 = sin_theta / sin_theta_0
return Quaternion(
s0 * q1.w + s1 * q2_adjusted.w,
s0 * q1.x + s1 * q2_adjusted.x,
s0 * q1.y + s1 * q2_adjusted.y,
s0 * q1.z + s1 * q2_adjusted.z
)
def __repr__(self) -> str:
return f"Quaternion(w={self.w:.4f}, x={self.x:.4f}, y={self.y:.4f}, z={self.z:.4f})"
Code Breakdown:
- Lines 30-35: from_axis_angle creates quaternion from rotation axis and angle
- Lines 37-61: from_rotation_matrix handles all 4 cases for numerical stability
- Lines 73-76: Conjugate reverses the rotation (useful for inverse)
- Lines 78-88: Quaternion multiplication implements the quaternion product formula
- Lines 103-132: slerp computes spherical interpolation; handles antipodal case (dot < 0)
- Lines 114-120: Near-identity fallback prevents numerical instability at θ ≈ 0
Visualization — SLERP interpolation animation:

Usage Example:
# Create quaternions for 90° rotations about different axes
q1 = Quaternion.from_axis_angle([0, 0, 1], np.pi / 2) # 90° about Z
q2 = Quaternion.from_axis_angle([0, 1, 0], np.pi / 2) # 90° about Y
# Compose rotations
q_combined = q1 * q2
# Interpolate between two orientations
trajectory = [Quaternion.slerp(q1, q2, t) for t in np.linspace(0, 1, 10)]
# Convert to rotation matrix
R = q_combined.to_rotation_matrix()
Homogeneous Transformations¶
Combining rotation and translation into a single 4x4 matrix.
Mathematical Formulation:
The homogeneous transformation matrix combines rotation \(R\) and translation \(d\):
Inverse Transformation:
Point Transformation:
Given point \(p_B\) in frame B, its coordinates in frame A are:
Or in homogeneous form:
Animation — See how rotation and translation combine:

Homogeneous Transform Class¶
Code Implementation:
import numpy as np
class HomogeneousTransform:
"""
4x4 homogeneous transformation matrix combining rotation and translation.
Represents transformation from frame B to frame A: T_AB
- R: 3x3 rotation matrix (orientation of B in A)
- d: 3x1 translation vector (origin of B in A)
"""
def __init__(self, R: np.ndarray = None, d: np.ndarray = None):
"""
Initialize homogeneous transformation.
Args:
R: 3x3 rotation matrix (defaults to identity)
d: 3x1 translation vector (defaults to zero)
"""
if R is None:
R = np.eye(3)
if d is None:
d = np.zeros(3)
self.R = np.asarray(R)
self.d = np.asarray(d).reshape(3)
# Construct 4x4 matrix
self._matrix = np.eye(4)
self._matrix[:3, :3] = self.R
self._matrix[:3, 3] = self.d
@classmethod
def from_matrix(cls, T: np.ndarray) -> 'HomogeneousTransform':
"""Create from 4x4 homogeneous transformation matrix."""
T = np.asarray(T)
R = T[:3, :3]
d = T[:3, 3]
return cls(R, d)
@classmethod
def from_quaternion(cls, q: 'Quaternion', d: np.ndarray) -> 'HomogeneousTransform':
"""Create from quaternion and translation vector."""
return cls(q.to_rotation_matrix(), d)
@property
def matrix(self) -> np.ndarray:
"""Return 4x4 transformation matrix."""
return self._matrix.copy()
def inverse(self) -> 'HomogeneousTransform':
"""
Compute inverse transformation T_BA = T_AB^(-1).
Uses formula: T^(-1) = [[R^T, -R^T*d], [0, 1]]
"""
R_inv = self.R.T
d_inv = -R_inv @ self.d
return HomogeneousTransform(R_inv, d_inv)
def compose(self, other: 'HomogeneousTransform') -> 'HomogeneousTransform':
"""
Compose transformations: T_AC = T_AB @ T_BC.
Args:
other: T_BC - transformation from C to B
Returns:
T_AC - transformation from C to A
"""
# T_AC = T_AB @ T_BC
R_new = self.R @ other.R
d_new = self.R @ other.d + self.d
return HomogeneousTransform(R_new, d_new)
def transform_point(self, p: np.ndarray) -> np.ndarray:
"""
Transform a point from frame B to frame A.
Args:
p: 3D point in frame B [x, y, z]
Returns:
Point coordinates in frame A
"""
p = np.asarray(p).reshape(3)
return self.R @ p + self.d
def transform_vector(self, v: np.ndarray) -> np.ndarray:
"""
Transform a vector (direction) from frame B to frame A.
Vectors are not affected by translation.
Args:
v: 3D vector in frame B [x, y, z]
Returns:
Vector in frame A
"""
v = np.asarray(v).reshape(3)
return self.R @ v
def __matmul__(self, other: 'HomogeneousTransform') -> 'HomogeneousTransform':
"""Matrix multiplication using @ operator."""
return self.compose(other)
def __repr__(self) -> str:
return f"HomogeneousTransform(R={self.R.tolist()}, d={self.d.tolist()})"
Code Breakdown:
- Lines 23-28: Constructor stores R and d, builds 4x4 matrix
- Lines 46-49: inverse() applies the transpose formula: \(T^{-1} = [[R^T, -R^Td], [0, 1]]\)
- Lines 52-58: compose() implements transformation chaining: \(T_{AC} = T_{AB} \cdot T_{BC}\)
- Lines 60-66: transform_point() applies rotation then translation
- Lines 68-75: transform_vector() applies only rotation (vectors have no origin)
- Line 80: Enables using @ operator: T1 @ T2 = composition
Usage Example:
# Create transformation: rotate 90° about Z, translate [1, 2, 0]
R = rot_z(np.pi / 2)
d = np.array([1.0, 2.0, 0.0])
T_AB = HomogeneousTransform(R, d)
# Transform a point from frame B to frame A
p_B = np.array([1.0, 0.0, 0.0]) # Point on X-axis of frame B
p_A = T_AB.transform_point(p_B)
print(f"Point in A: {p_A}") # [1, 3, 0] (rotated then translated)
# Compose transformations
T_BC = HomogeneousTransform(rot_y(np.pi/4), np.array([0, 0, 1]))
T_AC = T_AB @ T_BC # Equivalent to T_AB.compose(T_BC)
# Inverse transformation: get frame A coordinates in frame B
T_BA = T_AB.inverse()
p_B_recovered = T_BA.transform_point(p_A)