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