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
— the lidar's typical error; filterpy's R =
— one step's distance error, m: the speed error held for ; filterpy's Q =
— the wheels' speed error, m/s, from the wheels' driver
— the time between two scans, s
we set and once; the slider multiplies , stays as measured
— the scan's weight; — the estimate's typical error
Variances on the diagonal; off it, how the errors depend on each other.
Legend
— the covariance matrix: the doubt of several numbers; 3 × 3 for the pose , — the yaw; here only ,
, — the variance of each, cm²: on the diagonal, the same in both clouds
— the covariance: multiply the two errors of each pair, average over many pairs, cm²; off the diagonal; 0 when the errors are independent
, — the position errors, cm
green dots — many position errors; black cross — zero error, the true position
A, B — both errors the same way: ; C, D — opposite ways:
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/05-Multivariate-Gaussians.ipynb
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 and
— the error along the drive, — sideways, mm; — the yaw error, °
— the covariance matrix of
, , — the variance of each error: the diagonal
— the covariance of the and yaw errors, > 0; zeros — depends on neither (≈ 0 on these drives)
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/05-Multivariate-Gaussians.ipynb
The step turns the yaw's doubt into a sideways doubt and ties to .
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
— the sideways error, m; — the yaw error, rad: ±0.1 rad ≈ ±6° (example); the minus — after the step
— the step as a table: a row per new value, a column per old one. Not a covariance: the motion
, — the covariance of before and after the step
— turned over: rows become columns
— the doubt through the step: an error times has the variance , so stands on both sides
2 m × : for small angles,
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.
The robot's step turns with : , are curves. The Kalman filter needs a line, with fixed , — the fixed gives one.
Legend
black — Gazebo's ground truth; orange dashed — the Kalman filter stepping along (filterpy); dots — now
position error — the distance from the filter's to the truth, m; grey band — the turn
, — the predicted position; , — the estimates, m
— one step: speed × time
— the yaw now; — the yaw at the start, a fixed number
, — the Kalman filter's fixed tables; — the step's input,
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/11-Extended-Kalman-Filters.ipynb
| a line: the step | a curve: the step along , | |
|---|---|---|
| what comes out | a Gaussian again, × 2 | not a Gaussian: pressed against 1 m, its mean below |
| the Kalman filter | carries the doubt exactly: × 4 | cannot carry it as it is |
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
— the speed; — the step's time; — the step, m
, — the yaw, its estimate; — the step's part along
— the typical error, the ± on the picture; — the doubt, its variance
a line with slope 2: × 2, × 4
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb
its tangent line at carries the doubt:
A tangent line is straight, so the Kalman filter's rule works again; its slope, blue, goes into .
// 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 — , the curve; blue dashed — its tangent lines at 60° and 0°; green — the yaw's Gaussian, ±10°; left — what comes out
— the step along ; — the step: speed × time
— the yaw; — its estimate; — the yaw at the start
tangent line — the straight line touching the curve at , not ; its slope — the derivative,
sy, cy, cp — sin, cos of yaw, cos of pitch (1 on the floor); x_vel, delta_sec — , ; y_vel, z_vel — 0
x_coeff, dFx_dY — the slopes of cos and of in yaw
| Kalman filter | EKF | |
|---|---|---|
| the state | the distance to the wall, | the pose |
| predict: the estimate | a line: | a curve: |
| predict: the doubt, | fixed: 1 | — the tangent lines' slopes at , new every step |
| and the update | the same | the same |
| implementation | filterpy KalmanFilter |
robot_localization ekf_node |
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 to the truth, m
— the distance to the wall; , — the position, the yaw
a hat — the estimate; a minus — the prediction, «before the measurement»
— one step
, — the doubt (covariance) of the estimate, of the prediction
— the step's slopes; — turned over; — what a step adds, set by us; — the Kalman gain
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/11-Extended-Kalman-Filters.ipynb
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.
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb
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.
github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/09-Nonlinear-Filtering.ipynb
vy = 0 is a reading too — a wheeled robot does not drive sidewaysLegend
blue — what goes in; green — what comes out
vx, vy, vyaw — the speed forward, sideways and of turning
— the pose: the position and the yaw
— 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
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
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 0x, y, yaw: two that disagree make the estimate jumptwo_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
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
ekf_node on the wheel odometry's speeds alone · wanted: the pose and its doubt
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
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 |
The yaw error grows in proportion to time, even standing; standing, the true rate is 0, so the gyroscope shows its bias.
// 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 — give that to robot_localizationLegend
light blue — the gyroscope's yaw; purple — with subtracted; purple strip — the first 5 s, where is measured; black — the truth
— the yaw error; — time
— the bias: the same small error in every reading; — the bias, measured
— the gyroscope's turn rate, rad/s; — without the bias
checkState — does the robot stand? ax…wz — the IMU's readings
wz_bias_ — , a running mean; bias_alpha_ — a new reading's weight, 0.01
wz_unb —
$ 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
...
# - 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| 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 | filterpy KalmanFilter |
| EKF | one Gaussian | along a curve; the doubt through the tangent line | the weight | robot_localization ekf_node |
| UKF (unscented Kalman filter), not covered | one Gaussian | along a curve; the doubt through a few chosen points | the weight | robot_localization ukf_node |
| particles | a cloud of guesses | move each guess, with noise | weigh each by the likelihood, keep the likely | later: SLAM |
Lecture 8 — SLAM
A map from scans · where am I on that map · both at once — the simplest SLAM