Monado OpenXR Runtime
Loading...
Searching...
No Matches
imu_preintegration.hpp
Go to the documentation of this file.
1// Copyright 2026, Beyley Cardellio
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief Generic math helpers for IMU preintegration.
6 * @author Beyley Cardellio <ep1cm1n10n123@gmail.com>
7 * @ingroup tracking
8 */
9
10#pragma once
11
12#include "os/os_time.h"
13
15#include "math/m_quatexpmap.hpp"
17
18#include <Eigen/Core>
19#include <Eigen/Geometry>
20
21#include "ceres/jet.h"
22
23#include "internal_math.hpp"
24
25#include <cstring>
26#include <cmath>
27#include <mutex>
28#include <string>
29#include <algorithm>
30#include <span>
31#include <cassert>
32
33
34namespace xrt::tracking::constellation::optimizer {
35
36using namespace xrt::auxiliary::math;
37
38using Eigen::Map;
39using Eigen::Matrix;
40using Eigen::Matrix3d;
41using Eigen::MatrixBase;
42using Eigen::Quaternion;
43using Eigen::Quaterniond;
44using Eigen::Vector;
45using Eigen::Vector2;
46using Eigen::Vector3;
47using Eigen::Vector3d;
48using Eigen::Vector4;
49
50using ceres::Jet;
51
52// @todo Tune this per device. Right now these are CV1 HMD numbers.
53//! BMI055 gyroscope noise density, 0.014 deg/s/sqrt(Hz) from the datasheet, in rad/s/sqrt(Hz).
54constexpr double kGyroNoiseDensity = 2.44e-4;
55//! BMI055 accelerometer noise density, 150 ug/sqrt(Hz) from the datasheet, in (m/s^2)/sqrt(Hz).
56constexpr double kAccelNoiseDensity = 1.47e-3;
57
58// @todo These need to be computed properly, not guessed.
59constexpr double kAccelBiasRandomWalkSigma = 1e-3; // (m/s^2) / sqrt(s)
60constexpr double kGyroBiasRandomWalkSigma = 1e-4; // (rad/s) / sqrt(s)
61constexpr double kAccelScaleRandomWalkSigma = 1e-5; // 1 / sqrt(s)
62
63// @todo This needs to be computed properly, not hardcoded for CV1.
64constexpr double kAccelBiasAnchorSigma = 0.687; // m/s^2
65constexpr double kGyroBiasAnchorSigma = 0.0175; // rad/s
66constexpr double kAccelScaleAnchorSigma = 0.05; // dimensionless
67
68//! Largest accelerometer bias to allow, in m/s^2. The BMI055's zero-g offset is +-70mg, so this is ~1.4x that.
69constexpr double kMaxAccelBias = 1.0;
70//! Largest gyroscope bias to allow, in rad/s. The BMI055's zero-rate offset is +-1 deg/s, this is ~3 deg/s.
71constexpr double kMaxGyroBias = 0.05;
72
73// Some reasonable defaults. This should be pretty close to 1.0 already in most cases.
74constexpr double kMinAccelScale = 0.8;
75constexpr double kMaxAccelScale = 1.0 / kMinAccelScale;
76
77/*!
78 * Camera samples from the same mosaic closer together than this are treated as one keyframe. Sometimes frame timestamps
79 * are a couple microseconds off due to guesswork/interpolation, this groups them together.
80 *
81 * @todo We should by default group based on sequence_id! However, to make external users more reliable, let's not rely
82 * on that just yet until the infrasturcture for matching sequence ID across multiple cameras is available.
83 */
85
86enum class ImuBiasStateIndex : int
87{
88 AccelBiasX,
89 AccelBiasY,
90 AccelBiasZ,
91
92 GyroBiasX,
93 GyroBiasY,
94 GyroBiasZ,
95
96 AccelScaleX,
97 AccelScaleY,
98 AccelScaleZ,
99
100 NumIndices,
101};
102
103typedef ceres::Jet<double, static_cast<int>(ImuBiasStateIndex::NumIndices)> ImuBiasJet;
104
105/*!
106 * A full-state IMU bias, with Accelerometer Bias, Gyroscope Bias, and Accelerometer Scale.
107 *
108 * Everything here belongs to one physical IMU, so a solve over several devices holds one of these per device. The
109 * strength of gravity is shared by all of them and lives in @ref WorldGravity instead.
110 */
111template <typename T> struct ImuBias
112{
113 static_assert(!std::is_const_v<T>,
114 "block scalar must not be const — did a `T*` overload deduce T = const double?");
115
116 static constexpr int kNumParameters = static_cast<int>(ImuBiasStateIndex::NumIndices);
117
118 static constexpr int kAccelBiasIndex = static_cast<int>(ImuBiasStateIndex::AccelBiasX);
119 static constexpr int kGyroBiasIndex = static_cast<int>(ImuBiasStateIndex::GyroBiasX);
120 static constexpr int kAccelScaleIndex = static_cast<int>(ImuBiasStateIndex::AccelScaleX);
121
122 Vector3<T> accel_bias;
123 Vector3<T> gyro_bias;
124
125 Vector3<T> accel_scale;
126
127 ImuBias()
128 : accel_bias(Vector3<T>::Zero()), gyro_bias(Vector3<T>::Zero()), accel_scale(Vector3<T>::Constant(T(1)))
129 {}
130
131 ImuBias(const Vector3<T> &accel_bias, const Vector3<T> &gyro_bias, const Vector3<T> &accel_scale)
132 : accel_bias(accel_bias), gyro_bias(gyro_bias), accel_scale(accel_scale)
133 {}
134
135 ImuBias(Eigen::Ref<const Vector<T, kNumParameters>> parameters)
136 : accel_bias(parameters.template segment<3>(kAccelBiasIndex)),
137 gyro_bias(parameters.template segment<3>(kGyroBiasIndex)),
138 accel_scale(parameters.template segment<3>(kAccelScaleIndex))
139 {}
140
141 void
142 pack(Eigen::Ref<Vector<T, kNumParameters>> parameters)
143 {
144 parameters.template segment<3>(kAccelBiasIndex) = this->accel_bias;
145 parameters.template segment<3>(kGyroBiasIndex) = this->gyro_bias;
146 parameters.template segment<3>(kAccelScaleIndex) = this->accel_scale;
147 }
148
150 seed() const
151 {
152 auto seeded = ImuBias<ImuBiasJet>();
153 seeded.accel_bias = {{this->accel_bias.x(), kAccelBiasIndex + 0},
154 {this->accel_bias.y(), kAccelBiasIndex + 1},
155 {this->accel_bias.z(), kAccelBiasIndex + 2}};
156 seeded.gyro_bias = {{this->gyro_bias.x(), kGyroBiasIndex + 0},
157 {this->gyro_bias.y(), kGyroBiasIndex + 1},
158 {this->gyro_bias.z(), kGyroBiasIndex + 2}};
159 seeded.accel_scale = {{this->accel_scale.x(), kAccelScaleIndex + 0},
160 {this->accel_scale.y(), kAccelScaleIndex + 1},
161 {this->accel_scale.z(), kAccelScaleIndex + 2}};
162 return seeded;
163 }
164};
165
166enum class ImuBiasCovarianceIndex : int
167{
168 RotX,
169 RotY,
170 RotZ,
171
172 VelocityX,
173 VelocityY,
174 VelocityZ,
175
176 PositionX,
177 PositionY,
178 PositionZ,
179
180 NumIndices,
181};
182
183static constexpr int kImuBiasRotCovIndex = static_cast<int>(ImuBiasCovarianceIndex::RotX);
184static constexpr int kImuBiasVelCovIndex = static_cast<int>(ImuBiasCovarianceIndex::VelocityX);
185static constexpr int kImuBiasPosCovIndex = static_cast<int>(ImuBiasCovarianceIndex::PositionX);
186static constexpr int kNumImuBiasCovIndices = static_cast<int>(ImuBiasCovarianceIndex::NumIndices);
187
188//! [rotation; velocity; position], the order computeImuResidual writes its residual in.
189typedef Matrix<double, kNumImuBiasCovIndices, kNumImuBiasCovIndices> PreintegrationCovarianceMatrix;
190
192{
193 static constexpr int kNumResiduals = 9;
194
195 //! The preintegrated rotation from the IMU samples, as a quaternion.
196 Quaterniond Q_start_end;
197 //! This is really "delta integrated specific force", but we call it delta_velocity for simplicity.
199 //! This is really "delta double-integrated specific force", but we call it delta_position for simplicity.
201
202 //! The whitening matrix for this preintegration, the lower triangular.
204
205 double dt;
206
207 Vector3d accel_bias_at_integration;
208 Vector3d gyro_bias_at_integration;
209
210 Vector3d accel_scale_at_integration;
211
212 /*
213 * Bias Jacobians
214 */
215
216 Matrix3d J_R_bg; //!< d(Q_start_cur) / d(gyro_bias)
217
218 Matrix3d J_v_ba; //!< d(delta_velocity) / d(accel_bias)
219 Matrix3d J_v_sa; //!< d(delta_velocity) / d(accel_scale)
220 Matrix3d J_v_bg; //!< d(delta_velocity) / d(gyro_bias)
221
222 Matrix3d J_p_ba; //!< d(delta_position) / d(accel_bias)
223 Matrix3d J_p_sa; //!< d(delta_position) / d(accel_scale)
224 Matrix3d J_p_bg; //!< d(delta_position) / d(gyro_bias)
225
227 Identity()
228 {
229 return {
230 .Q_start_end = Quaterniond::Identity(),
231 .delta_velocity = Vector3d::Zero(),
232 .delta_position = Vector3d::Zero(),
233
234 .whitening = PreintegrationCovarianceMatrix::Zero(),
235
236 .dt = 0,
237
238 .accel_bias_at_integration = Vector3d::Zero(),
239 .gyro_bias_at_integration = Vector3d::Zero(),
240 .accel_scale_at_integration = Vector3d::Constant(1.0),
241
242 .J_R_bg = Matrix3d::Zero(),
243 .J_v_ba = Matrix3d::Zero(),
244 .J_v_sa = Matrix3d::Zero(),
245 .J_v_bg = Matrix3d::Zero(),
246 .J_p_ba = Matrix3d::Zero(),
247 .J_p_sa = Matrix3d::Zero(),
248 .J_p_bg = Matrix3d::Zero(),
249 };
250 }
251};
252
253/*!
254 * Does IMU pre-integration on a set of IMU samples, interpolates the starting sample to the start time, and returns the
255 * pre-integrated samples.
256 *
257 * @param imu_samples The IMU samples to preintegrate
258 * @param first_sample_after_end_time The first sample after the end timestamp
259 * @param imu_bias The Jet-seeded IMU biases
260 * @param start_time_ns The start time of the preintegration
261 * @param end_time_ns The end time of the preintegration
262 */
264preintegrate(const std::span<const xrt_imu_sample> &imu_samples,
265 const xrt_imu_sample &first_sample_after_end_time,
266 const ImuBias<ImuBiasJet> &imu_bias,
267 timepoint_ns start_time_ns,
268 timepoint_ns end_time_ns);
269
270
271/*
272 *
273 * IMU residuals
274 *
275 */
276
277template <typename T>
278static void
279computeImuResidual(const PreintegratedImuSamples &imu_preintegration,
280 const PoseWithVelocity<T> &start_T_world_device,
281 const PoseWithVelocity<T> &end_T_world_device,
282 const ImuBias<T> &imu_bias,
283 const WorldGravity<T> &world_gravity,
284 Vector<T, PreintegratedImuSamples::kNumResiduals> &residual)
285{
286 Quaternion<T> Q_world_device_start;
287 start_T_world_device.getQuaternion(Q_world_device_start);
288 auto start_Q_device_world = Q_world_device_start.conjugate();
289
290 Quaternion<T> end_Q_world_device;
291 end_T_world_device.getQuaternion(end_Q_world_device);
292
293 Vector3<T> gravity_vector = world_gravity.toVector();
294
295 // I'm sure we've all seen these exact formulas a million times.
296 auto end_predicted_Q_start = start_Q_device_world * end_Q_world_device;
297 auto predicted_start_delta_velocity = start_Q_device_world * //
298 (end_T_world_device.velocity - //
299 start_T_world_device.velocity - //
300 (gravity_vector * imu_preintegration.dt)); //
301 auto predicted_start_delta_position = //
302 start_Q_device_world * //
303 (end_T_world_device.translation - //
304 start_T_world_device.translation - //
305 start_T_world_device.velocity * imu_preintegration.dt - //
306 (T(0.5) * gravity_vector * imu_preintegration.dt * imu_preintegration.dt)); //
307
308 // Compute the deltas for how much IMU bias has changed from now to preintegration.
309 Vector3<T> delta_accel_bias = imu_bias.accel_bias - imu_preintegration.accel_bias_at_integration.cast<T>();
310 Vector3<T> delta_gyro_bias = imu_bias.gyro_bias - imu_preintegration.gyro_bias_at_integration.cast<T>();
311 Vector3<T> delta_accel_scale = imu_bias.accel_scale - imu_preintegration.accel_scale_at_integration.cast<T>();
312
313 // Correct the amount of rotation by the change in gyro bias
314 Quaternion<T> corrected_Q_start_end = //
315 imu_preintegration.Q_start_end.cast<T>() * //
316 quat_exp_so3(imu_preintegration.J_R_bg.cast<T>() * delta_gyro_bias); //
317
318 // Correct the change in velocity and position by the change by accel and gyro bias/scale.
319 Vector3<T> corrected_start_delta_velocity = //
320 imu_preintegration.delta_velocity.cast<T>() //
321 + imu_preintegration.J_v_ba.cast<T>() * delta_accel_bias //
322 + imu_preintegration.J_v_bg.cast<T>() * delta_gyro_bias //
323 + imu_preintegration.J_v_sa.cast<T>() * delta_accel_scale; //
324
325 Vector3<T> corrected_start_delta_position = //
326 imu_preintegration.delta_position.cast<T>() //
327 + imu_preintegration.J_p_ba.cast<T>() * delta_accel_bias //
328 + imu_preintegration.J_p_bg.cast<T>() * delta_gyro_bias //
329 + imu_preintegration.J_p_sa.cast<T>() * delta_accel_scale; //
330
331 // Compute the amount of rotation error for the predicted vs. measured
332 auto delta_rotation_error = corrected_Q_start_end.conjugate() * end_predicted_Q_start;
333 if (delta_rotation_error.w() < T(0)) {
334 delta_rotation_error.coeffs() = -delta_rotation_error.coeffs();
335 }
336
337 residual.template segment<3>(0) = quat_ln_so3(delta_rotation_error);
338 residual.template segment<3>(3) = predicted_start_delta_velocity - corrected_start_delta_velocity;
339 residual.template segment<3>(6) = predicted_start_delta_position - corrected_start_delta_position;
340}
341
342/*
343 *
344 * Cost functors
345 *
346 */
347
348//! IMU preintegration factor, between each camera keyframe.
350{
351 PreintegratedImuSamples preintegration;
352
353 template <typename T>
354 bool
355 operator()(const T *const world_gravity_parameters,
356 const T *const imu_bias_parameters,
357 const T *const start_keyframe_parameters,
358 const T *const end_keyframe_parameters,
359 T *residuals) const
360 {
361 const WorldGravity<T> world_gravity{
362 Map<const Vector<T, WorldGravity<T>::kNumParameters>>(world_gravity_parameters)};
363
364 const ImuBias<T> imu_bias{Map<const Vector<T, ImuBias<T>::kNumParameters>>(imu_bias_parameters)};
365
366 const PoseWithVelocity<T> start_T_world_device{
367 Map<const Vector<T, PoseWithVelocity<T>::kNumParameters>>(start_keyframe_parameters)};
368 const PoseWithVelocity<T> end_T_world_device{
369 Map<const Vector<T, PoseWithVelocity<T>::kNumParameters>>(end_keyframe_parameters)};
370
371 Vector<T, PreintegratedImuSamples::kNumResiduals> computed_residual;
372 computeImuResidual<T>(this->preintegration, //
373 start_T_world_device, //
374 end_T_world_device, //
375 imu_bias, //
376 world_gravity, //
377 computed_residual); //
378
379 Map<Vector<T, PreintegratedImuSamples::kNumResiduals>> out_residual(residuals);
380 out_residual = this->preintegration.whitening.cast<T>() * computed_residual;
381
382 return true;
383 }
384};
385
386typedef ceres::AutoDiffCostFunction<ImuCostFunctor, //
387 PreintegratedImuSamples::kNumResiduals, //
392 ImuCostFunction;
393
394//! IMU Bias random walk cost functor, between each keyframe.
396{
397 static constexpr int kNumResiduals = ImuBias<double>::kNumParameters;
398
399 //! Seconds spanned by this interval
400 double dt;
401
402 template <typename T>
403 bool
404 operator()(const T *const start_bias_parameters, //
405 const T *const end_bias_parameters, //
406 T *residuals) const //
407 {
408 const ImuBias<T> start{Map<const Vector<T, ImuBias<T>::kNumParameters>>(start_bias_parameters)};
409 const ImuBias<T> end{Map<const Vector<T, ImuBias<T>::kNumParameters>>(end_bias_parameters)};
410
411 assert(dt > 0);
412
413 const T sqrt_dt = T(std::sqrt(this->dt));
414
415 Map<Vector3<T>> accel_residual(residuals + 0);
416 Map<Vector3<T>> gyro_residual(residuals + 3);
417 Map<Vector3<T>> scale_residual(residuals + 6);
418
419 /*
420 * Assume that the difference between the start and ending bias is close to zero (it should be),
421 * whitened by how much we *expect* it to walk.
422 */
423 accel_residual = (end.accel_bias - start.accel_bias) / (T(kAccelBiasRandomWalkSigma) * sqrt_dt);
424 gyro_residual = (end.gyro_bias - start.gyro_bias) / (T(kGyroBiasRandomWalkSigma) * sqrt_dt);
425 scale_residual = (end.accel_scale - start.accel_scale) / (T(kAccelScaleRandomWalkSigma) * sqrt_dt);
426
427 // @todo Manually write the jacobians for these for performance
428
429 return true;
430 }
431};
432
433typedef ceres::AutoDiffCostFunction<ImuBiasRandomWalkCostFunctor, //
434 ImuBiasRandomWalkCostFunctor::kNumResiduals, //
437 ImuBiasRandomWalkCostFunction;
438
439//! IMU Bias anchor cost functor, to hold it at some value when marginalization has no guess for it.
441{
442 static constexpr int kNumResiduals = ImuBias<double>::kNumParameters;
443
444 ImuBias<double> anchor;
445
446 template <typename T>
447 bool
448 operator()(const T *const bias_parameters, //
449 T *residuals) const //
450 {
451 const ImuBias<T> bias{Map<const Vector<T, ImuBias<T>::kNumParameters>>(bias_parameters)};
452
453 Map<Vector3<T>> accel_residual(residuals + 0);
454 Map<Vector3<T>> gyro_residual(residuals + 3);
455 Map<Vector3<T>> scale_residual(residuals + 6);
456
457 // Assume the IMU bias is close to what we expect it to be.
458 accel_residual = (bias.accel_bias - this->anchor.accel_bias) / T(kAccelBiasAnchorSigma);
459 gyro_residual = (bias.gyro_bias - this->anchor.gyro_bias) / T(kGyroBiasAnchorSigma);
460 scale_residual = (bias.accel_scale - this->anchor.accel_scale) / T(kAccelScaleAnchorSigma);
461
462 // @todo Manually write the jacobians for these for performance
463
464 return true;
465 }
466};
467
468typedef ceres::AutoDiffCostFunction<ImuBiasAnchorCostFunctor, //
469 ImuBiasAnchorCostFunctor::kNumResiduals, //
471 ImuBiasAnchorCostFunction;
472
473}; // namespace xrt::tracking::constellation::optimizer
Definition utility_northstar.h:779
Definition utility_northstar.h:16
int64_t timepoint_ns
Integer timestamp type.
Definition u_time.h:77
int64_t time_duration_ns
Integer duration type in nanoseconds.
Definition u_time.h:88
constexpr double kMaxGyroBias
Largest gyroscope bias to allow, in rad/s. The BMI055's zero-rate offset is +-1 deg/s,...
Definition imu_preintegration.hpp:71
constexpr time_duration_ns kKeyframeGroupingToleranceNs
Camera samples from the same mosaic closer together than this are treated as one keyframe.
Definition imu_preintegration.hpp:84
PreintegratedImuSamples preintegrate(const std::span< const xrt_imu_sample > &imu_samples, const xrt_imu_sample &first_sample_after_end_time, const ImuBias< ImuBiasJet > &imu_bias, timepoint_ns start_time_ns, timepoint_ns end_time_ns)
Does IMU pre-integration on a set of IMU samples, interpolates the starting sample to the start time,...
Definition internal_math.cpp:374
constexpr double kMaxAccelBias
Largest accelerometer bias to allow, in m/s^2. The BMI055's zero-g offset is +-70mg,...
Definition imu_preintegration.hpp:69
constexpr double kGyroNoiseDensity
BMI055 gyroscope noise density, 0.014 deg/s/sqrt(Hz) from the datasheet, in rad/s/sqrt(Hz).
Definition imu_preintegration.hpp:54
constexpr double kAccelNoiseDensity
BMI055 accelerometer noise density, 150 ug/sqrt(Hz) from the datasheet, in (m/s^2)/sqrt(Hz).
Definition imu_preintegration.hpp:56
Matrix< double, kNumImuBiasCovIndices, kNumImuBiasCovIndices > PreintegrationCovarianceMatrix
[rotation; velocity; position], the order computeImuResidual writes its residual in.
Definition imu_preintegration.hpp:189
Generic math helpers for constellation optimizer code.
Interoperability helpers connecting internal math types and Eigen.
Base implementations for math library.
Helpers for quatexpmap math for bigceres usage.
C++-only functionality in the Math helper library.
Definition m_documentation.hpp:15
Eigen::Quaternion< typename Derived::Scalar > quat_exp_so3(Eigen::MatrixBase< Derived > const &vec)
Fully-templated free function for quaternion exponentiation, SO(3) version as described by Grassia.
Definition m_quatexpmap.hpp:119
Eigen::Matrix< Scalar, 3, 1 > quat_ln_so3(Eigen::Quaternion< Scalar > const &quat)
Fully-templated free function for quaternion log map, SO(3) version.
Definition m_quatexpmap.hpp:148
Wrapper around OS native time functions.
IMU Bias anchor cost functor, to hold it at some value when marginalization has no guess for it.
Definition imu_preintegration.hpp:441
A full-state IMU bias, with Accelerometer Bias, Gyroscope Bias, and Accelerometer Scale.
Definition imu_preintegration.hpp:112
IMU Bias random walk cost functor, between each keyframe.
Definition imu_preintegration.hpp:396
double dt
Seconds spanned by this interval.
Definition imu_preintegration.hpp:400
IMU preintegration factor, between each camera keyframe.
Definition imu_preintegration.hpp:350
Pose with 9 degrees of freedom, translation, rotation, and linear velocity.
Definition internal_math.hpp:334
Matrix3d J_p_sa
d(delta_position) / d(accel_scale)
Definition imu_preintegration.hpp:223
Matrix3d J_v_sa
d(delta_velocity) / d(accel_scale)
Definition imu_preintegration.hpp:219
Quaterniond Q_start_end
The preintegrated rotation from the IMU samples, as a quaternion.
Definition imu_preintegration.hpp:196
Matrix3d J_v_bg
d(delta_velocity) / d(gyro_bias)
Definition imu_preintegration.hpp:220
PreintegrationCovarianceMatrix whitening
The whitening matrix for this preintegration, the lower triangular.
Definition imu_preintegration.hpp:203
Matrix3d J_p_bg
d(delta_position) / d(gyro_bias)
Definition imu_preintegration.hpp:224
Matrix3d J_p_ba
d(delta_position) / d(accel_bias)
Definition imu_preintegration.hpp:222
Matrix3d J_v_ba
d(delta_velocity) / d(accel_bias)
Definition imu_preintegration.hpp:218
Vector3d delta_position
This is really "delta double-integrated specific force", but we call it delta_position for simplicity...
Definition imu_preintegration.hpp:200
Matrix3d J_R_bg
d(Q_start_cur) / d(gyro_bias)
Definition imu_preintegration.hpp:216
Vector3d delta_velocity
This is really "delta integrated specific force", but we call it delta_velocity for simplicity.
Definition imu_preintegration.hpp:198
The strength of gravity in the world, as a parameter block of its own.
Definition internal_math.hpp:187
Vector3< T > toVector() const
Gravity as a vector, which points along +Y since world up is -Y in the OpenCV space the optimizer wor...
Definition internal_math.hpp:213
IMU Sample.
Definition xrt_tracking.h:143
#define U_TIME_1MS_IN_NS
The number of nanoseconds in a millisecond.
Definition u_time.h:54