Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

RotateFactor

Open In Colab
import gtsam
import numpy as np
from gtsam import Rot3, Point3, Unit3, Values
from gtsam import RotateFactor, RotateDirectionsFactor
from gtsam import symbol_shorthand

R = symbol_shorthand.R # For Rotation key

This header defines factors that constrain an unknown rotation (Rot3) based on how it transforms either full rotations or directions.

  • RotateFactor: Relates two incremental rotations measured in different frames using an unknown rotation relating the frames. If Z=RPRTZ = R \cdot P \cdot R^T, where P=e[p]P = e^{[p]} and Z=e[z]Z = e^{[z]} are measured incremental rotations (expressed as angular velocity vectors pp and zz), this factor constrains the unknown rotation RR such that p=Rzp = R z. The error is RzpRz - p.

  • RotateDirectionsFactor: Relates two directions (unit vectors, Unit3) measured in different frames using an unknown rotation RR relating the frames. If pworld=Rzbodyp_{world} = R \cdot z_{body}, this factor constrains RR. The error is the angular difference between the predicted RzbodyR z_{body} and the measured pworldp_{world}.

1. RotateFactor

Constraints an unknown Rot3 based on corresponding incremental rotation measurements PP (predicted/world) and ZZ (measured/body).

# Assume P and Z are small incremental rotations
P = Rot3.Expmap(np.array([0.01, 0.02, 0.03]))
Z = Rot3.Expmap(np.array([0.03, -0.01, 0.02]))
rot_key = R(0)
noise1 = gtsam.noiseModel.Isotropic.Sigma(3, 0.001)

factor1 = RotateFactor(rot_key, P, Z, noise1)
factor1.print("RotateFactor: ")

# Evaluate error
values = Values()
# Ground truth R would satisfy P = R*Z
# R = P * Z.inverse()
gt_R = P * (Z.inverse())
values.insert(rot_key, gt_R)
error1_gt = factor1.error(values)
print(f"\nError at ground truth R: {error1_gt}")

noisy_R = gt_R * Rot3.Expmap(np.array([0.001, -0.001, 0.001]))
values.update(rot_key, noisy_R)
error1_noisy = factor1.error(values)
print(f"Error at noisy R: {error1_noisy}")
RotateFactor:   keys = { r0 }
isotropic dim=3 sigma=0.001
RotateFactor:]
p: 0.01 0.02 0.03
z:  0.03 -0.01  0.02

Error at ground truth R: 700.0
Error at noisy R: 699.2869223608442

2. RotateDirectionsFactor

Constraints an unknown Rot3 based on corresponding direction measurements pworldp_{world} (predicted/world) and zbodyz_{body} (measured/body).

p_world = Unit3(Point3(1, 0, 0)) # Direction in world frame
z_body = Unit3(Point3(0, 1, 0))  # Corresponding direction in body frame
noise2 = gtsam.noiseModel.Isotropic.Sigma(2, 0.01) # Noise on 2D tangent space

factor2 = RotateDirectionsFactor(rot_key, p_world, z_body, noise2)
factor2.print("RotateDirectionsFactor: ")

# Ground truth R rotates z_body (0,1,0) to p_world (1,0,0)
# This corresponds to a -90 deg yaw
gt_R_dir = Rot3.Yaw(-np.pi/2)
print(f"\nCheck: gt_R * z_body = \n{gt_R_dir.rotate(z_body)}")

# Evaluate error
values_dir = Values()
values_dir.insert(rot_key, gt_R_dir)
error2_gt = factor2.error(values_dir)
print(f"Error at ground truth R: {error2_gt}")

noisy_R_dir = gt_R_dir * Rot3.Expmap(np.array([0.01, 0, 0.01]))
values_dir.update(rot_key, noisy_R_dir)
error2_noisy = factor2.error(values_dir)
print(f"Error at noisy R: {error2_noisy}")
RotateDirectionsFactor:   keys = { r0 }
isotropic dim=2 sigma=0.01
RotateDirectionsFactor:
p:1
0
0
z:0
1
0

Check: gt_R * z_body = 
:          1
6.12323e-17
          0

Error at ground truth R: 1.874699728327322e-29
Error at noisy R: 0.999933335111092

WahbaFactor uses the same aRb, frame-bb direction, and measured frame-aa direction convention, but returns the three-dimensional chordal difference between unit vectors. That residual takes a 3D noise model and has an exact D=1 QCQP conversion. RotateDirectionsFactor instead returns the two-dimensional Unit3 tangent-space error. See CertifiableWahbaProblem for the complete chordal optimization and SDP tutorial.