π― Sensor Fusion
At this point, the robot has two independent ways to estimate its own pose, and neither one is good enough alone. Odometry runs every single loop, smooth and responsive, but drifts steadily with no way to correct itself. Vision-derived pose estimates don't drift at all, they're tied to fixed points on the field, but they arrive far less often, only when a tag is actually visible, and are noisier, occasionally wrong outright. Sensor fusion is combining the two so the result inherits odometry's smoothness and vision's freedom from drift, rather than being stuck picking one or the other.
Prediction and Correctionβ
WPILib's pose estimator classes, SwerveDrivePoseEstimator, DifferentialDrivePoseEstimator,
MecanumDrivePoseEstimator, are built around the same two-step pattern every loop:
- Predict. Every loop, exactly like plain odometry, the estimator integrates the latest wheel/gyro measurement
onto its current pose using the same
Twist2darc integration from Odometry. This happens constantly, whether or not a vision measurement is available. - Correct. Whenever a new vision-derived pose arrives, asynchronously, at whatever rate the coprocessor supplies it, the estimator blends it into the current estimate rather than overwriting it outright.
That blend is not a plain average. Overwriting the pose with vision entirely would throw away odometry's smoothness (and let one bad vision frame teleport the reported pose); ignoring vision entirely brings back unbounded drift. The right answer sits somewhere between the two, decided by how much each source is trusted.
Trust, as Standard Deviationsβ
Both the predict and correct steps are configured with a standard deviation for each state variable (x, y,
and ΞΈ), a smaller standard deviation means "trust this source more." The constructor takes two such vectors, one
for the odometry model and one for vision measurements:
SwerveDrivePoseEstimator poseEstimator = new SwerveDrivePoseEstimator(
kinematics,
gyroAngle,
modulePositions,
initialPose,
VecBuilder.fill(0.05, 0.05, Units.degreesToRadians(5)), // odometry std devs (x, y, ΞΈ)
VecBuilder.fill(0.5, 0.5, Units.degreesToRadians(30)) // vision std devs (x, y, ΞΈ)
);
Every loop, the predict step runs unconditionally:
poseEstimator.updateWithTime(
Timer.getFPGATimestamp(), gyroAngle, modulePositions);
And whenever a new vision pose arrives, it's folded in as a correction:
poseEstimator.addVisionMeasurement(visionPose, visionTimestampSeconds);
Internally, each axis is corrected in proportion to how confident the vision measurement is relative to how
confident the odometry model is at that moment, a small vision standard deviation pulls the estimate hard toward the
vision reading; a large one barely nudges it. Because Pose Estimation
already establishes that closer tags produce more precise estimates, it's common to scale the vision standard
deviation by distance to the tag dynamically, call addVisionMeasurement with tighter standard deviations for a
close, unambiguous reading and looser ones for a distant or borderline one, rather than using one fixed trust level
for every vision measurement. Heading is a common special case: a gyro's heading is normally trustworthy enough that
teams keep the odometry heading standard deviation small and let vision correct x/y far more than ΞΈ.
Why This Still Isn't the Whole Storyβ
This description has quietly glossed over one detail: a vision measurement passed to addVisionMeasurement isn't
describing the robot's pose right now, it's describing where the robot was at the moment the camera captured that
frame, which is already somewhat in the past by the time the estimator sees it. Blending a stale measurement into
the current pose as if it were fresh would introduce new error rather than removing it. Handling that correctly,
and what that timestampSeconds argument above is actually for, is covered next in Latency.