[wpimath] Fix pose estimator performance (#4111)

Fixes #4087.
This commit is contained in:
Tyler Veness
2022-04-26 18:43:59 -07:00
committed by GitHub
parent 51bc893bc5
commit d926dd1610
12 changed files with 159 additions and 97 deletions

View File

@@ -11,6 +11,7 @@ import edu.wpi.first.math.StateSpaceUtil;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
import edu.wpi.first.math.kinematics.DifferentialDriveWheelSpeeds;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N3;
@@ -55,7 +56,7 @@ import java.util.function.BiConsumer;
public class DifferentialDrivePoseEstimator {
final UnscentedKalmanFilter<N5, N3, N3> m_observer; // Package-private to allow for unit testing
private final BiConsumer<Matrix<N3, N1>, Matrix<N3, N1>> m_visionCorrect;
private final KalmanFilterLatencyCompensator<N5, N3, N3> m_latencyCompensator;
private final TimeInterpolatableBuffer<Pose2d> m_poseBuffer;
private final double m_nominalDt; // Seconds
private double m_prevTimeSeconds = -1.0;
@@ -135,7 +136,7 @@ public class DifferentialDrivePoseEstimator {
AngleStatistics.angleResidual(2),
AngleStatistics.angleAdd(2),
m_nominalDt);
m_latencyCompensator = new KalmanFilterLatencyCompensator<>();
m_poseBuffer = TimeInterpolatableBuffer.createBuffer(1.5);
// Initialize vision R
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
@@ -221,7 +222,7 @@ public class DifferentialDrivePoseEstimator {
public void resetPosition(Pose2d poseMeters, Rotation2d gyroAngle) {
// Reset state estimate and error covariance
m_observer.reset();
m_latencyCompensator.reset();
m_poseBuffer.clear();
m_observer.setXhat(fillStateVector(poseMeters, 0.0, 0.0));
@@ -246,6 +247,10 @@ public class DifferentialDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* DifferentialDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* @param visionRobotPoseMeters The pose of the robot as measured by the vision camera.
* @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you
* don't use your own time source by calling {@link
@@ -255,13 +260,13 @@ public class DifferentialDrivePoseEstimator {
* source in this case.
*/
public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) {
m_latencyCompensator.applyPastGlobalMeasurement(
Nat.N3(),
m_observer,
m_nominalDt,
StateSpaceUtil.poseTo3dVector(visionRobotPoseMeters),
m_visionCorrect,
timestampSeconds);
var sample = m_poseBuffer.getSample(timestampSeconds);
if (sample.isPresent()) {
m_visionCorrect.accept(
new MatBuilder<>(Nat.N3(), Nat.N1()).fill(0.0, 0.0, 0.0),
StateSpaceUtil.poseTo3dVector(
getEstimatedPosition().transformBy(visionRobotPoseMeters.minus(sample.get()))));
}
}
/**
@@ -271,6 +276,10 @@ public class DifferentialDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* DifferentialDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* <p>Note that the vision measurement standard deviations passed into this method will continue
* to apply to future measurements until a subsequent call to {@link
* DifferentialDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method.
@@ -354,7 +363,7 @@ public class DifferentialDrivePoseEstimator {
m_previousAngle = angle;
var localY = VecBuilder.fill(distanceLeftMeters, distanceRightMeters, angle.getRadians());
m_latencyCompensator.addObserverState(m_observer, u, localY, currentTimeSeconds);
m_poseBuffer.addSample(currentTimeSeconds, getEstimatedPosition());
m_observer.predict(u, dt);
m_observer.correct(u, localY);

View File

@@ -4,6 +4,7 @@
package edu.wpi.first.math.estimator;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.StateSpaceUtil;
@@ -11,6 +12,7 @@ import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
import edu.wpi.first.math.kinematics.MecanumDriveKinematics;
import edu.wpi.first.math.kinematics.MecanumDriveWheelSpeeds;
import edu.wpi.first.math.numbers.N1;
@@ -48,7 +50,7 @@ public class MecanumDrivePoseEstimator {
private final UnscentedKalmanFilter<N3, N3, N1> m_observer;
private final MecanumDriveKinematics m_kinematics;
private final BiConsumer<Matrix<N3, N1>, Matrix<N3, N1>> m_visionCorrect;
private final KalmanFilterLatencyCompensator<N3, N3, N1> m_latencyCompensator;
private final TimeInterpolatableBuffer<Pose2d> m_poseBuffer;
private final double m_nominalDt; // Seconds
private double m_prevTimeSeconds = -1.0;
@@ -134,7 +136,7 @@ public class MecanumDrivePoseEstimator {
AngleStatistics.angleAdd(2),
m_nominalDt);
m_kinematics = kinematics;
m_latencyCompensator = new KalmanFilterLatencyCompensator<>();
m_poseBuffer = TimeInterpolatableBuffer.createBuffer(1.5);
// Initialize vision R
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
@@ -184,7 +186,7 @@ public class MecanumDrivePoseEstimator {
public void resetPosition(Pose2d poseMeters, Rotation2d gyroAngle) {
// Reset state estimate and error covariance
m_observer.reset();
m_latencyCompensator.reset();
m_poseBuffer.clear();
m_observer.setXhat(StateSpaceUtil.poseTo3dVector(poseMeters));
@@ -209,6 +211,10 @@ public class MecanumDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* MecanumDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* @param visionRobotPoseMeters The pose of the robot as measured by the vision camera.
* @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you
* don't use your own time source by calling {@link MecanumDrivePoseEstimator#updateWithTime}
@@ -217,13 +223,13 @@ public class MecanumDrivePoseEstimator {
* Timer.getFPGATimestamp as your time source or sync the epochs.
*/
public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) {
m_latencyCompensator.applyPastGlobalMeasurement(
Nat.N3(),
m_observer,
m_nominalDt,
StateSpaceUtil.poseTo3dVector(visionRobotPoseMeters),
m_visionCorrect,
timestampSeconds);
var sample = m_poseBuffer.getSample(timestampSeconds);
if (sample.isPresent()) {
m_visionCorrect.accept(
new MatBuilder<>(Nat.N3(), Nat.N1()).fill(0.0, 0.0, 0.0),
StateSpaceUtil.poseTo3dVector(
getEstimatedPosition().transformBy(visionRobotPoseMeters.minus(sample.get()))));
}
}
/**
@@ -233,6 +239,10 @@ public class MecanumDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* MecanumDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* <p>Note that the vision measurement standard deviations passed into this method will continue
* to apply to future measurements until a subsequent call to {@link
* MecanumDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method.
@@ -296,7 +306,7 @@ public class MecanumDrivePoseEstimator {
m_previousAngle = angle;
var localY = VecBuilder.fill(angle.getRadians());
m_latencyCompensator.addObserverState(m_observer, u, localY, currentTimeSeconds);
m_poseBuffer.addSample(currentTimeSeconds, getEstimatedPosition());
m_observer.predict(u, dt);
m_observer.correct(u, localY);

View File

@@ -4,6 +4,7 @@
package edu.wpi.first.math.estimator;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.StateSpaceUtil;
@@ -11,6 +12,7 @@ import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
import edu.wpi.first.math.kinematics.SwerveDriveKinematics;
import edu.wpi.first.math.kinematics.SwerveModuleState;
import edu.wpi.first.math.numbers.N1;
@@ -48,7 +50,7 @@ public class SwerveDrivePoseEstimator {
private final UnscentedKalmanFilter<N3, N3, N1> m_observer;
private final SwerveDriveKinematics m_kinematics;
private final BiConsumer<Matrix<N3, N1>, Matrix<N3, N1>> m_visionCorrect;
private final KalmanFilterLatencyCompensator<N3, N3, N1> m_latencyCompensator;
private final TimeInterpolatableBuffer<Pose2d> m_poseBuffer;
private final double m_nominalDt; // Seconds
private double m_prevTimeSeconds = -1.0;
@@ -134,7 +136,7 @@ public class SwerveDrivePoseEstimator {
AngleStatistics.angleAdd(2),
m_nominalDt);
m_kinematics = kinematics;
m_latencyCompensator = new KalmanFilterLatencyCompensator<>();
m_poseBuffer = TimeInterpolatableBuffer.createBuffer(1.5);
// Initialize vision R
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
@@ -182,7 +184,7 @@ public class SwerveDrivePoseEstimator {
public void resetPosition(Pose2d poseMeters, Rotation2d gyroAngle) {
// Reset state estimate and error covariance
m_observer.reset();
m_latencyCompensator.reset();
m_poseBuffer.clear();
m_observer.setXhat(StateSpaceUtil.poseTo3dVector(poseMeters));
@@ -207,6 +209,10 @@ public class SwerveDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* SwerveDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* @param visionRobotPoseMeters The pose of the robot as measured by the vision camera.
* @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you
* don't use your own time source by calling {@link SwerveDrivePoseEstimator#updateWithTime}
@@ -215,13 +221,13 @@ public class SwerveDrivePoseEstimator {
* Timer.getFPGATimestamp as your time source or sync the epochs.
*/
public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) {
m_latencyCompensator.applyPastGlobalMeasurement(
Nat.N3(),
m_observer,
m_nominalDt,
StateSpaceUtil.poseTo3dVector(visionRobotPoseMeters),
m_visionCorrect,
timestampSeconds);
var sample = m_poseBuffer.getSample(timestampSeconds);
if (sample.isPresent()) {
m_visionCorrect.accept(
new MatBuilder<>(Nat.N3(), Nat.N1()).fill(0.0, 0.0, 0.0),
StateSpaceUtil.poseTo3dVector(
getEstimatedPosition().transformBy(visionRobotPoseMeters.minus(sample.get()))));
}
}
/**
@@ -231,6 +237,10 @@ public class SwerveDrivePoseEstimator {
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* SwerveDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the
* current pose estimate.
*
* <p>Note that the vision measurement standard deviations passed into this method will continue
* to apply to future measurements until a subsequent call to {@link
* SwerveDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method.
@@ -294,7 +304,7 @@ public class SwerveDrivePoseEstimator {
m_previousAngle = angle;
var localY = VecBuilder.fill(angle.getRadians());
m_latencyCompensator.addObserverState(m_observer, u, localY, currentTimeSeconds);
m_poseBuffer.addSample(currentTimeSeconds, getEstimatedPosition());
m_observer.predict(u, dt);
m_observer.correct(u, localY);

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.math.interpolation;
import edu.wpi.first.math.MathUtil;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.TreeMap;
/**
@@ -19,7 +20,7 @@ import java.util.TreeMap;
public final class TimeInterpolatableBuffer<T> {
private final double m_historySize;
private final InterpolateFunction<T> m_interpolatingFunc;
private final NavigableMap<Double, T> m_buffer = new TreeMap<>();
private final NavigableMap<Double, T> m_pastSnapshots = new TreeMap<>();
private TimeInterpolatableBuffer(
InterpolateFunction<T> interpolateFunction, double historySizeSeconds) {
@@ -70,7 +71,7 @@ public final class TimeInterpolatableBuffer<T> {
*/
public void addSample(double timeSeconds, T sample) {
cleanUp(timeSeconds);
m_buffer.put(timeSeconds, sample);
m_pastSnapshots.put(timeSeconds, sample);
}
/**
@@ -79,10 +80,10 @@ public final class TimeInterpolatableBuffer<T> {
* @param time The current timestamp.
*/
private void cleanUp(double time) {
while (!m_buffer.isEmpty()) {
var entry = m_buffer.firstEntry();
while (!m_pastSnapshots.isEmpty()) {
var entry = m_pastSnapshots.firstEntry();
if (time - entry.getKey() >= m_historySize) {
m_buffer.remove(entry.getKey());
m_pastSnapshots.remove(entry.getKey());
} else {
return;
}
@@ -91,45 +92,46 @@ public final class TimeInterpolatableBuffer<T> {
/** Clear all old samples. */
public void clear() {
m_buffer.clear();
m_pastSnapshots.clear();
}
/**
* Sample the buffer at the given time. If the buffer is empty, this will return null.
* Sample the buffer at the given time. If the buffer is empty, an empty Optional is returned.
*
* @param timeSeconds The time at which to sample.
* @return The interpolated value at that timestamp. Might be null.
* @return The interpolated value at that timestamp or an empty Optional.
*/
@SuppressWarnings("UnnecessaryParentheses")
public T getSample(double timeSeconds) {
if (m_buffer.isEmpty()) {
return null;
public Optional<T> getSample(double timeSeconds) {
if (m_pastSnapshots.isEmpty()) {
return Optional.empty();
}
// Special case for when the requested time is the same as a sample
var nowEntry = m_buffer.get(timeSeconds);
var nowEntry = m_pastSnapshots.get(timeSeconds);
if (nowEntry != null) {
return nowEntry;
return Optional.of(nowEntry);
}
var topBound = m_buffer.ceilingEntry(timeSeconds);
var bottomBound = m_buffer.floorEntry(timeSeconds);
var topBound = m_pastSnapshots.ceilingEntry(timeSeconds);
var bottomBound = m_pastSnapshots.floorEntry(timeSeconds);
// Return null if neither sample exists, and the opposite bound if the other is null
if (topBound == null && bottomBound == null) {
return null;
return Optional.empty();
} else if (topBound == null) {
return bottomBound.getValue();
return Optional.of(bottomBound.getValue());
} else if (bottomBound == null) {
return topBound.getValue();
return Optional.of(topBound.getValue());
} else {
// Otherwise, interpolate. Because T is between [0, 1], we want the ratio of (the difference
// between the current time and bottom bound) and (the difference between top and bottom
// bounds).
return m_interpolatingFunc.interpolate(
bottomBound.getValue(),
topBound.getValue(),
((timeSeconds - bottomBound.getKey()) / (topBound.getKey() - bottomBound.getKey())));
return Optional.of(
m_interpolatingFunc.interpolate(
bottomBound.getValue(),
topBound.getValue(),
((timeSeconds - bottomBound.getKey()) / (topBound.getKey() - bottomBound.getKey()))));
}
}