Udacity Self-Driving Car Engineer Nanodegree: Lidar and Radar Fusion with Kalman Filters in C++.
The task is to track a prdestrain moving in front of our autonomous vehicle.
This project can use multiple data sources originating from different sensors to estimate a more accurate object state.
The Kalman Filter algorithm will go through the following steps:
- First measurement.
- initialize state and covariance matrices.
- Predict.
- Update.
- Do another predict and update step (when receive another sensor measurement).
Because our state vector only tracks position and velocity, we are modeling acceleration as a random noise.
void KalmanFilter::Predict() {
x_ = F_ * x_;
MatrixXd Ft = F_.transpose();
P_ = F_ * P_ * Ft + Q_;
}
x
state vectorz
measurement vector- For a lidar sensor, the
z
vector contains theposition−x
andposition-y
measurements.
- For a lidar sensor, the
H
measurement matrix- H is the matrix that projects your belief about the object's current state into the measurement space of the sensor.
- For lidar, this is a fancy way of saying that we discard velocity information from the state variable since the lidar sensor only measures position
- Find the right H matrix to project from a 4D state to a 2D observation space.
kf_.H_ = MatrixXd(2, 4); kf_.H_ << 1, 0, 0, 0, 0, 1, 0, 0;
R
Measurement Noise Covariance Matrix- Because our state vector only tracks position and velocity, we are modeling acceleration as a random noise.
- Generally, the parameters for the random noise measurement matrix will be provided by the sensor manufacturer.
kf_.R_ = MatrixXd(2, 2); kf_.R_ << 0.0225, 0, 0, 0.0225;
// compute the time elapsed between the current and previous measurements
// dt - expressed in seconds
float dt = (measurement_pack.timestamp_ - previous_timestamp_) / 1000000.0;
previous_timestamp_ = measurement_pack.timestamp_;
float dt_2 = dt * dt;
float dt_3 = dt_2 * dt;
float dt_4 = dt_3 * dt;
// 1. Update state transition matrix F, so that the time is integrated
kf_.F_(0, 2) = dt;
kf_.F_(1, 3) = dt;
// 2. Update process covariance matrix Q based on new elapesd time
kf_.Q_ = MatrixXd(4, 4);
kf_.Q_ << dt_4 / 4 * noise_ax, 0, dt_3 / 2 * noise_ax, 0,
0, dt_4 / 4 * noise_ay, 0, dt_3 / 2 * noise_ay,
dt_3 / 2 * noise_ax, 0, dt_2*noise_ax, 0,
0, dt_3 / 2 * noise_ay, 0, dt_2*noise_ay;
// 3. Call the Kalman Filter predict() function
kf_.Predict();
// 4. Call the Kalman Filter update() function with the most recent raw measurements_
kf_.Update(measurement_pack.raw_measurements_);
void KalmanFilter::Update(const VectorXd &z) {
VectorXd z_pred = H_ * x_;
VectorXd y = z - z_pred;
MatrixXd Ht = H_.transpose();
MatrixXd S = H_ * P_ * Ht + R_;
MatrixXd Si = S.inverse();
MatrixXd PHt = P_ * Ht;
MatrixXd K = PHt * Si;
//new estimate
x_ = x_ + (K * y);
long x_size = x_.size();
MatrixXd I = MatrixXd::Identity(x_size, x_size);
P_ = (I - K * H_) * P_;
}
-
It works quite well when the pedestrian is moving along the straght line.
-
However, our linear motion model is not perfect, especially for the scenarios when the pedestrian is moving along a circular path.
-
To solve this problem, we can predict the state by using a more complex motion model such as the circular motion.
h
h
function that specifies how the predicted position and speed get mapped to the polar coordinates of range, bearing and range rate.h
non-linear -> KF update formula cannot be apply -> linearizeh
-> EKF
- firsr order taylor expansion
-
- evaluate the nonlinear function at the mean location
mu
(best estimation)
- evaluate the nonlinear function at the mean location
-
- extropulate the line with slope (first derivation) around
mu
- extropulate the line with slope (first derivation) around
-
MatrixXd CalculateJacobian(const VectorXd& x_state) {
MatrixXd Hj(3,4);
// recover state parameters
float px = x_state(0);
float py = x_state(1);
float vx = x_state(2);
float vy = x_state(3);
// pre-compute a set of terms to avoid repeated calculation
float c1 = px*px+py*py;
float c2 = sqrt(c1);
float c3 = (c1*c2);
// check division by zero
if (fabs(c1) < 0.0001) {
cout << "CalculateJacobian () - Error - Division by Zero" << endl;
return Hj;
}
// compute the Jacobian matrix
Hj << (px/c2), (py/c2), 0, 0,
-(py/c1), (px/c1), 0, 0,
py*(vx*py - vy*px)/c3, px*(px*vy - py*vx)/c3, px/c2, py/c2;
return Hj;
}
- linearize the nonlinear prediction and measurement function
- use the same mechanism to estimate the new state
The main differences betweem EKF and KF are:
- the
F
matrix will be replaced byF_j
when calculating ``P'. - the
H
matrix in the Kalman filter will be replaced by the Jacobian matrixH_j
when calculatingS
,K
, andP
. - to calculate
x'
, the prediction update function,f
, is used instead of theF
matrix. - to calculate
y
, theh
function is used instead of theH
matrix. - Because the linearization point change, we have to recalculate these matrix every loop.
- prediction update
- For this project, we are using a linear model for the prediction step, so we do not need to use the
f
function orF_j
. - If we had been using a non-linear model in the prediction step, we would need to replace the
F
matrix with its Jacobian,F_j
.
- For this project, we are using a linear model for the prediction step, so we do not need to use the
- measurement update
- The measurement update for lidar will also use the regular Kalman filter equations, since lidar uses linear equations.
- Only the measurement update for the radar sensor will use the extended Kalman filter equations.
VectorXd CalculateRMSE(const vector<VectorXd> &estimations,
const vector<VectorXd> &ground_truth) {
VectorXd rmse(4);
rmse << 0, 0, 0, 0;
// check the validity of the following inputs:
// * the estimation vector size should not be zero
// * the estimation vector size should equal ground truth vector size
if (estimations.size() != ground_truth.size()
|| estimations.size() == 0) {
cout << "Invalid estimation or ground_truth data" << endl;
return rmse;
}
//accumulate squared residuals
for (unsigned int i = 0; i < estimations.size(); ++i) {
VectorXd residual = estimations[i] - ground_truth[i];
//coefficient-wise multiplication
residual = residual.array()*residual.array();
rmse += residual;
}
//calculate the mean
rmse = rmse / estimations.size();
//calculate the squared root
rmse = rmse.array().sqrt();
//return the result
return rmse;
}