Skip to article frontmatterSkip to article content

Rot3

A gtsam.Rot3 represents an orientation or attitude in 3D space. It can be manipulated and presented as a rotation matrix RR3×3 R \in \mathbb{R}^{3 \times 3} , a unit quaternion, roll-pitch-yaw (Euler) angles (ϕ,θ,ψ) (\phi, \theta, \psi) , or as an axis-angle representation (ω^,θ) (\hat{\omega}, \theta) with ω^R3 \hat{\omega} \in \mathbb{R}^3 and θR \theta \in \mathbb{R} . It models a 3D orientation as both a manifold in SO(3) \mathcal{SO}(3) and as a Lie group in SO(3) \text{SO}(3) . Internally, it is stored as a 3×3 3 \times 3 rotation matrix but can be configured to use quaternions at build time for efficiency.

Open In Colab

%pip install --quiet gtsam-develop
import gtsam
from gtsam import Rot3, Point3, Quaternion
import numpy as np

Initialization

A Rot3 can be initialized in many different ways, which are detailed in this section. Note that printing a Rot3 displays its 3x3 rotation matrix representation, which in general is a 3x3 matrix where the columns are unit vectors that define the orientation’s coordinate frame.

Constructor

The Rot3 constructor provides for initialization with no arguments, yielding the identity rotation (equivalent to I3I_3), initialization with a precalculated rotation matrix (either as a 3x3 np.ndarray, as three 3-vectors, or as 9 floats), and initialization with a quaternion’s w,x,y,zw, x, y, z.

# No-argument constructor
a = Rot3()
print(a)

# Construct from a rotation matrix
theta = np.pi / 2
b = Rot3(np.array([ # Rotate around X axis by PI / 2
    [1, 0, 0],
    [0, np.cos(theta), -np.sin(theta)],
    [0, np.sin(theta), np.cos(theta)]
]))
print(b)

# Construct from three column vectors
c = Rot3([11, 21, 31], [12, 22, 32], [13, 23, 33])
print(c)

# Construct from 9 floats
d = Rot3(1, 2, 3, 4, 5, 6, 7, 8, 9)
print(d)

# Construct from quaternion values
e = Rot3(0, 0, 0, 1) # Rotate around Z axis by pi
print(e)
R: [
	1, 0, 0;
	0, 1, 0;
	0, 0, 1
]

R: [
	1, 0, 0;
	0, 6.12323e-17, -1;
	0, 1, 6.12323e-17
]

R: [
	11, 12, 13;
	21, 22, 23;
	31, 32, 33
]

R: [
	1, 2, 3;
	4, 5, 6;
	7, 8, 9
]

R: [
	-1, 0, 0;
	0, -1, 0;
	0, 0, 1
]

Named constructors

In addition to its constructors, Rot3 has several named constructors, or factory functions, that allow instantiation from a wide variety of methods.

Rot3.Identity() returns the 3x3 rotation identity matrix.

print(Rot3.Identity())
R: [
	1, 0, 0;
	0, 1, 0;
	0, 0, 1
]

Rx, Ry, Rz, and RzRyRx create rotations around these axes.

# Rotation around X axis
x = Rot3.Rx(np.pi / 2)
print(x)

# Rotation around Y axis
y = Rot3.Ry(np.pi / 4)
print(y)

# Rotation around Z axis
z = Rot3.Rz(np.pi / 6)
print(z)

# Rotate around Z, then Y, then X axes
# Note that the order of angles in the function signature (x, y, z) is the
# reverse of the order in which the rotations are applied (z, y, x).
zyx = Rot3.RzRyRx(np.pi / 2, np.pi / 4, np.pi / 6)
# Rot3.RzRyRx is overloaded: it also accepts an array of [x, y, z].
# e.g. the above is identical to zyx = Rot3.RzRyRx([np.pi / 2, np.pi / 4, np.pi / 6]).
print(zyx)
# Of course, zyx is the same as z * y * x, since we fed the same angles to each.
print(z * y * x)
R: [
	1, 0, 0;
	0, 6.12323e-17, -1;
	0, 1, 6.12323e-17
]

R: [
	0.707107, 0, 0.707107;
	0, 1, 0;
	-0.707107, 0, 0.707107
]

R: [
	0.866025, -0.5, 0;
	0.5, 0.866025, 0;
	0, 0, 1
]

R: [
	0.612372, 0.612372, 0.5;
	0.353553, 0.353553, -0.866025;
	-0.707107, 0.707107, 4.32978e-17
]

R: [
	0.612372, 0.612372, 0.5;
	0.353553, 0.353553, -0.866025;
	-0.707107, 0.707107, 4.32978e-17
]

Similarly, Yaw, Pitch, Roll, and Ypr are available.

# Yaw around Z axis (positive yaw is to the right, as in aircraft heading)
y = Rot3.Yaw(np.pi / 6)
print(y)

# Pitch around Y axis (positive pitch is up, as in increasing aircraft altitude)
p = Rot3.Pitch(np.pi / 4)
print(p)

# Roll around X axis
r = Rot3.Roll(np.pi / 2)
print(r)

# Yaw, pitch, then roll
# Unlike RzRyRx, rotations are applied in the same order as supplied.
# Ypr is not overloaded to support an array.
ypr = Rot3.Ypr(np.pi / 6, np.pi / 4, np.pi / 2)
print(ypr)
R: [
	0.866025, -0.5, 0;
	0.5, 0.866025, 0;
	0, 0, 1
]

R: [
	0.707107, 0, 0.707107;
	0, 1, 0;
	-0.707107, 0, 0.707107
]

R: [
	1, 0, 0;
	0, 6.12323e-17, -1;
	0, 1, 6.12323e-17
]

R: [
	0.612372, 0.612372, 0.5;
	0.353553, 0.353553, -0.866025;
	-0.707107, 0.707107, 4.32978e-17
]

Rot3.Quaternion is identical to the four-argument Rot3 constructor.

# Create from quaternion w, x, y, z
q = Rot3.Quaternion(0, 0, 0, 1)
print(q)
print(q.equals(Rot3(0, 0, 0, 1), 1e-8))
R: [
	-1, 0, 0;
	0, -1, 0;
	0, 0, 1
]

True

Rot3.AxisAngle creates a Rot3 from an axis and an angle around that axis.

aa = Rot3.AxisAngle([0, 1, 0], np.pi / 2)
print(aa)
R: [
	2.22045e-16, 0, 1;
	0, 1, 0;
	-1, 0, 2.22045e-16
]

Rot3.Rodrigues creates a Rot3 from incremental roll, pitch, and yaw values. It is identical to the exponential map at identity.

rod = Rot3.Rodrigues(np.pi / 6, np.pi / 4, np.pi / 2)
# Rodrigues is overloaded to support an array.
# e.g. Rot3.Rodrigues([np.pi / 6, np.pi / 4, np.pi / 2])
print(rod)
R: [
	-0.156058, -0.673795, 0.72225;
	0.982078, -0.0276074, 0.186445;
	-0.105686, 0.738402, 0.666028
]

Rot3.ClosestTo finds the closest valid Rot3 to the input matrix which minimizes the Frobenius norm. The Frobenius norm is a measure of matrix difference:

ABF=i,j(AijBij)2||A - B||_F = \sqrt{\sum_{i,j} (A_{ij} - B_{ij})^2}
closest = Rot3.ClosestTo([
    [1, 0, 0],
    [0, 2, 0],
    [0, 0, 3]
])
print(closest)
R: [
	1, 0, 0;
	0, 1, 0;
	0, 0, 1
]

Properties

The following properties are available from the standard interface:

  • matrix(): Returns the 3x3 rotation matrix.
  • transpose(): Returns the transpose of the 3x3 rotation matrix.
  • xyz(): Returns the 3 Euler angles as x,y,zx, y, z.
  • ypr(): Returns the 3 Euler angles as yaw, pitch, and roll.
  • rpy(): Returns the 3 Euler angles as roll, pitch, and yaw.
  • roll(): Returns the roll angle.
  • pitch(): Returns the pitch angle.
  • yaw(): Returns the yaw angle.
  • axisAngle(): Returns the axis-angle representation.
  • toQuaternion(): Returns the quaternion representation. The quaternion’s attributes can then be accessed either individually with w(), x(), y(), z() or together with coeffs().

Note that accessing roll(), pitch(), and yaw() separately is less efficient than calling rpy() or ypr().

props = Rot3.RzRyRx(0, np.pi / 6, np.pi / 2)

print("Matrix:\n", props.matrix())
print("Transpose:\n", props.transpose())
print()
print("x, y, z:", props.xyz())
print("y, p, r:", props.ypr())
print("r, p, y:", props.rpy())
print()
print("Roll:   ", props.roll())
print("Pitch:  ", props.pitch())
print("Yaw:    ", props.yaw())
print()
print("Axis-angle:\n", props.axisAngle())
print()
print("Quaternion:", props.toQuaternion().coeffs())
Matrix:
 [[ 5.30287619e-17 -1.00000000e+00  3.06161700e-17]
 [ 8.66025404e-01  6.12323400e-17  5.00000000e-01]
 [-5.00000000e-01  0.00000000e+00  8.66025404e-01]]
Transpose:
 [[ 5.30287619e-17  8.66025404e-01 -5.00000000e-01]
 [-1.00000000e+00  6.12323400e-17  0.00000000e+00]
 [ 3.06161700e-17  5.00000000e-01  8.66025404e-01]]

x, y, z: [0.         0.52359878 1.57079633]
y, p, r: [1.57079633 0.52359878 0.        ]
r, p, y: [0.         0.52359878 1.57079633]

Roll:    0.0
Pitch:   0.5235987755982988
Yaw:     1.5707963267948966

Axis-angle:
 (:-0.250563
 0.250563
 0.935113
, 1.6378338249998232)

Quaternion: [-0.1830127  0.1830127  0.6830127  0.6830127]

Basic operations

Rot3 can rotate and unrotate a 3D point or vector. Rotation is calculated by the simple matrix product RxRx, and unrotation by R1xR^{-1}x.

z90 = Rot3.Rz(np.pi / 2)
point = [2, 0, 0]

# Rotate by 90 degrees around the Z axis
rotated = z90.rotate(point)
print(rotated)
# Undo the rotation
print(z90.unrotate(rotated))
# Rotate backwards by 90 degrees around the Z axis
print(z90.unrotate(point))
[1.2246468e-16 2.0000000e+00 0.0000000e+00]
[2. 0. 0.]
[ 1.2246468e-16 -2.0000000e+00  0.0000000e+00]

Check whether two Rot3 instances are equal within a certain tolerance using equals(). Be careful with the == operator; it does not compare rotational equivalence, it compares object reference. If you wish to use more fine-grained equality comparison, convert to np.ndarray with matrix().

xyz = Rot3.RzRyRx(np.pi / 2, np.pi / 4, np.pi / 6)
ypr = Rot3.Ypr(np.pi / 6, np.pi / 4, np.pi / 2)

print("xyz.equals(ypr, 1e-8):", xyz.equals(ypr, 1e-8))
print("xyz == ypr:", xyz == ypr)
print("xyz == xyz:", xyz == xyz)
print("xyz.matrix() == ypr.matrix():", np.all(xyz.matrix() == ypr.matrix()))
xyz.equals(ypr, 1e-8): True
xyz == ypr: False
xyz == xyz: True
xyz.matrix() == ypr.matrix(): True

Use SLERP (spherical linear interpolation) to interpolate between two Rot3 instances. In terms of the Lie algebra (see below), SLERP can be calculated by scaling the log mapped relative rotation by the interpolation term tt, then converting back to SO(3)\text{SO}(3) using the exponential map. The formula is thus:

R(t)=R1exp(tlog(R11R2))R(t) = R_1 \exp(t \cdot \log(R_1^{-1}R_2))

where R1R_1 and R2R_2 are the start Rot3 and end Rot3 of the interpolation and tt is the interpolation term, usually but not necessarily in the range [0,1][0, 1].

a = Rot3.RzRyRx(0, np.pi / 4, 0)
b = Rot3.RzRyRx(np.pi / 6, 0, 0)

print(a.slerp(0.5, b))
R: [
	0.922613, 0.0523387, 0.382159;
	0.0523387, 0.964602, -0.258464;
	-0.382159, 0.258464, 0.887215
]

Lie group SO(3)\text{SO}(3)

Group operations

Rot3 implements the group operations inverse, compose, between and identity. For more information on groups and their use here, see GTSAM concepts.

a = Rot3.Rz(np.pi / 4)
b = Rot3.Roll(np.pi / 2)

print("a:\n", a.matrix(), "\nb:\n", b.matrix())
a:
 [[ 0.70710678 -0.70710678  0.        ]
 [ 0.70710678  0.70710678  0.        ]
 [ 0.          0.          1.        ]] 
b:
 [[ 1.000000e+00  0.000000e+00  0.000000e+00]
 [ 0.000000e+00  6.123234e-17 -1.000000e+00]
 [ 0.000000e+00  1.000000e+00  6.123234e-17]]

The inverse of an SO(3)\text{SO}(3) rotation matrix is the same as its transpose.

print(a.inverse())
# The inverse is the same as the transpose.
print(a.inverse().equals(Rot3(a.transpose()), 1e-8))
R: [
	0.707107, 0.707107, 0;
	-0.707107, 0.707107, 0;
	0, 0, 1
]

True

The product of the composition operation ABA * B is the rotation matrix which applies the rotation of AA and then the rotation of BB. The composition of two rotation matrices is just the product of the two matrices.

print(a.compose(b))

# The * operator is syntactic sugar for the compose operation.
print(a.compose(b).equals(a * b, 1e-8))

# The composition of two rotation matrices is just the product of the matrices.
print(np.all(a.compose(b).matrix() == a.matrix() @ b.matrix()))
R: [
	0.707107, -4.32978e-17, 0.707107;
	0.707107, 4.32978e-17, -0.707107;
	0, 1, 6.12323e-17
]

True
True

The between operation calculates the rotation from one Rot3 to another. It is defined as simply:

Rrelative=R11R2R_{relative} = R_1^{-1}R_2
print(a.between(b))
print(a.between(b).equals(a.inverse() * b, 1e-8))
R: [
	0.707107, 4.32978e-17, -0.707107;
	-0.707107, 4.32978e-17, -0.707107;
	0, 1, 6.12323e-17
]

True

The identity is I3I_3, as described above.

print(Rot3.Identity())
R: [
	1, 0, 0;
	0, 1, 0;
	0, 0, 1
]

Group operation invariants

See that the following group invariants hold:

print("Compose(a, Inverse(a)) == Identity: ", (a * a.inverse()).equals(Rot3.Identity(), 1e-8))
print("Compose(a, Between(a, b)) == b:", (a * a.between(b)).equals(b, 1e-8))
print("Between(a, b) == Compose(Inverse(a), b):", a.between(b).equals(a.inverse() * b, 1e-8))
Compose(a, Inverse(a)) == Identity:  True
Compose(a, Between(a, b)) == b: True
Between(a, b) == Compose(Inverse(a), b): True

Lie group operations

Rot3 implements the Lie group operations for exponential mapping and log mapping. For more information on Lie groups and their use here, see GTSAM concepts.

The exponential map for SO(3)\text{SO}(3) converts a 3D rotation vector (Lie algebra element in so(3)\mathfrak{so}(3)) into a rotation matrix (Lie group element in SO(3)\text{SO}(3)). This is used to map a rotation vector ωR3\boldsymbol{\omega} \in \mathbb{R}^3 to a rotation matrix RSO(3)R \in \text{SO}(3).

Given a rotation vector ω=(ωx,ωy,ωz)\boldsymbol{\omega} = (\omega_x, \omega_y, \omega_z), define:

  • The rotation axis as the unit vector:

    ω^=ωθ,where θ=ω\hat{\omega} = \frac{\boldsymbol{\omega}}{\theta}, \quad \text{where } \theta = \|\boldsymbol{\omega}\|
  • The skew-symmetric matrix [ω]×[\boldsymbol{\omega}]_\times:

    [ω]×=[0ωzωyωz0ωxωyωx0][\boldsymbol{\omega}]_\times = \begin{bmatrix} 0 & -\omega_z & \omega_y \\ \omega_z & 0 & -\omega_x \\ -\omega_y & \omega_x & 0 \end{bmatrix}

Using Rodrigues’ formula, the exponential map is:

exp([ω]×)=I+sinθθ[ω]×+1cosθθ2[ω]×2\exp([\boldsymbol{\omega}]_\times) = I + \frac{\sin\theta}{\theta} [\boldsymbol{\omega}]_\times + \frac{1 - \cos\theta}{\theta^2} [\boldsymbol{\omega}]_\times^2

which results in the rotation matrix:

R=I+sinθθ[ω]×+1cosθθ2[ω]×2R = I + \frac{\sin\theta}{\theta} [\boldsymbol{\omega}]_\times + \frac{1 - \cos\theta}{\theta^2} [\boldsymbol{\omega}]_\times^2

where:

  • I I is the 3×3 3 \times 3 identity matrix.
  • θ \theta is the magnitude of the rotation vector (rotation angle).
  • [ω]× [\boldsymbol{\omega}]_\times represents the skew-symmetric matrix, the infinitesimal rotation generator.

For very small θ \theta , we use the Taylor series approximation:

RI+[ω]×R \approx I + [\boldsymbol{\omega}]_\times

since sinθθ \sin\theta \approx \theta and 1cosθθ22 1 - \cos\theta \approx \frac{\theta^2}{2} .

r1 = Rot3.RzRyRx(np.pi / 6, np.pi / 2, 0)
r2 = Rot3.RzRyRx(0, 0, np.pi / 4)
p1 = [np.pi / 2, 0, 0]

# The exponential map at identity creates a rotation using Rodrigues' formula.
print(Rot3.Expmap(p1))
# The retract function takes the exponential map of the supplied vector and
# composes it with the calling Rot3. In other words, it maps from the tangent
# space to the manifold.
print(r1)
print(r1.retract(p1))
R: [
	1, 0, 0;
	0, 2.22045e-16, -1;
	0, 1, 2.22045e-16
]

R: [
	6.12323e-17, 0.5, 0.866025;
	0, 0.866025, -0.5;
	-1, 3.06162e-17, 5.30288e-17
]

R: [
	6.12323e-17, 0.866025, -0.5;
	0, -0.5, -0.866025;
	-1, 5.30288e-17, -3.06162e-17
]

The logarithm map for SO(3) \text{SO}(3) is the inverse of the exponential map It converts a rotation matrix RSO(3) R \in SO(3) into a 3D rotation vector (a Lie algebra element in so(3) \mathfrak{so}(3) ).

Given a rotation matrix R R , the corresponding rotation vector ωR3 \boldsymbol{\omega} \in \mathbb{R}^3 is computed as:

ω=θω^\boldsymbol{\omega} = \theta \hat{\omega}

where:

  • θ \theta is the rotation angle:

    θ=cos1(Tr(R)12)\theta = \cos^{-1} \left( \frac{\text{Tr}(R) - 1}{2} \right)
  • ω^ \hat{\omega} is the rotation axis, obtained from the skew-symmetric matrix [ω]× [\boldsymbol{\omega}]_\times :

    [ω]×=θ2sinθ(RRT)[\boldsymbol{\omega}]_\times = \frac{\theta}{2 \sin\theta} (R - R^T)
  • Extracting the components:

    ω=θ2sinθ(RRT)\boldsymbol{\omega} = \frac{\theta}{2 \sin\theta} (R - R^T)^\vee

For small θ \theta , we use the Taylor series approximation:

ω12(RRT)\boldsymbol{\omega} \approx \frac{1}{2} (R - R^T)^\vee

In both cases, (RRT) (R - R^T)^\vee extracts the unique 3D vector from a skew-symmetric matrix:

(RRT)=[R32R23R13R31R21R12](R - R^T)^\vee= \begin{bmatrix} R_{32} - R_{23} \\ R_{13} - R_{31} \\ R_{21} - R_{12} \end{bmatrix}

where Rij R_{ij} are the elements of R R .

# Calculate the log map of r at identity. Returns the coordinates of the rotation
# in the tangent space.
print(Rot3.Logmap(r1))

# Transform r2 into its vector representation relative to r1.
print(r1.logmap(r2))
# logmap is the same as calculating the coordinate of the second Rot3 in the
# local frame of the first, which localCoordinates (inherited from LieGroup) does.
print(r1.localCoordinates(r2))
[ 0.41038024  1.53155991 -0.41038024]
[-1.01420581 -1.32173874  1.01420581]
[-1.01420581 -1.32173874  1.01420581]
# Applying localCoordinates and then retract cancels out, returning r2 given any
# r1. This is because it transforms r2 from the manifold to the tangent space
# using the log map, then transforms that result back into the manifold using
# the exponential map.
print(r2)
print(r1.retract(r1.localCoordinates(r2)))
R: [
	0.707107, -0.707107, 0;
	0.707107, 0.707107, 0;
	-0, 0, 1
]

R: [
	0.707107, -0.707107, 9.04269e-17;
	0.707107, 0.707107, 4.0637e-17;
	-6.77245e-17, 6.77245e-17, 1
]

Serialization

A Rot3 can be serialized to a string for saving, then later used by deserializing the string.

a = Rot3.Rx(np.pi / 2)
print("Before serialization:", a)

str_val = a.serialize()
print(str_val)
print("The serialized value is a string:", type(str_val))
# Save to file, etc...

b = Rot3()
b.deserialize(str_val)
print("After deserialization:", b)
Before serialization: R: [
	1, 0, 0;
	0, 6.12323e-17, -1;
	0, 1, 6.12323e-17
]

22 serialization::archive 15 1 0
0 1.00000000000000000e+00 0.00000000000000000e+00 0.00000000000000000e+00 0.00000000000000000e+00 6.12323399573676604e-17 -1.00000000000000000e+00 0.00000000000000000e+00 1.00000000000000000e+00 6.12323399573676604e-17

The serialized value is a string: <class 'str'>
After deserialization: R: [
	1, 0, 0;
	0, 6.12323e-17, -1;
	0, 1, 6.12323e-17
]