blob: 5663ae4ff78e6dc5f1c864763f547ec8bcd6810e [file] [log] [blame]
#include "sensor_fusion.h"
#include <algorithm>
#include <cmath>
#include <private/dvr/eigen.h>
namespace android {
namespace dvr {
namespace {
// --- start of added bits for porting to eigen
// In general, we prefer to add wrappers for things like Inverse() to minimize
// the changes to the imported code, so that merging in upstream changes becomes
// simpler.
inline Matrix3d Inverse(const Matrix3d& matrix) { return matrix.inverse(); }
inline Matrix3d Transpose(const Matrix3d& matrix) { return matrix.transpose(); }
inline Matrix3d RotationMatrixNH(const Rotationd& rotation) {
return rotation.toRotationMatrix();
}
inline double Length(const Vector3d& vector) { return vector.norm(); }
using uint64 = uint64_t;
// --- end of added bits for porting to eigen
static const double kFiniteDifferencingEpsilon = 1e-7;
static const double kEpsilon = 1e-15;
// Default gyroscope frequency. This corresponds to 200 Hz.
static const double kDefaultGyroscopeTimestep_s = 0.005f;
// Maximum time between gyroscope before we start limiting the integration.
static const double kMaximumGyroscopeSampleDelay_s = 0.04f;
// Compute a first-order exponential moving average of changes in accel norm per
// frame.
static const double kSmoothingFactor = 0.5;
// Minimum and maximum values used for accelerometer noise covariance matrix.
// The smaller the sigma value, the more weight is given to the accelerometer
// signal.
static const double kMinAccelNoiseSigma = 0.75;
static const double kMaxAccelNoiseSigma = 7.0;
// Initial value for the diagonal elements of the different covariance matrices.
static const double kInitialStateCovarianceValue = 25.0;
static const double kInitialProcessCovarianceValue = 1.0;
// Maximum accelerometer norm change allowed before capping it covariance to a
// large value.
static const double kMaxAccelNormChange = 0.15;
// Timestep IIR filtering coefficient.
static const double kTimestepFilterCoeff = 0.95;
// Minimum number of sample for timestep filtering.
static const uint32_t kTimestepFilterMinSamples = 10;
// Z direction in start space.
static const Vector3d kCanonicalZDirection(0.0, 0.0, 1.0);
// Computes a axis angle rotation from the input vector.
// angle = norm(a)
// axis = a.normalized()
// If norm(a) == 0, it returns an identity rotation.
static Rotationd RotationFromVector(const Vector3d& a) {
const double norm_a = Length(a);
if (norm_a < kEpsilon) {
return Rotationd::Identity();
}
return Rotationd(AngleAxisd(norm_a, a / norm_a));
}
// --- start of functions ported from pose_prediction.cc
namespace pose_prediction {
// Returns a rotation matrix based on the integration of the gyroscope_value
// over the timestep_s in seconds.
// TODO(pfg): Document the space better here.
//
// @param gyroscope_value gyroscope sensor values.
// @param timestep_s integration period in seconds.
// @return Integration of the gyroscope value the rotation is from Start to
// Sensor Space.
Rotationd GetRotationFromGyroscope(const Vector3d& gyroscope_value,
double timestep_s) {
const double velocity = Length(gyroscope_value);
// When there is no rotation data return an identity rotation.
if (velocity < kEpsilon) {
return Rotationd::Identity();
}
// Since the gyroscope_value is a start from sensor transformation we need to
// invert it to have a sensor from start transformation, hence the minus sign.
// For more info:
// http://developer.android.com/guide/topics/sensors/sensors_motion.html#sensors-motion-gyro
return Rotationd(AngleAxisd(-timestep_s * velocity,
gyroscope_value / velocity));
}
} // namespace pose_prediction
// --- end of functions ported from pose_prediction.cc
} // namespace
SensorFusion::SensorFusion()
: execute_reset_with_next_accelerometer_sample_(false) {
ResetState();
}
void SensorFusion::Reset() {
execute_reset_with_next_accelerometer_sample_ = true;
}
void SensorFusion::ResetState() {
current_state_.timestamp_ns = 0;
current_state_.sensor_from_start_rotation = Rotationd::Identity();
current_state_.sensor_from_start_rotation_velocity = Vector3d::Zero();
current_accelerometer_timestamp_ns_ = 0;
state_covariance_ = Matrix3d::Identity() * kInitialStateCovarianceValue;
process_covariance_ = Matrix3d::Identity() * kInitialProcessCovarianceValue;
accelerometer_measurement_covariance_ =
Matrix3d::Identity() * kMinAccelNoiseSigma * kMinAccelNoiseSigma;
innovation_covariance_.setIdentity();
accelerometer_measurement_jacobian_ = Matrix3d::Zero();
kalman_gain_ = Matrix3d::Zero();
innovation_ = Vector3d::Zero();
accelerometer_measurement_ = Vector3d::Zero();
prediction_ = Vector3d::Zero();
control_input_ = Vector3d::Zero();
state_update_ = Vector3d::Zero();
moving_average_accelerometer_norm_change_ = 0.0;
is_timestep_filter_initialized_ = false;
is_gyroscope_filter_valid_ = false;
is_aligned_with_gravity_ = false;
}
// Here I am doing something wrong relative to time stamps. The state timestamps
// always correspond to the gyrostamps because it would require additional
// extrapolation if I wanted to do otherwise.
// TODO(pfg): investigate about published an updated pose after accelerometer
// data was used for filtering.
PoseState SensorFusion::GetLatestPoseState() const {
std::unique_lock<std::mutex> lock(mutex_);
return current_state_;
}
void SensorFusion::ProcessGyroscopeSample(float v_x, float v_y, float v_z,
uint64 timestamp_ns) {
std::unique_lock<std::mutex> lock(mutex_);
// Don't accept gyroscope sample when waiting for a reset.
if (execute_reset_with_next_accelerometer_sample_) {
return;
}
// Discard outdated samples.
if (current_state_.timestamp_ns >= timestamp_ns) {
// TODO(pfg): Investigate why this happens.
return;
}
// Checks that we received at least one gyroscope sample in the past.
if (current_state_.timestamp_ns != 0) {
// TODO(pfg): roll this in filter gyroscope timestep function.
double current_timestep_s =
static_cast<double>(timestamp_ns - current_state_.timestamp_ns) * 1e-9;
if (current_timestep_s > kMaximumGyroscopeSampleDelay_s) {
if (is_gyroscope_filter_valid_) {
// Replaces the delta timestamp by the filtered estimates of the delta
// time.
current_timestep_s = filtered_gyroscope_timestep_s_;
} else {
current_timestep_s = kDefaultGyroscopeTimestep_s;
}
} else {
FilterGyroscopeTimestep(current_timestep_s);
}
// Only integrate after receiving a accelerometer sample.
if (is_aligned_with_gravity_) {
const Rotationd rotation_from_gyroscope =
pose_prediction::GetRotationFromGyroscope(Vector3d(v_x, v_y, v_z),
current_timestep_s);
current_state_.sensor_from_start_rotation =
rotation_from_gyroscope * current_state_.sensor_from_start_rotation;
current_state_.sensor_from_start_rotation.normalize();
UpdateStateCovariance(RotationMatrixNH(rotation_from_gyroscope));
state_covariance_ =
state_covariance_ +
(process_covariance_ * (current_timestep_s * current_timestep_s));
}
}
// Saves gyroscope event for future prediction.
current_state_.timestamp_ns = timestamp_ns;
current_state_.sensor_from_start_rotation_velocity = Vector3d(v_x, v_y, v_z);
}
// TODO(pfg): move to rotation object for the input.
Vector3d SensorFusion::ComputeInnovation(const Rotationd& pose) {
const Vector3d predicted_down_direction =
RotationMatrixNH(pose) * kCanonicalZDirection;
const Rotationd rotation = Rotationd::FromTwoVectors(
predicted_down_direction, accelerometer_measurement_);
AngleAxisd angle_axis(rotation);
return angle_axis.axis() * angle_axis.angle();
}
void SensorFusion::ComputeMeasurementJacobian() {
for (int dof = 0; dof < 3; dof++) {
// TODO(pfg): Create this delta rotation in the constructor and used unitX..
Vector3d delta = Vector3d::Zero();
delta[dof] = kFiniteDifferencingEpsilon;
const Rotationd epsilon_rotation = RotationFromVector(delta);
const Vector3d delta_rotation = ComputeInnovation(
epsilon_rotation * current_state_.sensor_from_start_rotation);
const Vector3d col =
(innovation_ - delta_rotation) / kFiniteDifferencingEpsilon;
accelerometer_measurement_jacobian_(0, dof) = col[0];
accelerometer_measurement_jacobian_(1, dof) = col[1];
accelerometer_measurement_jacobian_(2, dof) = col[2];
}
}
void SensorFusion::ProcessAccelerometerSample(float acc_x, float acc_y,
float acc_z,
uint64 timestamp_ns) {
std::unique_lock<std::mutex> lock(mutex_);
// Discard outdated samples.
if (current_accelerometer_timestamp_ns_ >= timestamp_ns) {
// TODO(pfg): Investigate why this happens.
return;
}
// Call reset state if required.
if (execute_reset_with_next_accelerometer_sample_.exchange(false)) {
ResetState();
}
accelerometer_measurement_ = Vector3d(acc_x, acc_y, acc_z);
current_accelerometer_timestamp_ns_ = timestamp_ns;
if (!is_aligned_with_gravity_) {
// This is the first accelerometer measurement so it initializes the
// orientation estimate.
current_state_.sensor_from_start_rotation = Rotationd::FromTwoVectors(
kCanonicalZDirection, accelerometer_measurement_);
is_aligned_with_gravity_ = true;
previous_accelerometer_norm_ = Length(accelerometer_measurement_);
return;
}
UpdateMeasurementCovariance();
innovation_ = ComputeInnovation(current_state_.sensor_from_start_rotation);
ComputeMeasurementJacobian();
// S = H * P * H' + R
innovation_covariance_ = accelerometer_measurement_jacobian_ *
state_covariance_ *
Transpose(accelerometer_measurement_jacobian_) +
accelerometer_measurement_covariance_;
// K = P * H' * S^-1
kalman_gain_ = state_covariance_ *
Transpose(accelerometer_measurement_jacobian_) *
Inverse(innovation_covariance_);
// x_update = K*nu
state_update_ = kalman_gain_ * innovation_;
// P = (I - K * H) * P;
state_covariance_ = (Matrix3d::Identity() -
kalman_gain_ * accelerometer_measurement_jacobian_) *
state_covariance_;
// Updates pose and associate covariance matrix.
const Rotationd rotation_from_state_update =
RotationFromVector(state_update_);
current_state_.sensor_from_start_rotation =
rotation_from_state_update * current_state_.sensor_from_start_rotation;
UpdateStateCovariance(RotationMatrixNH(rotation_from_state_update));
}
void SensorFusion::UpdateStateCovariance(const Matrix3d& motion_update) {
state_covariance_ =
motion_update * state_covariance_ * Transpose(motion_update);
}
void SensorFusion::FilterGyroscopeTimestep(double gyroscope_timestep_s) {
if (!is_timestep_filter_initialized_) {
// Initializes the filter.
filtered_gyroscope_timestep_s_ = gyroscope_timestep_s;
num_gyroscope_timestep_samples_ = 1;
is_timestep_filter_initialized_ = true;
return;
}
// Computes the IIR filter response.
filtered_gyroscope_timestep_s_ =
kTimestepFilterCoeff * filtered_gyroscope_timestep_s_ +
(1 - kTimestepFilterCoeff) * gyroscope_timestep_s;
++num_gyroscope_timestep_samples_;
if (num_gyroscope_timestep_samples_ > kTimestepFilterMinSamples) {
is_gyroscope_filter_valid_ = true;
}
}
void SensorFusion::UpdateMeasurementCovariance() {
const double current_accelerometer_norm = Length(accelerometer_measurement_);
// Norm change between current and previous accel readings.
const double current_accelerometer_norm_change =
std::abs(current_accelerometer_norm - previous_accelerometer_norm_);
previous_accelerometer_norm_ = current_accelerometer_norm;
moving_average_accelerometer_norm_change_ =
kSmoothingFactor * current_accelerometer_norm_change +
(1. - kSmoothingFactor) * moving_average_accelerometer_norm_change_;
// If we hit the accel norm change threshold, we use the maximum noise sigma
// for the accel covariance. For anything below that, we use a linear
// combination between min and max sigma values.
const double norm_change_ratio =
moving_average_accelerometer_norm_change_ / kMaxAccelNormChange;
const double accelerometer_noise_sigma = std::min(
kMaxAccelNoiseSigma,
kMinAccelNoiseSigma +
norm_change_ratio * (kMaxAccelNoiseSigma - kMinAccelNoiseSigma));
// Updates the accel covariance matrix with the new sigma value.
accelerometer_measurement_covariance_ = Matrix3d::Identity() *
accelerometer_noise_sigma *
accelerometer_noise_sigma;
}
} // namespace dvr
} // namespace android