EurekaMoments

This blog is to make a memo about Programming and Autonomous driving technologies which I studied.

Python sample program of Self-Localization simulation by Extended Kalman Filter

Introduction

Self-Localization technology is very essential for autonomous driving system to know the vehicle's position and pose. In this entry, I'm introducing a summary of Extended Kalman Filter(EKF) which is commonly used for self-localization algorithm and the Python sample program.

Table of contents

Source code at GitHub

All source codes are located at the following GitHub repository.

Extended Kalman Filter localization module
github.com

Self-Localization simulation
github.com

Basis of Kalman filter

Kalman filter is one of the optimization estimation algorithm and it is mainly used for the following purposes.

  1. Estimating a state which can not be observed directly based on a sensor's observation and a system model.
  2. Estimating the plausible state by fusing multiple observations.

I published this entry to introduce a simple sample program of 1D position and velocity estimation by Kalman filter. If you want to study the basis of Kalman filter, please refer to this entry.
www.eureka-moments-blog.com

Why EKF?

Basically, Kalman filter can be used only for a linear system, but almost system in the world is nonlinear. So, when we want to estimate a vehicle's state by Kalman filter, we need to linearize the system's state equation and observation equation. Kalman filter with the system's linearization is called Extended Kalman Filter(EKF). If you want to understand EKF in detail, please refer to this entry.
en.wikipedia.org

Implementation of Python program

From here, I'm explaining about implementing Python program of self localization by EKF.

1. Initialization

First, an initialization method of EKF localization module class is implemented as follow. This module can estimate a vehicle's state including position(x, y), yaw angle and velocity. So, a state vector is defined as 4 x 1 vector. And then, an error's covariance matrix can also be estimated. So, the matrix variable is defined as 4 x 4 identity matrix, "cov_mat". INPUT_NOISE_VAR_MAT is a variance matrix of acceleration input and yaw rate input for state equation. This is used for generating random noise of those input data. OBSRV_NOISE_VAR_MAT is a variance matrix of position(x, y) which is observed with GNSS. This is used for generating random noise of the observation data. JACOB_H is a Jacobian of observation equation. This is used for linearizing the observation equation. DRAW_COLOR is a color type to plot estimation result.

class ExtendedKalmanFilterLocalizer:
    """
    Self localization by Extended Kalman Filter class
    """

    def __init__(self, accel_noise=0.2, yaw_rate_noise=10.0,
                 obsrv_x_noise=1.0, obsrv_y_noise=1.0, color='r'):
        """
        Constructor
        color: Color of drawing error covariance ellipse
        """
        
        self.state = np.zeros((4, 1))
        self.cov_mat = np.eye(4)
        self.INPUT_NOISE_VAR_MAT = np.diag([accel_noise, np.deg2rad(yaw_rate_noise)]) ** 2
        self.OBSRV_NOISE_VAR_MAT = np.diag([obsrv_x_noise, obsrv_y_noise]) ** 2
        self.JACOB_H = np.array([[1, 0, 0, 0],
                                 [0, 1, 0, 0]])
        self.DRAW_COLOR = color

2. State module class

Next, an state module class is implemented as follow. This is used for updating and managing a vehicle's state including position(x, y), yaw angle and velocity. When this class is initialized, 4 initial state parameters, x, y, yaw and velocity are passed to the initialization method. An argument, color is to use set the color to plot computed position(x, y). The maximum/minimum values and stop value to limit the computed velocity are also defined as member constant values.

class State:
    """
    Vehicle state(x, y, yaw, speed) data and logic class
    """

    def __init__(self, x_m=0.0, y_m=0.0, yaw_rad=0.0, speed_mps=0.0, color='k'):
        """
        Constructor
        x_m: Vehicle's position, x[m]
        y_m: Vehicle's position, y[m]
        yaw_rad: Vehicle's yaw angle[rad]
        speed_mps: Vehicle's speed[m/s]
        """

        self.STOP_SPEED_MPS = 0.5 / 3.6 # 0.5[km/h]
        self.MAX_SPEED_MPS = 60 / 3.6 # 60[km/h]
        self.MIN_SPEED_MPS = -10 / 3.6 # -10[km/h]
        self.DRAW_COLOR = color

        self.x_m = x_m
        self.y_m = y_m
        self.yaw_rad = yaw_rad
        self.speed_mps = speed_mps

        self.x_history = [self.x_m]
        self.y_history = [self.y_m]

A state equation to compute state is defined as follow. This is defined based on constant acceleration linear motion model and output a new vehicle's state at the next time step. In the case of Kalman filter, this method is used for predicting the vehicle's state at the next time step.

    @staticmethod
    def motion_model(state, input, time_s):
        """
        Static function of motion model of vehicle state
        state: Vehicle's state (x, y, yaw, speed) object
        input: Motion input (acceleration, yaw rate) object
        time_s: Time interval per cycle[sec]
        """

        # to fix DeprecationWarning: Conversion of an array with ndim > 0 
        # to a scalar is deprecated, and will error in future. 
        # Ensure you extract a single element from your array 
        # before performing this operation. (Deprecated NumPy 1.25.)
        yaw_rad = state.item(2) # do not extract an element like state[2]
        
        A = np.array([[1, 0, 0, cos(yaw_rad) * time_s],
                      [0, 1, 0, sin(yaw_rad) * time_s],
                      [0, 0, 1, 0],
                      [0, 0, 0, 1]])
        
        B = np.array([[(cos(yaw_rad) * time_s**2) / 2, 0],
                      [(sin(yaw_rad) * time_s**2) / 2, 0],
                      [0, time_s],
                      [time_s, 0]])
        
        return A @ state + B @ input

3. Observation model

Then, an observation equation is defined as follow. This is used for generating GNSS observation data and predicting an observation at the next time step based on the predicted vehicle's state.

    def _observation_model(self, state):
        """
        Private function of observation model
        state: Predicted state
        Return Predicted observation data
        """
        
        H = np.array([[1, 0, 0, 0],
                      [0, 1, 0, 0]])
        
        x = np.array([[state[0, 0]],
                      [state[1, 0]],
                      [state[2, 0]],
                      [state[3, 0]]])
        
        return H @ x

4. Jacobian

Then, the state equation's Jacobian F and G are defined here. These Jacobians are used to linearize those equation and predict an error covariance matrix at the next time step.
Jacobian F

    def _jacobian_F(self, state, input, time_s):
        """
        Private function to calculate jacobian F
        state: Predicted state
        input: Input vector[accel, yaw rate] from controller
        time_s: Simulation interval time[sec]
        Return: Jacobian F
        """
        
        yaw = state[2, 0]
        spd = state[3, 0]
        acl = input[0, 0]
        t = time_s

        sin_yaw = sin(yaw)
        cos_yaw = cos(yaw)

        jF = np.array([[1, 0, -spd*sin_yaw*t-acl*sin_yaw*t**2/2, cos_yaw*t],
                       [0, 1, spd*cos_yaw*t+acl*cos_yaw*t**2/2, sin_yaw*t],
                       [0, 0, 1, 0],
                       [0, 0, 0, 1]])
        
        return jF

Jacobian G

    def _jacobian_G(self, state, time_s):
        """
        Private function to calculate jacobian G
        state: Predicted state
        time_s: Simulation interval time[sec]
        Return: Jacobian G
        """
        
        yaw = state[2, 0]
        t = time_s

        jG = np.array([[cos(yaw) * t**2 / 2, 0],
                       [sin(yaw) * t**2 / 2, 0],
                       [0, t],
                       [t, 0]])
        
        return jG

5. Predict and Update

Then, Kalman filter's main process to predict and update the vehicle's state is implemented here. The process in this method is separated into the following steps.

  • Generate acceleration and yaw rate inputs with random gaussian noise
  • Predict the vehicle's state and error covariance at the next time step based on motion model and jacobian F, G
  • Predict the GNSS's observation data and error covariance at the next time step based on observation model and jacobian H
  • Compute Kalman gain to correct the vehicle's state
  • Update the vehicle's state and error covariance at the next time step based on the computed Kalman gain
    def update(self, state, accel_mps2, yaw_rate_rps, time_s, gnss):
        """
        Function to update data
        state: Last extimated state data
        accel_mps2: Acceleration input from controller
        yaw_rate_rps: Yaw rate input from controller
        time_s: Simulation interval time[sec]
        Return: Estimated state data
        """
        
        last_state = np.array([[state.get_x_m()],
                               [state.get_y_m()],
                               [state.get_yaw_rad()],
                               [state.get_speed_mps()]])
        
        # input with noise
        input_org = np.array([[accel_mps2],
                              [yaw_rate_rps]])
        input_noise = self.INPUT_NOISE_VAR_MAT @ np.random.randn(2, 1)
        next_input = input_org + input_noise
        
        # predict state
        pred_state = State.motion_model(last_state, next_input, time_s)
        jF = self._jacobian_F(pred_state, next_input, time_s)
        jG = self._jacobian_G(pred_state, time_s)
        Q = self.INPUT_NOISE_VAR_MAT
        last_cov = self.cov_mat
        pred_cov = jF @ last_cov @jF.T + jG @ Q @ jG.T

        # predict observation
        pred_obsrv = self._observation_model(pred_state)
        jH = self.JACOB_H
        R = self.OBSRV_NOISE_VAR_MAT
        pred_obsrv_cov = jH @ pred_cov @ jH.T + R
        
        # kalman gain
        k = pred_cov @ jH.T @ np.linalg.inv(pred_obsrv_cov)

        # update
        inov = gnss - pred_obsrv
        est_state = pred_state + k @ inov
        est_cov = pred_cov - k @ pred_obsrv_cov @ k.T

        self.state = est_state
        self.cov_mat = est_cov

        return est_state

6. Error covariance elipse

And finally, a method to draw the estimated error covariance as an elipse is implemented here. This is to represent the shape of error covariance visually. The covariance is approximated to Gaussian distribution.

    def draw(self, axes, elems, pose):
        """
        Function to draw error covariance ellipse
        axes: Axes objects of figure
        elems: List of plot object
        pose: Vehicle's pose[x, y, yaw]
        """
        
        eig_val, eig_vec = np.linalg.eig(self.cov_mat)
        if eig_val[0] >= eig_val[1]: big_idx, small_idx = 0, 1
        else: big_idx, small_idx = 1, 0
        a, b = sqrt(3.0 * eig_val[big_idx]), sqrt(3.0 * eig_val[small_idx])
        angle = atan2(eig_vec[1, big_idx], eig_vec[0, big_idx])

        t = np.arange(0, 2 * pi + 0.1, 0.1)
        xs = [a * cos(it) for it in t]
        ys = [b * sin(it) for it in t]
        xys = np.array([xs, ys])
        xys_array = XYArray(xys)

        transformed_xys = xys_array.homogeneous_transformation(pose[0, 0], pose[1, 0], angle)
        elip_plot, = axes.plot(transformed_xys.get_x_data(), transformed_xys.get_y_data(), color=self.DRAW_COLOR)
        elems.append(elip_plot) 

Simulation result

A simulation with the above implemented program is executed as follow. You can see that the vehicle's state position(x, y) is estimated accurately and the error covariance is minimized while moving.