Vehicle Tracking with Noisy Sensors and Missing Data

We will try to pinpoint the location of a moving vehicle with high accuracy using only noisy (and sometimes missing) sensor data. We'll do this with a simple model for the physics of the vehicle, and an assumption that the vehicle's input acceleration is piecewise constant, with only occasional changes. That is, changes to the acceleration are sparse.

Model

We'll model the vehicle with a discrete-time linear dynamical system with state vector $x_t \in \mathbf{R}^4$ at time $t$, where $(x_{t,0}, x_{t,1})$ is the position of the vehicle in two dimensions, and $(x_{t,2}, x_{t,3})$ is the vehicle velocity. The system has a control input $u_t \in \mathbf{R}^2$, which is the input acceleration in two dimensions. We only observe noisy measurements, $y_t \in \mathbf{R}^2$, of the vehicle's position at each time step.

The system evolves according to the equations

$$ \begin{align} x_{t+1} &= A x_t + B u_t \\ y_{t} &= C x_t + \nu_t, \end{align} $$

with matrices $$ A = \begin{bmatrix} 1 & 0 & \left(1-\frac{\gamma}{2}\Delta t\right) \Delta t & 0 \\ 0 & 1 & 0 & \left(1-\frac{\gamma}{2} \Delta t\right) \Delta t\\ 0 & 0 & 1-\gamma \Delta t & 0 \\ 0 & 0 & 0 & 1-\gamma \Delta t \end{bmatrix}, $$

$$ B = \begin{bmatrix} \frac{1}{2}\Delta t^2 & 0 \\ 0 & \frac{1}{2}\Delta t^2 \\ \Delta t & 0 \\ 0 & \Delta t \\ \end{bmatrix}, $$$$ C = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{bmatrix}, $$

where $\nu_t$ is measurement noise for the vehicle position, and $\gamma$ is a damping parameter.

1D Model

The recurrence is derived from the following relations in a single dimension. For this subsection, let $x_t, v_t, a_t$ be the vehicle position, velocity, and input acceleration. The true acceleration of the vehicle is $a_t - \gamma v_t$, with $\gamma$ a damping parameter.

The discretized dynamics are then $$ \begin{align} x_{t+1} &= x_t + \left(1-\frac{\gamma \Delta t}{2}\right)v_t \Delta t + \frac{1}{2}a_{t} \Delta t^2\\ v_{t+1} &= \left(1-\gamma\right)v_t + a_t \Delta t. \end{align} $$

Contents

  • Exploiting Sparsity: $\ell_1$ Regularization
  • Handling Outliers: $\ell_1$ Loss for Robust Kalman Filtering
  • Missing Data
  • Do we need convex optimization?: Comparison with Least-Squares Approaches
    • $\ell_2$ Regularization
    • $\ell_2$ Smoothing: Acceleration
    • $\ell_2$ Smoothing: Velocity
    • $\ell_2$ Smoothing: Position
  • (possible) Noisy dynamics

Helper Functions


In [56]:
import matplotlib.pyplot as plt
import numpy as np

def accel_sched(vectors, times, n):
    times = np.array(times, dtype=float)
    times /= times.sum()
    times = np.int_(np.floor(times*n))
    times[-1] += n - times.sum()
    
    count = 0
    u = np.zeros((2,n))
    for v, k in zip(vectors,times):
        v = np.array(v).reshape(2,1)
        u[:,count:count+k] = np.tile(v,(1,k))
        count += k
        
    return u

def plot_state(actual, estimated=None):
    '''
    plot position, speed, and acceleration in the x and y coordinates for
    the actual data, and optionally for the estimated data
    '''
    trajectories = [actual]
    if estimated is not None:
        trajectories.append(estimated)
        
    fig, ax = plt.subplots(3, 2, sharex='col', sharey='row', figsize=(8,8))
    for x, u in trajectories:  
        ax[0,0].plot(t,x[0,:])
        ax[0,1].plot(t,x[1,:])
        ax[1,0].plot(t,x[2,:])
        ax[1,1].plot(t,x[3,:])
        ax[2,0].plot(t,u[0,:])
        ax[2,1].plot(t,u[1,:])
        
    ax[0,0].set_ylabel('x position')
    ax[1,0].set_ylabel('x speed')
    ax[2,0].set_ylabel('x acceleration')
    
    ax[0,1].set_ylabel('y position')
    ax[1,1].set_ylabel('y speed')
    ax[2,1].set_ylabel('y acceleration')
    
    ax[0,1].yaxis.tick_right()
    ax[1,1].yaxis.tick_right()
    ax[2,1].yaxis.tick_right()
    
    ax[0,1].yaxis.set_label_position("right")
    ax[1,1].yaxis.set_label_position("right")
    ax[2,1].yaxis.set_label_position("right")
    
    ax[2,0].set_xlabel('time')
    ax[2,1].set_xlabel('time')

def plot_positions(xtrue, y=None, recovered=None):
    '''
    show point clouds for true, observed, and recovered positions
    '''
    titles = ['True', 'Noisy', 'Recovered']
    trajectories = [xtrue]
    if y is not None:
        trajectories += [y]
    if recovered is not None:
        trajectories += [recovered]

    n = len(trajectories)

    fig, ax = plt.subplots(1, n, sharex=True, sharey=True,figsize=(12, 4))
    if n == 1:
        ax = [ax]
    
    for i,x in enumerate(trajectories):
        ax[i].plot(x[0,:], x[1,:], 'ro', alpha=.1)
        ax[i].set_title(titles[i])
        #ax[i].set_aspect('equal')
        #ax[i].axis('equal')
        
    #x0,x1 = ax[0].get_xlim()
    #y0,y1 = ax[0].get_ylim()
    #ax[0].set_aspect(abs(x1-x0)/abs(y1-y0))

Problem Data

  • set problem parameters
  • form dynamics matrices
  • simulate vehicle movement
  • form noisy observations of just the vehicle position

In [61]:
n = 1000 # number of timesteps
T = 10 # time will vary from 0 to T with step delt
t, delt = np.linspace(0,T,n,endpoint=True, retstep=True)
gamma = 1 # damping, 0 is no damping
noise_level = .15 # sensing errors

A = np.zeros((4,4))
B = np.zeros((4,2))

A[0,0] = 1
A[1,1] = 1
A[0,2] = (1-gamma*delt/2)*delt
A[1,3] = (1-gamma*delt/2)*delt
A[2,2] = 1 - gamma*delt
A[3,3] = 1 - gamma*delt

B[0,0] = delt**2/2
B[1,1] = delt**2/2
B[2,0] = delt
B[3,1] = delt

x = np.zeros((4,n))
x[:,0] = [0,0,0,0]

# form the piecewise constant acceleration schedule
u = accel_sched([(1,1.2),(-1,.6),(0,-1.3), (-1.2,.6),], [300,400,500,700], n)

# simulate the system forward in time
for i in range(n-1):
    x[:,i+1] = A.dot(x[:,i]) + B.dot(u[:,i])
    
xtrue = x.copy()
utrue = u.copy()

# form noisy observations
y = noise_level*np.random.randn(2,n) + xtrue[0:2,:]

# plot system state
plot_state((xtrue,utrue))

# plot true and noisy position observations
plot_positions(xtrue,y)


Exploiting Sparsity: Total Variation Regularization

The noise in the position measurements makes it difficult to pinpoint the vehicle's location. However, we can exploit the fact that the measurments don't look like how a vehicle would actually move. The measurments have the vehicle jumping around erratically. Thus, we'll require our recovered vehicle's movements to follow the known dynamics exactly as

$$ x_{t+1} = A x_t + B u_t, $$

which will constrain the way in which the model vehicle can move.

This isn't enough, however, beacause no matter what the observed positions $y_t$ are, there is some control schedule $u_t$, which hits those points exactly. But for noisy data like this, that control schedule will probably vary erratically. We can use exploit the fact that we know the changes in the acceleration schedule are sparse. We can try to induce such sparsity by adding total variation regularization to our model:

$$ \begin{array}{ll} \mbox{minimize} & \sum_{t=0}^{n} \|y_t - x_{t,0:1}\|_2^2 + \rho\sum_{t=1}^{n} \|u_t - u_{t-1}\|_1\\ \mbox{subject to} & x_{t+1} = A x_t + B u_t \end{array} $$

The first term in the objective penalizes differences between the model and observed positions. The second term penalizes changes in the control schedule. The parameter $\rho$ trades off between the two. For example, setting $\rho = 0$ would match the observed positions exactly, but the acceleration schedule wouln't match our expecation.

CVXPY Implementation

The code below uses CVXPY to implement and solve the model. The plots compare the true and recovered system state, which end up being very close!


In [60]:
import cvxpy as cvx

x = cvx.Variable(4,n)
u = cvx.Variable(2,n)
rho = .1
    
objective = cvx.norm(x[0,:].T - y[0,:])/n + cvx.norm(x[1,:].T - y[1,:])/n
objective += rho*cvx.norm1(u[0,0:n-1] - u[0,1:n])/n + rho*cvx.norm1(u[1,0:n-1] - u[1,1:n])/n
objective = cvx.Minimize(objective)

constraints = []
for i in range(n-1):
    constraints += [x[:,i+1] == A*x[:,i] + B*u[:,i]]

prob = cvx.Problem(objective, constraints)

result = prob.solve(solver=cvx.ECOS, verbose=True)

x = np.array(x.value)
u = np.array(u.value)

plot_state((xtrue,utrue),(x,u))
plot_positions(xtrue,y,x)


ECOS 1.0.4 - (c) A. Domahidi, Automatic Control Laboratory, ETH Zurich, 2012-2014.

It     pcost         dcost      gap     pres    dres     k/t     mu      step     IR
 0   +2.391e-21   +2.374e-18   +2e+02   6e+00   6e-01   1e+00   6e-02    N/A     2 1 -
 1   +1.618e-01   +3.036e-02   +2e+02   1e+00   1e-01   1e-01   4e-02   0.3755   3 2 2
 2   +2.297e-02   +1.292e-02   +1e+02   2e-01   3e-02   3e-02   4e-02   0.1834   3 3 2
 3   +6.427e-03   +9.810e-03   +3e+01   2e-02   2e-03   7e-03   1e-02   0.8383   3 2 2
 4   +9.668e-03   +1.003e-02   +1e+01   8e-03   8e-04   2e-03   5e-03   0.7990   3 2 2
 5   +1.032e-02   +1.005e-02   +9e+00   4e-03   5e-04   5e-04   3e-03   0.7732   3 3 2
 6   +1.027e-02   +1.010e-02   +5e+00   2e-03   2e-04   2e-04   1e-03   0.5721   2 2 2
 7   +1.020e-02   +1.013e-02   +2e+00   7e-04   8e-05   5e-05   5e-04   0.7329   3 2 2
 8   +1.019e-02   +1.012e-02   +1e+00   4e-04   5e-05   9e-07   3e-04   0.9899   2 2 2
 9   +1.015e-02   +1.013e-02   +4e-01   1e-04   1e-05   2e-07   1e-04   0.7585   2 2 3
10   +1.014e-02   +1.013e-02   +1e-01   5e-05   5e-06   1e-08   3e-05   0.9836   3 3 3
11   +1.014e-02   +1.013e-02   +1e-01   3e-05   4e-06   6e-09   3e-05   0.5450   3 3 3
12   +1.013e-02   +1.013e-02   +3e-02   1e-05   1e-06   2e-09   7e-06   0.8286   3 3 3
13   +1.013e-02   +1.013e-02   +2e-02   7e-06   8e-07   1e-09   5e-06   0.4857   3 3 3
14   +1.013e-02   +1.013e-02   +6e-03   2e-06   2e-07   3e-10   2e-06   0.7208   3 3 3
15   +1.013e-02   +1.013e-02   +2e-03   7e-07   8e-08   1e-10   5e-07   0.7829   2 2 2
16   +1.013e-02   +1.013e-02   +2e-04   6e-08   6e-09   9e-12   4e-08   0.9339   3 3 3
17   +1.013e-02   +1.013e-02   +4e-06   1e-09   1e-10   2e-13   9e-10   0.9786   3 3 3
18   +1.013e-02   +1.013e-02   +1e-07   1e-10   5e-12   7e-15   3e-11   0.9640   2 3 3
19   +1.013e-02   +1.013e-02   +6e-09   7e-11   3e-13   3e-16   2e-12   0.9572   2 2 2

OPTIMAL (within feastol=7.4e-11, reltol=6.2e-07, abstol=6.3e-09).
Runtime: 0.239650 seconds.

Robust Kalman Filtering


In [4]:
noise_level = .1
outlier_percentage = .10

outlier_inds = np.random.permutation(n)[:int(np.ceil(outlier_percentage*n))]
# noisy observations
y = noise_level*np.random.randn(2,n) + xtrue[0:2,:]
y[:,outlier_inds] += 10*noise_level*np.random.randn(2,len(outlier_inds)) 

plot_state((xtrue,utrue))
plot_positions(xtrue,y)



In [5]:
import cvxpy as cvx

x = cvx.Variable(4,n)
u = cvx.Variable(2,n)
rho = 1
    
objective = cvx.norm1(x[0,:].T - y[0,:])/n + cvx.norm1(x[1,:].T - y[1,:])/n
objective += rho*cvx.norm1(u[0,0:n-1] - u[0,1:n])/n + rho*cvx.norm1(u[1,0:n-1] - u[1,1:n])/n
objective = cvx.Minimize(objective)

constraints = []
for i in range(n-1):
    constraints += [x[:,i+1] == A*x[:,i] + B*u[:,i]]

prob = cvx.Problem(objective, constraints)

result = prob.solve(solver=cvx.ECOS, verbose=True)

x = np.array(x.value)
u = np.array(u.value)

plot_state((xtrue,utrue),(x,u))
plot_positions(xtrue,y)


ECOS 1.0.4 - (c) A. Domahidi, Automatic Control Laboratory, ETH Zurich, 2012-2014.

It     pcost         dcost      gap     pres    dres     k/t     mu      step     IR
 0   -4.022e-17   +7.876e-15   +2e+02   4e+00   9e-01   1e+00   3e-02    N/A     1 1 -
 1   -7.226e-01   +4.412e-01   +2e+02   6e-01   2e-01   1e+00   3e-02   0.3231   2 2 2
 2   +1.950e-01   +2.881e-01   +3e+01   7e-02   2e-02   1e-01   5e-03   0.8380   2 2 2
 3   +2.772e-01   +2.953e-01   +7e+00   1e-02   3e-03   2e-02   1e-03   0.7967   2 2 2
 4   +2.933e-01   +3.004e-01   +3e+00   6e-03   2e-03   9e-03   5e-04   0.5935   2 2 2
 5   +2.994e-01   +3.025e-01   +2e+00   3e-03   7e-04   4e-03   2e-04   0.5584   2 2 2
 6   +3.014e-01   +3.031e-01   +1e+00   2e-03   5e-04   2e-03   2e-04   0.4179   2 2 2
 7   +3.028e-01   +3.035e-01   +8e-01   1e-03   3e-04   1e-03   1e-04   0.5067   2 2 2
 8   +3.036e-01   +3.039e-01   +5e-01   8e-04   2e-04   5e-04   7e-05   0.5392   2 2 2
 9   +3.039e-01   +3.040e-01   +4e-01   7e-04   2e-04   3e-04   6e-05   0.3422   2 2 2
10   +3.042e-01   +3.042e-01   +2e-01   4e-04   9e-05   1e-04   3e-05   0.5917   2 2 2
11   +3.042e-01   +3.042e-01   +2e-01   4e-04   9e-05   1e-04   3e-05   0.1854   2 2 2
12   +3.043e-01   +3.043e-01   +1e-01   2e-04   5e-05   5e-05   2e-05   0.5988   2 2 2
13   +3.044e-01   +3.043e-01   +8e-02   1e-04   3e-05   2e-05   1e-05   0.6520   2 2 2
14   +3.044e-01   +3.044e-01   +3e-02   5e-05   1e-05   5e-06   4e-06   0.6679   3 3 3
15   +3.044e-01   +3.044e-01   +3e-02   4e-05   9e-06   2e-06   3e-06   0.7001   3 3 3
16   +3.044e-01   +3.044e-01   +1e-02   2e-05   5e-06   9e-07   2e-06   0.4626   3 3 3
17   +3.044e-01   +3.044e-01   +8e-03   1e-05   3e-06   2e-08   1e-06   0.9899   3 3 3
18   +3.044e-01   +3.044e-01   +2e-03   4e-06   9e-07   4e-09   3e-07   0.8801   3 3 3
19   +3.044e-01   +3.044e-01   +2e-03   3e-06   8e-07   3e-09   3e-07   0.4304   3 3 3
20   +3.044e-01   +3.044e-01   +8e-04   1e-06   3e-07   1e-09   1e-07   0.6443   3 3 3
21   +3.044e-01   +3.044e-01   +6e-04   9e-07   2e-07   8e-10   8e-08   0.3966   3 3 3
22   +3.044e-01   +3.044e-01   +5e-04   7e-07   2e-07   5e-10   6e-08   0.8864   3 3 3
23   +3.044e-01   +3.044e-01   +2e-04   4e-07   9e-08   3e-10   3e-08   0.5631   3 3 3
24   +3.044e-01   +3.044e-01   +3e-05   4e-08   1e-08   3e-11   4e-09   0.9899   3 3 3
25   +3.044e-01   +3.044e-01   +5e-07   8e-10   2e-10   6e-13   6e-11   0.9836   3 2 2
26   +3.044e-01   +3.044e-01   +5e-09   8e-10   3e-12   6e-15   7e-13   0.9835   2 1 1

OPTIMAL (within feastol=7.9e-10, reltol=1.7e-08, abstol=5.3e-09).
Runtime: 0.325814 seconds.

Missing Data

We randomly observe only 40% of the data points. These observations are still noisy.


In [47]:
noise_level = .1
present_percentage = .40

inds = np.random.permutation(n)[:int(np.ceil(present_percentage*n))]

# noisy observations
y = noise_level*np.random.randn(2,n) + xtrue[0:2,:]
y = y[:,inds]

plot_state((xtrue,utrue))
plot_positions(xtrue,y)



In [52]:
import cvxpy as cvx

x = cvx.Variable(4,n)
u = cvx.Variable(2,n)
rho = 1

m = len(inds)
y2 = np.zeros((2,n))
y2[:,inds] = y
    
objective = sum(cvx.square(x[0,ind] - y2[0,ind])/m + cvx.square(x[1,ind] - y2[1,ind])/m for ind in inds )
objective += rho*cvx.norm1(u[0,0:n-1] - u[0,1:n])/n + rho*cvx.norm1(u[1,0:n-1] - u[1,1:n])/n
objective = cvx.Minimize(objective)

constraints = []
for i in range(n-1):
    constraints += [x[:,i+1] == A*x[:,i] + B*u[:,i]]

prob = cvx.Problem(objective, constraints)

result = prob.solve(solver=cvx.ECOS, verbose=True)

x = np.array(x.value)
u = np.array(u.value)

plot_state((xtrue,utrue),(x,u))
plot_positions(xtrue,y,x)


ECOS 1.0.4 - (c) A. Domahidi, Automatic Control Laboratory, ETH Zurich, 2012-2014.

It     pcost         dcost      gap     pres    dres     k/t     mu      step     IR
 0   -1.777e-17   -1.366e+03   +8e+03   1e+00   7e+01   1e+00   1e+00    N/A     1 1 -
 1   +2.806e+00   -9.916e+01   +1e+03   9e-02   5e+00   8e-01   2e-01   0.9899   1 1 1
 2   +2.704e+00   -8.830e-01   +4e+01   3e-03   2e-01   1e-02   8e-03   0.9611   2 1 1
 3   +3.837e-01   -2.697e-01   +2e+01   6e-04   3e-02   3e-03   3e-03   0.6906   2 2 2
 4   +5.859e-02   -6.326e-03   +2e+00   6e-05   3e-03   3e-04   4e-04   0.8817   2 2 2
 5   +3.513e-02   +2.121e-02   +5e-01   1e-05   7e-04   5e-05   8e-05   0.8070   2 2 2
 6   +3.046e-02   +2.637e-02   +1e-01   4e-06   2e-04   1e-05   2e-05   0.7573   2 2 2
 7   +2.891e-02   +2.759e-02   +4e-02   1e-06   6e-05   3e-06   8e-06   0.8077   2 2 2
 8   +2.832e-02   +2.792e-02   +1e-02   4e-07   2e-05   6e-07   2e-06   0.7714   3 2 2
 9   +2.824e-02   +2.796e-02   +9e-03   3e-07   1e-05   3e-07   2e-06   0.4787   3 3 3
10   +2.810e-02   +2.801e-02   +3e-03   8e-08   4e-06   8e-08   5e-07   0.7467   3 3 3
11   +2.805e-02   +2.803e-02   +7e-04   2e-08   1e-06   7e-09   1e-07   0.9561   3 3 3
12   +2.804e-02   +2.803e-02   +2e-04   4e-09   2e-07   1e-09   3e-08   0.8978   3 3 3
13   +2.804e-02   +2.803e-02   +2e-05   6e-10   3e-08   1e-10   4e-09   0.9426   3 3 3
14   +2.803e-02   +2.803e-02   +3e-06   5e-10   5e-09   2e-11   6e-10   0.9184   3 3 3
15   +1.879e+03   +1.879e+03   -4e+35   3e+12   2e+03   -2e-11   -6e+31   0.0001   0 0 0
Unreliable search direction detected, recovering best iterate (14) and stopping.

Close to OPTIMAL (within feastol=4.9e-09, reltol=1.2e-04, abstol=3.3e-06).
Runtime: 0.163660 seconds.


In [ ]: