The recursive average: the same average, keeping no old scans.
Four scans (example):
| , m | the share | , m | |
|---|---|---|---|
| 1 | 2.04 | 1 | 2.04 |
| 2 | 2.02 | ½ | 2.03 |
| 3 | 2.03 | ⅓ | 2.03 |
| 4 | 2.07 | ¼ | 2.04 |
d_hat = z.expanding().mean() # running average
Legend
— the number of the scan
— the -th scan of the lidar's forward ray, m
— the average of the first scans; — before the new one
— the share of the new scan
— how much the new scan disagrees
same as : multiply both sides by
z — all scans in a pandas column (Series), one per row
expanding() — windows that grow by one scan: ; ; ; …
.mean() — each window's average: d_hat =
red dots — the scans; green line — their average; black line — the truth, 2.000 m
light band — ±σ_z/√n: the average's expected error; σ_z — one scan's error, 10.1 mm
pandas.pydata.org/docs/reference/api/pandas.Series.expanding.html
How far the average of the last scans stays behind a moving robot; measured: 18.8 cm.
d_avg = z.rolling(20).mean()
Legend
lag — how far the average is behind the truth, cm
— scans in the window, 20
— speed toward the wall, m/s
— time between scans, 0.1 s
— the true distance to the wall, — the lidar's forward ray, m
— the window's middle lags this many scans: the mean of 0, 1, …,
z — all scans in a pandas column (Series), one per row
rolling(20) — a window of the last 20 scans that slides by one: ; ; …
.mean() — each window's average: d_avg, the orange line
black — the truth · red — the ray · orange — the mean of the last 20 scans
grey vertical line — the moment the scene shows; dots — the truth and the mean at it
pandas.pydata.org/docs/reference/api/pandas.Series.rolling.html
Our robot before the wall, five cells of 10 cm (example):
Bayes' rule: from "how likely this scan is at each distance" to "how likely each distance is, given this scan".
Legend
— a cell: a distance to the wall, m
— the scan: what the lidar's forward ray reads, m
— a probability, 0 to 1; the bar reads «given»
— the belief before the scan (prior): blue bars
— the likelihood: how likely the ray reads if the wall is at : red bars
purple bars — the products, cell by cell
— their sum; divided by it, the bars add up to 1
— the belief after the scan (posterior): green bars
Predict: each cell collects what the motion brings into it.
Legend
— a cell after the motion: a distance to the wall, m
— a cell before it; the sum runs over all of them
— the motion: the wheels' step , here 1.8 cm
— the belief after the last scan: green bars
— the chance that the step takes the robot from to : the wheels err
— the belief before the next scan: blue bars; the bar on top — before the scan
outlined bars — the green bars, moved by : each blue bar collects from those around it
the belief — one number per cell; the robot keeps nothing else
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/02-Discrete-Bayes.ipynb
Update: Bayes' rule on every cell — multiply by how well the cell explains the scan, then rescale.
Legend
— a cell: a distance to the wall, m
— the scan: what the lidar's forward ray reads, m
— the belief before the scan, from predict: blue bars
— the likelihood: how likely the scan is if the wall is at ; a Gaussian around , as wide as the lidar's error : red curve
— the belief after the scan: green bars; outlined — the blue bars, to compare
— 1 / the sum of the products, so the bars add up to 1: the of Bayes' rule
dashed line — the truth
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/02-Discrete-Bayes.ipynb
predict — move and blur: · scan — its likelihood · update — multiply, rescale: · repeat
, — a cell now and before the motion · — the motion · — the scan · — 1 / the sum · in the rows — a typical error · — time from the start
np.roll / shift — move every cell by the motion (offset, in cells)convolve(…, kernel) — the formula's sum: each cell collects its neighbours' belief with the weights kernel = [0.1, 0.8, 0.1] (example) — a blur by the wheels' errormode='wrap' — the cells close into a ring; 'constant' — a wall: our drive; cval — what fills the cells that come in from beyond the wall: 0prior * likelihood — cell by cellnormalize — divide by the sum: this is ; np.asarray(…, dtype=float) — the belief as an array of fractionsdef predict(pdf, offset, kernel, mode='wrap', cval=0.):
if mode == 'wrap':
return convolve(np.roll(pdf, offset), kernel, mode='wrap')
return convolve(shift(pdf, offset, cval=cval), kernel,
cval=cval, mode='constant')
def update(likelihood, prior):
posterior = prior * likelihood
return normalize(posterior)
def normalize(pdf):
pdf /= sum(np.asarray(pdf, dtype=float))
return pdf
pdf — the belief, one number per cellprior, posteriorlikelihood; — 1 / sum github.com/rlabbe/filterpy
colab.research.google.com/github/StanfordASL/pora-exercises/blob/main/ch11/discrete_bayes.ipynb
predict — the wheel odometry moves the guess; the doubt grows
update — the scan pulls the guess; the doubt shrinks
The grid's recipe for one Gaussian: two numbers per step — the guess and its doubt.
| Bayes filter on a grid | Kalman filter | |
|---|---|---|
| the belief | one number per cell: 201 numbers | one Gaussian: and |
| it needs | nothing: any shape, even two peaks | a belief that stays one Gaussian |
Legend
each Gaussian — where the robot may be: the likeliest distance at its centre, less likely to the sides
— the estimate of the distance to the wall, the green centre; the hat — an estimate; dashed green — the last one
— its typical error, the width; — its variance, the doubt
, — the prediction and its error; the minus — before the scan
— one step by the wheel odometry: speed × time
— the error one step adds
— the scan of the lidar's forward ray; — its error: the lidar's own, constant
— the Kalman gain: the scan's weight, from 0 to 1
— the innovation: how far the scan is from the prediction
The estimate is off by ±1 cm; one step of the wheels adds ±2 cm (example):
| estimate | step | prediction | squared |
|---|---|---|---|
| +1 | +2 | +3 | 9 |
| +1 | −2 | −1 | 1 |
| −1 | +2 | +1 | 1 |
| −1 | −2 | −3 | 9 |
mean square 5 = 1 + 4: the variances add
The Gaussian moves with the robot, its σ grows: variances add.
def add(mean1, var1, mean2, var2):
return (mean1+mean2, var1+var2)
Only a measurement shrinks the doubt — the update, next.
Legend
— the estimate of the distance to the wall after the last scan, before the robot moves; — its typical error, 1 cm (example)
— the step: how far the wheel odometry says the robot moved between two scans
— the error one step adds: the wheels', 2 cm (example); — Q in filterpy and robot_localization
, — the prediction and its error; the minus — before the scan
— a variance; variances add, σ do not: 1 + 4 = 5, so = √5, not 1 + 2
mean1, var1 — the estimate: , ; mean2, var2 — the step: ,
green — the estimate; dashed — the step's error; blue — the prediction
The same weighted mix as in the complementary filter: which is best?
| complementary filter | Kalman filter | |
|---|---|---|
| the weight | fixed, picked by eye: 0.02 | computed at every step from the two variances |
| the doubt | not kept | kept as : how sure the estimate is |
Legend
— the true distance to the wall
— the prediction: the last estimate moved by the wheel odometry's step; the minus — before the scan
— the prediction's typical error
— the estimate one step earlier, at step
— the lidar's forward ray; — its typical error
— the mix, the fused estimate: the new estimate
— how far the robot moved in one step: speed × time
— the scan's weight: any number from 0 to 1. The loop's is the best — found next
blue, red — where the wall may be, by the wheel odometry and by the lidar: likeliest at the centre; dotted line — where it truly is; dashed green — the mix, not known yet
Each error is + or −, equally likely: lidar ±1 cm, wheels ±2 cm (example). Mix with :
| lidar | wheels | mix | squared |
|---|---|---|---|
| +1 | +2 | 1.2 | 1.44 |
| +1 | −2 | 0.4 | 0.16 |
| −1 | +2 | −0.4 | 0.16 |
| −1 | −2 | −1.2 | 1.44 |
mean square 0.8 cm² — less than the lidar's 1 cm²
Legend
— the scan's weight: here 0.8 for the lidar, 0.2 for the wheels
lidar, wheels — the error of each, cm; every sign equally likely
mix — 0.8 × the lidar's error + 0.2 × the wheels' error
mean square — the average of the error²
— the mix's typical error: the root of the mean square
lidar alone: = 1 cm · wheels alone: = 2 cm (example)
green curve — for every weight; red dashed — the lidar alone; shaded — where the mix beats it
The error of the mix for every weight: the sum of the two parts has a bottom.
, cm² (example)
Legend
— the mix's variance, its doubt: the average of its error² over many scans, cm²
— the scan's weight, from 0 to 1
— the prediction's variance: the average of its error²
— the lidar's variance
— the prediction's part: its error × (1 − w), so its variance × (1 − w)²: blue dashed
— the lidar's part: its error × , its variance × : red dashed
green — their sum, ; dots — a few weights, with their values
The best weight: the prediction's share of the total doubt.
Three pairs of noises, cm² (example); the picture is the first:
| case | |||
|---|---|---|---|
| wheels unsure | = 0.8 | ||
| lidar unsure | = 0.2 | ||
| equally sure | = 0.5 |
Legend
green curve — the mix's variance for every weight of the scan; green dot — its bottom, the best
dashed line — from the bottom down to the bar: the bar splits right under it
bar — the total doubt : blue — the prediction's part, red — the lidar's
— the Kalman gain: the best weight of the scan, from 0 to 1
— the prediction's variance: its typical error, squared
— the lidar's variance
— the mix's variance, its doubt, cm²
→ : keep the prediction · → : take the scan
| our drive: one number | the matrix form: many numbers | |
|---|---|---|
| the state | a column of numbers: the pose | |
| its doubt | , : each number's variance on the diagonal; off it, how two errors depend on each other — not diagonal | |
| what the sensor reads | , the distance itself | state: , , picks from the state what the sensor reads |
| the sensor's noise | , : diagonal when each reading errs on its own | |
| a doubt through a number | : on both sides | |
| divide by | multiply by , the inverse matrix | |
| the gain | one number: the scan's weight | : a reading moves every number whose error depends on it |
Legend
— the reading: the lidar's distance, the IMU's yaw
— the measurement matrix: picks from the state what the sensor reads; — state numbers, — readings
, — the prediction's and the sensor's covariance matrices
— a number an error is multiplied by; — that error's variance
— turned over; — the reading's whole doubt; — the inverse of ; — the Kalman gain
grey — 's ones; framed — what they pick
Legend
— picks from the state what the sensor reads; — the same ones as a column
— the prediction's covariance matrix: variances on the diagonal, blue
cov — a covariance: how two errors depend on each other
, , — the variances of , and the yaw ; — our prediction's
— a number an error is multiplied by; — its variance
framed — what the ones pick; dashed — cov
The matrix form, as filterpy writes it: — our 1D gain, piece by piece:
| piece | our drive, 1D | matrices | what it is |
|---|---|---|---|
| 1 | the doubt of the reading we expect | ||
| 2 | plus the sensor's noise: variances add | ||
| 3 | on top | once: each state number's error paired with the reading's | |
| 4 | divide by the sum | multiply by | the inverse instead of a division |
| how far each state number moves |
With one number, every piece is the 1D one: is the bottom of the parabola again.
Legend
— the Kalman gain; — picks what the sensor reads
, — the prediction's and the sensor's covariance matrices
— the reading's whole doubt
bar — (example): blue — the prediction's part, red — the lidar's; under the blue part —
Linear algebra
— each row of times each column of , summed;
— rows become columns
— the inverse: , the identity; for a number,