A Pose2
represents a position and orientation in 2D space. It is both a pose manifold and a Lie group of transforms. It consists of a 2D position (variously represented as , , or depending on the context) and a rotation (similarly, , , or θ). Its 3-dimensional analog is a Pose3
. It is included in the top-level gtsam
package.
%pip install gtsam
import gtsam
from gtsam import Pose2, Point2, Rot2, Point3
import numpy as np
Initialization and properties¶
A Pose2
can be initialized with no arguments, which yields the identity pose, or it can be constructed with a position and rotation.
# Identity pose
p1 = Pose2()
print(p1)
R = Rot2.fromDegrees(90)
# Point2 is not an object, it is a utility function that creates a 2d float np.ndarray
t = Point2(1, 2) # or (1, 2) or [1, 2] or np.array([1, 2])
# Pose at (1, 2) and facing north
p2 = Pose2(R, t)
print(p2)
(0, 0, 0)
(1, 2, 1.5708)
The pose’s properties can be accessed using the following members:
x()
y()
translation()
(which returns a two-elementnp.ndarray
representingx, y
)theta()
rotation()
(which returns aRot2
)matrix()
The matrix()
function returns the pose’s rotation and translation in the following form:
print(f"Location: ({p2.x()}, {p2.y()}); also accessible with translation(): {p2.translation()}")
# .rotation() returns a Rot2 object; the float value can be accessed with .theta()
print(f"Rotation: {p2.theta()}; also accessible with rotation(): {p2.rotation().theta()}")
print(f"Position-rotation 3x3 matrix:\n{p2.matrix()}")
Location: (1.0, 2.0); also accessible with translation(): [1. 2.]
Rotation: 1.5707963267948966; also accessible with rotation(): 1.5707963267948966
Position-rotation 3x3 matrix:
[[ 6.123234e-17 -1.000000e+00 1.000000e+00]
[ 1.000000e+00 6.123234e-17 2.000000e+00]
[ 0.000000e+00 0.000000e+00 1.000000e+00]]
Basic operations¶
Points in the global space can be transformed to and from the local space of the Pose2
using transformTo
and transformFrom
.
global_point = Point2(5, 5)
origin = Pose2(Rot2.fromAngle(np.pi), Point2(1, 1))
# For a point at (1, 1) that is rotated 180 degrees, a point at (5, 5) in global
# space is at (-4, -4) in local space.
transformed = origin.transformTo(global_point)
print(f"{global_point} transformed by {origin} into local space -> {transformed}")
reversed = origin.transformFrom(transformed)
print(f"{transformed} transformed by {origin} into global space -> {reversed}")
[5. 5.] transformed by (1, 1, 3.14159)
into local space -> [-4. -4.]
[-4. -4.] transformed by (1, 1, 3.14159)
into global space -> [5. 5.]
Bearings (angles) and ranges (distances) can be calculated to points using the associated functions bearing
and range
.
p1 = Pose2(Rot2.fromDegrees(90), Point2(-3, -3))
point1 = Point2(-2, -3)
print(f"Bearing from {p1} to {point1}: {p1.bearing(point1).theta()}")
p2 = Pose2(Rot2.fromDegrees(-45), Point2(1, 1))
p3 = Pose2(Rot2.fromDegrees(180), Point2(0, 2))
print(f"Bearing from {p2} to {p3.translation()}: {p2.bearing(p3.translation()).theta()}")
p4 = Pose2(Rot2.fromDegrees(-90), Point2(4, 0))
point2 = Point2(0, 3)
print(f"Range from {p4} to {point2}: {p4.range(point2)}")
Bearing from (-3, -3, 1.5708)
to [-2. -3.]: -1.5707963267948966
Bearing from (1, 1, -0.785398)
to [0. 2.]: 3.141592653589793
Range from (4, 0, -1.5708)
to [0. 3.]: 5.0
Manifold ¶
The manifold represents poses in 2D space and is defined by elements where:
- : The orientation or attitude.
- : The position.
This manifold can be thought of as all possible values that can specify the position and orientation of a rigid body in some reference frame. It does not have a notion of composition; we would never think to compose two poses of two different rigid bodies.
Manifold operations¶
Local¶
The function maps into the local coordinate system of . Essentially, it “subtracts” from on the manifold, giving the relative pose in the local frame of . In other words, it computes the difference between two points on a manifold and expresses the result in the tangent space. It is a form of the Lie group operation .
Mathematically, the local function is computed as follows:
where:
so:
- is the relative rotation.
- is the relative translation in the frame of .
Call the localCoordinates
member function to use the local function in your code. In GTSAM, the result of the local function is interpreted as a vector in the tangent space, so localCoordinates
returns a 3-element np.ndarray
.
p = Pose2(Rot2.fromDegrees(90), Point2(-5, -3))
q = Pose2(Rot2.fromDegrees(-45), Point2(1, 4))
print(p.localCoordinates(q))
[ 7. -6. -2.35619449]
Retract¶
The function takes a point on the manifold and a perturbation from the tangent space and maps it back onto the manifold to get a new point. It is the inverse of the function, so it may be considered as .
Given:
- An initial pose
- A perturbation
The retract function updates the pose using the following formula:
where:
- is the rotation matrix:
- are computed based on the motion model:
Call the retract
member function to use the retract function in your code. Since is in the tangent space, it must be passed as a 3D vector (a 3-element np.ndarray
). retract
returns the adjusted .
p = Pose2(Rot2.fromDegrees(90), Point2(-5, -3))
v = Point3(2, -1, Rot2.fromDegrees(180).theta())
q = Pose2(Rot2.fromDegrees(-45), Point2(1, 4))
print(p.retract(v))
print(q)
# Applies local and then retract, which cancel out. This statement prints q given
# any p.
print(p.retract(p.localCoordinates(q)))
(-4, -1, -1.5708)
(1, 4, -0.785398)
(1, 4, -0.785398)
Optimization on ¶
Many problems involve optimization over the pose manifold, such as:
- Localization: Estimating the pose of a robot or sensor in a global or local frame.
- SLAM (Simultaneous Localization and Mapping): Optimizing a graph of poses and landmarks to minimize error.
Examples of these problems can be found at the end of this page.
Lie group ¶
Group operations¶
A Pose2
implements the group operations identity
, inverse
, compose
, and between
. For more information on groups and their use here, see GTSAM concepts.
Identity¶
The Pose2
identity is .
print(Pose2.Identity())
(0, 0, 0)
Inverse¶
The inverse of a pose represents the transformation that undoes the pose. In other words, if you have a pose that moves from frame A to frame B, its inverse moves from frame B back to frame A. The equation to compute the inverse is as follows:
p5 = Pose2(0, Point2(-5, 2))
print(f"Inverse of {p5} -> {p5.inverse()}")
p6 = Pose2(Rot2.fromDegrees(45), Point2(6, 4))
print(f"Inverse of {p6} -> {p6.inverse()}")
Inverse of (-5, 2, 0)
-> (5, -2, -0)
Inverse of (6, 4, 0.785398)
-> (-7.07107, 1.41421, -0.785398)
Composition¶
The composition of two Pose2
objects follows the rules of transformation.
Given two poses:
- Pose A:
- Pose B:
The composition of these two poses results in:
Therefore:
In other words:
- The rotation of Pose A is applied to the translation of Pose B before adding it.
- The final rotation is just the sum of the two rotations.
p7 = Pose2(0, Point2(8, 10))
p8 = Pose2(Rot2.fromDegrees(135), Point2(4, -7))
print(f"Composition: {p7} * {p8} -> {p7 * p8}")
print(f"Composition is not commutative: {p8} * {p7} = {p8 * p7}")
Composition: (8, 10, 0)
* (4, -7, 2.35619)
-> (12, 3, 2.35619)
Composition is not commutative: (4, -7, 2.35619)
* (8, 10, 0)
= (-8.72792, -8.41421, 2.35619)
Between¶
Given two poses and , the between function returns the pose of in the local coordinate frame of ; in other words, the transformation needed to move from to .
The between function is given by:
a = Pose2(Rot2.fromDegrees(0), Point2(1, 4))
b = Pose2(Rot2.fromDegrees(45), Point2(-3, 0))
print(a.between(b))
(-4, -4, 0.785398)
Lie group operations¶
A Pose2
also implements the Lie group operations for exponential mapping, log mapping, and adjoint mapping, as well as other Lie group functionalities. For more information on Lie groups and their use here, see GTSAM concepts.
Exponential mapping¶
The exponential map function expmap
is used to convert a small motion, like a velocity or perturbation, in the Lie algebra (tangent space) into a Pose2
transformation in the Lie group . It is used because optimization is easier in the tangent space; transformations behave like vectors there.
In tangent space, small motions are represented as:
where:
- are small translations in the local frame.
- ω is a small rotation.
The exponential map converts this small motion into a full pose:
This accounts for rotational effects when mapping from the tangent space back to .
twist = gtsam.Point3(0.5, 0.5, Rot2.fromDegrees(90).theta())
print(Pose2.Expmap(twist))
# There is no pose.expmap(...), only Pose2.Expmap(...). If you are looking to
# convert a small motion to a pose relative to a pose, use retract.
(0, 0.63662, 1.5708)
Log mapping¶
The log map function logmap
is used to convert a transformation in (such as a Pose2
) into a vector in tangent space. It can be used to convert a pose to its small motion representation or compute the difference between two poses.
For a pose , logmap
finds the equivalent motion in tangent space:
where:
pose = Pose2(Rot2.fromDegrees(135), Point2(4, -7))
diff = Pose2(Rot2.fromDegrees(135), Point2(6, -7))
# Convert a pose to its small motion representation
print(Pose2.Logmap(pose))
# Compute the difference between two poses
print(pose.logmap(pose))
print(pose.logmap(diff))
[-6.29474529 -8.12827598 2.35619449]
[0. 0. 0.]
[-1.41421356 -1.41421356 0. ]