EKF with GPS, odometry and magnetometer outputs wrong heading

35 Views Asked by At

I am using this (sort of) Extended Kalman filter to estimate the position of a robot moving in a 2D space, equipped with GPS, magnetometer and wheel odometry.

class EKF(object):
    '''
    State: [x, y, vx, vy, h]
    '''
    def __init__(self, x, y, vx, vy, h):
        self.wheel_distance = 0.34 

        # State variables
        self.X = np.array([x, y, vx, vy, h])

        # Initial covariance
        self.P = np.eye(5)  

        # Process noise

        self.Q = np.eye(5)*0.1

        # Measurement noise
        self.R_gps = np.eye(2) * 5 
        self.R_mag = np.eye(1) * 0.1 
        self.R_enc = np.eye(2) * 0.1 

        # Measurement matrices
        self.H_gps = np.zeros((2, 5))
        self.H_gps[0, 0] = 1.0
        self.H_gps[1, 1] = 1.0

        self.H_mag = np.zeros((1, 5))
        self.H_mag[0, 4] = 1.0

        self.H_enc = np.zeros((2, 5))

    def update(self, vl, vr, gx, gy, mx, my, dt):

        # Prediction    

        A = np.eye(5)
        # x = x0 + vx
        A[0, 2] = dt
        # x = xy + vy
        A[1, 3] = dt
    
        self.X = np.dot(A, self.X)
        self.P = np.dot(np.dot(A, self.P), A.T) + self.Q

        # Magnetometer update
        K_mag = np.dot(np.dot(self.P, self.H_mag.T),
                                np.linalg.inv(np.dot(np.dot(self.H_mag, self.P), 
                                            self.H_mag.T) + self.R_mag))
        tmp = np.arctan2(mx, my) # measured h
        # convert to my reference system 
        if mx >= 0 and my >= 0:
            tmp = 2*np.pi - tmp
        elif mx >= 0 and my < 0:
            tmp = -tmp
        elif mx < 0 and my < 0:
            tmp = -tmp
        elif mx < 0 and my >= 0:
            tmp = 2*np.pi - tmp
        tmp += np.pi/2
        if tmp < 0:
            tmp += 2*np.pi
        elif tmp > 2*np.pi:
            tmp -= 2*np.pi
        
        Z_mag = np.array([tmp])
        innovation_mag = Z_mag - np.dot(self.H_mag, self.X)
        self.X = self.X + np.dot(K_mag, innovation_mag)     

        self.P = np.dot((np.eye(5) - np.dot(K_mag, self.H_mag)), self.P)

        # Encoder update
        
        dleft = vl*dt 
        dright = vr*dt 
        dcenter = (dleft + dright)/2
        w = (dleft - dright) / (2*self.wheel_distance)  
        vcenter = (vl + vr)/2
        theta = self.X[4] + w # TODO: w/2??
        x = self.X[0] + dcenter*np.sin(theta)
        y = self.X[1] + dcenter*np.cos(theta)
        vx = vcenter*np.sin(theta)
        vy = vcenter*np.cos(theta)  

        sin = np.sin(theta)
        cos = np.cos(theta)

        self.H_enc[0, 2] = 2/sin 
        self.H_enc[0, 3] = -2/cos
        self.H_enc[1, 2] = 2/sin
        self.H_enc[1, 3] = 2/cos
    
        K_enc = np.dot(np.dot(self.P, self.H_enc.T), 
                       np.linalg.inv(np.dot(np.dot(self.H_enc, self.P), 
                                            self.H_enc.T) + self.R_enc))
        Z_enc = np.array([vl, vr])
        innovation_enc = Z_enc - np.dot(self.H_enc, self.X)
        
        self.X = self.X + np.dot(K_enc, innovation_enc)
        self.P = np.dot((np.eye(5) - np.dot(K_enc, self.H_enc)), self.P)
        

        # GPS update
        
        # the GPS measurements have a lower sampling frequency
        if gx is not None:
            K_gps = np.dot(np.dot(self.P, self.H_gps.T), 
                           np.linalg.inv(np.dot(np.dot(self.H_gps, self.P), 
                                                self.H_gps.T) + self.R_gps))
            Z_gps = np.array([gx, gy])
            innovation_gps = Z_gps - np.dot(self.H_gps, self.X)
            
            
            self.X = self.X + np.dot(K_gps, innovation_gps)
            self.P = np.dot((np.eye(5) - np.dot(K_gps, self.H_gps)), self.P)
        
        # Wrap angle
        if self.X[4] < 0:
            self.X[4] += 2*np.pi
        if self.X[4] > 2*np.pi:
            self.X[4] -= 2*np.pi

        return self.X

In my reference system, the north is at 0° and the increase is clockwise. The magnetometer has the X and Y axes switched. For these reasons I'm applying the conversions included in the code. This has already been tested.

The filter should perform an interpolation of the GPS samples and improve the GPS uncertainty with proper tuning of the measurement noise matrices. However, as showed in the zoomed plots, the heading computation is not correct.

Is there some error in my filter definition? How can I fix it?

enter image description here

enter image description here

enter image description here

0

There are 0 best solutions below