mirror of
https://github.com/mii443/rust-openvr.git
synced 2025-08-25 09:39:45 +00:00
added linux makefile
This commit is contained in:
904
modules/oculus_sdk_linux/LibOVR/Src/OVR_SensorFusion.cpp
Normal file
904
modules/oculus_sdk_linux/LibOVR/Src/OVR_SensorFusion.cpp
Normal file
@ -0,0 +1,904 @@
|
||||
/************************************************************************************
|
||||
|
||||
Filename : OVR_SensorFusion.cpp
|
||||
Content : Methods that determine head orientation from sensor data over time
|
||||
Created : October 9, 2012
|
||||
Authors : Michael Antonov, Steve LaValle, Dov Katz, Max Katsev, Dan Gierl
|
||||
|
||||
Copyright : Copyright 2014 Oculus VR, Inc. All Rights reserved.
|
||||
|
||||
Licensed under the Oculus VR Rift SDK License Version 3.1 (the "License");
|
||||
you may not use the Oculus VR Rift SDK except in compliance with the License,
|
||||
which is provided at the time of installation or download, or which
|
||||
otherwise accompanies this software in either electronic or hard copy form.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.oculusvr.com/licenses/LICENSE-3.1
|
||||
|
||||
Unless required by applicable law or agreed to in writing, the Oculus VR SDK
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*************************************************************************************/
|
||||
|
||||
#include "OVR_SensorFusion.h"
|
||||
#include "Kernel/OVR_Log.h"
|
||||
#include "Kernel/OVR_System.h"
|
||||
#include "OVR_JSON.h"
|
||||
#include "OVR_Profile.h"
|
||||
#include "OVR_Stereo.h"
|
||||
#include "OVR_Recording.h"
|
||||
|
||||
// Temporary for debugging
|
||||
bool Global_Flag_1 = true;
|
||||
|
||||
//Convenient global variables to temporarily extract this data.
|
||||
float TPH_CameraPoseOrientationWxyz[4];
|
||||
double TPH_CameraPoseConfidence;
|
||||
double TPH_CameraPoseConfidenceThresholdOverrideIfNonZero = 0;
|
||||
bool TPH_IsPositionTracked = false;
|
||||
|
||||
|
||||
namespace OVR {
|
||||
|
||||
const Transformd DefaultWorldFromCamera(Quatd(), Vector3d(0, 0, -1));
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
// ***** Sensor Fusion
|
||||
|
||||
SensorFusion::SensorFusion(SensorDevice* sensor)
|
||||
: ExposureRecordHistory(100), LastMessageExposureFrame(NULL),
|
||||
FocusDirection(Vector3d(0, 0, 0)), FocusFOV(0.0),
|
||||
FAccelInImuFrame(1000), FAccelInCameraFrame(1000), FAngV(20),
|
||||
EnableGravity(true), EnableYawCorrection(true), MagCalibrated(false),
|
||||
EnableCameraTiltCorrection(true),
|
||||
MotionTrackingEnabled(true), VisionPositionEnabled(true),
|
||||
CenterPupilDepth(0.0)
|
||||
{
|
||||
pHandler = new BodyFrameHandler(this);
|
||||
|
||||
// And the clock is running...
|
||||
LogText("*** SensorFusion Startup: TimeSeconds = %f\n", Timer::GetSeconds());
|
||||
|
||||
if (sensor)
|
||||
AttachToSensor(sensor);
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
SensorFusion::~SensorFusion()
|
||||
{
|
||||
delete(pHandler);
|
||||
}
|
||||
|
||||
bool SensorFusion::AttachToSensor(SensorDevice* sensor)
|
||||
{
|
||||
pHandler->RemoveHandlerFromDevices();
|
||||
Reset();
|
||||
|
||||
if (sensor != NULL)
|
||||
{
|
||||
// cache mag calibration state
|
||||
MagCalibrated = sensor->IsMagCalibrated();
|
||||
|
||||
// Load IMU position
|
||||
Array<PositionCalibrationReport> reports;
|
||||
bool result = sensor->GetAllPositionCalibrationReports(&reports);
|
||||
if (result)
|
||||
{
|
||||
PositionCalibrationReport imu = reports[reports.GetSize() - 1];
|
||||
OVR_ASSERT(imu.PositionType == PositionCalibrationReport::PositionType_IMU);
|
||||
// convert from vision to the world frame
|
||||
// TBD convert rotation as necessary?
|
||||
imu.Position.x *= -1.0;
|
||||
imu.Position.z *= -1.0;
|
||||
|
||||
ImuFromScreen = Transformd(Quatd(imu.Normal, imu.Angle), imu.Position).Inverted();
|
||||
|
||||
Recording::GetRecorder().RecordLedPositions(reports);
|
||||
Recording::GetRecorder().RecordDeviceIfcVersion(sensor->GetDeviceInterfaceVersion());
|
||||
}
|
||||
|
||||
// Repopulate CPFOrigin
|
||||
SetCenterPupilDepth(CenterPupilDepth);
|
||||
|
||||
// Subscribe to sensor updates
|
||||
sensor->AddMessageHandler(pHandler);
|
||||
|
||||
// Initialize the sensor state
|
||||
// TBD: This is a hack to avoid a race condition if sensor status is checked immediately
|
||||
// after sensor creation but before any data has flowed through. We should probably
|
||||
// not depend strictly on data flow to determine capabilities like orientation and position
|
||||
// tracking, or else use some sort of synchronous method to wait for data
|
||||
LocklessState init;
|
||||
init.StatusFlags = Status_OrientationTracked;
|
||||
UpdatedState.SetState(init);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resets the current orientation
|
||||
void SensorFusion::Reset()
|
||||
{
|
||||
Lock::Locker lockScope(pHandler->GetHandlerLock());
|
||||
|
||||
UpdatedState.SetState(LocklessState());
|
||||
WorldFromImu = PoseState<double>();
|
||||
WorldFromImu.Pose = ImuFromCpf.Inverted(); // place CPF at the origin, not the IMU
|
||||
CameraFromImu = PoseState<double>();
|
||||
VisionError = PoseState<double>();
|
||||
WorldFromCamera = DefaultWorldFromCamera;
|
||||
WorldFromCameraConfidence = -1;
|
||||
|
||||
ExposureRecordHistory.Clear();
|
||||
NextExposureRecord = ExposureRecord();
|
||||
LastMessageExposureFrame = MessageExposureFrame(NULL);
|
||||
LastVisionAbsoluteTime = 0;
|
||||
Stage = 0;
|
||||
|
||||
MagRefs.Clear();
|
||||
MagRefIdx = -1;
|
||||
MagCorrectionIntegralTerm = Quatd();
|
||||
AccelOffset = Vector3d();
|
||||
|
||||
FAccelInCameraFrame.Clear();
|
||||
FAccelInImuFrame.Clear();
|
||||
FAngV.Clear();
|
||||
|
||||
setNeckPivotFromPose ( WorldFromImu.Pose );
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
// Vision & message processing
|
||||
|
||||
void SensorFusion::OnVisionFailure()
|
||||
{
|
||||
// do nothing
|
||||
Recording::GetRecorder().RecordVisionSuccess(false);
|
||||
}
|
||||
|
||||
void SensorFusion::OnVisionPreviousFrame(const Transform<double>& cameraFromImu)
|
||||
{
|
||||
// simply save the observation for use in the next OnVisionSuccess call;
|
||||
// this should not have unintended side-effects for position filtering,
|
||||
// since the vision time is not updated and the system keeps thinking we don't have vision yet
|
||||
CameraFromImu.Pose = cameraFromImu;
|
||||
}
|
||||
|
||||
void SensorFusion::OnVisionSuccess(const Transform<double>& cameraFromImu, UInt32 exposureCounter)
|
||||
{
|
||||
Lock::Locker lockScope(pHandler->GetHandlerLock());
|
||||
|
||||
Recording::GetRecorder().RecordVisionSuccess(true);
|
||||
|
||||
LastVisionAbsoluteTime = GetTime();
|
||||
|
||||
// ********* LastVisionExposureRecord *********
|
||||
|
||||
// Skip old data and use the record that matches the exposure counter
|
||||
while (!ExposureRecordHistory.IsEmpty() &&
|
||||
(ExposureRecordHistory.PeekFront().ExposureCounter <= exposureCounter))
|
||||
{
|
||||
LastVisionExposureRecord = ExposureRecordHistory.PopFront();
|
||||
}
|
||||
|
||||
// Use current values if we don't have historical data
|
||||
// Right now, this will happen if we get first frame after prediction failure,
|
||||
// and this exposure wasn't in the buffer. (TBD: Unlikely.. unless IMU message wasn't sent?)
|
||||
if (LastVisionExposureRecord.ExposureCounter != exposureCounter)
|
||||
LastVisionExposureRecord = ExposureRecord(exposureCounter, GetTime(), WorldFromImu, PoseState<double>());
|
||||
|
||||
// ********* CameraFromImu *********
|
||||
|
||||
// This is stored in the camera frame, so need to be careful when combining with the IMU data,
|
||||
// which is in the world frame
|
||||
|
||||
Transformd cameraFromImuPrev = CameraFromImu.Pose;
|
||||
CameraFromImu.Pose = cameraFromImu;
|
||||
CameraFromImu.TimeInSeconds = LastVisionExposureRecord.ExposureTime;
|
||||
|
||||
// Check LastVisionExposureRecord.Delta.TimeInSeconds to avoid divide by zero, which we could (rarely)
|
||||
// get if we didn't have exposures delta for history (skipped exposure counters
|
||||
// due to video mode change that stalls USB, etc).
|
||||
if (LastVisionExposureRecord.ImuOnlyDelta.TimeInSeconds > 0.001)
|
||||
{
|
||||
Vector3d visionVelocityInImuFrame = (cameraFromImu.Translation - cameraFromImuPrev.Translation) /
|
||||
LastVisionExposureRecord.ImuOnlyDelta.TimeInSeconds;
|
||||
// Use the accel data to estimate the velocity at the exposure time
|
||||
// (as opposed to the average velocity between exposures)
|
||||
Vector3d imuVelocityInWorldFrame = LastVisionExposureRecord.ImuOnlyDelta.LinearVelocity -
|
||||
LastVisionExposureRecord.ImuOnlyDelta.Pose.Translation / LastVisionExposureRecord.ImuOnlyDelta.TimeInSeconds;
|
||||
CameraFromImu.LinearVelocity = visionVelocityInImuFrame +
|
||||
WorldFromCamera.Inverted().Rotate(imuVelocityInWorldFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
CameraFromImu.LinearVelocity = Vector3d(0,0,0);
|
||||
}
|
||||
}
|
||||
|
||||
PoseStated SensorFusion::computeVisionError()
|
||||
{
|
||||
PoseStated worldFromImuVision = WorldFromCamera * CameraFromImu;
|
||||
// Here we need to compute the difference between worldFromImuVision and WorldFromImu.
|
||||
// However this difference needs to be represented in the World frame, not IMU frame.
|
||||
// Therefore the computation is different from simply worldFromImuVision.Pose * WorldFromImu.Pose.Inverted().
|
||||
PoseStated err;
|
||||
err.Pose.Rotation = worldFromImuVision.Pose.Rotation *
|
||||
LastVisionExposureRecord.WorldFromImu.Pose.Rotation.Inverted();
|
||||
err.Pose.Translation = worldFromImuVision.Pose.Translation -
|
||||
LastVisionExposureRecord.WorldFromImu.Pose.Translation;
|
||||
err.LinearVelocity = worldFromImuVision.LinearVelocity -
|
||||
LastVisionExposureRecord.WorldFromImu.LinearVelocity;
|
||||
return err;
|
||||
}
|
||||
|
||||
Transform<double> SensorFusion::GetVisionPrediction(UInt32 exposureCounter)
|
||||
{
|
||||
Lock::Locker lockScope(pHandler->GetHandlerLock());
|
||||
|
||||
// Combine the small deltas together
|
||||
// Should only be one iteration, unless we are skipping camera frames
|
||||
ExposureRecord record;
|
||||
PoseState<double> delta = PoseState<double>();
|
||||
|
||||
while (!ExposureRecordHistory.IsEmpty() &&
|
||||
(ExposureRecordHistory.PeekFront().ExposureCounter <= exposureCounter))
|
||||
{
|
||||
record = ExposureRecordHistory.PopFront();
|
||||
delta.AdvanceByDelta(record.ImuOnlyDelta);
|
||||
}
|
||||
// Put the combine exposure record back in the history, for use in HandleVisionSuccess(...)
|
||||
record.ImuOnlyDelta = delta;
|
||||
ExposureRecordHistory.PushFront(record);
|
||||
|
||||
Transformd result;
|
||||
if (record.VisionTrackingAvailable)
|
||||
{
|
||||
// if the tracking is working normally, use the change in the main state (SFusion output)
|
||||
// to compute the prediction
|
||||
result = CameraFromImu.Pose *
|
||||
LastVisionExposureRecord.WorldFromImu.Pose.Inverted() * record.WorldFromImu.Pose;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we just acquired vision, the main state probably doesn't have the correct position,
|
||||
// so can't rely on it for prediction
|
||||
|
||||
// solution: use the accelerometer and vision velocity to propagate the previous sample forward
|
||||
// (don't forget to transform IMU to the camera frame)
|
||||
result = Transform<double>
|
||||
(
|
||||
CameraFromImu.Pose.Rotation * delta.Pose.Rotation,
|
||||
CameraFromImu.Pose.Translation + CameraFromImu.LinearVelocity * delta.TimeInSeconds +
|
||||
WorldFromCamera.Inverted().Rotate(delta.Pose.Translation)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SensorFusion::handleMessage(const MessageBodyFrame& msg)
|
||||
{
|
||||
if (msg.Type != Message_BodyFrame || !IsMotionTrackingEnabled())
|
||||
return;
|
||||
|
||||
// Put the sensor readings into convenient local variables
|
||||
Vector3d gyro(msg.RotationRate);
|
||||
Vector3d accel(msg.Acceleration);
|
||||
Vector3d mag(msg.MagneticField);
|
||||
double DeltaT = msg.TimeDelta;
|
||||
|
||||
// Keep track of time
|
||||
WorldFromImu.TimeInSeconds = msg.AbsoluteTimeSeconds;
|
||||
// We got an update in the last 60ms and the data is not very old
|
||||
bool visionIsRecent = (GetTime() - LastVisionAbsoluteTime < 0.07) && (GetVisionLatency() < 0.25);
|
||||
Stage++;
|
||||
|
||||
// Insert current sensor data into filter history
|
||||
FAngV.PushBack(gyro);
|
||||
FAccelInImuFrame.Update(accel, DeltaT, Quatd(gyro, gyro.Length() * DeltaT));
|
||||
|
||||
// Process raw inputs
|
||||
// in the future the gravity offset can be calibrated using vision feedback
|
||||
Vector3d accelInWorldFrame = WorldFromImu.Pose.Rotate(accel) - Vector3d(0, 9.8, 0);
|
||||
|
||||
// Recompute the vision error to account for all the corrections and the new data
|
||||
VisionError = computeVisionError();
|
||||
|
||||
// Update headset orientation
|
||||
WorldFromImu.StoreAndIntegrateGyro(gyro, DeltaT);
|
||||
// Tilt correction based on accelerometer
|
||||
if (EnableGravity)
|
||||
applyTiltCorrection(DeltaT);
|
||||
// Yaw correction based on camera
|
||||
if (EnableYawCorrection && visionIsRecent)
|
||||
applyVisionYawCorrection(DeltaT);
|
||||
// Yaw correction based on magnetometer
|
||||
if (EnableYawCorrection && MagCalibrated) // MagCalibrated is always false for DK2 for now
|
||||
applyMagYawCorrection(mag, DeltaT);
|
||||
// Focus Correction
|
||||
if ((FocusDirection.x != 0.0f || FocusDirection.z != 0.0f) && FocusFOV < Mathf::Pi)
|
||||
applyFocusCorrection(DeltaT);
|
||||
|
||||
// Update camera orientation
|
||||
if (EnableCameraTiltCorrection && visionIsRecent)
|
||||
applyCameraTiltCorrection(accel, DeltaT);
|
||||
|
||||
// The quaternion magnitude may slowly drift due to numerical error,
|
||||
// so it is periodically normalized.
|
||||
if ((Stage & 0xFF) == 0)
|
||||
{
|
||||
WorldFromImu.Pose.Rotation.Normalize();
|
||||
WorldFromCamera.Rotation.Normalize();
|
||||
}
|
||||
|
||||
// Update headset position
|
||||
if (VisionPositionEnabled && visionIsRecent)
|
||||
{
|
||||
// Integrate UMI and velocity here up to a fixed amount of time after vision.
|
||||
WorldFromImu.StoreAndIntegrateAccelerometer(accelInWorldFrame + AccelOffset, DeltaT);
|
||||
// Position correction based on camera
|
||||
applyPositionCorrection(DeltaT);
|
||||
// Compute where the neck pivot would be.
|
||||
setNeckPivotFromPose(WorldFromImu.Pose);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back onto internal head model
|
||||
// Use the last-known neck pivot position to figure out the expected IMU position.
|
||||
// (should be the opposite of SensorFusion::setNeckPivotFromPose)
|
||||
WorldFromNeck.Rotation = WorldFromImu.Pose.Rotation;
|
||||
WorldFromImu.Pose = WorldFromNeck * (ImuFromCpf * CpfFromNeck).Inverted();
|
||||
|
||||
// We can't trust velocity past this point.
|
||||
WorldFromImu.LinearVelocity = Vector3d(0,0,0);
|
||||
WorldFromImu.LinearAcceleration = accelInWorldFrame;
|
||||
}
|
||||
|
||||
// Compute the angular acceleration
|
||||
WorldFromImu.AngularAcceleration = (FAngV.GetSize() >= 12 && DeltaT > 0) ?
|
||||
(FAngV.SavitzkyGolayDerivative12() / DeltaT) : Vector3d();
|
||||
|
||||
// Update the dead reckoning state used for incremental vision tracking
|
||||
NextExposureRecord.ImuOnlyDelta.StoreAndIntegrateGyro(gyro, DeltaT);
|
||||
NextExposureRecord.ImuOnlyDelta.StoreAndIntegrateAccelerometer(accelInWorldFrame, DeltaT);
|
||||
NextExposureRecord.ImuOnlyDelta.TimeInSeconds = WorldFromImu.TimeInSeconds - LastMessageExposureFrame.CameraTimeSeconds;
|
||||
NextExposureRecord.VisionTrackingAvailable &= (VisionPositionEnabled && visionIsRecent);
|
||||
|
||||
Recording::GetRecorder().LogData("sfTimeSeconds", WorldFromImu.TimeInSeconds);
|
||||
Recording::GetRecorder().LogData("sfStage", (double)Stage);
|
||||
Recording::GetRecorder().LogData("sfPose", WorldFromImu.Pose);
|
||||
//Recorder::LogData("sfAngAcc", State.AngularAcceleration);
|
||||
//Recorder::LogData("sfAngVel", State.AngularVelocity);
|
||||
//Recorder::LogData("sfLinAcc", State.LinearAcceleration);
|
||||
//Recorder::LogData("sfLinVel", State.LinearVelocity);
|
||||
|
||||
// Store the lockless state.
|
||||
LocklessState lstate;
|
||||
lstate.StatusFlags = Status_OrientationTracked;
|
||||
if (VisionPositionEnabled)
|
||||
lstate.StatusFlags |= Status_PositionConnected;
|
||||
if (VisionPositionEnabled && visionIsRecent)
|
||||
lstate.StatusFlags |= Status_PositionTracked;
|
||||
|
||||
//A convenient means to temporarily extract this flag
|
||||
TPH_IsPositionTracked = visionIsRecent;
|
||||
|
||||
lstate.State = WorldFromImu;
|
||||
lstate.Temperature = msg.Temperature;
|
||||
lstate.Magnetometer = mag;
|
||||
UpdatedState.SetState(lstate);
|
||||
}
|
||||
|
||||
void SensorFusion::handleExposure(const MessageExposureFrame& msg)
|
||||
{
|
||||
NextExposureRecord.ExposureCounter = msg.CameraFrameCount;
|
||||
NextExposureRecord.ExposureTime = msg.CameraTimeSeconds;
|
||||
NextExposureRecord.WorldFromImu = WorldFromImu;
|
||||
NextExposureRecord.ImuOnlyDelta.TimeInSeconds = msg.CameraTimeSeconds - LastMessageExposureFrame.CameraTimeSeconds;
|
||||
ExposureRecordHistory.PushBack(NextExposureRecord);
|
||||
|
||||
// Every new exposure starts from zero
|
||||
NextExposureRecord = ExposureRecord();
|
||||
LastMessageExposureFrame = msg;
|
||||
}
|
||||
|
||||
// If you have a known-good pose, this sets the neck pivot position.
|
||||
void SensorFusion::setNeckPivotFromPose(Transformd const &worldFromImu)
|
||||
{
|
||||
WorldFromNeck = worldFromImu * ImuFromCpf * CpfFromNeck;
|
||||
}
|
||||
|
||||
// These two functions need to be moved into Quat class
|
||||
// Compute a rotation required to transform "from" into "to".
|
||||
Quatd vectorAlignmentRotation(const Vector3d &from, const Vector3d &to)
|
||||
{
|
||||
Vector3d axis = from.Cross(to);
|
||||
if (axis.LengthSq() == 0)
|
||||
// this handles both collinear and zero-length input cases
|
||||
return Quatd();
|
||||
double angle = from.Angle(to);
|
||||
return Quatd(axis, angle);
|
||||
}
|
||||
|
||||
// Compute the part of the quaternion that rotates around Y axis
|
||||
Quatd extractYawRotation(const Quatd &error)
|
||||
{
|
||||
if (error.y == 0)
|
||||
return Quatd();
|
||||
double phi = atan2(error.w, error.y);
|
||||
double alpha = Mathd::Pi - 2 * phi;
|
||||
return Quatd(Axis_Y, alpha);
|
||||
}
|
||||
|
||||
void SensorFusion::applyPositionCorrection(double deltaT)
|
||||
{
|
||||
// Each component of gainPos is equivalent to a Kalman gain of (sigma_process / sigma_observation)
|
||||
const Vector3d gainPos = Vector3d(10, 10, 8);
|
||||
const Vector3d gainVel = gainPos.EntrywiseMultiply(gainPos) * 0.5;
|
||||
const Vector3d gainAccel = gainVel * 0.5;
|
||||
const double snapThreshold = 0.1; // Large value (previously 0.01, which caused frequent jumping)
|
||||
|
||||
Vector3d correctionPos, correctionVel;
|
||||
if (VisionError.Pose.Translation.LengthSq() > (snapThreshold * snapThreshold) ||
|
||||
!(UpdatedState.GetState().StatusFlags & Status_PositionTracked))
|
||||
{
|
||||
// high error or just reacquired position from vision - apply full correction
|
||||
|
||||
// to know where we are right now, take the vision pose (which is slightly old)
|
||||
// and update it using the imu data since then
|
||||
PoseStated worldFromImuVision = WorldFromCamera * CameraFromImu;
|
||||
for (unsigned int i = 0; i < ExposureRecordHistory.GetSize(); i++)
|
||||
worldFromImuVision.AdvanceByDelta(ExposureRecordHistory.PeekFront(i).ImuOnlyDelta);
|
||||
worldFromImuVision.AdvanceByDelta(NextExposureRecord.ImuOnlyDelta);
|
||||
|
||||
correctionPos = worldFromImuVision.Pose.Translation - WorldFromImu.Pose.Translation;
|
||||
correctionVel = worldFromImuVision.LinearVelocity - WorldFromImu.LinearVelocity;
|
||||
AccelOffset = Vector3d();
|
||||
}
|
||||
else
|
||||
{
|
||||
correctionPos = VisionError.Pose.Translation.EntrywiseMultiply(gainPos) * deltaT;
|
||||
correctionVel = VisionError.Pose.Translation.EntrywiseMultiply(gainVel) * deltaT;
|
||||
AccelOffset += VisionError.Pose.Translation.EntrywiseMultiply(gainAccel) * deltaT;
|
||||
}
|
||||
|
||||
WorldFromImu.Pose.Translation += correctionPos;
|
||||
WorldFromImu.LinearVelocity += correctionVel;
|
||||
|
||||
// Update the exposure records so that we don't apply the same correction twice
|
||||
LastVisionExposureRecord.WorldFromImu.Pose.Translation += correctionPos;
|
||||
LastVisionExposureRecord.WorldFromImu.LinearVelocity += correctionVel;
|
||||
for (unsigned int i = 0; i < ExposureRecordHistory.GetSize(); i++)
|
||||
{
|
||||
PoseStated& state = ExposureRecordHistory.PeekBack(i).WorldFromImu;
|
||||
state.Pose.Translation += correctionPos;
|
||||
state.LinearVelocity += correctionVel;
|
||||
}
|
||||
}
|
||||
|
||||
void SensorFusion::applyVisionYawCorrection(double deltaT)
|
||||
{
|
||||
const double gain = 0.25;
|
||||
const double snapThreshold = 0.1;
|
||||
|
||||
Quatd yawError = extractYawRotation(VisionError.Pose.Rotation);
|
||||
|
||||
Quatd correction;
|
||||
if (Alg::Abs(yawError.w) < cos(snapThreshold / 2)) // angle(yawError) > snapThreshold
|
||||
// high error, jump to the vision position
|
||||
correction = yawError;
|
||||
else
|
||||
correction = yawError.Nlerp(Quatd(), gain * deltaT);
|
||||
|
||||
WorldFromImu.Pose.Rotation = correction * WorldFromImu.Pose.Rotation;
|
||||
|
||||
// Update the exposure records so that we don't apply the same correction twice
|
||||
LastVisionExposureRecord.WorldFromImu.Pose.Rotation = correction * LastVisionExposureRecord.WorldFromImu.Pose.Rotation;
|
||||
for (unsigned int i = 0; i < ExposureRecordHistory.GetSize(); i++)
|
||||
{
|
||||
PoseStated& state = ExposureRecordHistory.PeekBack(i).WorldFromImu;
|
||||
state.Pose.Rotation = correction * state.Pose.Rotation;
|
||||
}
|
||||
}
|
||||
|
||||
void SensorFusion::applyMagYawCorrection(Vector3d mag, double deltaT)
|
||||
{
|
||||
const double minMagLengthSq = Mathd::Tolerance; // need to use a real value to discard very weak fields
|
||||
const double maxMagRefDist = 0.1;
|
||||
const double maxTiltError = 0.05;
|
||||
const double proportionalGain = 0.01;
|
||||
const double integralGain = 0.0005;
|
||||
|
||||
Vector3d magInWorldFrame = WorldFromImu.Pose.Rotate(mag);
|
||||
// verify that the horizontal component is sufficient
|
||||
if (magInWorldFrame.x * magInWorldFrame.x + magInWorldFrame.z * magInWorldFrame.z < minMagLengthSq)
|
||||
return;
|
||||
magInWorldFrame.Normalize();
|
||||
|
||||
// Delete a bad point
|
||||
if (MagRefIdx >= 0 && MagRefs[MagRefIdx].Score < 0)
|
||||
{
|
||||
MagRefs.RemoveAtUnordered(MagRefIdx);
|
||||
MagRefIdx = -1;
|
||||
}
|
||||
|
||||
// Update the reference point if needed
|
||||
if (MagRefIdx < 0 || mag.Distance(MagRefs[MagRefIdx].InImuFrame) > maxMagRefDist)
|
||||
{
|
||||
// Find a new one
|
||||
MagRefIdx = -1;
|
||||
double bestDist = maxMagRefDist;
|
||||
for (unsigned int i = 0; i < MagRefs.GetSize(); i++)
|
||||
{
|
||||
double dist = mag.Distance(MagRefs[i].InImuFrame);
|
||||
if (bestDist > dist)
|
||||
{
|
||||
bestDist = dist;
|
||||
MagRefIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Create one if needed
|
||||
if (MagRefIdx < 0 && MagRefs.GetSize() < MagMaxReferences)
|
||||
{
|
||||
MagRefs.PushBack(MagReferencePoint(mag, WorldFromImu.Pose, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (MagRefIdx >= 0)
|
||||
{
|
||||
Vector3d magRefInWorldFrame = MagRefs[MagRefIdx].WorldFromImu.Rotate(MagRefs[MagRefIdx].InImuFrame);
|
||||
magRefInWorldFrame.Normalize();
|
||||
|
||||
// If the vertical angle is wrong, decrease the score and do nothing
|
||||
if (Alg::Abs(magRefInWorldFrame.y - magInWorldFrame.y) > maxTiltError)
|
||||
{
|
||||
MagRefs[MagRefIdx].Score -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
MagRefs[MagRefIdx].Score += 2;
|
||||
#if 0
|
||||
// this doesn't seem to work properly, need to investigate
|
||||
Quatd error = vectorAlignmentRotation(magW, magRefW);
|
||||
Quatd yawError = extractYawRotation(error);
|
||||
#else
|
||||
// Correction is computed in the horizontal plane
|
||||
magInWorldFrame.y = magRefInWorldFrame.y = 0;
|
||||
Quatd yawError = vectorAlignmentRotation(magInWorldFrame, magRefInWorldFrame);
|
||||
#endif
|
||||
Quatd correction = yawError.Nlerp(Quatd(), proportionalGain * deltaT) *
|
||||
MagCorrectionIntegralTerm.Nlerp(Quatd(), deltaT);
|
||||
MagCorrectionIntegralTerm = MagCorrectionIntegralTerm * yawError.Nlerp(Quatd(), integralGain * deltaT);
|
||||
|
||||
WorldFromImu.Pose.Rotation = correction * WorldFromImu.Pose.Rotation;
|
||||
}
|
||||
}
|
||||
|
||||
void SensorFusion::applyTiltCorrection(double deltaT)
|
||||
{
|
||||
const double gain = 0.25;
|
||||
const double snapThreshold = 0.1;
|
||||
const Vector3d up(0, 1, 0);
|
||||
|
||||
Vector3d accelInWorldFrame = WorldFromImu.Pose.Rotate(FAccelInImuFrame.GetFilteredValue());
|
||||
Quatd error = vectorAlignmentRotation(accelInWorldFrame, up);
|
||||
|
||||
Quatd correction;
|
||||
if (FAccelInImuFrame.GetSize() == 1 ||
|
||||
((Alg::Abs(error.w) < cos(snapThreshold / 2) && FAccelInImuFrame.Confidence() > 0.75)))
|
||||
// full correction for start-up
|
||||
// or large error with high confidence
|
||||
correction = error;
|
||||
else if (FAccelInImuFrame.Confidence() > 0.5)
|
||||
correction = error.Nlerp(Quatd(), gain * deltaT);
|
||||
else
|
||||
// accelerometer is unreliable due to movement
|
||||
return;
|
||||
|
||||
WorldFromImu.Pose.Rotation = correction * WorldFromImu.Pose.Rotation;
|
||||
}
|
||||
|
||||
void SensorFusion::applyCameraTiltCorrection(Vector3d accel, double deltaT)
|
||||
{
|
||||
const double snapThreshold = 0.02; // in radians
|
||||
const double maxCameraPositionOffset = 0.2;
|
||||
const Vector3d up(0, 1, 0), forward(0, 0, -1);
|
||||
|
||||
// for startup use filtered value instead of instantaneous for stability
|
||||
if (FAccelInCameraFrame.IsEmpty())
|
||||
accel = FAccelInImuFrame.GetFilteredValue();
|
||||
|
||||
Transformd cameraFromImu = WorldFromCamera.Inverted() * VisionError.Pose * WorldFromImu.Pose;
|
||||
// this is what the hypothetical camera-mounted accelerometer would show
|
||||
Vector3d accelInCameraFrame = cameraFromImu.Rotate(accel);
|
||||
FAccelInCameraFrame.Update(accelInCameraFrame, deltaT);
|
||||
Vector3d cameraAccelInWorldFrame = WorldFromCamera.Rotate(FAccelInCameraFrame.GetFilteredValue());
|
||||
|
||||
Quatd error1 = vectorAlignmentRotation(cameraAccelInWorldFrame, up);
|
||||
// cancel out yaw rotation
|
||||
Vector3d forwardCamera = (error1 * WorldFromCamera.Rotation).Rotate(forward);
|
||||
forwardCamera.y = 0;
|
||||
Quatd error2 = vectorAlignmentRotation(forwardCamera, forward);
|
||||
// combined error
|
||||
Quatd error = error2 * error1;
|
||||
|
||||
double confidence = FAccelInCameraFrame.Confidence();
|
||||
// penalize the confidence if looking away from the camera
|
||||
// TODO: smooth fall-off
|
||||
if (CameraFromImu.Pose.Rotate(forward).Angle(forward) > 1)
|
||||
confidence *= 0.5;
|
||||
|
||||
//Convenient global variable to temporarily extract this data.
|
||||
TPH_CameraPoseConfidence = confidence;
|
||||
//Allow override of confidence threshold
|
||||
double confidenceThreshold = 0.75f;
|
||||
if (TPH_CameraPoseConfidenceThresholdOverrideIfNonZero)
|
||||
{
|
||||
confidenceThreshold = TPH_CameraPoseConfidenceThresholdOverrideIfNonZero;
|
||||
}
|
||||
|
||||
Quatd correction;
|
||||
if (FAccelInCameraFrame.GetSize() == 1 ||
|
||||
confidence > WorldFromCameraConfidence + 0.2 ||
|
||||
// disabled due to false positives when moving side to side
|
||||
// (Alg::Abs(error.w) < cos(5 * snapThreshold / 2) && confidence > 0.55) ||
|
||||
(Alg::Abs(error.w) < cos(snapThreshold / 2) && confidence > confidenceThreshold))
|
||||
{
|
||||
// large error with high confidence
|
||||
correction = error;
|
||||
// update the confidence level
|
||||
WorldFromCameraConfidence = confidence;
|
||||
}
|
||||
else
|
||||
{
|
||||
// accelerometer is unreliable due to movement
|
||||
return;
|
||||
}
|
||||
|
||||
Transformd newWorldFromCamera(correction * WorldFromCamera.Rotation, Vector3d());
|
||||
|
||||
// compute a camera position change that together with the camera rotation would result in zero player movement
|
||||
newWorldFromCamera.Translation += (WorldFromCamera * CameraFromImu.Pose).Translation -
|
||||
(newWorldFromCamera * CameraFromImu.Pose).Translation;
|
||||
// if the new position is too far, reset to default
|
||||
// (can't hide the rotation, might as well use it to reset the position)
|
||||
if (newWorldFromCamera.Translation.DistanceSq(DefaultWorldFromCamera.Translation) > maxCameraPositionOffset * maxCameraPositionOffset)
|
||||
newWorldFromCamera.Translation = DefaultWorldFromCamera.Translation;
|
||||
|
||||
WorldFromCamera = newWorldFromCamera;
|
||||
|
||||
//Convenient global variable to temporarily extract this data.
|
||||
TPH_CameraPoseOrientationWxyz[0] = (float) WorldFromCamera.Rotation.w;
|
||||
TPH_CameraPoseOrientationWxyz[1] = (float) WorldFromCamera.Rotation.x;
|
||||
TPH_CameraPoseOrientationWxyz[2] = (float) WorldFromCamera.Rotation.y;
|
||||
TPH_CameraPoseOrientationWxyz[3] = (float) WorldFromCamera.Rotation.z;
|
||||
}
|
||||
|
||||
void SensorFusion::applyFocusCorrection(double deltaT)
|
||||
{
|
||||
Vector3d up = Vector3d(0, 1, 0);
|
||||
double gain = 0.01;
|
||||
Vector3d currentDir = WorldFromImu.Pose.Rotate(Vector3d(0, 0, 1));
|
||||
|
||||
Vector3d focusYawComponent = FocusDirection.ProjectToPlane(up);
|
||||
Vector3d currentYawComponent = currentDir.ProjectToPlane(up);
|
||||
|
||||
double angle = focusYawComponent.Angle(currentYawComponent);
|
||||
|
||||
if( angle > FocusFOV )
|
||||
{
|
||||
Quatd yawError;
|
||||
if ( FocusFOV != 0.0f)
|
||||
{
|
||||
Vector3d lFocus = Quatd(up, -FocusFOV).Rotate(focusYawComponent);
|
||||
Vector3d rFocus = Quatd(up, FocusFOV).Rotate(focusYawComponent);
|
||||
double lAngle = lFocus.Angle(currentYawComponent);
|
||||
double rAngle = rFocus.Angle(currentYawComponent);
|
||||
if(lAngle < rAngle)
|
||||
{
|
||||
yawError = vectorAlignmentRotation(currentDir, lFocus);
|
||||
}
|
||||
else
|
||||
{
|
||||
yawError = vectorAlignmentRotation(currentDir, rFocus);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yawError = vectorAlignmentRotation(currentYawComponent, focusYawComponent);
|
||||
}
|
||||
|
||||
Quatd correction = yawError.Nlerp(Quatd(), gain * deltaT);
|
||||
WorldFromImu.Pose.Rotation = correction * WorldFromImu.Pose.Rotation;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Focus filter setting functions
|
||||
|
||||
void SensorFusion::SetFocusDirection()
|
||||
{
|
||||
SetFocusDirection(WorldFromImu.Pose.Rotate(Vector3d(0.0, 0.0, 1.0)));
|
||||
}
|
||||
|
||||
void SensorFusion::SetFocusDirection(Vector3d direction)
|
||||
{
|
||||
FocusDirection = direction;
|
||||
}
|
||||
|
||||
void SensorFusion::SetFocusFOV(double fov)
|
||||
{
|
||||
OVR_ASSERT(fov >= 0.0);
|
||||
FocusFOV = fov;
|
||||
}
|
||||
|
||||
void SensorFusion::ClearFocus()
|
||||
{
|
||||
FocusDirection = Vector3d(0.0, 0.0, 0.0);
|
||||
FocusFOV = 0.0f;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
// Head model functions.
|
||||
|
||||
// Sets up head-and-neck model and device-to-pupil dimensions from the user's profile.
|
||||
void SensorFusion::SetUserHeadDimensions(Profile const &profile, HmdRenderInfo const &hmdRenderInfo)
|
||||
{
|
||||
float neckeye[2];
|
||||
int count = profile.GetFloatValues(OVR_KEY_NECK_TO_EYE_DISTANCE, neckeye, 2);
|
||||
// Make sure these are vaguely sensible values.
|
||||
if (count == 2)
|
||||
{
|
||||
OVR_ASSERT ( ( neckeye[0] > 0.05f ) && ( neckeye[0] < 0.5f ) );
|
||||
OVR_ASSERT ( ( neckeye[1] > 0.05f ) && ( neckeye[1] < 0.5f ) );
|
||||
SetHeadModel ( Vector3f ( 0.0, neckeye[1], -neckeye[0] ) );
|
||||
}
|
||||
|
||||
// Find the distance from the center of the screen to the "center eye"
|
||||
// This center eye is used by systems like rendering & audio to represent the player,
|
||||
// and they will handle the offsets needed from there to each actual eye.
|
||||
|
||||
// HACK HACK HACK
|
||||
// We know for DK1 the screen->lens surface distance is roughly 0.049f, and that the faceplate->lens is 0.02357f.
|
||||
// We're going to assume(!!!!) that all HMDs have the same screen->faceplate distance.
|
||||
// Crystal Cove was measured to be roughly 0.025 screen->faceplate which agrees with this assumption.
|
||||
// TODO: do this properly! Update: Measured this at 0.02733 with a CC prototype, CES era (PT7), on 2/19/14 -Steve
|
||||
float screenCenterToMidplate = 0.02733f;
|
||||
float centerEyeRelief = hmdRenderInfo.GetEyeCenter().ReliefInMeters;
|
||||
float centerPupilDepth = screenCenterToMidplate + hmdRenderInfo.LensSurfaceToMidplateInMeters + centerEyeRelief;
|
||||
SetCenterPupilDepth ( centerPupilDepth );
|
||||
|
||||
Recording::GetRecorder().RecordUserParams(GetHeadModel(), GetCenterPupilDepth());
|
||||
}
|
||||
|
||||
Vector3f SensorFusion::GetHeadModel() const
|
||||
{
|
||||
return (Vector3f)CpfFromNeck.Inverted().Translation;
|
||||
}
|
||||
|
||||
void SensorFusion::SetHeadModel(const Vector3f &headModel, bool resetNeckPivot /*= true*/ )
|
||||
{
|
||||
Lock::Locker lockScope(pHandler->GetHandlerLock());
|
||||
// The head model should look something like (0, 0.12, -0.12), so
|
||||
// these asserts are to try to prevent sign problems, as
|
||||
// they can be subtle but nauseating!
|
||||
OVR_ASSERT ( headModel.y > 0.0f );
|
||||
OVR_ASSERT ( headModel.z < 0.0f );
|
||||
CpfFromNeck = Transformd(Quatd(), (Vector3d)headModel).Inverted();
|
||||
if ( resetNeckPivot )
|
||||
{
|
||||
setNeckPivotFromPose ( WorldFromImu.Pose );
|
||||
}
|
||||
}
|
||||
|
||||
float SensorFusion::GetCenterPupilDepth() const
|
||||
{
|
||||
return CenterPupilDepth;
|
||||
}
|
||||
|
||||
void SensorFusion::SetCenterPupilDepth(float centerPupilDepth)
|
||||
{
|
||||
CenterPupilDepth = centerPupilDepth;
|
||||
|
||||
Transformd screenFromCpf(Quatd(), Vector3d(0, 0, centerPupilDepth));
|
||||
ImuFromCpf = ImuFromScreen * screenFromCpf;
|
||||
|
||||
setNeckPivotFromPose ( WorldFromImu.Pose );
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
|
||||
// This is a "perceptually tuned predictive filter", which means that it is optimized
|
||||
// for improvements in the VR experience, rather than pure error. In particular,
|
||||
// jitter is more perceptible at lower speeds whereas latency is more perceptible
|
||||
// after a high-speed motion. Therefore, the prediction interval is dynamically
|
||||
// adjusted based on speed. Significant more research is needed to further improve
|
||||
// this family of filters.
|
||||
static Transform<double> calcPredictedPose(const PoseState<double>& poseState, double predictionDt)
|
||||
{
|
||||
Transform<double> pose = poseState.Pose;
|
||||
const double linearCoef = 1.0;
|
||||
Vector3d angularVelocity = poseState.AngularVelocity;
|
||||
double angularSpeed = angularVelocity.Length();
|
||||
|
||||
// This could be tuned so that linear and angular are combined with different coefficients
|
||||
double speed = angularSpeed + linearCoef * poseState.LinearVelocity.Length();
|
||||
|
||||
const double slope = 0.2; // The rate at which the dynamic prediction interval varies
|
||||
double candidateDt = slope * speed; // TODO: Replace with smoothstep function
|
||||
|
||||
double dynamicDt = predictionDt;
|
||||
|
||||
// Choose the candidate if it is shorter, to improve stability
|
||||
if (candidateDt < predictionDt)
|
||||
dynamicDt = candidateDt;
|
||||
|
||||
if (angularSpeed > 0.001)
|
||||
pose.Rotation = pose.Rotation * Quatd(angularVelocity, angularSpeed * dynamicDt);
|
||||
|
||||
pose.Translation += poseState.LinearVelocity * dynamicDt;
|
||||
|
||||
return pose;
|
||||
}
|
||||
|
||||
|
||||
Transformf SensorFusion::GetPoseAtTime(double absoluteTime) const
|
||||
{
|
||||
SensorState ss = GetSensorStateAtTime ( absoluteTime );
|
||||
return ss.Predicted.Pose;
|
||||
}
|
||||
|
||||
|
||||
SensorState SensorFusion::GetSensorStateAtTime(double absoluteTime) const
|
||||
{
|
||||
const LocklessState lstate = UpdatedState.GetState();
|
||||
// Delta time from the last available data
|
||||
const double pdt = absoluteTime - lstate.State.TimeInSeconds;
|
||||
|
||||
SensorState ss;
|
||||
ss.Recorded = PoseStatef(lstate.State);
|
||||
ss.Temperature = lstate.Temperature;
|
||||
ss.Magnetometer = Vector3f(lstate.Magnetometer);
|
||||
ss.StatusFlags = lstate.StatusFlags;
|
||||
|
||||
ss.Predicted = ss.Recorded;
|
||||
ss.Predicted.TimeInSeconds = absoluteTime;
|
||||
|
||||
// Do prediction logic and ImuFromCpf transformation
|
||||
ss.Recorded.Pose = Transformf(lstate.State.Pose * ImuFromCpf);
|
||||
ss.Predicted.Pose = Transformf(calcPredictedPose(lstate.State, pdt) * ImuFromCpf);
|
||||
return ss;
|
||||
}
|
||||
|
||||
unsigned SensorFusion::GetStatus() const
|
||||
{
|
||||
return UpdatedState.GetState().StatusFlags;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
|
||||
void SensorFusion::OnMessage(const MessageBodyFrame& msg)
|
||||
{
|
||||
OVR_ASSERT(!IsAttachedToSensor());
|
||||
handleMessage(msg);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------
|
||||
|
||||
void SensorFusion::BodyFrameHandler::OnMessage(const Message& msg)
|
||||
{
|
||||
Recording::GetRecorder().RecordMessage(msg);
|
||||
if (msg.Type == Message_BodyFrame)
|
||||
pFusion->handleMessage(static_cast<const MessageBodyFrame&>(msg));
|
||||
if (msg.Type == Message_ExposureFrame)
|
||||
pFusion->handleExposure(static_cast<const MessageExposureFrame&>(msg));
|
||||
}
|
||||
|
||||
} // namespace OVR
|
Reference in New Issue
Block a user