自动驾驶车辆运动学模型 Python 实现:从 3 个坐标系到单车模型仿真
在自动驾驶技术快速发展的今天,理解车辆运动学模型已成为算法工程师和控制工程师的必备技能。不同于复杂的动力学模型,运动学模型从几何关系出发,为初学者提供了一个直观理解车辆运动的窗口。本文将带领读者从零开始,用Python实现一个完整的单车运动学模型,涵盖大地坐标系、车身坐标系和Frenet坐标系之间的转换,最终构建一个可交互的仿真环境。
1. 坐标系基础与Python表示
1.1 三大坐标系精要
自动驾驶系统中常用的三个坐标系各有其独特作用:
- 大地坐标系(全局坐标系):固定不变的参考系,通常采用WGS84或UTM坐标系
- 车身坐标系:原点在车辆质心,x轴指向车辆前方,y轴指向左侧
- Frenet坐标系:沿参考路径建立,将车辆位置分解为纵向和横向分量
class CoordinateSystem: """坐标系基类""" def __init__(self, origin=(0,0), rotation=0): self.origin = np.array(origin) # 原点坐标 self.rotation = rotation # 旋转角度(弧度)1.2 坐标系转换数学原理
坐标系间的转换本质上是矩阵运算。以车身到大地坐标系的转换为例:
$$ \begin{bmatrix} X_{global} \ Y_{global} \end{bmatrix}
\begin{bmatrix} \cos\varphi & -\sin\varphi \ \sin\varphi & \cos\varphi \end{bmatrix} \begin{bmatrix} x_{body} \ y_{body} \end{bmatrix} + \begin{bmatrix} X_{origin} \ Y_{origin} \end{bmatrix} $$
Python实现时,我们使用NumPy进行高效矩阵运算:
def body_to_global(body_point, vehicle_pose): """ 车身坐标系转大地坐标系 :param body_point: 车身坐标系下的点(x,y) :param vehicle_pose: 车辆位姿(x,y,heading) :return: 全局坐标系下的点 """ x, y, heading = vehicle_pose rotation_matrix = np.array([ [np.cos(heading), -np.sin(heading)], [np.sin(heading), np.cos(heading)] ]) return rotation_matrix @ body_point + np.array([x, y])2. 单车运动学模型实现
2.1 模型假设与参数定义
单车模型基于以下关键假设:
- 前后轮分别简化为单轮
- 低速行驶忽略轮胎侧偏
- 仅考虑平面运动(X,Y,偏航角)
- 后轮转向角为零
class BicycleModel: def __init__(self, wheelbase=2.7, max_steer=np.radians(30)): """ :param wheelbase: 轴距(m) :param max_steer: 最大前轮转角(rad) """ self.wheelbase = wheelbase self.max_steer = max_steer self.state = np.zeros(3) # [x, y, heading]2.2 运动学方程离散化实现
基于运动学微分方程,我们采用欧拉方法进行离散化:
$$ \begin{cases} x_{k+1} = x_k + v \cdot \cos(\varphi_k) \cdot \Delta t \ y_{k+1} = y_k + v \cdot \sin(\varphi_k) \cdot \Delta t \ \varphi_{k+1} = \varphi_k + \frac{v}{L} \tan(\delta_f) \cdot \Delta t \end{cases} $$
Python实现如下:
def update(self, velocity, steer_angle, dt): """ 更新车辆状态 :param velocity: 车速(m/s) :param steer_angle: 前轮转角(rad) :param dt: 时间步长(s) """ steer_angle = np.clip(steer_angle, -self.max_steer, self.max_steer) x, y, heading = self.state self.state[0] = x + velocity * np.cos(heading) * dt self.state[1] = y + velocity * np.sin(heading) * dt self.state[2] = heading + velocity * np.tan(steer_angle) / self.wheelbase * dt # 归一化角度到[-π, π] self.state[2] = (self.state[2] + np.pi) % (2 * np.pi) - np.pi return self.state3. Frenet坐标系转换实践
3.1 参考路径表示
Frenet坐标系依赖于参考路径,我们使用三次样条曲线表示路径:
from scipy.interpolate import CubicSpline class ReferencePath: def __init__(self, waypoints): """ :param waypoints: 路径点列表[(x0,y0), (x1,y1), ...] """ waypoints = np.array(waypoints) self.cumulative_distances = np.cumsum(np.sqrt(np.sum(np.diff(waypoints, axis=0)**2, axis=1))) self.cumulative_distances = np.insert(self.cumulative_distances, 0, 0) self.spline_x = CubicSpline(self.cumulative_distances, waypoints[:,0]) self.spline_y = CubicSpline(self.cumulative_distances, waypoints[:,1])3.2 全局坐标与Frenet坐标互转
实现全局坐标系到Frenet坐标系的转换:
def global_to_frenet(self, point): """ 全局坐标转Frenet坐标 :param point: 全局坐标(x,y) :return: (s, d) s为纵向位移,d为横向偏移 """ # 找到最近点 def distance_to_spline(s): spline_point = np.array([self.spline_x(s), self.spline_y(s)]) return np.linalg.norm(spline_point - point) from scipy.optimize import minimize res = minimize(distance_to_spline, x0=0, bounds=[(0, self.cumulative_distances[-1])]) s = res.x[0] # 计算横向偏移 tangent_vector = np.array([ self.spline_x(s, 1), # x的一阶导数 self.spline_y(s, 1) # y的一阶导数 ]) normal_vector = np.array([-tangent_vector[1], tangent_vector[0]]) normal_vector = normal_vector / np.linalg.norm(normal_vector) spline_point = np.array([self.spline_x(s), self.spline_y(s)]) d = np.dot(point - spline_point, normal_vector) return s, d4. 完整仿真系统实现
4.1 可视化与交互设计
使用Matplotlib构建交互式仿真界面:
import matplotlib.pyplot as plt from matplotlib.widgets import Slider class BicycleSimulator: def __init__(self, model, path): self.model = model self.path = path self.fig, self.ax = plt.subplots(figsize=(10, 8)) plt.subplots_adjust(bottom=0.25) # 添加控制滑块 ax_vel = plt.axes([0.2, 0.1, 0.6, 0.03]) ax_steer = plt.axes([0.2, 0.05, 0.6, 0.03]) self.vel_slider = Slider(ax_vel, 'Velocity', -5.0, 5.0, valinit=0) self.steer_slider = Slider(ax_steer, 'Steer', -np.pi/4, np.pi/4, valinit=0) # 初始化绘图元素 self.vehicle_plot, = self.ax.plot([], [], 'bo-') self.path_plot, = self.ax.plot([], [], 'g-') self.trajectory_plot, = self.ax.plot([], [], 'r--') self.update_plot() def update(self, event=None): v = self.vel_slider.val steer = self.steer_slider.val self.model.update(v, steer, 0.1) self.update_plot() def update_plot(self): # 更新路径显示 s = np.linspace(0, self.path.cumulative_distances[-1], 100) path_points = np.column_stack((self.path.spline_x(s), self.path.spline_y(s))) self.path_plot.set_data(path_points[:,0], path_points[:,1]) # 更新车辆显示 x, y, heading = self.model.state vehicle_shape = np.array([ [x + 2*np.cos(heading), y + 2*np.sin(heading)], [x - 1*np.cos(heading) - 0.5*np.sin(heading), y - 1*np.sin(heading) + 0.5*np.cos(heading)], [x, y], [x - 1*np.cos(heading) + 0.5*np.sin(heading), y - 1*np.sin(heading) - 0.5*np.cos(heading)] ]) self.vehicle_plot.set_data(vehicle_shape[:,0], vehicle_shape[:,1]) # 更新轨迹 if not hasattr(self, 'trajectory'): self.trajectory = [] self.trajectory.append([x, y]) self.trajectory_plot.set_data(np.array(self.trajectory).T) self.ax.relim() self.ax.autoscale_view() self.fig.canvas.draw_idle()4.2 仿真案例与参数调优
通过调整模型参数观察车辆行为变化:
| 参数 | 典型值范围 | 影响效果 |
|---|---|---|
| 轴距(L) | 2.5-3.0m | 影响转向灵敏度 |
| 最大转向角 | 25-35度 | 决定最小转弯半径 |
| 速度范围 | -5~5 m/s | 负值表示倒车 |
| 时间步长(dt) | 0.01-0.1s | 影响仿真精度和计算效率 |
# 创建仿真实例 waypoints = [(0,0), (10,5), (20,-5), (30,10), (40,0)] ref_path = ReferencePath(waypoints) model = BicycleModel(wheelbase=2.7, max_steer=np.radians(30)) simulator = BicycleSimulator(model, ref_path) # 连接滑块事件 simulator.vel_slider.on_changed(simulator.update) simulator.steer_slider.on_changed(simulator.update) plt.show()5. 模型验证与误差分析
5.1 理论验证方法
验证运动学模型的准确性可通过以下方法:
- 圆周运动测试:恒定转向角和速度下,车辆应做匀速圆周运动
- 直线运动测试:零转向角时,车辆应保持直线行驶
- 路径跟踪测试:比较仿真轨迹与理论轨迹的偏差
def test_circular_motion(): """验证圆周运动""" model = BicycleModel(wheelbase=2.7) states = [] for _ in range(100): states.append(model.update(2.0, np.radians(10), 0.1).copy()) # 计算实际转弯半径 positions = np.array(states)[:,:2] centroid = np.mean(positions, axis=0) radii = np.linalg.norm(positions - centroid, axis=1) avg_radius = np.mean(radii) # 理论转弯半径 theoretical_radius = model.wheelbase / np.tan(np.radians(10)) print(f"实测平均半径: {avg_radius:.2f}m, 理论半径: {theoretical_radius:.2f}m") return abs(avg_radius - theoretical_radius) / theoretical_radius5.2 误差来源与改进
运动学模型的主要误差来源包括:
- 轮胎侧偏忽略:实际车辆转弯时轮胎会产生侧偏角
- 悬架系统影响:车身侧倾会改变轮胎接地特性
- 执行器延迟:转向和驱动系统存在响应延迟
提示:在低速场景(<5m/s)下,运动学模型误差通常在可接受范围内。对于高速场景,建议切换到动力学模型。
6. 进阶应用与扩展
6.1 与控制系统集成
将运动学模型作为模型预测控制(MPC)的预测模型:
def mpc_controller(model, reference_path, horizon=10): """简化的MPC控制器""" current_state = model.state s, d = reference_path.global_to_frenet(current_state[:2]) # 预测未来状态 predicted_states = [] state = current_state.copy() for i in range(horizon): # 简单控制策略:减小横向偏差 target_steer = -0.5 * d state = model.update(2.0, target_steer, 0.1) predicted_states.append(state) _, d = reference_path.global_to_frenet(state[:2]) return predicted_states6.2 多模型对比分析
扩展模型类支持不同变体:
class ExtendedBicycleModel(BicycleModel): """考虑轮胎侧偏的扩展模型""" def __init__(self, wheelbase, max_steer, cornering_stiffness): super().__init__(wheelbase, max_steer) self.cornering_stiffness = cornering_stiffness def update(self, velocity, steer_angle, dt): # 计算侧偏角 if abs(velocity) > 0.1: beta = np.arctan(0.5 * np.tan(steer_angle)) effective_steer = steer_angle - beta else: effective_steer = steer_angle # 调用父类更新方法 return super().update(velocity, effective_steer, dt)7. 性能优化技巧
提升运动学模型计算效率的关键方法:
- 向量化运算:使用NumPy进行批量状态更新
- 查表法:预计算常用参数组合
- 并行计算:利用多核处理多个轨迹预测
def batch_predict(initial_states, controls, dt): """ 批量预测车辆状态 :param initial_states: 初始状态数组(N,3) :param controls: 控制输入数组(N,2) (velocity, steer) :param dt: 时间步长 :return: 预测状态数组(N,3) """ velocities = controls[:,0] steers = controls[:,1] headings = initial_states[:,2] # 向量化计算 new_x = initial_states[:,0] + velocities * np.cos(headings) * dt new_y = initial_states[:,1] + velocities * np.sin(headings) * dt new_heading = headings + velocities * np.tan(steers) / self.wheelbase * dt # 角度归一化 new_heading = (new_heading + np.pi) % (2 * np.pi) - np.pi return np.column_stack((new_x, new_y, new_heading))8. 实际工程注意事项
在实车应用中需考虑以下实际问题:
- 参数标定:精确测量车辆轴距、最大转向角等参数
- 传感器融合:结合GPS、IMU和轮速计数据提高状态估计精度
- 执行器限制:考虑转向电机响应速度和加速度限制
注意:仿真与实车之间存在差距,建议通过以下步骤验证:
- 在仿真环境中充分测试
- 进行低速实车测试
- 逐步提高测试速度
- 根据实测数据修正模型参数