A gtsam.Rot3
represents an orientation or attitude in 3D space. It can be manipulated and presented as a rotation matrix , a unit quaternion, roll-pitch-yaw (Euler) angles , or as an axis-angle representation with and . It models a 3D orientation as both a manifold in and as a Lie group in . Internally, it is stored as a rotation matrix but can be configured to use quaternions at build time for efficiency.
%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 ), 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 .
# 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:
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 .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 withw()
,x()
,y()
,z()
or together withcoeffs()
.
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 , and unrotation by .
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 , then converting back to using the exponential map. The formula is thus:
where and are the start Rot3
and end Rot3
of the interpolation and is the interpolation term, usually but not necessarily in the range .
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 ¶
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 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 is the rotation matrix which applies the rotation of and then the rotation of . 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:
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 , 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 converts a 3D rotation vector (Lie algebra element in ) into a rotation matrix (Lie group element in ). This is used to map a rotation vector to a rotation matrix .
Given a rotation vector , define:
The rotation axis as the unit vector:
The skew-symmetric matrix :
Using Rodrigues’ formula, the exponential map is:
which results in the rotation matrix:
where:
- is the identity matrix.
- is the magnitude of the rotation vector (rotation angle).
- represents the skew-symmetric matrix, the infinitesimal rotation generator.
For very small , we use the Taylor series approximation:
since and .
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 is the inverse of the exponential map It converts a rotation matrix into a 3D rotation vector (a Lie algebra element in ).
Given a rotation matrix , the corresponding rotation vector is computed as:
where:
is the rotation angle:
is the rotation axis, obtained from the skew-symmetric matrix :
Extracting the components:
For small , we use the Taylor series approximation:
In both cases, extracts the unique 3D vector from a skew-symmetric matrix:
where are the elements of .
# 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
]