Skip to content

Coordinate systems

Robotics / math

Tait–Bryan

Tait–Bryan angles are a way to describe orientation in 3D space using three rotations about three different axes.

alt text

  • Roll (φ) → rotation about the X-axis
  • Pitch (θ) → rotation about the Y-axis
  • Yaw (ψ) → rotation about the Z-axis

Intrinsic and Extrinsic

  • Intrinsic: The rotation relative to current body axes , it's use in robotics and ROS
  • Extrinsic: The rotation always relative to the fixed world axes

Intrinsic

rotations are about the body axes. \(\(R_{\text{intrinsic}} = R_z(\text{yaw}) \cdot R_y(\text{pitch}) \cdot R_x(\text{roll})\)\)

code
import numpy as np
import matplotlib.pyplot as plt

# --- Rotation Matrices ---
def Rx(theta):
    return np.array([[1, 0, 0],
                     [0, np.cos(theta), -np.sin(theta)],
                     [0, np.sin(theta),  np.cos(theta)]])

def Ry(theta):
    return np.array([[np.cos(theta), 0, np.sin(theta)],
                     [0, 1, 0],
                     [-np.sin(theta), 0, np.cos(theta)]])

def Rz(theta):
    return np.array([[np.cos(theta), -np.sin(theta), 0],
                     [np.sin(theta),  np.cos(theta), 0],
                     [0, 0, 1]])

# --- Plot Function ---
def plot_axes(R, ax, title):
    origin = np.zeros(3)
    x_axis = R @ np.array([1, 0, 0])
    y_axis = R @ np.array([0, 1, 0])
    z_axis = R @ np.array([0, 0, 1])

    ax.quiver(*origin, *x_axis, color="r")
    ax.quiver(*origin, *y_axis, color="g")
    ax.quiver(*origin, *z_axis, color="b")

    ax.set_xlim([-1.5, 1.5])
    ax.set_ylim([-1.5, 1.5])
    ax.set_zlim([-1.5, 1.5])
    ax.set_title(title)

# --- Angles ---
roll = np.deg2rad(90)
pitch = np.deg2rad(45)
yaw = np.deg2rad(180)

# --- Intrinsic sequence ---
R0 = np.eye(3)
R1 = R0 @ Rx(roll)      # roll about body X
R2 = R1 @ Ry(pitch)     # then pitch about new Y
R3 = R2 @ Rz(yaw)       # then yaw about new Z

# --- Plot ---
fig = plt.figure(figsize=(10,10))
plot_axes(R0, fig.add_subplot(221, projection="3d"), "Step 1: Rest")
plot_axes(R1, fig.add_subplot(222, projection="3d"), "Step 2: Roll 90° (body X)")
plot_axes(R2, fig.add_subplot(223, projection="3d"), "Step 3: + Pitch 45° (body Y)")
plot_axes(R3, fig.add_subplot(224, projection="3d"), "Step 4: + Yaw 180° (body Z)")
plt.tight_layout()
plt.show()

alt text

Extrinsic

rotations are about the fixed world axes.

\[R_{\text{extrinsic}} = R_x(\text{roll}) \cdot R_y(\text{pitch}) \cdot R_z(\text{yaw})\]
code
import numpy as np
import matplotlib.pyplot as plt

# --- Rotation Matrices ---
def Rx(theta):
    return np.array([[1, 0, 0],
                     [0, np.cos(theta), -np.sin(theta)],
                     [0, np.sin(theta),  np.cos(theta)]])

def Ry(theta):
    return np.array([[np.cos(theta), 0, np.sin(theta)],
                     [0, 1, 0],
                     [-np.sin(theta), 0, np.cos(theta)]])

def Rz(theta):
    return np.array([[np.cos(theta), -np.sin(theta), 0],
                     [np.sin(theta),  np.cos(theta), 0],
                     [0, 0, 1]])

# --- Plot Function ---
def plot_axes(R, ax, title):
    origin = np.zeros(3)
    x_axis = R @ np.array([1, 0, 0])
    y_axis = R @ np.array([0, 1, 0])
    z_axis = R @ np.array([0, 0, 1])

    ax.quiver(*origin, *x_axis, color="r")
    ax.quiver(*origin, *y_axis, color="g")
    ax.quiver(*origin, *z_axis, color="b")

    ax.set_xlim([-1.5, 1.5])
    ax.set_ylim([-1.5, 1.5])
    ax.set_zlim([-1.5, 1.5])
    ax.set_title(title)

# --- Angles ---
roll = np.deg2rad(90)
pitch = np.deg2rad(45)
yaw = np.deg2rad(180)

# --- Extrinsic sequence ---
R0 = np.eye(3)
R1 = Rx(roll) @ R0      # roll about world X
R2 = Ry(pitch) @ R1     # then pitch about world Y
R3 = Rz(yaw) @ R2       # then yaw about world Z

# --- Plot ---
fig = plt.figure(figsize=(10,10))
plot_axes(R0, fig.add_subplot(221, projection="3d"), "Step 1: Rest")
plot_axes(R1, fig.add_subplot(222, projection="3d"), "Step 2: Roll 90° (world X)")
plot_axes(R2, fig.add_subplot(223, projection="3d"), "Step 3: + Pitch 45° (world Y)")
plot_axes(R3, fig.add_subplot(224, projection="3d"), "Step 4: + Yaw 180° (world Z)")
plt.tight_layout()
plt.show()

alt text


Rotate using scipy

  • Lowercase ("xyz") = intrinsic (body-fixed).
  • Uppercase ("XYZ") = extrinsic (world-fixed).
code
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.transform import Rotation as R

# --- Angles (degrees) ---
roll, pitch, yaw = 90, 45, 180

# --- Intrinsic rotation (body axes: roll -> pitch -> yaw) ---
rot_intrinsic = R.from_euler("zyx", [yaw, pitch, roll], degrees=True)
zyx_intrinsic = rot_intrinsic.as_matrix()

rot_intrinsic2 = R.from_euler("xyz", [roll, pitch, yaw], degrees=True)
xyz_intrinsic = rot_intrinsic2.as_matrix()

# --- Extrinsic rotation (world axes: roll -> pitch -> yaw) ---
rot_extrinsic = R.from_euler("ZYX", [yaw, pitch, roll], degrees=True)
ZYX_extrinsic = rot_extrinsic.as_matrix()

rot_extrinsic2 = R.from_euler("XYZ", [roll, pitch, yaw], degrees=True)
XYZ_extrinsic = rot_extrinsic2.as_matrix()

# --- Helper plot function ---
def plot_axes(R, ax, title):
    origin = np.zeros(3)
    x_axis = R @ np.array([1, 0, 0])
    y_axis = R @ np.array([0, 1, 0])
    z_axis = R @ np.array([0, 0, 1])

    ax.quiver(*origin, *x_axis, color="r", label="X")
    ax.quiver(*origin, *y_axis, color="g", label="Y")
    ax.quiver(*origin, *z_axis, color="b", label="Z")

    ax.set_xlim([-1.5, 1.5])
    ax.set_ylim([-1.5, 1.5])
    ax.set_zlim([-1.5, 1.5])
    ax.set_xlabel("X")
    ax.set_ylabel("Y")
    ax.set_zlabel("Z")
    ax.set_title(title)
    ax.legend()

# --- Plot both ---
fig = plt.figure(figsize=(12,6))

# Vector
R0 = np.eye(3)

# Rotate vector
v_intrinsic = zyx_intrinsic @ R0
v_extrinsic = ZYX_extrinsic @ R0

ax1 = fig.add_subplot(221, projection="3d")
plot_axes(v_intrinsic, ax1, "zyx (body frame)")
ax1.get_legend().remove() 

ax1 = fig.add_subplot(223, projection="3d")
plot_axes(xyz_intrinsic, ax1, "xyz")
ax1.get_legend().remove() 

ax2 = fig.add_subplot(222, projection="3d")
plot_axes(v_extrinsic, ax2, "ZYX")
ax2.get_legend().remove()

ax2 = fig.add_subplot(224, projection="3d")
plot_axes(XYZ_extrinsic, ax2, "XYZ ()")
ax2.get_legend().remove()
# plt.tight_layout()
plt.show()

# --- Print matrices ---
np.set_printoptions(precision=4, suppress=True)
print("Intrinsic Rotation Matrix:\n", zyx_intrinsic)
print("\nExtrinsic Rotation Matrix:\n", ZYX_extrinsic)

alt text

Intrinsic (body axes, lowercase) Equivalent Extrinsic (world axes, uppercase reversed)
"xyz" → roll-pitch-yaw (body x→y→z) "ZYX" → yaw-pitch-roll (world z→y→x)
"xzy" "YZX"
"yxz" "ZXY"
"yzx" "XZY"
"zxy" "YXZ"
"zyx" "XYZ"

Reference