Fusing a fix
Odometry prediction, a tag as the measurement, and a gain that rises as the estimate ages.
Do this lesson in the simulatorPrint it: worksheet (PDF) · answers (PDF) · as a web page
Guide: Kalman filters explained
U3.5 took a fix by replacing the estimate. That was crude in two ways: it threw away good dead reckoning, and it trusted the tag completely. With a variance to hand, both are fixed.
The rule
Predict every tick. Correct only when a fix arrives, with the gain the variances give you.
# every tick
est += flow()[1] * DT
p += Q
# when a tag is seen
measured = TAG_Y - tag_distance
k = p / (p + R_TAG)
est += k * (measured - est)
p = (1 - k) * p
Nothing new. It is the U6.3 filter with the correction step made conditional, which is all that "fusing an intermittent measurement" means.
The sawtooth
In the simulator the camera reads tag 4 on every tick once it is close enough. Real fixes are rarer: a phone's GPS gives one a second, a ship's navigator one star sight a night. So this demo takes a fix only on the whole seconds, i % 10 == 0, and dead reckons in between.
from bugbot import *
connect()
DT, TAG_Y = 0.1, 165.0
Q, R_TAG = 0.6, 9.0
set_cv("apriltag")
est, p = 0.0, 4.0
forward(65)
for i in range(90):
est += flow()[1] * DT
p += Q
seen = [t for t in apriltags() if t[0] == 4]
if seen and seen[0][3] < 130 and i % 10 == 0: # a fix once a second
measured = TAG_Y - seen[0][3]
k = p / (p + R_TAG)
est += k * (measured - est)
p = (1 - k) * p
plot("estimate", est)
plot("variance", p)
wait(DT)
stop()
The variance line is the one to watch. It climbs steadily while the robot is dead reckoning, by Q every tick, from 4.6 to 28.0 before the first fix. The tag comes within 130 cm at 3.2 seconds, and the first fix, at 4 seconds, drops the variance to 6.9. After that it climbs for a second and drops at each fix, settling into teeth that climb to about 10 and fall back to about 5. That sawtooth is the picture of every navigation system ever built, from a ship's log and a star sight to a phone with GPS in a tunnel.
Take out and i % 10 == 0 and there is only one tooth: the variance climbs to 23.2, the first fix drops it to 6.5, and with a fix every tick it settles flat at 2.04.
R for a tag
A tag's range error grows with distance, so a single R_TAG is a simplification. A better model:
R_tag = 4.0 + 0.002 * d * d
Near tags are trusted, far ones are not, automatically. The constants come from measuring the detector at several ranges, which is an afternoon well spent on any real system.
The simulator's detector is kinder than a real one: it reports the range to the nearest millimetre at every distance. So R_TAG = 9 is cautious here, and a range model will change the estimate very little in the simulator. It matters on a real camera, where it is worth doing properly.
Gating, again
A fix that disagrees wildly with the estimate is more likely to be a mistake than a revelation. The filter has the numbers to judge:
if (measured - est) ** 2 < 9 * (p + R_TAG): # inside three sigma
...correct...
else:
ignore, and count it
The gate widens on its own when the filter is unsure, which is exactly when a surprising fix is most likely to be genuine. This is validation gating, and it is what stops one misread tag throwing a robot across the room.
Task: fuse a tag fix
Drive at least 80 cm towards tag 4, predicting with flow and correcting once a second while the tag is in view. Plot estimate and variance, and print my y:. No position().
from bugbot import *
connect()
DT, TAG_Y = 0.1, 165.0
Q, R_TAG = 0.6, 9.0
set_cv("apriltag")
est, p = 0.0, 4.0
Challenges
- Make
R_TAGgrow with range and compare the estimate. - Add a three sigma gate and count the fixes it refuses.
- Print the innovation at each fix. Does it shrink as the run goes on?