Lecture 7 — State Estimation and Fusion (II)

One estimate from noisy sensors: from Kalman in matrices to the extended Kalman filter (EKF)


Topics: Kalman gain · Covariance · EKF · robot_localization

The Kalman Filter: Predict, Then Update

A loop of three boxes: blue predict — the Gaussian of the old estimate, dashed green, shifts into the prediction, its σ larger; red scan z — a Gaussian with a small σ; green update — combine: the two give one Gaussian between them, its σ smaller than both; and back to predict

predict — the wheel odometry moves the guess; the doubt grows

d−=d^−v Δtσd−2=σd^2+σu2\textcolor{#1f77b4}{d^-} = \hat d - v\,\Delta t \qquad \textcolor{#1f77b4}{\sigma_{d^-}^2} = \sigma_{\hat d}^2 + \sigma_u^2

update — the scan pulls the guess; the doubt shrinks

K=σd−2σd−2+σz2K = \frac{\sigma_{d^-}^2}{\sigma_{d^-}^2 + \sigma_z^2}

d^=d−+K (z−d−)σd^2=(1−K) σd−2\textcolor{#2ca02c}{\hat d} = \textcolor{#1f77b4}{d^-} + K\,(\textcolor{#d62728}{z} - \textcolor{#1f77b4}{d^-}) \qquad \textcolor{#2ca02c}{\sigma_{\hat d}^2} = (1 - K)\,\textcolor{#1f77b4}{\sigma_{d^-}^2}

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: d^\hat d and σd^\sigma_{\hat d}
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
d^\hat d — the estimate of the distance to the wall, the green centre; the hat — an estimate; dashed green — the last one
σd^\sigma_{\hat d} — its typical error, the width; σ2\sigma^2 — its variance, the doubt
d−\textcolor{#1f77b4}{d^-}, σd−\sigma_{d^-} — the prediction and its error; the minus — before the scan
v Δtv\,\Delta t — one step by the wheel odometry: speed × time
σu\sigma_u — the error one step adds
z\textcolor{#d62728}{z} — the scan of the lidar's forward ray; σz\sigma_z — its error: the lidar's own, constant
KK — the Kalman gain: the scan's weight, from 0 to 1
z−d−z - d^- — the innovation: how far the scan is from the prediction

▶️ youtu.be/ul3u2yLPwU0

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Kalman Predict: the Doubt Grows

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

Normal distributions on the error axis: the estimate with σ 1, the step with σ 2 dashed, the prediction with σ √5, larger than both

d−=d^−v Δtσd−2=σd^2+σu2\textcolor{#1f77b4}{d^-} = \hat d - v\,\Delta t \qquad \textcolor{#1f77b4}{\sigma_{d^-}^2} = \sigma_{\hat d}^2 + \sigma_u^2

The Gaussian moves with the robot, its σ grows: variances add.

Python · filterpy/stats/stats.py — filterpy's helper that adds two Gaussians, library code
def add(mean1, var1, mean2, var2):
    return (mean1+mean2, var1+var2)

Only a measurement shrinks the doubt — the update, next.

Legend
d^\hat d — the estimate of the distance to the wall after the last scan, before the robot moves; σd^\sigma_{\hat d} — its typical error, 1 cm (example)
v Δtv\,\Delta t — the step: how far the wheel odometry says the robot moved between two scans
σu\sigma_u — the error one step adds: the wheels', 2 cm (example); σu2\sigma_u^2 — Q in filterpy and robot_localization
d−\textcolor{#1f77b4}{d^-}, σd−\textcolor{#1f77b4}{\sigma_{d^-}} — the prediction and its error; the minus — before the scan
σ2\sigma^2 — a variance; variances add, σ do not: 1 + 4 = 5, so σd−\sigma_{d^-} = √5, not 1 + 2
mean1, var1 — the estimate: d^\hat d, σd^2\sigma_{\hat d}^2; mean2, var2 — the step: −v Δt-v\,\Delta t, σu2\sigma_u^2
green — the estimate; dashed — the step's error; blue — the prediction

▶️ youtu.be/d_qvLDhkg00

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Wheels and Lidar: Which Weight Is Best?

The robot from the side before a wall at distance d, a dashed copy one step v Δt back, the red forward ray to the wall; above the wall a blue Gaussian d⁻ ±2 cm, a red Gaussian z ±1 cm and a dashed green mix d̂ with a question mark; a dotted line up from the wall marks where it truly is
  • The wheels predict: d−=d^k−1−v Δt\textcolor{#1f77b4}{d^-} = \hat d_{k-1} - v\,\Delta t, off by σd−\sigma_{d^-} = 2 cm (example)
  • The lidar measures with its forward ray, one of 360, straight at the wall: z=d+noise\textcolor{#d62728}{z} = d + \text{noise}, off by σz\sigma_z = 1 cm (example)
  • Mix them with a weight ww for the scan:

d^=(1−w) d−+w z=d−+w (z−d−)\textcolor{#2ca02c}{\hat d} = (1-w)\,\textcolor{#1f77b4}{d^-} + w\,\textcolor{#d62728}{z} = \textcolor{#1f77b4}{d^-} + w\,(\textcolor{#d62728}{z} - \textcolor{#1f77b4}{d^-})

The same weighted mix as in the complementary filter: which ww 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 σ2\sigma^2: how sure the estimate is

Legend
dd — the true distance to the wall
d−\textcolor{#1f77b4}{d^-} — the prediction: the last estimate moved by the wheel odometry's step; the minus — before the scan
σd−\sigma_{d^-} — the prediction's typical error
d^k−1\hat d_{k-1} — the estimate one step earlier, at step k−1k-1
z\textcolor{#d62728}{z} — the lidar's forward ray; σz\sigma_z — its typical error
d^\textcolor{#2ca02c}{\hat d} — the mix, the fused estimate: the new estimate
v Δtv\,\Delta t — how far the robot moved in one step: speed × time
ww — the scan's weight: any number from 0 to 1. The loop's KK is the best ww — 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

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/04-One-Dimensional-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Right Weight Beats Both Sensors

Each error is + or −, equally likely: lidar ±1 cm, wheels ±2 cm (example). Mix with w=0.8w = 0.8:

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²

Sigma of the mix against the scan weight w from 0 to 1: 2 cm with wheels only, 1.12 half and half, 0.89 at w = 0.8, 1 cm with the lidar only; shaded where the mix beats the lidar

Legend
ww — 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²
σd^\sigma_{\hat d} — the mix's typical error: the root of the mean square
lidar alone: σz\sigma_z = 1 cm · wheels alone: σd−\sigma_{d^-} = 2 cm (example)
green curve — σd^\sigma_{\hat d} for every weight; red dashed — the lidar alone; shaded — where the mix beats it

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Error of the Mix Is a Parabola

σd^2(w)=(1−w)2 σd−2+w2 σz2\textcolor{#2ca02c}{\sigma_{\hat d}^2}(w) = (1-w)^2\,\textcolor{#1f77b4}{\sigma_{d^-}^2} + w^2\,\textcolor{#d62728}{\sigma_z^2}

The error of the mix for every weight: the sum of the two parts has a bottom.

σd−2=4\sigma_{d^-}^2 = 4, σz2=1\sigma_z^2 = 1 cm² (example)

The mix's variance against w: the prediction part falls, the lidar part grows, their sum has its bottom at w = 0.8; dots at w = 0, 0.2, 0.5, 0.8 and 1 with their values

Legend
σd^2(w)\textcolor{#2ca02c}{\sigma_{\hat d}^2}(w) — the mix's variance, its doubt: the average of its error² over many scans, cm²
ww — the scan's weight, from 0 to 1
σd−2\textcolor{#1f77b4}{\sigma_{d^-}^2} — the prediction's variance: the average of its error²
σz2\textcolor{#d62728}{\sigma_z^2} — the lidar's variance
(1−w)2σd−2(1-w)^2 \sigma_{d^-}^2 — the prediction's part: its error × (1 − w), so its variance × (1 − w)²: blue dashed
w2σz2w^2 \sigma_z^2 — the lidar's part: its error × ww, its variance × w2w^2: red dashed
green — their sum, σd^2\sigma_{\hat d}^2; dots — a few weights, with their values

▶️ youtu.be/ul3u2yLPwU0

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Bottom of the Parabola Is the Kalman Gain KK

K=σd−2σd−2+σz2K = \frac{\textcolor{#1f77b4}{\sigma_{d^-}^2}}{\textcolor{#1f77b4}{\sigma_{d^-}^2} + \textcolor{#d62728}{\sigma_z^2}}

The best weight: the prediction's share of the total doubt.

Three pairs of noises, cm² (example); the picture is the first:

case σd−2\sigma_{d^-}^2 σz2\sigma_z^2 KK
wheels unsure 4\textcolor{#1f77b4}{4} 1\textcolor{#d62728}{1} 4/(4+1)\textcolor{#1f77b4}{4} / (\textcolor{#1f77b4}{4} + \textcolor{#d62728}{1}) = 0.8
lidar unsure 1\textcolor{#1f77b4}{1} 4\textcolor{#d62728}{4} 1/(1+4)\textcolor{#1f77b4}{1} / (\textcolor{#1f77b4}{1} + \textcolor{#d62728}{4}) = 0.2
equally sure 1\textcolor{#1f77b4}{1} 1\textcolor{#d62728}{1} 1/(1+1)\textcolor{#1f77b4}{1} / (\textcolor{#1f77b4}{1} + \textcolor{#d62728}{1}) = 0.5
Top: the parabola of the mix's variance σ²_d̂(w) with its bottom at w = 0.8. Bottom: on the same w axis a bar of the total doubt, 5: the prediction's variance 4 in blue and the lidar's 1 in red; the bar splits at 0.8, right under the bottom

Legend
green curve — the mix's variance σd^2\sigma_{\hat d}^2 for every weight ww of the scan; green dot — its bottom, the best ww
dashed line — from the bottom down to the bar: the bar splits right under it
bar — the total doubt σd−2+σz2\sigma_{d^-}^2 + \sigma_z^2: blue — the prediction's part, red — the lidar's
KK — the Kalman gain: the best weight of the scan, from 0 to 1
σd−2\textcolor{#1f77b4}{\sigma_{d^-}^2} — the prediction's variance: its typical error, squared
σz2\textcolor{#d62728}{\sigma_z^2} — the lidar's variance
σd^2\sigma_{\hat d}^2 — the mix's variance, its doubt, cm²
σd−=0\sigma_{d^-} = 0 → K=0K = 0: keep the prediction · σz=0\sigma_z = 0 → K=1K = 1: take the scan

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/04-One-Dimensional-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

From One Number to Matrices

our drive: one number the matrix form: many numbers
the state dd a column of nn numbers: the pose x,y,θx, y, \theta
its doubt σd−2\textcolor{#1f77b4}{\sigma^2_{d^-}} P−\textcolor{#1f77b4}{P^-}, n×nn \times n: each number's variance on the diagonal; off it, how two errors depend on each other — not diagonal
what the sensor reads z=dz = d, the distance itself z=H⋅z = H\cdotstate: HH, m×nm \times n, picks from the state what the sensor reads
the sensor's noise σz2\textcolor{#d62728}{\sigma^2_z} R\textcolor{#d62728}{R}, m×mm \times m: diagonal when each reading errs on its own
a doubt through a number a σ2a=a2σ2a\,\sigma^2 a = a^2\sigma^2 H P−H⊤H\,\textcolor{#1f77b4}{P^-}H^\top: HH on both sides
divide by SS 1/S1/S multiply by S−1S^{-1}, the inverse matrix
the gain KK one number: the scan's weight n×mn \times m: a reading moves every number whose error depends on it
H picks from the state what the sensor reads. Top, our drive: z = 1 · d = d. Bottom, the pose (x, y, θ) and the IMU's yaw: z = [0 0 1] · (x, y, θ) = θ; H's ones grey, what they pick framed

Legend
zz — the reading: the lidar's distance, the IMU's yaw
HH — the measurement matrix: picks from the state what the sensor reads; nn — state numbers, mm — readings
P−\textcolor{#1f77b4}{P^-}, R\textcolor{#d62728}{R} — the prediction's and the sensor's covariance matrices
aa — a number an error is multiplied by; σ2\sigma^2 — that error's variance
H⊤H^\top — HH turned over; SS — the reading's whole doubt; S−1S^{-1} — the inverse of SS; KK — the Kalman gain
grey — HH's ones; framed — what they pick

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/06-Multivariate-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

What H P−H⊤H\,P^-H^\top Computes

H P⁻ Hᵀ — the doubt of what the sensor should read. Top, our drive: 1 · σ²_d⁻ · 1 = σ²_d⁻; a σ² a = a²σ²: an error times a, its variance times a². Bottom, the pose (x, y, θ) and the IMU's yaw: [0 0 1] · P⁻ · [0 0 1]ᵀ = σ²_θ; the ones pick θ's row and column of the 3 × 3 table P⁻; dashed, cov(y, θ): 2 m forward, θ off by 0.1 rad, y off by 0.2 m
  • H P−H⊤H\,\textcolor{#1f77b4}{P^-}H^\top — the doubt of the reading we expect: the prediction's doubt, carried into what the sensor reads
  • HH twice: the error passes through HH, and a variance is a square — so HH on both sides, as aa in a σ2aa\,\sigma^2 a
  • Who fills P−\textcolor{#1f77b4}{P^-}: the filter — predict ties the errors, update shrinks them; we set only the start and the noises

Legend
HH — picks from the state what the sensor reads; H⊤H^\top — the same ones as a column
P−\textcolor{#1f77b4}{P^-} — the prediction's covariance matrix: variances on the diagonal, blue
cov — a covariance: how two errors depend on each other
σx2\sigma^2_x, σy2\sigma^2_y, σθ2\sigma^2_\theta — the variances of xx, yy and the yaw θ\theta; σd−2\sigma^2_{d^-} — our prediction's
aa — a number an error is multiplied by; σ2\sigma^2 — its variance
framed — what the ones pick; dashed — cov(y,θ)(y, \theta)

▶️ www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Kalman Gain in Matrices, Piece by Piece

The matrix form, as filterpy writes it: K=P−H⊤(HP−H⊤+R)−1K = \textcolor{#1f77b4}{P^-}H^\top(H\textcolor{#1f77b4}{P^-}H^\top + \textcolor{#d62728}{R})^{-1} — our 1D gain, piece by piece:

piece our drive, 1D matrices what it is
1 σd−2\textcolor{#1f77b4}{\sigma^2_{d^-}} H P−H⊤H\,\textcolor{#1f77b4}{P^-}H^\top the doubt of the reading we expect
2 σd−2+σz2\textcolor{#1f77b4}{\sigma^2_{d^-}} + \textcolor{#d62728}{\sigma^2_z} S=HP−H⊤+RS = H\textcolor{#1f77b4}{P^-}H^\top + \textcolor{#d62728}{R} plus the sensor's noise: variances add
3 σd−2\textcolor{#1f77b4}{\sigma^2_{d^-}} on top P−H⊤\textcolor{#1f77b4}{P^-}H^\top HH once: each state number's error paired with the reading's
4 divide by the sum multiply by S−1S^{-1} the inverse instead of a division
KK σd−2σd−2+σz2\dfrac{\textcolor{#1f77b4}{\sigma^2_{d^-}}}{\textcolor{#1f77b4}{\sigma^2_{d^-}} + \textcolor{#d62728}{\sigma^2_z}} P−H⊤S−1\textcolor{#1f77b4}{P^-}H^\top S^{-1} how far each state number moves
The whole doubt S = 5 (example) as a bar: the prediction's part, 4, in blue, the lidar's, 1, in red; the inverse of S, for one number 1/S, is 0.2; a bracket under the blue part: K = 4 × 0.2 = 0.8, the prediction's share

With one number, every piece is the 1D one: KK is the bottom of the parabola again.

Legend
KK — the Kalman gain; HH — picks what the sensor reads
P−\textcolor{#1f77b4}{P^-}, R\textcolor{#d62728}{R} — the prediction's and the sensor's covariance matrices
SS — the reading's whole doubt
bar — SS (example): blue — the prediction's part, red — the lidar's; under the blue part — KK

Linear algebra
A BA\,B — each row of AA times each column of BB, summed; (m×n)(n×p)=m×p(m \times n)(n \times p) = m \times p
A⊤A^\top — rows become columns
A−1A^{-1} — the inverse: A−1A=IA^{-1}A = I, the identity; for a number, 1/A1/A

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/06-Multivariate-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Formula ↔ filterpy: the Kalman Gain

K=P−H⊤ (HP−H⊤+R)−1K = \textcolor{#1f77b4}{P^-}H^\top\,(H\textcolor{#1f77b4}{P^-}H^\top + \textcolor{#d62728}{R})^{-1}

Python · filterpy/kalman/kalman_filter.py — filterpy's KalmanFilter.update, library code
# common subexpression for speed
PHT = dot(self.P, H.T)

# S = HPH' + R
# project system uncertainty into measurement space
self.S = dot(H, PHT) + R
self.SI = self.inv(self.S)
# K = PH'inv(S)
# map system uncertainty into kalman gain
self.K = dot(PHT, self.SI)

 
P−H⊤\textcolor{#1f77b4}{P^-H^\top}
 
 
 
S=HP−H⊤+RS = \textcolor{#1f77b4}{HP^-H^\top} + \textcolor{#d62728}{R}
S−1S^{-1} — the inverse of SS
 
 
K=P−H⊤ S−1K = \textcolor{#1f77b4}{P^-H^\top}\,S^{-1}

The formula's pieces, line for line.

  • S−1S^{-1} — the inverse of SS (inv): a matrix is not divided but multiplied by its inverse; for one number, 1/S1/S
  • We set R\textcolor{#d62728}{R} and, for predict, Q=σu2Q = \sigma_u^2 · the filter computes P−\textcolor{#1f77b4}{P^-}, SS, KK at every scan

Legend
blue — the prediction's part, red — the lidar's
KK — the Kalman gain, the scan's weight: self.K
P−\textcolor{#1f77b4}{P^-} — the prediction's doubt, our σd−2\sigma_{d^-}^2: self.P
R\textcolor{#d62728}{R} — the lidar's doubt, our σz2\sigma_z^2: R
HH — picks what the sensor reads: 1, the distance itself
H⊤H^\top — HH turned over: H.T
PHT — P−H⊤P^-H^\top, computed once for speed
SS — the whole doubt: self.S; SI — S−1S^{-1}
dot — the matrix product: rows of the left times columns of the right

🎯 github.com/rlabbe/filterpy

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Move the Noises: KK Follows the More Accurate Sensor

The filter computes, every scan: σd−2=σd^2+σu2\textcolor{#1f77b4}{\sigma_{d^-}^2} = \sigma_{\hat d}^2 + \sigma_u^2 · we set once: σu\sigma_u and σz\textcolor{#d62728}{\sigma_z} · here both doubts are sliders: what if the prediction were less or more sure

Legend
σd−\textcolor{#1f77b4}{\sigma_{d^-}} — the prediction's doubt as a typical error: the upper slider
σd^\sigma_{\hat d} — the estimate's typical error after the last scan
σu\sigma_u — what one step of the wheel odometry adds: its speed error × the time between scans
σz\textcolor{#d62728}{\sigma_z} — the lidar's typical error, measured standing: the lower slider
ww — the scan's weight; KK — the best ww, the bottom of the curve
σd^2(w)\sigma^2_{\hat d}(w) — the estimate's variance for each ww
start (example): σd−\sigma_{d^-} = 2 cm, σz\sigma_z = 1 cm

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Update Combines Two Gaussians

d^=d−+K (z−d−)\textcolor{#2ca02c}{\hat d} = \textcolor{#1f77b4}{d^-} + K\,(\textcolor{#d62728}{z} - \textcolor{#1f77b4}{d^-})

σd^2=(1−K) σd−2\textcolor{#2ca02c}{\sigma_{\hat d}^2} = (1-K)\,\textcolor{#1f77b4}{\sigma_{d^-}^2}

Python · filterpy/stats/stats.py · mul — filterpy's helper: the product of two Gaussians, library code
mean = (var1*mean2 + var2*mean1) / (var1 + var2)
var = 1 / (1/var1 + 1/var2)

As on the grid, the update multiplies the belief by the scan's Gaussian.

Legend
blue — the prediction, red — the forward ray, green — the estimate
d^\textcolor{#2ca02c}{\hat d}, σd^\textcolor{#2ca02c}{\sigma_{\hat d}} — the estimate after the update, its typical error
d−\textcolor{#1f77b4}{d^-}, σd−\textcolor{#1f77b4}{\sigma_{d^-}} — the prediction, its typical error: the filter computes both in predict
z\textcolor{#d62728}{z} — the ray's reading at this scan; σz\textcolor{#d62728}{\sigma_z} — its typical error, from the datasheet or measured standing: we set it once
KK — the Kalman gain, the scan's best weight
σ2\sigma^2 — a variance; ±σ holds 68 %
start: 51 ± 2 cm (example); the ray 49.7 ± 1.01 cm, measured
mean1, var1 — d−d^-, σd−2\sigma_{d^-}^2; mean2, var2 — zz, σz2\sigma_z^2; mean, var — d^\hat d, σd^2\sigma_{\hat d}^2

▶️ www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Starting the Filter

The Kalman filter on the drive to the wall from three starts, green lines: correct (solid), I do not know (dashed), wrong and sure (dotted), each start a hollow dot; a bold dot where the wrong and sure start comes within 2 cm of the truth, after 13 scans; the truth in black, the ray's scans as red dots

K1=σ02σ02+σz2K_1 = \frac{\textcolor{#1f77b4}{\sigma_0^2}}{\textcolor{#1f77b4}{\sigma_0^2} + \textcolor{#d62728}{\sigma_z^2}}

The first scan's weight: the start's doubt stands where the prediction's was.

  • We set the start before the first scan: a first guess d^0\hat d_0 and σ0\textcolor{#1f77b4}{\sigma_0} — how far d^0\hat d_0 may be from the truth; in robot_localization — initial_estimate_covariance
  • σ0\sigma_0 is the error of whatever gave d^0\hat d_0: a tape measure from the wall — its accuracy, about 1 cm; the first scan — σz\sigma_z; nothing known — so large that ±3σ0\pm 3\sigma_0 covers every place the robot can be
  • Unsure? Take it larger: a large σ0\sigma_0 costs one scan; a wrong small one — many

Legend
green lines — the filter's estimate from three starts: solid — correct, the true 2 m, σ0=σz\sigma_0 = \sigma_z; dashed — "I do not know", 0 m, σ0\sigma_0 = 10 m; dotted — wrong and sure, 0.5 m short, σ0\sigma_0 = 1 mm; hollow dots — the starts; the bold dot — within 2 cm
black — the truth, Gazebo's ground truth; red dots — the scans zz of the forward ray
K1K_1 — the Kalman gain of the first scan
d^0\hat d_0 — the first guess of the distance to the wall; σ0\textcolor{#1f77b4}{\sigma_0} — its typical error: how far it may be from the truth
σz\textcolor{#d62728}{\sigma_z} — the lidar's typical error, 10.1 mm on our drive

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/04-One-Dimensional-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Formula ↔ filterpy: the Loop

predict  d−=d^−v Δtσd−2=σd^2+σu2\ \textcolor{#1f77b4}{d^-} = \hat d - v\,\Delta t \qquad \textcolor{#1f77b4}{\sigma_{d^-}^2} = \sigma_{\hat d}^2 + \sigma_u^2
update  d^=d−+K (z−d−)σd^2=(1−K) σd−2\ \textcolor{#2ca02c}{\hat d} = \textcolor{#1f77b4}{d^-} + K\,(\textcolor{#d62728}{z} - \textcolor{#1f77b4}{d^-}) \qquad \textcolor{#2ca02c}{\sigma_{\hat d}^2} = (1-K)\,\textcolor{#1f77b4}{\sigma_{d^-}^2}

Python · filterpy/kalman/kalman_filter.py — filterpy's KalmanFilter.predict() and update(), library code
# predict
# x = Fx + Bu
self.x = dot(F, self.x) + dot(B, u)
# P = FPF' + Q
self.P = self._alpha_sq * dot(dot(F, self.P), F.T) + Q

# update: S, SI and K first, as in the Kalman gain's code
self.y = z - dot(H, self.x)
self.x = self.x + dot(self.K, self.y)
I_KH = self._I - dot(self.K, H)
self.P = dot(dot(I_KH, self.P), I_KH.T) + dot(dot(self.K, R), self.K.T)
  • We set Q and R once · the filter computes x, P and KK at every step

Legend
Q — σu2\sigma_u^2, one odometry step's doubt
R — σz2\sigma_z^2, the lidar's doubt
x — the state: d−d^-, then d^\hat d
P — its variance: σd−2\sigma_{d^-}^2, then σd^2\sigma_{\hat d}^2
y — the scan minus the prediction, z−d−z - d^-
KK — the Kalman gain; I_KH — 1−K1 - K
FF — the state's own change: 1 here
B uB\,u — the step −v Δt-v\,\Delta t: speed × time
HH — picks what the sensor reads: 1
dot — the matrix product, rows × columns; for numbers, ×
.T — turned over; _alpha_sq — 1

🎯 github.com/rlabbe/filterpy
▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/06-Multivariate-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Filter on Our Drive: No Lag, Less Noise

Left, Gazebo from the side with a dark Gazebo label: the robot drives to the wall; under it a grey bracket of ±6 cm around the truth and three lanes zooming into it — the truth d, the jumping ray z and the filter's estimate d̂ in its light green ±2σ band. Right, the whole drive over time with a grey cursor at the frame's time: distances above — the truth, the ray, the mean of the last 20 scans, the filter; errors below — the ray, the wheels alone, the filter with its ±2σ band; RMSE in the legend

RMSE=1N∑k=1Nek2\mathrm{RMSE} = \sqrt{\frac{1}{N}\sum_{k=1}^{N} e_k^2}

  • RMSE (root mean square error) — a line's typical error over the drive, in mm: one measure for the filter, the ray, the wheels and the average
  • eke_k — the line's error at scan kk: the line minus the truth dd; NN — the 84 scans while moving · grey bracket — the 12 cm zoomed in under the robot
  • Conclusion: the filter has the smallest error and no lag — it predicts the motion
KAU · Autonomous Robotic Systems Engineering · Lecture 7

Tuning: the Lidar's Noise and the Wheels' Noise

σz: measured standingσu=σv Δt\textcolor{#d62728}{\sigma_z}\text{: measured standing} \qquad \sigma_u = \sigma_v\,\Delta t

Conclusion: the measured noises give the smallest error

Legend
black — the truth, zero error; red dots — each scan's error; green — the estimate's error, its band ±2σd^\pm 2\sigma_{\hat d}
σz\textcolor{#d62728}{\sigma_z} — the lidar's typical error; filterpy's R = σz2\sigma_z^2
σu\sigma_u — one step's distance error, m: the speed error held for Δt\Delta t; filterpy's Q = σu2\sigma_u^2
σv\sigma_v — the wheels' speed error, m/s, from the wheels' driver
Δt\Delta t — the time between two scans, s
we set σz\sigma_z and σu\sigma_u once; the slider multiplies σu2\sigma_u^2, σz\sigma_z stays as measured
KK — the scan's weight; σd^\sigma_{\hat d} — the estimate's typical error

▶️ youtu.be/ul3u2yLPwU0

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Covariance Matrix

P=[σx2cov(x,y)cov(x,y)σy2]cov(x,y)=the mean of x⋅yP = \begin{bmatrix} \sigma_x^2 & \mathrm{cov}(x, y) \\ \mathrm{cov}(x, y) & \sigma_y^2 \end{bmatrix} \qquad \mathrm{cov}(x, y) = \text{the mean of } x \cdot y

Variances on the diagonal; off it, how the errors depend on each other.

Two clouds of position errors with the same spread along x and y. Left, cov(x, y) = 0: the errors are independent, a round cloud. Right, cov(x, y) > 0: the errors depend on each other, a cloud stretched at 45 degrees, with four points: A and B err the same way, C and D opposite ways

Legend
PP — the covariance matrix: the doubt of several numbers; 3 × 3 for the pose (x,y,θ)(x, y, \theta), θ\theta — the yaw; here only xx, yy
σx2\sigma_x^2, σy2\sigma_y^2 — the variance of each, cm²: on the diagonal, the same in both clouds
cov(x,y)\mathrm{cov}(x, y) — the covariance: multiply the two errors of each pair, average over many pairs, cm²; off the diagonal; 0 when the errors are independent
xx, yy — the position errors, cm
green dots — many position errors; black cross — zero error, the true position
A, B — both errors the same way: x⋅y>0x \cdot y > 0; C, D — opposite ways: x⋅y<0x \cdot y < 0

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/05-Multivariate-Gaussians.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Covariance Ellipse: ±σ\pm\sigma in 2D

  • ρ=cov(x,y) / (σxσy)\rho = \mathrm{cov}(x, y)\,/\,(\sigma_x\sigma_y) — the correlation: the covariance scaled to −1 … 1, so its slider tilts the cloud and leaves the spread
  • solid ellipse — 1σ, dashed — 2σ; σx\sigma_x, σy\sigma_y — the typical error along xx, yy; PP — the covariance matrix; start — the slanted cloud (example)
  • Conclusion: the 1σ ellipse holds fewer than half the points — RViz draws 1σ, so an honest filter is judged at 2σ
KAU · Autonomous Robotic Systems Engineering · Lecture 7

Three Variables: the Ellipse Becomes an Ellipsoid

A 3D view of x, y and the yaw θ: the end poses of simulated drives as grey dots, their 2σ ellipsoid as a green surface, tilted because the y error depends on the yaw error; on the floor of the box its shadow, the 2σ ellipse of x and y alone

P=[σx2000σy2cov(y,θ)0cov(y,θ)σθ2]P = \begin{bmatrix} \sigma_x^2 & 0 & 0 \\ 0 & \sigma_y^2 & \mathrm{cov}(y, \theta) \\ 0 & \mathrm{cov}(y, \theta) & \sigma_\theta^2 \end{bmatrix}

  • The pose has three numbers: PP is 3 × 3, the ellipse becomes an ellipsoid
  • It leans: the yy error depends on the yaw error — a robot that faces a little left drives to the left
  • The xx error comes from the speed, not the yaw: zeros
  • Its shadow on the floor is the xx–yy ellipse RViz draws

Legend
grey dots — 1000 simulated drives of 2 m with our wheels' noise
blue dots — 20 Gazebo runs: where the wheel odometry says each ended
green — their 2σ ellipsoid; dashed — its shadow, the 2σ ellipse of xx and yy
xx — the error along the drive, yy — sideways, mm; θ\theta — the yaw error, °
PP — the covariance matrix of (x,y,θ)(x, y, \theta)
σx2\sigma_x^2, σy2\sigma_y^2, σθ2\sigma_\theta^2 — the variance of each error: the diagonal
cov(y,θ)\mathrm{cov}(y, \theta) — the covariance of the yy and yaw errors, > 0; zeros — xx depends on neither (≈ 0 on these drives)

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/05-Multivariate-Gaussians.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

A Heading Error Becomes a Sideways Error

Left, from above, to scale: the robot drives 2 m with a heading off by −0.1, 0 or +0.1 rad and ends 20 cm right, on the line or 20 cm left. Right, the (θ, y) plane: before the drive the three heading errors share y = 0; after 2 m they lie on the line y = 2 m · θ — the y error depends on the yaw error

y−=1⋅y+2⋅θθ−=0⋅y+1⋅θ    ⇒    F=[1201]P−=F P F⊤:    [0000.01]→[0.040.020.020.01]\begin{aligned} y^- &= 1\cdot y + 2\cdot\theta \\ \theta^- &= 0\cdot y + 1\cdot\theta \end{aligned} \;\;\Rightarrow\;\; F = \begin{bmatrix} 1 & 2 \\ 0 & 1 \end{bmatrix} \qquad \textcolor{#1f77b4}{P^-} = F\,P\,F^\top:\;\; \begin{bmatrix} 0 & 0 \\ 0 & 0.01 \end{bmatrix} \to \begin{bmatrix} \textcolor{#1f77b4}{0.04} & \textcolor{#1f77b4}{0.02} \\ \textcolor{#1f77b4}{0.02} & 0.01 \end{bmatrix}

The step turns the yaw's doubt into a sideways doubt and ties yy to θ\theta.

Legend
grey lines — copies of the robot driving 2 m, each with its own yaw error; blue dots — where they end; grey rings — before the drive
yy — the sideways error, m; θ\theta — the yaw error, rad: ±0.1 rad ≈ ±6° (example); the minus — after the step
FF — the step as a table: a row per new value, a column per old one. Not a covariance: the motion
PP, P−\textcolor{#1f77b4}{P^-} — the covariance of (y,θ)(y, \theta) before and after the step
F⊤F^\top — FF turned over: rows become columns
FPF⊤FPF^\top — the doubt through the step: an error times aa has the variance a2σ2a^2\sigma^2, so FF stands on both sides
2 m × θ\theta: for small angles, sin⁡θ≈θ\sin\theta \approx \theta

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/06-Multivariate-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Kalman Filter Loses a Turning Robot

Run E's first U-turn from above: the truth in black; a Kalman filter whose step keeps the start's yaw, orange dashed, goes on straight; below, its position error over time: near zero on the straight, then growing by metres after the turn starts

The drive, by command: 1 m straight, a right U-turn, 1 m back. Known: the speed v (wheel odometry) and the yaw (gyroscope). Nobody measures x, y.

One step from above: the robot faces its yaw now, theta hat, and its step v delta t splits into v delta t cos theta hat along x and v delta t sin theta hat along y; the Kalman filter's step, orange dashed, goes along the start's yaw theta zero

x−=x^+v Δtcos⁡θ0y−=y^+v Δtsin⁡θ0x−=x^+v Δtcos⁡θ^y−=y^+v Δtsin⁡θ^\begin{aligned} \textcolor{#e8710a}{x^-} &\textcolor{#e8710a}{= \hat x + v\,\Delta t\cos\theta_0} & \textcolor{#e8710a}{y^-} &\textcolor{#e8710a}{= \hat y + v\,\Delta t\sin\theta_0} \\ x^- &= \hat x + v\,\Delta t\cos\hat\theta & y^- &= \hat y + v\,\Delta t\sin\hat\theta \end{aligned}

The robot's step turns with θ^\hat\theta: cos⁡θ^\cos\hat\theta, sin⁡θ^\sin\hat\theta are curves. The Kalman filter needs a line, x−=Fx^+Bux^- = F\hat x + Bu with fixed FF, BB — the fixed θ0\theta_0 gives one.

Legend
black — Gazebo's ground truth; orange dashed — the Kalman filter stepping along θ0\theta_0 (filterpy); dots — now
position error — the distance from the filter's (x,y)(x, y) to the truth, m; grey band — the turn
x−x^-, y−y^- — the predicted position; x^\hat x, y^\hat y — the estimates, m
v Δtv\,\Delta t — one step: speed × time
θ^\hat\theta — the yaw now; θ0\theta_0 — the yaw at the start, a fixed number
FF, BB — the Kalman filter's fixed tables; uu — the step's input, v Δtv\,\Delta t

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/11-Extended-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

A Gaussian Through a Line and Through a Curve

One step of our robot, two cases. Left, a line: the speed 0.3 ± 0.05 m/s, green, through the step v Δt with Δt = 2 s comes out 0.6 ± 0.1 m, blue — a Gaussian again. Right, a curve: the yaw 0° ± 10°, green, through the step along x, 1 m · cos θ, comes out as a hump pressed against 1 m, blue, its mean 0.985 m below 1 — not a Gaussian
a line: the step v Δtv\,\Delta t a curve: the step along xx, v Δtcos⁡θv\,\Delta t\cos\theta
what comes out a Gaussian again, σ\sigma × 2 not a Gaussian: pressed against 1 m, its mean below
the Kalman filter carries the doubt exactly: σ2\sigma^2 × 4 cannot carry it as it is
  • Conclusion: a turning robot steps by cos⁡θ^\cos\hat\theta and sin⁡θ^\sin\hat\theta — curves — so the plain Kalman filter cannot carry its doubt; the EKF fixes this next

Legend
green — what goes in; blue — what comes out: 200 000 random inputs pushed through
black — the line and the curve: what one step does to the number
grey dashed — the input's centre carried through; blue dot — the mean of what comes out
vv — the speed; Δt\Delta t — the step's time; v Δtv\,\Delta t — the step, m
θ\theta, θ^\hat\theta — the yaw, its estimate; Δx\Delta x — the step's part along xx
σ\sigma — the typical error, the ± on the picture; σ2\sigma^2 — the doubt, its variance
a line with slope 2: σ\sigma × 2, σ2\sigma^2 × 4

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The EKF Replaces the Curve With a Tangent Line

cos theta, the step along x for a 1 m step, with its tangent lines at 60 and 0 degrees; 10 degree Gaussians of the yaw under them and, on the left, what comes out: blue through the tangent line, the EKF, grey 200 000 yaws through cos theta, the truth; at 60 degrees they match, at 0 degrees the tangent line is flat and says no spread

Kalman filter:    Δx=v Δtcos⁡θ0a line: θ0 is fixedEKF:    Δx=v Δtcos⁡θ^a curve: it moves the estimate\begin{aligned} \textcolor{#e8710a}{\text{Kalman filter:}}\;\; & \textcolor{#e8710a}{\Delta x = v\,\Delta t\cos\theta_0} && \textcolor{#e8710a}{\text{a line: } \theta_0 \text{ is fixed}} \\ \text{EKF:}\;\; & \Delta x = v\,\Delta t\cos\hat\theta && \text{a curve: it moves the estimate} \end{aligned}

its tangent line at θ^\hat\theta carries the doubt:

Δx≈v Δtcos⁡θ^  − v Δtsin⁡θ^ ⋅(θ−θ^)\Delta x \approx v\,\Delta t\cos\hat\theta\; \textcolor{#1f77b4}{-\, v\,\Delta t\sin\hat\theta}\,\cdot(\theta - \hat\theta)

A tangent line is straight, so the Kalman filter's rule works again; its slope, blue, goes into FF.

C++ · robot_localization/src/ekf.cpp — the predict step of the EKF inside ekf_node
// the step itself: x grows by cos(yaw) · vx · Δt — the curve; it moves the estimate
transfer_function_(StateMemberX, StateMemberVx) = cy * cp * delta_sec;
// the tangent line's slope in yaw: −sin(yaw) · v · Δt
x_coeff = -sy * cp;
double dFx_dY =
  (x_coeff * x_vel + y_coeff * y_vel + z_coeff * z_vel) * delta_sec + ...
...
transfer_function_jacobian_(StateMemberX, StateMemberYaw) = dFx_dY;  // into F: row x, column yaw

Legend
black — cos⁡θ\cos\theta, the curve; blue dashed — its tangent lines at 60° and 0°; green — the yaw's Gaussian, ±10°; left — what comes out
Δx\Delta x — the step along xx; v Δtv\,\Delta t — the step: speed × time
θ\theta — the yaw; θ^\hat\theta — its estimate; θ0\theta_0 — the yaw at the start
tangent line — the straight line touching the curve at θ^\hat\theta, not tan⁡θ\tan\theta; its slope — the derivative, (cos⁡θ)′=−sin⁡θ(\cos\theta)' = -\sin\theta
sy, cy, cp — sin, cos of yaw, cos of pitch (1 on the floor); x_vel, delta_sec — vv, Δt\Delta t; y_vel, z_vel — 0
x_coeff, dFx_dY — the slopes of cos and of xx in yaw

🎯 github.com/cra-ros-pkg/robot_localization

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Kalman Filter vs EKF: Only the Predict Changes

Kalman filter EKF
the state the distance to the wall, dd the pose (x,y,θ)(x, y, \theta)
predict: the estimate a line: d−=d^−v Δtd^- = \hat d - v\,\Delta t a curve: x−=x^+v Δtcos⁡θ^x^- = \hat x + v\,\Delta t\cos\hat\theta
predict: the doubt, P−=FPF⊤+QP^- = F P F^\top + Q FF fixed: 1 FF — the tangent lines' slopes at θ^\hat\theta, new every step
KK and the update the same the same
implementation filterpy KalmanFilter robot_localization ekf_node
  • Conclusion: only predict changes — the estimate moves along the curve, its doubt through the tangent line
The same U-turn: the truth in black, the Kalman filter stepping along the start's yaw orange dashed, going on straight, and robot_localization's EKF in green, turning with the robot; below, the position errors: the orange grows by metres, the green stays near zero

The same drive: 1 m, a right U-turn, 1 m back.

Legend
black — the truth; orange dashed — the Kalman filter; green — the EKF
position error — the distance from the filter's (x,y)(x, y) to the truth, m
dd — the distance to the wall; xx, θ\theta — the position, the yaw
a hat — the estimate; a minus — the prediction, «before the measurement»
v Δtv\,\Delta t — one step
PP, P−P^- — the doubt (covariance) of the estimate, of the prediction
FF — the step's slopes; F⊤F^\top — FF turned over; QQ — what a step adds, set by us; KK — the Kalman gain

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/11-Extended-Kalman-Filters.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Monte Carlo: 20 Straight Drives Check the EKF's Ellipse

The same 2 m straight drive 20 times in Gazebo: one truth, the wheel odometry's tracks fanning out, their ends and the EKF's own 1 sigma and 2 sigma ellipse around the true end; right, the 20 ends to scale in mm with both ellipses and the true end as a cross, 19 of 20 ends inside 2 sigma

The same 2 m straight drive, 20 times in Gazebo: one truth, only the random noise on the wheel odometry's readings differs. The EKF: ekf_node on the wheel odometry alone.

  • Conclusion: 19 of the 20 ends lie inside the EKF's 2σ ellipse — on a straight drive its doubt is honest

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Monte Carlo on an Arc: the Ends Bend

1000 copies of the robot on a 2 m arc to the left, each with its own errors of the speed and of the turn rate: the ends lie on a thin bent crescent; the dashed 2 sigma ellipse of a Gaussian with the ends' centre and spread covers the empty floor inside the crescent, its centre a cross where no copy is, and misses the crescent's tips

The robot drives a 2 m arc to the left, 1000 times in Python: each copy with its own random errors — of the speed at every step, of the turn rate for the whole drive.

  • Conclusion: on an arc the ends bend into a thin crescent, and no ellipse fits it — there the EKF's Gaussian is only an approximation

▶️ github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb

KAU · Autonomous Robotic Systems Engineering · Lecture 7

robot_localization: What Goes In and Out

The wheel odometry's /odom with vx, vy, vyaw and the gyroscope's /imu with vyaw go into ekf_node's update; inside, one state — the pose, the speeds, the accelerations, 15 numbers — and its covariance P, moved by predict; out of the same state come /odometry/filtered, the pose, the speeds and P, and the TF odom → base_link, the same pose as a transform, publish_tf: true
  • Given: the speeds from the wheel odometry and the gyroscope · wanted: the pose (x,y,θ)(x, y, \theta) and its doubt PP
  • Fuse speeds, not poses: the wheel odometry's pose drifts, its speed is a fresh reading every time; vy = 0 is a reading too — a wheeled robot does not drive sideways
  • The wheel odometry is a measurement here, in update; at the wall it was the predict

Legend
blue — what goes in; green — what comes out
vx, vy, vyaw — the speed forward, sideways and of turning
(x,y,θ)(x, y, \theta) — the pose: the position and the yaw
PP — the covariance: the doubt of every number of the state
TF odom → base_link — where the robot's body is from the place the odometry started
publish_tf — whether ekf_node sends that TF

▶️ docs.nav2.org/jazzy/configuration_and_development/first_time_robot_setup_guide/odom/setup_robot_localization/
🎯 github.com/cra-ros-pkg/robot_localization

KAU · Autonomous Robotic Systems Engineering · Lecture 7

ekf.yaml: Which Variables to Fuse

YAML · ekf.yaml — the parameters of robot_localization's ekf_node (excerpt)
odom0: /odom
# true — a measurement from /odom, false — ignored
odom0_config: [false, false, false,   # x, y, z
               false, false, false,   # roll, pitch, yaw
               true,  true,  false,   # vx, vy, vz
               false, false, true,    # vroll, vpitch, vyaw
               false, false, false]   # ax, ay, az
imu0: /imu
# the same 15 flags: only vyaw is true
imu0_config:  [false, false, false, false, false, false,
               false, false, false, false, false, true,
               false, false, false]
two_d_mode: true    # a flat floor: z, roll, pitch fused as 0
publish_tf: true    # odom → base_link comes from here
  • How many true? Any, all 15 too: each is one more number in update. Only what the sensor really measures — a true where the driver sends 0 pulls the estimate to 0
  • One source per absolute value — x, y, yaw: two that disagree make the estimate jump
  • two_d_mode: true — the filter fuses 0 for z, roll, pitch and their speeds: the estimate stays on the floor. Without it nothing measures them, and their doubt grows without bound

▶️ docs.nav2.org/jazzy/configuration_and_development/first_time_robot_setup_guide/odom/setup_robot_localization/
🎯 github.com/cra-ros-pkg/robot_localization

KAU · Autonomous Robotic Systems Engineering · Lecture 7

robot_localization Draws the Ellipse

RViz from above after two loops: white the truth, blue the wheel odometry, the EKF on the wheel odometry alone in orange under the blue with its yellow covariance ellipses, green the EKF with the gyroscope. One ellipse on the bottom straight is framed and zoomed into the middle of the loop: its 1σ outline from the filter's numbers in white, the estimate at its centre, the half-axes 8 and 5 cm; a white ring marks where the robot truly was then, 1.6 m away

RViz after two loops: white — the truth · blue — the wheel odometry · yellow — the EKF on the wheel odometry alone: its 1σ ellipses · green — the EKF with the gyroscope, later

  • The run: two loops by command · ekf_node on the wheel odometry's speeds alone · wanted: the pose and its doubt PP
  • The ellipses grow: nobody measures xx, yy
  • Conclusion: the ellipse is small, the truth far away — the filter does not know the wheels slip

▶️ docs.nav2.org/jazzy/configuration_and_development/first_time_robot_setup_guide/odom/setup_robot_localization/

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Four Wheels Slip in Turns

Left, RViz from above during the two loops, three times real time: the truth white, the wheel odometry blue, leaving it after each U-turn, the EKF with the gyroscope green, a dark RViz ×3 label; right, the yaw of the same run, the wheel odometry against the truth, with a cursor at the frame's time; at the end of the first U-turn a dot on each line: the wheel odometry −180°, the robot −137°

left, RViz ×3: white — the truth, blue — the wheel odometry, green — the EKF with the gyroscope, red — the robot model · right — the yaw: blue — the wheel odometry, black — the truth; the cursor — the frame's time

  • Four wheels cannot all roll in a turn: they slip, and the wheel odometry counts each turn as if they did not
  • Conclusion: the wheel odometry's yaw is wrong after every turn, and the EKF on it alone stays sure — its PP knows only the noise it is told. We need a sensor of the turn itself: the gyroscope

🎯 github.com/gazebosim/gz-sim/issues/872

KAU · Autonomous Robotic Systems Engineering · Lecture 7

Wheel Odometry + Gyroscope: Fusion on the Loop

Run E's two loops. Left, the yaw error over time: the wheel odometry in blue falls in steps to −165°, one step per U-turn; the gyroscope alone, light blue, and the EKF on the wheel odometry and the gyroscope, green, run together slowly down to −21° and −22°. Right, the position error, the distance to the truth: the wheel odometry up to 2.4 m and 2.1 m at the end, the EKF 0.5 m

The same two loops · ekf_node fuses the wheel odometry's speeds and the gyroscope's turn rate: in imu0_config only vyaw is true

wheel odometry gyroscope
how it errs slips in turns bias: the same small error in every reading
its yaw error grows with the number of turns time — even standing
  • Conclusion: the EKF takes the yaw from the gyroscope and so fixes the turns; what stays is the gyroscope's bias
KAU · Autonomous Robotic Systems Engineering · Lecture 7

Measure the Gyroscope's Bias While the Robot Stands

Δθ=b tb^=the mean of ω while the robot standsω−b^\Delta\theta = b\,t \qquad\qquad \textcolor{#7b3fa0}{\hat b} = \text{the mean of } \textcolor{#6baed6}{\omega} \text{ while the robot stands} \qquad \textcolor{#6baed6}{\omega} - \textcolor{#7b3fa0}{\hat b}

The yaw error grows in proportion to time, even standing; standing, the true rate is 0, so the gyroscope shows its bias.

Left: the robot stands still 61 s; the gyroscope's summed yaw goes down evenly to minus 34.8 degrees while the true yaw stays at 0; with the bias measured over the first 5 s subtracted it stays at minus 0.5. Right: two loops, the gyroscope's yaw error reaches minus 21.1 degrees; with the bias from 5.4 s at rest subtracted, plus 0.5
C++ · complementary_filter.cpp — imu_tools: the bias estimate inside complementary_filter_node
// standing still?
steady_state_ = checkState(ax, ay, az, wx, wy, wz);
if (steady_state_)
{
    ...
    wz_bias_ += bias_alpha_ * (wz - wz_bias_);
}
...
// every step: the rate minus the bias
double wz_unb = wz - wz_bias_;
  • imu_tools does it by itself: do_bias_estimation: true; its /imu/data carries ω−b^\omega - \hat b — give that to robot_localization

Legend
light blue — the gyroscope's yaw; purple — with b^\hat b subtracted; purple strip — the first 5 s, where b^\hat b is measured; black — the truth
Δθ\Delta\theta — the yaw error; tt — time
bb — the bias: the same small error in every reading; b^\textcolor{#7b3fa0}{\hat b} — the bias, measured
ω\textcolor{#6baed6}{\omega} — the gyroscope's turn rate, rad/s; ω−b^\omega - \hat b — without the bias
checkState — does the robot stand? ax…wz — the IMU's readings
wz_bias_ — b^\hat b, a running mean; bias_alpha_ — a new reading's weight, 0.01
wz_unb — ω−b^\omega - \hat b

🎯 github.com/CCNYRoboticsLab/imu_tools

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The EKF runs, and the robot in RViz jumps between two places. Why?

KAU · Autonomous Robotic Systems Engineering · Lecture 7
RViz from above: the robot model jumps between two poses, the wheel odometry's from DiffDrive and the EKF's, ringed in blue and green, up to 48 cm and 38 degrees apart; after DiffDrive's TF bridge stops at 53.6 s the robot is calm
$ ros2 topic info /tf --verbose     # who publishes on /tf
Publisher count: 3
Node name: robot_state_publisher
...
Node name: ekf_filter_node
...
Node name: ros_gz_bridge
...
YAML · bridge.yaml — ros_gz_bridge's topics: DiffDrive's TF taken out
# - ros_topic_name: "/tf"
#   gz_topic_name: "/model/demo_bot/tf"
#   ros_type_name: "tf2_msgs/msg/TFMessage"
#   gz_type_name: "gz.msgs.Pose_V"
#   direction: GZ_TO_ROS
$ ros2 topic info /tf --verbose
Publisher count: 2                  # robot_state_publisher, ekf_filter_node
  • /tf may have many publishers: each sends its own links. One TF link — one publisher

▶️ docs.nav2.org/jazzy/configuration_and_development/first_time_robot_setup_guide/odom/setup_robot_localization/

KAU · Autonomous Robotic Systems Engineering · Lecture 7

The Filter Family: Belief, Predict, Update

filter the belief predict update implementation
complementary one angle + the gyroscope's turn a fixed share of the gap to the accelerometer imu_complementary_filter
Bayes on a grid any shape, on cells move and blur every cell × the likelihood, rescale filterpy.discrete_bayes
Kalman one Gaussian along a straight line the weight KK filterpy KalmanFilter
EKF one Gaussian along a curve; the doubt through the tangent line the weight KK robot_localization ekf_node
UKF (unscented Kalman filter), not covered one Gaussian along a curve; the doubt through a few chosen points the weight KK robot_localization ukf_node
particles a cloud of guesses move each guess, with noise weigh each by the likelihood, keep the likely later: SLAM
  • Conclusion: a few numbers, a centre and PP — Kalman, EKF, UKF; many, of any shape — the grid, particles
  • Tracking: we know where we started. "Where am I on the map?" needs many guesses — the next lecture

📖 porabook.com/book/

KAU · Autonomous Robotic Systems Engineering · Lecture 7

What's Next

Lecture 8 — SLAM

A map from scans · where am I on that map · both at once — the simplest SLAM

📖 porabook.com/book/

KAU · Autonomous Robotic Systems Engineering · Lecture 7