Monado OpenXR Runtime
t_imu_fusion.hpp
Go to the documentation of this file.
1// Copyright 2019, Collabora, Ltd.
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief C++ sensor fusion/filtering code that uses flexkalman
6 * @author Rylie Pavlik <rylie.pavlik@collabora.com>
7 * @ingroup aux_tracking
8 */
9
10#pragma once
11
12#ifndef __cplusplus
13#error "This header is C++-only."
14#endif
15
18#include "math/m_api.h"
19#include "util/u_time.h"
20#include "util/u_debug.h"
21#include "util/u_logging.h"
22
23#include <Eigen/Core>
24#include <Eigen/Geometry>
25
26#include "flexkalman/EigenQuatExponentialMap.h"
27
28DEBUG_GET_ONCE_LOG_OPTION(simple_imu_log, "SIMPLE_IMU_LOG", U_LOGGING_WARN)
29
30#define SIMPLE_IMU_TRACE(...) U_LOG_IFL_T(log_level, __VA_ARGS__)
31#define SIMPLE_IMU_DEBUG(...) U_LOG_IFL_D(log_level, __VA_ARGS__)
32#define SIMPLE_IMU_INFO(...) U_LOG_IFL_I(log_level, __VA_ARGS__)
33#define SIMPLE_IMU_WARN(...) U_LOG_IFL_W(log_level, __VA_ARGS__)
34#define SIMPLE_IMU_ERROR(...) U_LOG_IFL_E(log_level, __VA_ARGS__)
35
36namespace xrt::auxiliary::tracking {
37
38/*!
39 * @brief A simple IMU fusion class.
40 */
42{
43public:
44 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
45 /*!
46 * @param gravity_rate Value in [0, 1] indicating how much the
47 * accelerometer should affect the orientation each second.
48 */
49 explicit SimpleIMUFusion(double gravity_rate = 0.9)
50 : gravity_scale_(gravity_rate), log_level(debug_get_log_option_simple_imu_log())
51 {
52 SIMPLE_IMU_DEBUG("Creating instance");
53 }
54
55 /*!
56 * @return true if the filter has started up
57 */
58 bool
59 valid() const noexcept
60 {
61 return started_;
62 }
63
64 /*!
65 * Get the current state orientation.
66 */
67 Eigen::Quaterniond
68 getQuat() const
69 {
70 return quat_;
71 }
72
73 /*!
74 * Get the current state orientation as a rotation vector.
75 */
76 Eigen::Vector3d
78 {
79 return flexkalman::util::quat_ln(quat_);
80 }
81
82 /*!
83 * Get the current state orientation as a rotation matrix.
84 */
85 Eigen::Matrix3d
87 {
88 return quat_.toRotationMatrix();
89 }
90
91 /*!
92 * @brief Get the predicted orientation at some point in the future.
93 *
94 * Here, we do **not** clamp the delta-t, so only ask for reasonable
95 * points in the future. (The gyro handler math does clamp delta-t for
96 * the purposes of integration in case of long times without signals,
97 * etc, which is OK since the accelerometer serves as a correction.)
98 */
99 Eigen::Quaterniond
100 getPredictedQuat(timepoint_ns timestamp) const;
101
102 /*!
103 * Get the angular velocity in body space.
104 */
105 Eigen::Vector3d const &
106 getAngVel() const
107 {
108 return angVel_;
109 }
110
111 /*!
112 * Process a gyroscope report.
113 *
114 * @note At startup, no gyro reports will be considered until at least
115 * one accelerometer report has been processed, to provide us with an
116 * initial estimate of the direction of gravity.
117 *
118 * @param gyro Angular rate in radians per second, in body space.
119 * @param timestamp Nanosecond timestamp of this measurement.
120 *
121 * @return true if the report was used to update the state.
122 */
123 bool
124 handleGyro(Eigen::Vector3d const &gyro, timepoint_ns timestamp);
125
126
127 /*!
128 * Process an accelerometer report.
129 *
130 * @param accel Body-relative acceleration measurement in m/s/s.
131 * @param timestamp Nanosecond timestamp of this measurement.
132 *
133 * @return true if the report was used to update the state.
134 */
135 bool
136 handleAccel(Eigen::Vector3d const &accel, timepoint_ns timestamp);
137
138 /*!
139 * Use this to obtain the residual, world-space acceleration in m/s/s
140 * **not** associated with gravity, after incorporating a measurement.
141 *
142 * @param accel Body-relative acceleration measurement in m/s/s.
143 */
144 Eigen::Vector3d
145 getCorrectedWorldAccel(Eigen::Vector3d const &accel) const
146 {
147 Eigen::Vector3d adjusted_accel = accel * getAccelScaleFactor_();
148 return (quat_ * adjusted_accel) - (Eigen::Vector3d::UnitY() * MATH_GRAVITY_M_S2);
149 }
150
151 /*!
152 * @brief Normalize internal state.
153 *
154 * Call periodically, like after you finish handling both the accel and
155 * gyro from one packet.
156 */
157 void
159 {
160 quat_.normalize();
161 }
162
163private:
164 /*!
165 * Returns a coefficient to correct the scale of the accelerometer
166 * reading.
167 */
168 double
169 getAccelScaleFactor_() const
170 {
171 // For a "perfect" accelerometer, gravity_filter_.getState()
172 // should return MATH_GRAVITY_M_S2, making this method return 1.
173 return MATH_GRAVITY_M_S2 / gravity_filter_.getState();
174 }
175
176 //! Body-space angular velocity in radian/s
177 Eigen::Vector3d angVel_{Eigen::Vector3d::Zero()};
178 //! Current orientation
179 Eigen::Quaterniond quat_{Eigen::Quaterniond::Identity()};
180 double gravity_scale_;
181
182 /*!
183 * @brief Low-pass filter for extracting the gravity direction from the
184 * full accel signal.
185 *
186 * High-frequency components of the accel are either noise or
187 * user-caused acceleration, and do not reflect the direction of
188 * gravity.
189 */
190 math::LowPassIIRVectorFilter<3, double> accel_filter_{200 /* hz cutoff frequency */};
191
192 /*!
193 * @brief Even-lower low pass filter on the length of the acceleration
194 * vector, used to estimate a corrective scale for the accelerometer
195 * data.
196 *
197 * Over time, the length of the accelerometer data will average out to
198 * be the acceleration due to gravity.
199 */
200 math::LowPassIIRFilter<double> gravity_filter_{1 /* hz cutoff frequency */};
201 uint64_t last_accel_timestamp_{0};
202 uint64_t last_gyro_timestamp_{0};
203 double gyro_min_squared_length_{1.e-8};
204 bool started_{false};
205 enum u_logging_level log_level;
206};
207
208inline Eigen::Quaterniond
210{
211 timepoint_ns state_time = std::max(last_accel_timestamp_, last_gyro_timestamp_);
212 if (state_time == 0) {
213 // no data yet.
214 return Eigen::Quaterniond::Identity();
215 }
216 time_duration_ns delta_ns = timestamp - state_time;
217 double dt = time_ns_to_s(delta_ns);
218 return quat_ * flexkalman::util::quat_exp(angVel_ * dt * 0.5);
219}
220inline bool
221SimpleIMUFusion::handleGyro(Eigen::Vector3d const &gyro, timepoint_ns timestamp)
222{
223 if (!started_) {
224
225 SIMPLE_IMU_DEBUG(
226 "Discarding gyro report before first usable accel "
227 "report");
228 return false;
229 }
230 time_duration_ns delta_ns =
231 (last_gyro_timestamp_ == 0) ? (time_duration_ns)1e6 : timestamp - last_gyro_timestamp_;
232 if (delta_ns > 1e10) {
233
234 SIMPLE_IMU_DEBUG("Clamping integration period");
235 // Limit integration to 1/10th of a second
236 // Does not affect updating the last gyro timestamp.
237 delta_ns = (time_duration_ns)1e10;
238 }
239 float dt = time_ns_to_s(delta_ns);
240 last_gyro_timestamp_ = timestamp;
241 Eigen::Vector3d incRot = gyro * dt;
242 // Crude handling of "approximately zero"
243 if (incRot.squaredNorm() < gyro_min_squared_length_) {
244 SIMPLE_IMU_TRACE("Discarding gyro data that is approximately zero");
245 return false;
246 }
247
248 angVel_ = gyro;
249
250 // Update orientation
251 quat_ = quat_ * flexkalman::util::quat_exp(incRot * 0.5);
252
253 return true;
254}
255inline bool
256SimpleIMUFusion::handleAccel(Eigen::Vector3d const &accel, timepoint_ns timestamp)
257{
258 uint64_t delta_ns = (last_accel_timestamp_ == 0) ? 1e6 : timestamp - last_accel_timestamp_;
259 float dt = time_ns_to_s(delta_ns);
260 if (!started_) {
261 auto diff = std::abs(accel.norm() - MATH_GRAVITY_M_S2);
262 if (diff > 1.) {
263 // We're moving, don't start it now.
264
265 SIMPLE_IMU_DEBUG(
266 "Can't start tracker with this accel "
267 "sample: we're moving too much.");
268 return false;
269 }
270
271 // Initially, set it to totally trust gravity.
272 started_ = true;
273 quat_ = Eigen::Quaterniond::FromTwoVectors(accel.normalized(), Eigen::Vector3d::UnitY());
274 accel_filter_.addSample(accel, timestamp);
275 gravity_filter_.addSample(accel.norm(), timestamp);
276 last_accel_timestamp_ = timestamp;
277
278 SIMPLE_IMU_DEBUG("Got a usable startup accel report");
279 return true;
280 }
281 last_accel_timestamp_ = timestamp;
282 accel_filter_.addSample(accel, timestamp);
283 gravity_filter_.addSample(accel.norm(), timestamp);
284
285 // Adjust scale of accelerometer
286 Eigen::Vector3d adjusted_accel = accel_filter_.getState() * getAccelScaleFactor_();
287
288 // How different is the acceleration length from gravity?
289 auto diff = std::abs(adjusted_accel.norm() - MATH_GRAVITY_M_S2);
290 auto scale = 1. - diff;
291 if (scale <= 0) {
292 // Too far from gravity to be useful/trusted for orientation
293 // purposes.
294 SIMPLE_IMU_TRACE("Too far from gravity to be useful/trusted.");
295 return false;
296 }
297
298 // This should match the global gravity vector if the rotation
299 // is right.
300 Eigen::Vector3d measuredGravityDirection = (quat_ * adjusted_accel).normalized();
301 auto incremental = Eigen::Quaterniond::FromTwoVectors(measuredGravityDirection, Eigen::Vector3d::UnitY());
302
303 double alpha = scale * gravity_scale_ * dt;
304 Eigen::Quaterniond scaledIncrementalQuat = Eigen::Quaterniond::Identity().slerp(alpha, incremental);
305
306 // Update orientation
307 quat_ = scaledIncrementalQuat * quat_;
308
309 return true;
310}
311
312} // namespace xrt::auxiliary::tracking
void addSample(Scalar sample, timepoint_ns timestamp_ns, Scalar weight=1)
Filter a sample, with an optional weight.
Definition: m_lowpass_float.hpp:143
Scalar getState() const noexcept
Get the filtered value.
Definition: m_lowpass_float.hpp:152
Vector const & getState() const noexcept
Get the filtered value.
Definition: m_lowpass_float_vector.hpp:74
void addSample(Vector const &sample, std::uint64_t timestamp_ns, Scalar weight=1)
Filter a sample, with an optional weight.
Definition: m_lowpass_float_vector.hpp:65
A simple IMU fusion class.
Definition: t_imu_fusion.hpp:42
Eigen::Quaterniond getPredictedQuat(timepoint_ns timestamp) const
Get the predicted orientation at some point in the future.
Definition: t_imu_fusion.hpp:209
bool valid() const noexcept
Definition: t_imu_fusion.hpp:59
bool handleAccel(Eigen::Vector3d const &accel, timepoint_ns timestamp)
Process an accelerometer report.
Definition: t_imu_fusion.hpp:256
void postCorrect()
Normalize internal state.
Definition: t_imu_fusion.hpp:158
Eigen::Matrix3d getRotationMatrix() const
Get the current state orientation as a rotation matrix.
Definition: t_imu_fusion.hpp:86
bool handleGyro(Eigen::Vector3d const &gyro, timepoint_ns timestamp)
Process a gyroscope report.
Definition: t_imu_fusion.hpp:221
Eigen::Vector3d const & getAngVel() const
Get the angular velocity in body space.
Definition: t_imu_fusion.hpp:106
Eigen::Vector3d getRotationVec() const
Get the current state orientation as a rotation vector.
Definition: t_imu_fusion.hpp:77
Eigen::Vector3d getCorrectedWorldAccel(Eigen::Vector3d const &accel) const
Use this to obtain the residual, world-space acceleration in m/s/s not associated with gravity,...
Definition: t_imu_fusion.hpp:145
EIGEN_MAKE_ALIGNED_OPERATOR_NEW SimpleIMUFusion(double gravity_rate=0.9)
Definition: t_imu_fusion.hpp:49
Eigen::Quaterniond getQuat() const
Get the current state orientation.
Definition: t_imu_fusion.hpp:68
u_logging_level
Logging level enum.
Definition: u_logging.h:40
@ U_LOGGING_WARN
Warning messages: indicating a potential problem.
Definition: u_logging.h:44
#define MATH_GRAVITY_M_S2
Standard gravity acceleration constant.
Definition: m_api.h:51
int64_t timepoint_ns
Integer timestamp type.
Definition: u_time.h:70
static double time_ns_to_s(time_duration_ns ns)
Convert nanoseconds duration to double seconds.
Definition: u_time.h:90
int64_t time_duration_ns
Integer duration type in nanoseconds.
Definition: u_time.h:81
C interface to math library.
Low-pass IIR filter.
Low-pass IIR filter on vectors.
Small debug helpers.
Basic logging functionality.
Time-keeping: a clock that is steady, convertible to system time, and ideally high-resolution.