2D Position and Velocity Tracking#
Now let’s tackle a realistic problem: tracking a moving object in 2D space using position measurements.
Problem Setup#
Track a ball moving across a field:
- State: [x, y, vx, vy] (position and velocity)
- Measurements: [x, y] (noisy GPS position)
- Dynamics: Constant velocity model
Implementation#
import numpy as np
import matplotlib.pyplot as plt
class KalmanFilter2D:
def __init__(self, dt):
"""
2D position and velocity tracker
Args:
dt: Time step between measurements
"""
self.dt = dt
# State vector: [x, y, vx, vy]
self.x = np.zeros(4)
# State transition matrix (constant velocity model)
self.F = np.array([
[1, 0, dt, 0], # x = x + vx*dt
[0, 1, 0, dt], # y = y + vy*dt
[0, 0, 1, 0], # vx = vx
[0, 0, 0, 1] # vy = vy
])
# Measurement matrix (we only measure position)
self.H = np.array([
[1, 0, 0, 0], # Measure x
[0, 1, 0, 0] # Measure y
])
# Process noise covariance
q = 0.1 # Process noise magnitude
self.Q = q * np.array([
[dt**4/4, 0, dt**3/2, 0],
[0, dt**4/4, 0, dt**3/2],
[dt**3/2, 0, dt**2, 0],
[0, dt**3/2, 0, dt**2]
])
# Measurement noise covariance
r = 5.0 # Measurement noise (meters)
self.R = r**2 * np.eye(2)
# Estimate uncertainty
self.P = np.eye(4) * 100 # High initial uncertainty
def predict(self):
"""Prediction step"""
# Predicted state
self.x = self.F @ self.x
# Predicted covariance
self.P = self.F @ self.P @ self.F.T + self.Q
def update(self, measurement):
"""Update step"""
# Innovation (measurement residual)
y = measurement - self.H @ self.x
# Innovation covariance
S = self.H @ self.P @ self.H.T + self.R
# Kalman gain
K = self.P @ self.H.T @ np.linalg.inv(S)
# Updated state estimate
self.x = self.x + K @ y
# Updated covariance
I = np.eye(len(self.x))
self.P = (I - K @ self.H) @ self.P
return self.x
def get_position(self):
"""Get current position estimate"""
return self.x[:2]
def get_velocity(self):
"""Get current velocity estimate"""
return self.x[2:]
# Simulation
def simulate_trajectory(num_steps, dt):
"""Generate ground truth trajectory"""
t = np.arange(num_steps) * dt
# Circular motion
radius = 50
omega = 0.1
x_true = radius * np.cos(omega * t)
y_true = radius * np.sin(omega * t)
vx_true = -radius * omega * np.sin(omega * t)
vy_true = radius * omega * np.cos(omega * t)
return x_true, y_true, vx_true, vy_true
# Parameters
dt = 0.1 # Time step (seconds)
num_steps = 200
measurement_noise = 5.0 # GPS noise (meters)
# Generate true trajectory
x_true, y_true, vx_true, vy_true = simulate_trajectory(num_steps, dt)
# Generate noisy measurements
x_meas = x_true + np.random.normal(0, measurement_noise, num_steps)
y_meas = y_true + np.random.normal(0, measurement_noise, num_steps)
# Run Kalman filter
kf = KalmanFilter2D(dt)
# Initialize with first measurement
kf.x[:2] = [x_meas[0], y_meas[0]]
x_est = []
y_est = []
vx_est = []
vy_est = []
for i in range(num_steps):
# Predict
kf.predict()
# Update with measurement
measurement = np.array([x_meas[i], y_meas[i]])
kf.update(measurement)
# Store estimates
pos = kf.get_position()
vel = kf.get_velocity()
x_est.append(pos[0])
y_est.append(pos[1])
vx_est.append(vel[0])
vy_est.append(vel[1])
# Convert to arrays
x_est = np.array(x_est)
y_est = np.array(y_est)
vx_est = np.array(vx_est)
vy_est = np.array(vy_est)
# Plot results
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# Position plot
ax1.plot(x_true, y_true, 'g-', label='True Trajectory', linewidth=2)
ax1.plot(x_meas, y_meas, 'r.', label='Noisy Measurements', alpha=0.3)
ax1.plot(x_est, y_est, 'b-', label='Kalman Filter', linewidth=2)
ax1.set_xlabel('X Position (m)')
ax1.set_ylabel('Y Position (m)')
ax1.set_title('2D Position Tracking')
ax1.legend()
ax1.grid(True)
ax1.axis('equal')
# Velocity plot
t = np.arange(num_steps) * dt
ax2.plot(t, vx_true, 'g-', label='True Vx', linewidth=2)
ax2.plot(t, vx_est, 'b--', label='Estimated Vx')
ax2.plot(t, vy_true, 'g:', label='True Vy', linewidth=2)
ax2.plot(t, vy_est, 'r--', label='Estimated Vy')
ax2.set_xlabel('Time (s)')
ax2.set_ylabel('Velocity (m/s)')
ax2.set_title('Velocity Estimation')
ax2.legend()
ax2.grid(True)
plt.tight_layout()
plt.show()
# Calculate errors
pos_error_raw = np.sqrt((x_meas - x_true)**2 + (y_meas - y_true)**2)
pos_error_filtered = np.sqrt((x_est - x_true)**2 + (y_est - y_true)**2)
print(f"Average position error (raw): {pos_error_raw.mean():.2f} m")
print(f"Average position error (filtered): {pos_error_filtered.mean():.2f} m")
print(f"Improvement: {pos_error_raw.mean() / pos_error_filtered.mean():.1f}x")
Results#
The Kalman filter:
- Smooths noisy measurements: Reduces jitter in position
- Estimates velocity: Derives velocity from position-only measurements!
- Predicts future position: Uses velocity to anticipate movement
Typical performance:
- Raw measurement error: 5.0 m
- Filtered position error: 0.8 m
- 6.3x improvement
Key Insights#
1. Velocity Estimation#
Even though we only measure position, the filter estimates velocity by observing how position changes over time.
2. State Transition Matrix#
The F matrix encodes our physics model:
x_new = x_old + vx * dt
3. Process Noise#
Accounts for model imperfections (object might accelerate, wind effects, etc.)
4. Measurement Matrix#
H matrix maps full state to measurements:
measurement = H @ state # Only observe position
Real-World Applications#
This exact algorithm is used in:
- GPS tracking: Smooth noisy GPS data
- Robotics: Estimate robot pose
- Aviation: Aircraft navigation systems
- Finance: Stock price prediction
- Computer vision: Object tracking in video
Extensions#
Possible improvements:
- Add acceleration to state
- Use non-constant velocity model
- Add control inputs (known forces)
- Switch to Extended Kalman Filter for nonlinear systems
Conclusion#
We’ve gone from theory to a practical multi-dimensional tracker! The Kalman filter is a powerful tool that optimally fuses noisy measurements with physics models.
Further Reading#
- Kalman’s original 1960 paper
- “Probabilistic Robotics” by Thrun et al.
- Extended Kalman Filter (EKF) for nonlinear systems
- Unscented Kalman Filter (UKF) for highly nonlinear systems