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 
16 #include "math/m_lowpass_float.hpp"
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 
28 DEBUG_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 
36 namespace xrt::auxiliary::tracking {
37 
38 /*!
39  * @brief A simple IMU fusion class.
40  */
42 {
43 public:
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 
163 private:
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 
208 inline 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 }
220 inline bool
221 SimpleIMUFusion::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 }
255 inline bool
256 SimpleIMUFusion::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
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
Vector const & getState() const noexcept
Get the filtered value.
Definition: m_lowpass_float_vector.hpp:74
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.