用Python从零实现自动驾驶的‘自行车模型’:手把手教你用NumPy和Matplotlib模拟车辆转向

在自动驾驶技术快速发展的今天,理解车辆运动的基本原理是每个开发者的必修课。运动学自行车模型作为自动驾驶领域的基础模型之一,以其简洁性和实用性备受青睐。不同于复杂的动力学模型,它通过简化的几何关系描述车辆运动,非常适合初学者入门和快速验证算法。

本文将带你从零开始,用Python实现一个完整的运动学自行车模型模拟器。我们将使用NumPy进行状态计算,Matplotlib进行轨迹可视化,并通过对比不同参考点(后轴、前轴、重心)的模拟结果,深入理解模型特性。无论你是自动驾驶爱好者、机器人专业学生,还是希望扩展技术视野的开发者,这篇实践指南都能让你获得可直接应用于项目的实用技能。

1. 环境准备与基础概念

在开始编码前,我们需要配置开发环境并理解自行车模型的核心假设。推荐使用Python 3.8+环境,主要依赖库包括:

pip install numpy matplotlib ipython

运动学自行车模型基于几个关键假设:

  • 车辆简化为两轮自行车(前轮转向,后轮驱动)
  • 无横向滑移(非完整约束)
  • 转向角变化瞬时完成
  • 地面平坦且忽略悬架影响

模型的核心参数包括:

  • 轴距L:前后轮中心距离
  • 参考点位置:后轴、前轴或重心
  • 状态变量:[x, y, θ, δ](位置x/y,航向角θ,转向角δ)
  • 控制输入:[v, φ](速度v,转向速率φ)

注意:虽然实际车辆转向角变化有速率限制,但基础模型通常忽略这一动态特性,这是后续可扩展的方向之一。

2. 后轴参考点模型实现

后轴参考是最常见的建模方式,我们先实现这一版本。模型微分方程为:

ẋ = v * cos(θ)
ẏ = v * sin(θ)
θ̇ = (v * tan(δ)) / L

对应的Python实现如下:

import numpy as np

def bicycle_model_rear(v, delta, state, L, dt):
    """
    后轴参考点自行车模型
    参数:
        v: 速度 (m/s)
        delta: 转向角 (rad)
        state: 当前状态 [x, y, theta]
        L: 轴距 (m)
        dt: 时间步长 (s)
    返回:
        new_state: 更新后的状态
    """
    x, y, theta = state
    new_theta = theta + (v * np.tan(delta) / L) * dt
    new_x = x + v * np.cos(theta) * dt
    new_y = y + v * np.sin(theta) * dt
    return np.array([new_x, new_y, new_theta])

可视化函数可以帮助我们验证模型行为:

import matplotlib.pyplot as plt

def plot_trajectory(trajectory, L, title):
    plt.figure(figsize=(10,6))
    plt.plot(trajectory[:,0], trajectory[:,1], label='轨迹')
    
    # 绘制最后一帧的车辆示意
    last_state = trajectory[-1]
    rear_x, rear_y, theta = last_state
    front_x = rear_x + L * np.cos(theta)
    front_y = rear_y + L * np.sin(theta)
    
    plt.plot([rear_x, front_x], [rear_y, front_y], 'r-', linewidth=2, label='车辆朝向')
    plt.scatter(rear_x, rear_y, c='blue', label='后轴')
    plt.scatter(front_x, front_y, c='green', label='前轴')
    
    plt.axis('equal')
    plt.xlabel('X位置 (m)')
    plt.ylabel('Y位置 (m)')
    plt.title(title)
    plt.legend()
    plt.grid(True)
    plt.show()

测试一个圆周运动的例子:

# 参数设置
L = 2.5  # 轴距(m)
dt = 0.1  # 时间步长(s)
total_time = 10  # 总时长(s)
steps = int(total_time / dt)
v = 2.0  # 恒定速度(m/s)
delta = np.arctan(L/5)  # 转向角(rad),对应半径5m的圆

# 模拟运行
state = np.array([0.0, 0.0, 0.0])  # 初始状态
trajectory = [state.copy()]
for _ in range(steps):
    state = bicycle_model_rear(v, delta, state, L, dt)
    trajectory.append(state.copy())
trajectory = np.array(trajectory)

# 可视化
plot_trajectory(trajectory, L, "后轴参考点 - 圆周运动")

3. 前轴与重心参考点模型对比

不同参考点选择会导致模型行为的差异,这对控制器设计有重要影响。我们扩展实现前轴和重心参考模型。

3.1 前轴参考点模型

前轴参考点模型方程:

ẋ = v * cos(θ + δ)
ẏ = v * sin(θ + δ)
θ̇ = (v * sin(δ)) / L

Python实现:

def bicycle_model_front(v, delta, state, L, dt):
    """
    前轴参考点自行车模型
    """
    x, y, theta = state
    new_theta = theta + (v * np.sin(delta) / L) * dt
    new_x = x + v * np.cos(theta + delta) * dt
    new_y = y + v * np.sin(theta + delta) * dt
    return np.array([new_x, new_y, new_theta])

3.2 重心参考点模型

重心参考点需要考虑侧滑角β,模型更复杂:

β = arctan((Lr * tan(δ)) / L)
ẋ = v * cos(θ + β)
ẏ = v * sin(θ + β)
θ̇ = (v * cos(β) * tan(δ)) / L

其中Lr是重心到后轴距离。实现代码:

def bicycle_model_cg(v, delta, state, L, Lr, dt):
    """
    重心参考点自行车模型
    Lr: 重心到后轴距离
    """
    x, y, theta = state
    beta = np.arctan((Lr * np.tan(delta)) / L)
    new_theta = theta + (v * np.cos(beta) * np.tan(delta) / L) * dt
    new_x = x + v * np.cos(theta + beta) * dt
    new_y = y + v * np.sin(theta + beta) * dt
    return np.array([new_x, new_y, new_theta])

3.3 模型对比实验

我们设计一个实验对比三种模型的行为差异:

def compare_models():
    # 公共参数
    L = 2.5
    Lr = 1.2  # 重心到后轴距离
    dt = 0.05
    total_time = 8
    steps = int(total_time / dt)
    
    # 控制输入:先直行,然后左转
    def get_control(t):
        if t < 2:
            return 2.0, 0.0  # 直行
        elif t < 5:
            return 2.0, np.radians(15)  # 15度转向
        else:
            return 1.5, np.radians(-10)  # 减速并右转
    
    # 初始化
    state_rear = np.array([0.0, 0.0, 0.0])
    state_front = np.array([0.0, 0.0, 0.0])
    state_cg = np.array([0.0, 0.0, 0.0])
    
    traj_rear = [state_rear.copy()]
    traj_front = [state_front.copy()]
    traj_cg = [state_cg.copy()]
    
    # 模拟运行
    for i in range(steps):
        t = i * dt
        v, delta = get_control(t)
        
        state_rear = bicycle_model_rear(v, delta, state_rear, L, dt)
        state_front = bicycle_model_front(v, delta, state_front, L, dt)
        state_cg = bicycle_model_cg(v, delta, state_cg, L, Lr, dt)
        
        traj_rear.append(state_rear.copy())
        traj_front.append(state_front.copy())
        traj_cg.append(state_cg.copy())
    
    # 可视化
    plt.figure(figsize=(12,8))
    plt.plot(np.array(traj_rear)[:,0], np.array(traj_rear)[:,1], label='后轴参考')
    plt.plot(np.array(traj_front)[:,0], np.array(traj_front)[:,1], label='前轴参考')
    plt.plot(np.array(traj_cg)[:,0], np.array(traj_cg)[:,1], label='重心参考')
    
    plt.axis('equal')
    plt.xlabel('X位置 (m)')
    plt.ylabel('Y位置 (m)')
    plt.title('不同参考点模型轨迹对比')
    plt.legend()
    plt.grid(True)
    plt.show()

compare_models()

通过对比可以发现:

  • 后轴参考轨迹转弯半径最小
  • 前轴参考轨迹转弯半径最大
  • 重心参考介于两者之间
  • 直线行驶时三种模型一致

4. 高级应用与扩展

基础模型实现后,我们可以进一步扩展其应用场景和功能。

4.1 转向速率限制

实际车辆转向角不可能瞬时变化,我们可以引入转向速率限制:

def bicycle_model_with_steering_rate(v, phi, state, L, dt, max_delta=np.radians(30)):
    """
    带转向速率限制的模型
    phi: 转向速率 (rad/s)
    max_delta: 最大转向角
    """
    x, y, theta, delta = state
    
    # 限制转向角变化
    new_delta = delta + phi * dt
    new_delta = np.clip(new_delta, -max_delta, max_delta)
    
    # 更新状态
    new_theta = theta + (v * np.tan(new_delta) / L) * dt
    new_x = x + v * np.cos(theta) * dt
    new_y = y + v * np.sin(theta) * dt
    
    return np.array([new_x, new_y, new_theta, new_delta])

4.2 轨迹跟踪控制器

基于自行车模型,我们可以实现一个简单的纯追踪控制器:

def pure_pursuit_controller(current_state, target_path, L, lookahead_dist=3.0):
    """
    纯追踪控制器
    current_state: [x, y, theta]
    target_path: [[x0,y0], [x1,y1], ...]
    lookahead_dist: 前瞻距离
    返回: 期望转向角
    """
    x, y, theta = current_state
    
    # 寻找路径上最近点
    distances = np.linalg.norm(target_path - np.array([x,y]), axis=1)
    closest_idx = np.argmin(distances)
    
    # 选择前瞻点
    lookahead_idx = closest_idx
    while lookahead_idx < len(target_path)-1 and distances[lookahead_idx] < lookahead_dist:
        lookahead_idx += 1
    
    lookahead_point = target_path[lookahead_idx]
    
    # 计算转向角
    alpha = np.arctan2(lookahead_point[1]-y, lookahead_point[0]-x) - theta
    delta = np.arctan(2 * L * np.sin(alpha) / lookahead_dist)
    
    return delta

4.3 模型验证与调试技巧

在开发过程中,有几个实用的调试技巧:

  1. 单位检查 :确保所有物理量单位一致(米、弧度、秒)
  2. 极限测试
    • 零速度时应保持位置不变
    • 零转向角时应直线行驶
    • 最大转向角时应出现最小转弯半径
  3. 可视化工具
    def plot_state_sequence(states, L):
        plt.figure(figsize=(12,6))
        
        # 轨迹
        plt.subplot(1,2,1)
        plt.plot(states[:,0], states[:,1])
        plt.axis('equal')
        plt.title('车辆轨迹')
        
        # 航向与转向角
        plt.subplot(1,2,2)
        plt.plot(np.degrees(states[:,2]), label='航向角(°)')
        if states.shape[1] > 3:
            plt.plot(np.degrees(states[:,3]), label='转向角(°)')
        plt.title('角度变化')
        plt.legend()
        
        plt.tight_layout()
        plt.show()
    
  4. 性能优化 :对于大规模仿真,可以使用Numba加速:
    from numba import jit
    
    @jit(nopython=True)
    def bicycle_model_numba(v, delta, state, L, dt):
        x, y, theta = state
        new_theta = theta + (v * np.tan(delta) / L) * dt
        new_x = x + v * np.cos(theta) * dt
        new_y = y + v * np.sin(theta) * dt
        return np.array([new_x, new_y, new_theta])
    

5. 实际应用案例

让我们通过一个完整的案例演示如何应用自行车模型进行路径跟踪仿真。

5.1 生成参考路径

def generate_reference_path():
    # 直线段
    x_straight = np.linspace(0, 50, 100)
    y_straight = np.zeros(100)
    
    # 圆弧段
    theta_arc = np.linspace(0, np.pi/2, 50)
    x_arc = 50 + 20 * np.sin(theta_arc)
    y_arc = 20 - 20 * np.cos(theta_arc)
    
    # 另一条直线段
    x_straight2 = np.linspace(70, 70, 50)
    y_straight2 = np.linspace(20, 60, 50)
    
    path = np.column_stack([
        np.concatenate([x_straight, x_arc, x_straight2]),
        np.concatenate([y_straight, y_arc, y_straight2])
    ])
    return path

reference_path = generate_reference_path()

5.2 路径跟踪仿真

def path_following_simulation():
    L = 2.5
    dt = 0.1
    total_time = 30
    steps = int(total_time / dt)
    
    state = np.array([0.0, 0.0, 0.0, 0.0])  # 包含转向角
    trajectory = [state[:3].copy()]
    actual_path = [state[:2].copy()]
    
    for i in range(steps):
        # 获取控制命令
        v = 3.0  # 恒定速度
        delta = pure_pursuit_controller(state[:3], reference_path, L)
        
        # 应用转向速率限制
        max_steering_rate = np.radians(15)  # 15°/s
        delta = np.clip(delta, state[3]-max_steering_rate*dt, state[3]+max_steering_rate*dt)
        
        # 更新状态
        state = bicycle_model_with_steering_rate(v, 0, state, L, dt)
        state[3] = delta  # 更新转向角
        
        trajectory.append(state[:3].copy())
        actual_path.append(state[:2].copy())
    
    # 可视化
    plt.figure(figsize=(12,6))
    plt.plot(reference_path[:,0], reference_path[:,1], 'b--', label='参考路径')
    plt.plot(np.array(actual_path)[:,0], np.array(actual_path)[:,1], 'r-', label='实际轨迹')
    plt.axis('equal')
    plt.legend()
    plt.title('路径跟踪性能')
    plt.xlabel('X位置 (m)')
    plt.ylabel('Y位置 (m)')
    plt.grid(True)
    plt.show()
    
    return np.array(trajectory)

trajectory = path_following_simulation()
plot_state_sequence(trajectory, L)

5.3 性能评估指标

为量化跟踪性能,我们可以计算以下指标:

def evaluate_performance(actual_path, reference_path):
    # 找到每个实际点对应的最近参考点
    distances = []
    for point in actual_path:
        dists = np.linalg.norm(reference_path - point, axis=1)
        distances.append(np.min(dists))
    
    # 计算统计指标
    max_error = np.max(distances)
    avg_error = np.mean(distances)
    rms_error = np.sqrt(np.mean(np.array(distances)**2))
    
    print(f"最大跟踪误差: {max_error:.2f}m")
    print(f"平均跟踪误差: {avg_error:.2f}m")
    print(f"RMS跟踪误差: {rms_error:.2f}m")
    
    # 绘制误差曲线
    plt.figure(figsize=(10,4))
    plt.plot(distances)
    plt.title('跟踪误差随时间变化')
    plt.xlabel('时间步')
    plt.ylabel('误差 (m)')
    plt.grid(True)
    plt.show()

evaluate_performance(np.array(actual_path), reference_path)

更多推荐