多目标轨迹优化车道变换规划:自动驾驶轨迹规划新范式:基于Frenet坐标系的车道变换算法全解析

基于Frenet坐标系的车道变换轨迹规划器,适用于自动驾驶或路径规划场景。以下是代码的结构和功能分析:


1. 核心类与功能

1.1 FrenetConverter

功能:在Frenet坐标系(s, d)和笛卡尔坐标系(x, y)之间进行转换。

  • 属性

    • ref_path: 由起点和终点生成的参考路径(直线)。
    • s_coords: 每个参考路径点的累计纵向距离(s)。
    • spline: 三次样条插值,用于将s映射到笛卡尔坐标。
  • 方法

    • cartesian_to_frenet(point)
      • 找到参考路径上距离point最近的点。
      • 根据该点的切向量计算法向量,得到横向偏移d
    • frenet_to_cartesian(s, d)
      • 使用样条插值获得参考点的笛卡尔坐标。
      • 根据切向量计算法向量,加上d得到最终位置。

用途:将轨迹规划从Frenet坐标系转换为实际的笛卡尔坐标,便于可视化和避障。


1.2 LaneChangePlanner

功能:生成并评估车道变换轨迹,选择最优路径。

  • 属性

    • ego_car: 自车的初始状态(x, y, v, heading)。
    • obs: 障碍物的状态列表(每个障碍物为 [x, y, v, heading])。
    • ref_path: 参考路径(用于生成Frenet坐标)。
  • 方法

    • generate_lateral_trajectories()
      • 使用多项式拟合生成横向轨迹(d方向),目标是实现车道变换。
    • generate_longitudinal_trajectories()
      • 生成纵向轨迹(s方向),表示自车的前进路径。
    • initial_screening()
      • 筛选满足加速度、jerk、曲率约束的轨迹。
    • risk_field()
      • 计算轨迹与障碍物的碰撞风险,使用指数衰减模型。
    • efficiency_metric()
      • 用纵向位移增量衡量轨迹效率。
    • comfort_metric()
      • 用横向jerk的均方根倒数衡量舒适度。
    • evaluate_trajectory()
      • 综合安全、效率、舒适度指标,加权评分。
    • select_optimal_trajectory()
      • 生成候选轨迹,筛选并选择最优路径。
    • visualize()
      • 可视化参考路径、车道线、自车轨迹、障碍物预测轨迹。

2. 代码流程

  1. 初始化

    • 设置自车状态、障碍物状态和参考路径。
  2. 轨迹生成

    • 使用多项式拟合生成横向(d)和纵向(s)轨迹,组合成候选轨迹。
  3. 初步筛选

    • 检查轨迹是否满足加速度、jerk、曲率约束。
  4. 综合评估

    • 计算轨迹的安全风险效率舒适度,加权求和得到评分。
  5. 选择最优轨迹

    • 根据评分选择最优轨迹,并输出轨迹点坐标。
  6. 可视化

    • 绘制参考路径、车道线、自车轨迹、障碍物预测轨迹。

    import numpy as np
    from scipy.spatial.distance import cdist
    from scipy.interpolate import CubicSpline
    import matplotlib.pyplot as plt

    class FrenetConverter:
    """Frenet坐标系转换工具(直线参考路径简化版)"""
    def init(self, start_point, end_point):
    self.ref_path = np.linspace(start_point, end_point, 100)
    self.s_coords = np.cumsum(np.sqrt(np.sum(np.diff(self.ref_path, axis=0)**2, axis=1)))
    self.s_coords = np.insert(self.s_coords, 0, 0)
    self.spline = CubicSpline(self.s_coords, self.ref_path)

    复制代码
     def cartesian_to_frenet(self, point):
         """笛卡尔坐标转Frenet坐标"""
         dists = cdist([point], self.ref_path)
         min_idx = np.argmin(dists)
         s = self.s_coords[min_idx]
         
         # 计算法向距离
         tangent = self.ref_path[min_idx] - self.ref_path[min_idx-1] if min_idx > 0 else self.ref_path[1] - self.ref_path[0]
         normal = np.array([-tangent[1], tangent[0]])
         normal /= np.linalg.norm(normal)
         
         d = np.dot(point - self.ref_path[min_idx], normal)
         return s, d
     
     def frenet_to_cartesian(self, s, d):
         """Frenet坐标转笛卡尔坐标"""
         ref_point = self.spline(s)
         if len(ref_point) > 2:
             ref_point = ref_point[:, 0]
         
         # 计算法向向量
         tangent = self.spline(s, 1)  # 一阶导数(切向量)
         if tangent[0] == 0 and tangent[1] == 0:  # 防止零切向量
             tangent = np.array([1e-5, 0])
         normal = np.array([-tangent[1], tangent[0]])
         normal /= np.linalg.norm(normal)
         
         return ref_point + d * normal

    class LaneChangePlanner:
    def init(self, ego_car, obstacles, ref_path=None, dt=0.1, T=5.0):
    """
    初始化参数
    ego_car: 自车状态 [x, y, v, heading]
    obstacles: 障碍车列表 [[x, y, v, heading], ...]
    ref_path: 参考路径 [(x0,y0), (x1,y1), ...]
    dt: 时间步长
    T: 换道总时间
    """
    self.ego = ego_car
    self.obs = obstacles
    self.dt = dt
    self.T = T
    self.time_steps = int(T / dt)

    复制代码
         # 安全阈值参数
         self.safe_distance = 5.0  # 最小安全距离
         self.max_accel = 3.0      # 最大加速度(m/s²)
         self.max_jerk = 5.0       # 最大加加速度(m/s³)
         self.max_curv = 0.2       # 最大曲率(1/m)
         self.max_steering = 0.5   # 最大转向角
         
         # 设置默认参考路径(直线)
         if ref_path is None:
             ref_path = np.array([[0, 0], [200, 0]])
         self.ref_converter = FrenetConverter(ref_path[0], ref_path[-1])
         
         # 目标车道横向位移(正常3.5米车道)
         self.target_d = 3.5 if self.ego[1] < 0 else -3.5
         
     def predict_obstacle(self, ob_state, t):
         """预测障碍车t秒后的位置(匀速直线模型)"""
         x, y, v, heading = ob_state
         distance = v * t
         return np.array([
             x + distance * np.cos(heading),
             y + distance * np.sin(heading)
         ])
    
     def calc_curvature(self, s_traj, d_traj):
         """计算轨迹曲率(Frenet坐标系下)"""
         t = np.arange(len(s_traj)) * self.dt
         
         # 计算导数
         v_s = np.gradient(s_traj, t)  # 纵向速度
         a_s = np.gradient(v_s, t)     # 纵向加速度
         
         v_d = np.gradient(d_traj, t)  # 横向速度
         a_d = np.gradient(v_d, t)     # 横向加速度
         
         # 计算曲率(近似公式)
         curv = np.abs(a_d) / (v_s**2 + 1e-5)  # 防止除以0
         return curv
    
     def generate_lateral_trajectories(self):
         """生成横向轨迹簇(五次多项式)"""
         t = np.linspace(0, self.T, self.time_steps)
         _, d0 = self.ref_converter.cartesian_to_frenet(self.ego[:2])
         v_d0 = self.ego[3]  # 初始横向速度
         
         # 横向位移目标范围
         target_d_min = max(0, self.target_d - 0.5)
         target_d_max = min(0, self.target_d + 0.5)
         
         trajectories = []
         coeffs_list = []
         
         # 采样不同的终端状态
         for d_end in np.linspace(target_d_min, target_d_max, 5):
             # 五次多项式系数 (d(t) = a0 + a1*t + a2*t² + a3*t³ + a4*t⁴ + a5*t⁵)
             A = np.array([
                 [1, 0, 0, 0, 0, 0],             # d(0) = d0
                 [0, 1, 0, 0, 0, 0],             # d'(0) = v_d0
                 [0, 0, 2, 0, 0, 0],             # d''(0) = 0 (默认初始加速度为0)
                 [1, self.T, self.T**2, self.T**3, self.T**4, self.T**5],
                 [0, 1, 2*self.T, 3*self.T**2, 4*self.T**3, 5*self.T**4],
                 [0, 0, 2, 6*self.T, 12*self.T**2, 20*self.T**3]
             ])
             b = np.array([d0, v_d0, 0, d_end, 0, 0])  # 终端速度和加速度为0
             
             try:
                 coeffs = np.linalg.solve(A, b)[::-1]  # 翻转顺序用于polyval
                 d_traj = np.polyval(coeffs, t)
                 trajectories.append(d_traj)
                 coeffs_list.append(coeffs)
             except np.linalg.LinAlgError:
                 continue
                 
         return trajectories, coeffs_list
    
     def generate_longitudinal_trajectories(self):
         """生成纵向轨迹簇(四次多项式)"""
         t = np.linspace(0, self.T, self.time_steps)
         s0, _ = self.ref_converter.cartesian_to_frenet(self.ego[:2])
         v_s0 = self.ego[2]  # 初始纵向速度
         
         trajectories = []
         
         # 采样不同的终端速度
         for v_end in np.linspace(self.ego[2]*0.8, self.ego[2]*1.2, 5):
             # 四次多项式 (s(t) = a0 + a1*t + a2*t² + a3*t³ + a4*t⁴)
             # 边界条件: s(0)=s0, s'(0)=v_s0, s'(T)=v_end, s''(0)=0, s''(T)=0
             A = np.array([
                 [1, 0, 0, 0, 0],
                 [0, 1, 0, 0, 0],
                 [0, 0, 2, 0, 0],
                 [0, 1, 2*self.T, 3*self.T**2, 4*self.T**3],
                 [0, 0, 2, 6*self.T, 12*self.T**2]
             ])
             b = np.array([s0, v_s0, 0, v_end, 0])
             
             try:
                 coeffs = np.linalg.solve(A, b)[::-1]
                 s_traj = np.polyval(coeffs, t)
                 trajectories.append(s_traj)
             except np.linalg.LinAlgError:
                 continue
                 
         return trajectories
    
     def initial_screening(self, s_trajs, d_trajs):
         """基于动力学约束的初筛"""
         valid_trajs = []
         
         for s_traj in s_trajs:
             for d_traj in d_trajs:
                 t = np.arange(len(s_traj)) * self.dt
                 
                 # 纵向动力学约束
                 v_s = np.gradient(s_traj, t)
                 a_s = np.gradient(v_s, t)
                 jerk_s = np.gradient(a_s, t)
                 
                 # 横向动力学约束
                 v_d = np.gradient(d_traj, t)
                 a_d = np.gradient(v_d, t)
                 jerk_d = np.gradient(a_d, t)
                 
                 # 组合约束检查
                 if (np.max(np.abs(a_s)) < self.max_accel and 
                     np.max(np.abs(jerk_s)) < self.max_jerk and
                     np.max(np.abs(a_d)) < self.max_accel and 
                     np.max(np.abs(jerk_d)) < self.max_jerk):
                     
                     # 曲率约束检查
                     curv = self.calc_curvature(s_traj, d_traj)
                     if np.max(np.abs(curv)) < self.max_curv:
                         valid_trajs.append((s_traj, d_traj))
                         
         return valid_trajs
    
     def risk_field(self, traj):
         """计算轨迹风险值"""
         total_risk = 0
         s_traj, d_traj = traj
         
         for i, (s, d) in enumerate(zip(s_traj, d_traj)):
             t = i * self.dt
             ego_pos = self.ref_converter.frenet_to_cartesian(s, d)
             
             for ob in self.obs:
                 # 预测障碍车位置
                 ob_pos = self.predict_obstacle(ob, t)
                 # 计算欧氏距离
                 distance = np.linalg.norm(ego_pos - ob_pos)
                 
                 # 风险函数(指数衰减模型)
                 if distance < self.safe_distance:
                     risk = np.exp(-distance/self.safe_distance * 3)
                     total_risk += risk
                     
         return total_risk
    
     def efficiency_metric(self, s_traj):
         """计算效率指标"""
         # 纵向位移增量作为效率衡量(越大越好)
         return s_traj[-1] - s_traj[0]
    
     def comfort_metric(self, d_traj):
         """计算舒适度指标"""
         t = np.arange(len(d_traj)) * self.dt
         # 横向加速度变化率(jerk)
         v_d = np.gradient(d_traj, t)
         a_d = np.gradient(v_d, t)
         jerk_d = np.gradient(a_d, t)
         # 舒适度反比于jerk的均方根
         return 1 / (np.sqrt(np.mean(jerk_d**2)) + 1e-5)
    
     def evaluate_trajectory(self, traj):
         """轨迹综合评价"""
         s_traj, d_traj = traj
         
         # 安全指标(风险越低越好)
         safety_score = -self.risk_field(traj) 
         
         # 效率指标(纵向位移越大越好)
         efficiency_score = self.efficiency_metric(s_traj) 
         
         # 舒适度指标(横向jerk越小越好)
         comfort_score = self.comfort_metric(d_traj)
         
         # 归一化权重
         safety_score = safety_score * 0.6
         efficiency_score = efficiency_score * 0.3
         comfort_score = comfort_score * 0.1
         
         return safety_score + efficiency_score + comfort_score
    
     def select_optimal_trajectory(self):
         """主规划函数"""
         # 生成轨迹簇
         lat_trajs, _ = self.generate_lateral_trajectories()
         lon_trajs = self.generate_longitudinal_trajectories()
         
         # 组合横纵向轨迹
         candidate_trajs = []
         for s_traj in lon_trajs:
             for d_traj in lat_trajs:
                 candidate_trajs.append((s_traj, d_traj))
         
         # 初筛
         valid_trajs = self.initial_screening(
             [t[0] for t in candidate_trajs], 
             [t[1] for t in candidate_trajs]
         )
         
         if not valid_trajs:
             raise RuntimeError("未找到满足动力学约束的轨迹")
         
         # 评估并选择最优
         best_score = -np.inf
         best_traj = None
         for traj in valid_trajs:
             score = self.evaluate_trajectory(traj)
             if score > best_score:
                 best_score = score
                 best_traj = traj
                 
         return best_traj
    
     def visualize(self, best_traj):
         """可视化最佳轨迹"""
         s_traj, d_traj = best_traj
         
         # 转换到笛卡尔坐标系
         cartesian_traj = []
         for s, d in zip(s_traj, d_traj):
             cartesian_traj.append(self.ref_converter.frenet_to_cartesian(s, d))
         cartesian_traj = np.array(cartesian_traj)
         
         # 障碍物轨迹
         obstacle_traj = []
         for ob in self.obs:
             for t in np.linspace(0, self.T, 10):
                 obstacle_traj.append(self.predict_obstacle(ob, t))
         obstacle_traj = np.array(obstacle_traj)
         
         plt.figure(figsize=(10, 6))
         
         # 绘制参考路径
         plt.plot(self.ref_converter.ref_path[:, 0], 
                 self.ref_converter.ref_path[:, 1], 
                 'k--', label='参考路径')
         
         # 绘制车道线
         plt.plot(self.ref_converter.ref_path[:, 0], 
                 self.ref_converter.ref_path[:, 1] + 3.5, 
                 'b-', linewidth=0.5)
         plt.plot(self.ref_converter.ref_path[:, 0], 
                 self.ref_converter.ref_path[:, 1] - 3.5, 
                 'b-', linewidth=0.5)
         
         # 绘制自车轨迹
         plt.plot(cartesian_traj[:, 0], cartesian_traj[:, 1], 
                 'r-', linewidth=2, label='最优换道轨迹')
         
         # 绘制障碍物轨迹
         if obstacle_traj.any():
             plt.scatter(obstacle_traj[:, 0], obstacle_traj[:, 1], 
                        c='purple', s=30, alpha=0.5, label='障碍物预测轨迹')
         
         # 绘制起点和终点
         plt.scatter(cartesian_traj[0, 0], cartesian_traj[0, 1], 
                    s=100, c='green', marker='o', label='起始位置')
         plt.scatter(cartesian_traj[-1, 0], cartesian_traj[-1, 1], 
                    s=100, c='blue', marker='*', label='目标位置')
         
         plt.title('车道变换轨迹规划')
         plt.xlabel('纵向位置 (m)')
         plt.ylabel('横向位置 (m)')
         plt.legend()
         plt.grid(True)
         plt.axis('equal')
         plt.tight_layout()
         plt.show()

    ===== 测试用例 =====

    if name == "main":
    # 初始化车辆状态
    ego_car = [0, 0, 20, 0] # [x, y, v, heading]

    复制代码
     # 障碍物状态 [x, y, v, heading]
     obstacles = [
         [30, 0, 18, 0],     # 同车道前车
         [25, -3.5, 22, 0],   # 目标车道后车
         [50, 3.5, 18, 0]     # 目标车道前车
     ]
     
     # 参考路径(直线)
     ref_path = np.array([[0, 0], [100, 0]])
     
     # 创建规划器
     planner = LaneChangePlanner(ego_car, obstacles, ref_path)
     
     try:
         # 执行轨迹规划
         optimal_traj = planner.select_optimal_trajectory()
         s_traj, d_traj = optimal_traj
         
         # 输出轨迹点
         print("最优轨迹点序列:")
         for i, (s, d) in enumerate(zip(s_traj, d_traj)):
             cart_pos = planner.ref_converter.frenet_to_cartesian(s, d)
             print(f"t={i*planner.dt:.1f}s: 位置=({cart_pos[0]:.2f}, {cart_pos[1]:.2f})m, "
                   f"s={s:.2f}m, d={d:.2f}m")
         
         # 可视化结果
         planner.visualize(optimal_traj)
         
     except RuntimeError as e:
         print(f"轨迹规划失败: {e}")