My Project 1.0
Holistic Fusion: A Generic Framework for Multimodal Sensor Fusion
Loading...
Searching...
No Matches
GmsfUnaryExpressionAbsoluteHeading.h
Go to the documentation of this file.
1/*
2Copyright 2026 by Julian Nubert, Robotic Systems Lab, ETH Zurich.
3All rights reserved.
4This file is released under the "BSD-3-Clause License".
5Please see the LICENSE file that has been included as part of this package.
6 */
7
8#ifndef GMSF_UNARY_EXPRESSION_ABSOLUTE_HEADING_H
9#define GMSF_UNARY_EXPRESSION_ABSOLUTE_HEADING_H
10
11// C++
12#include <stdexcept>
13
14// GTSAM
15#include <gtsam/base/OptionalJacobian.h>
16#include <gtsam/geometry/Rot2.h>
17#include <gtsam/geometry/Rot3.h>
18#include <gtsam/inference/Symbol.h>
19#include <gtsam/slam/expressions.h>
20
21// Workspace
24
25namespace holistic_fusion {
26
27// Rotation angle of R_W_Wmeas about the z-axis of W, as a planar rotation, so that the factor error wraps around at +-pi.
28// It reads the rotation vector, so it is defined for every attitude.
29inline gtsam::Rot2 worldRotationErrorAsHeadingError(const gtsam::Rot3& R_W_Wmeas, gtsam::OptionalJacobian<1, 3> H) {
30 gtsam::Matrix3 H_log;
31 const gtsam::Vector3 rotationVector = gtsam::Rot3::Logmap(R_W_Wmeas, H ? &H_log : nullptr);
32 if (H) {
33 *H = H_log.row(2);
34 }
35 return gtsam::Rot2::fromAngle(rotationVector.z());
36}
37
38// Heading of the estimated orientation R_W_S relative to the measured orientation R_M_Smeas, about the z-axis of the world W.
39// The measurement is brought to W through R_W_M, so a tilted fixed frame M does not tilt the constrained axis.
40inline gtsam::Expression<gtsam::Rot2> headingInWorld(const gtsam::Rot3_& exp_R_W_S, const gtsam::Rot3_& exp_R_W_M,
41 const gtsam::Rot3& R_M_Smeas) {
42 const gtsam::Rot3_ exp_R_W_Wmeas = exp_R_W_S * gtsam::Rot3_(R_M_Smeas.inverse()) * inverseRot3(exp_R_W_M);
43 return gtsam::Expression<gtsam::Rot2>(&worldRotationErrorAsHeadingError, exp_R_W_Wmeas);
44}
45
53class GmsfUnaryExpressionAbsoluteHeading final : public GmsfUnaryExpressionAbsolut<gtsam::Rot2, 'c'> {
54 public:
55 // Constructor
57 const std::string& imuFrameName, const Eigen::Isometry3d& T_I_sensorFrame,
58 const double createReferenceAlignmentKeyframeEveryNSeconds)
59 : GmsfUnaryExpressionAbsolut(headingUnaryMeasurementPtr, imuFrameName, T_I_sensorFrame,
60 createReferenceAlignmentKeyframeEveryNSeconds),
61 headingUnaryMeasurementPtr_(headingUnaryMeasurementPtr),
62 exp_R_W_S_(gtsam::Rot3::Identity()),
63 exp_R_W_fixedFrame_(gtsam::Rot3::Identity()) {}
64
65 // Destructor
67
68 // Noise as GTSAM Datatype
69 [[nodiscard]] const gtsam::Vector getNoiseDensity() const override { return headingUnaryMeasurementPtr_->unaryMeasurementNoiseDensity(); }
70
71 // The expression is the heading offset to the measured orientation, which the factor drives to zero
72 [[nodiscard]] const gtsam::Rot2 getGtsamMeasurementValue() const override { return gtsam::Rot2::Identity(); }
73
74 protected:
75 // i) Generate Expression for Basic IMU State in World Frame at Key -------------------------------------
76 void generateImuStateInWorldFrameAtKey(const gtsam::Key& closestGeneralKey) final {
77 exp_R_W_S_ = gtsam::rotation(gtsam::Expression<gtsam::Pose3>(gtsam::symbol_shorthand::X(closestGeneralKey))); // R_W_I at this point
78 }
79
80 // ii) Holistically Optimize over Fixed Frames -----------------------------------------------------------
81 bool measuresPosition() const final { return false; }
82
83 // A heading measurement never creates a keyframe, so the graph never uses this guess
84 gtsam::Pose3 computeT_W_fixedFrame_initial(const gtsam::NavState& /*W_currentPropagatedState*/) final { return gtsam::Pose3::Identity(); }
85
86 const Eigen::Vector3d getMeasurementPosition() final { return Eigen::Vector3d::Zero(); }
87
88 void setMeasurementPosition(const Eigen::Vector3d& /*position*/) final {
89 throw std::logic_error("GmsfUnaryExpressionAbsoluteHeading: a heading measurement has no position.");
90 }
91
92 // The state stays in the world, as the heading is taken about the z-axis of the world
93 void transformStateToReferenceFrameMeasurement(const gtsam::Pose3_& exp_T_W_fixedFrame) override {
94 exp_R_W_fixedFrame_ = gtsam::rotation(exp_T_W_fixedFrame);
95 }
96
97 // iii) Transform Measurement to Core Imu Frame -----------------------------------------------------------
98 void transformImuStateToSensorFrameState() final { exp_R_W_S_ = exp_R_W_S_ * gtsam::Rot3_(gtsam::Rot3(T_I_sensorFrameInit_.rotation())); }
99
100 // iv) Extrinsic Calibration ---------------------------------------------------------------------
102 throw std::logic_error("GmsfUnaryExpressionAbsoluteHeading: extrinsic calibration is not supported for heading measurements.");
103 }
104
105 void applyExtrinsicCalibrationCorrection(const gtsam::Expression<gtsam::Rot2>& /*exp_C_sensorFrame_sensorFrameCorrected*/) final {
106 throw std::logic_error("GmsfUnaryExpressionAbsoluteHeading: extrinsic calibration is not supported for heading measurements.");
107 }
108
109 // Only extrinsic calibration converts, which heading measurements do not support
110 gtsam::Pose3 convertToPose3(const gtsam::Rot2& /*heading*/) final {
111 throw std::logic_error("GmsfUnaryExpressionAbsoluteHeading: extrinsic calibration is not supported for heading measurements.");
112 }
113
114 gtsam::Rot2 convertFromPose3(const gtsam::Pose3& /*pose*/) final {
115 throw std::logic_error("GmsfUnaryExpressionAbsoluteHeading: extrinsic calibration is not supported for heading measurements.");
116 }
117
118 // Return Expression
119 [[nodiscard]] const gtsam::Expression<gtsam::Rot2> getGtsamExpression() const override {
120 return headingInWorld(exp_R_W_S_, exp_R_W_fixedFrame_, gtsam::Rot3(headingUnaryMeasurementPtr_->unaryMeasurement()));
121 }
122
123 private:
124 // Full Measurement Type
125 std::shared_ptr<UnaryMeasurementXDAbsolute<Eigen::Matrix3d, 1>> headingUnaryMeasurementPtr_;
126
127 // Expressions
128 gtsam::Expression<gtsam::Rot3> exp_R_W_S_;
129 gtsam::Expression<gtsam::Rot3> exp_R_W_fixedFrame_;
130};
131
132} // namespace holistic_fusion
133
134#endif // GMSF_UNARY_EXPRESSION_ABSOLUTE_HEADING_H
GmsfUnaryExpressionAbsolut(const std::shared_ptr< UnaryMeasurementAbsolute > &gmsfUnaryAbsoluteMeasurementPtr, const std::string &imuFrameName, const Eigen::Isometry3d &T_I_sensorFrame, const double createReferenceAlignmentKeyframeEveryNSeconds)
GmsfUnaryExpressionAbsoluteHeading(const std::shared_ptr< UnaryMeasurementXDAbsolute< Eigen::Matrix3d, 1 > > &headingUnaryMeasurementPtr, const std::string &imuFrameName, const Eigen::Isometry3d &T_I_sensorFrame, const double createReferenceAlignmentKeyframeEveryNSeconds)
std::shared_ptr< UnaryMeasurementXDAbsolute< Eigen::Matrix3d, 1 > > headingUnaryMeasurementPtr_
void applyExtrinsicCalibrationCorrection(const gtsam::Expression< gtsam::Rot2 > &) final
void transformSensorFrameStateToSensorFrameCorrectedState(DynamicDictionaryContainer &) final
void generateImuStateInWorldFrameAtKey(const gtsam::Key &closestGeneralKey) final
void transformStateToReferenceFrameMeasurement(const gtsam::Pose3_ &exp_T_W_fixedFrame) override
gtsam::Pose3 computeT_W_fixedFrame_initial(const gtsam::NavState &) final
const gtsam::Expression< gtsam::Rot2 > getGtsamExpression() const override
Class to represent x-dimensional absolute unary measurement in the graph.
gtsam::Rot3_ inverseRot3(const gtsam::Rot3_ &R)
gtsam::Rot2 worldRotationErrorAsHeadingError(const gtsam::Rot3 &R_W_Wmeas, gtsam::OptionalJacobian< 1, 3 > H)
gtsam::Expression< gtsam::Rot2 > headingInWorld(const gtsam::Rot3_ &exp_R_W_S, const gtsam::Rot3_ &exp_R_W_M, const gtsam::Rot3 &R_M_Smeas)