# 算法说明 本文档详细说明 AGV 路径跟踪与控制系统中使用的算法原理和实现。 ## 目录 - [AGV 运动学模型](#agv-运动学模型) - [Pure Pursuit 算法](#pure-pursuit-算法) - [Stanley 算法](#stanley-算法) - [路径平滑算法](#路径平滑算法) - [算法比较](#算法比较) --- ## AGV 运动学模型 ### 阿克曼转向模型 本系统使用阿克曼转向几何模型来描述 AGV 的运动。 #### 模型假设 1. AGV 在平面上运动 2. 使用前轮转向,后轮驱动 3. 不考虑侧滑 4. 速度和转向角在限制范围内 #### 状态变量 AGV 的状态由四个变量描述: ``` x - x 坐标(米) y - y 坐标(米) θ - 航向角(弧度) v - 速度(米/秒) ``` #### 控制输入 ``` v - 驱动速度(米/秒) δ - 前轮转向角(弧度) ``` #### 运动学方程 ``` ẋ = v · cos(θ) ẏ = v · sin(θ) θ̇ = (v / L) · tan(δ) v̇ = a ``` 其中: - `L` 是轴距(前后轮距离) - `a` 是加速度 #### 离散化实现 使用欧拉法进行离散化: ```cpp State AGVModel::step(double v, double delta, double dt) const { State new_state = current_state_; // 限制转向角 delta = std::max(-max_steering_angle_, std::min(max_steering_angle_, delta)); // 更新状态 new_state.x += v * cos(current_state_.theta) * dt; new_state.y += v * sin(current_state_.theta) * dt; new_state.theta += (v / wheelbase_) * tan(delta) * dt; new_state.v = v; // 归一化角度到 [-π, π] while (new_state.theta > M_PI) new_state.theta -= 2 * M_PI; while (new_state.theta < -M_PI) new_state.theta += 2 * M_PI; return new_state; } ``` --- ## Pure Pursuit 算法 Pure Pursuit 是一种基于几何的路径跟踪算法,通过追踪前视点来计算转向角。 ### 算法原理 1. 在参考路径上选择一个前视点(lookahead point) 2. 计算从当前位置到前视点的曲率半径 3. 根据曲率计算转向角 ### 数学推导 #### 前视距离 ``` L_d = k_v · v + L_base ``` 其中: - `k_v`: 速度相关系数(通常为 1.0-2.0) - `v`: 当前速度 - `L_base`: 基础前视距离 #### 前视点选择 沿参考路径找到距离当前位置约为 `L_d` 的点。 #### 转向角计算 根据几何关系: ``` α = atan2(l_y, l_x) # 前视点相对角度 δ = atan(2 · L · sin(α) / L_d) ``` 其中: - `L`: 轴距 - `α`: 前视点相对于车辆坐标系的角度 - `L_d`: 前视距离 ### 实现代码 ```cpp double ControlGenerator::purePursuit( const AGVModel::State& state, const PathCurve& path, double lookahead_distance ) { const auto& path_points = path.getPathPoints(); if (path_points.empty()) return 0.0; // 找到最近点 size_t closest_idx = 0; double min_dist = 1e9; for (size_t i = 0; i < path_points.size(); ++i) { double dx = path_points[i].x - state.x; double dy = path_points[i].y - state.y; double dist = sqrt(dx * dx + dy * dy); if (dist < min_dist) { min_dist = dist; closest_idx = i; } } // 找到前视点 size_t lookahead_idx = closest_idx; for (size_t i = closest_idx; i < path_points.size(); ++i) { double dx = path_points[i].x - state.x; double dy = path_points[i].y - state.y; double dist = sqrt(dx * dx + dy * dy); if (dist >= lookahead_distance) { lookahead_idx = i; break; } } // 计算转向角 const auto& target = path_points[lookahead_idx]; double dx = target.x - state.x; double dy = target.y - state.y; // 转换到车体坐标系 double cos_theta = cos(state.theta); double sin_theta = sin(state.theta); double local_x = dx * cos_theta + dy * sin_theta; double local_y = -dx * sin_theta + dy * cos_theta; // 计算转向角 double alpha = atan2(local_y, local_x); double L = 2.0; // 轴距 double steering = atan(2 * L * sin(alpha) / lookahead_distance); return steering; } ``` ### 参数调整 - **lookahead_distance**: - 过小:响应快,但可能振荡 - 过大:平滑,但转弯时可能跟踪不及时 - 推荐:速度的 1-2 倍 ### 优缺点 **优点**: - 算法简单,计算量小 - 对路径平滑性要求低 - 适合高速场景 **缺点**: - 跟踪精度相对较低 - 不考虑横向误差 - 低速时性能下降 --- ## Stanley 算法 Stanley 算法结合了横向误差和航向误差,提供更精确的路径跟踪。 ### 算法原理 Stanley 算法由两部分组成: 1. **航向误差控制**:修正车辆朝向与路径切线的偏差 2. **横向误差控制**:修正车辆位置与路径的垂直距离 ### 数学公式 ``` δ = θ_e + atan(k · e / v) ``` 其中: - `θ_e`: 航向误差(车辆朝向与路径切线的夹角) - `e`: 横向误差(车辆到路径的垂直距离) - `k`: 增益参数 - `v`: 当前速度 ### 详细推导 #### 航向误差 ``` θ_e = θ_path - θ_vehicle ``` 归一化到 `[-π, π]`。 #### 横向误差 计算车辆前轮中心到路径的垂直距离: ``` e = (x_vehicle - x_path) · sin(θ_path) - (y_vehicle - y_path) · cos(θ_path) ``` 正值表示在路径左侧,负值表示右侧。 ### 实现代码 ```cpp double ControlGenerator::stanley( const AGVModel::State& state, const PathCurve& path, double k ) { const auto& path_points = path.getPathPoints(); if (path_points.empty()) return 0.0; // 找到最近的路径点 size_t closest_idx = 0; double min_dist = 1e9; for (size_t i = 0; i < path_points.size(); ++i) { double dx = path_points[i].x - state.x; double dy = path_points[i].y - state.y; double dist = sqrt(dx * dx + dy * dy); if (dist < min_dist) { min_dist = dist; closest_idx = i; } } const auto& closest_point = path_points[closest_idx]; // 计算横向误差 double dx = state.x - closest_point.x; double dy = state.y - closest_point.y; double path_theta = closest_point.theta; double cross_track_error = -dx * sin(path_theta) + dy * cos(path_theta); // 计算航向误差 double heading_error = state.theta - path_theta; while (heading_error > M_PI) heading_error -= 2 * M_PI; while (heading_error < -M_PI) heading_error += 2 * M_PI; // Stanley 控制律 double v = std::max(0.1, std::abs(state.v)); // 避免除零 double steering = heading_error + atan(k * cross_track_error / v); return steering; } ``` ### 参数调整 - **k(增益参数)**: - 过小:横向误差响应慢 - 过大:可能引起振荡 - 推荐范围:0.5 - 2.0 - 低速场景:使用较大的 k - 高速场景:使用较小的 k ### 优缺点 **优点**: - 跟踪精度高 - 同时考虑位置和方向误差 - 适合低速和高精度场景 - 理论上能保证稳定性 **缺点**: - 对路径平滑性要求较高 - 需要路径切线信息 - 参数调整相对复杂 --- ## 路径平滑算法 ### 三次样条插值 为了生成平滑的路径,系统使用三次样条插值。 #### 算法原理 对于 n 个路径点,三次样条插值构造 n-1 段三次多项式: ``` S_i(t) = a_i + b_i·t + c_i·t² + d_i·t³, t ∈ [t_i, t_{i+1}] ``` 满足条件: 1. 通过所有路径点 2. 一阶导数连续(速度连续) 3. 二阶导数连续(加速度连续) #### 实现 使用自然样条(边界二阶导数为零): ```cpp PathCurve PathCurve::generateSmoothPath( const std::vector& waypoints, int points_per_segment ) { std::vector smooth_points; for (size_t i = 0; i < waypoints.size() - 1; ++i) { const auto& p0 = waypoints[i]; const auto& p1 = waypoints[i + 1]; for (int j = 0; j < points_per_segment; ++j) { double t = static_cast(j) / points_per_segment; // 三次 Hermite 插值 double h00 = 2*t*t*t - 3*t*t + 1; double h10 = t*t*t - 2*t*t + t; double h01 = -2*t*t*t + 3*t*t; double h11 = t*t*t - t*t; PathPoint pt; pt.x = h00 * p0.x + h10 * dx0 + h01 * p1.x + h11 * dx1; pt.y = h00 * p0.y + h10 * dy0 + h01 * p1.y + h11 * dy1; smooth_points.push_back(pt); } } return PathCurve(smooth_points); } ``` --- ## 算法比较 ### 性能对比表 | 特性 | Pure Pursuit | Stanley | |------|--------------|---------| | 计算复杂度 | 低 | 中 | | 跟踪精度 | 中 | 高 | | 稳定性 | 好 | 很好 | | 高速性能 | 优秀 | 良好 | | 低速性能 | 一般 | 优秀 | | 参数调整 | 简单 | 中等 | | 适用场景 | 平滑路径 | 精确跟踪 | ### 选择建议 **使用 Pure Pursuit 当**: - 速度较高(> 2 m/s) - 路径相对平滑 - 对精度要求不高 - 计算资源有限 **使用 Stanley 当**: - 需要高精度跟踪 - 低速场景(< 1 m/s) - 路径复杂多变 - 有充足计算资源 ### 混合策略 可以根据速度动态切换: ```cpp std::string selectAlgorithm(double velocity) { if (velocity > 2.0) { return "pure_pursuit"; } else { return "stanley"; } } ``` --- ## 误差分析 ### 跟踪误差定义 ``` e_lateral = |位置到路径的垂直距离| e_heading = |车辆朝向 - 路径切线方向| ``` ### 误差来源 1. **算法固有误差** - Pure Pursuit: 前视点选择 - Stanley: 参数设置 2. **离散化误差** - 时间步长 dt 的选择 - 路径点密度 3. **模型误差** - 理想运动学模型 vs 实际物理 - 侧滑、轮胎摩擦等忽略因素 ### 误差优化 - 减小时间步长 dt - 增加路径点密度 - 调整算法参数 - 使用更精确的模型 --- ## 实际应用示例 ### 示例 1: 圆形路径跟踪 ```cpp // 生成半径 5m 的圆形路径 PathCurve path = PathCurve::generateCircularPath(5.0, 100); // Pure Pursuit tracker.generateControlSequence("pure_pursuit", 0.1, 10.0, 1.5); // 前视距离: 1.5 * 1.5 = 2.25m // Stanley tracker.generateControlSequence("stanley", 0.1, 10.0, 0.5); // k = 1.0(默认) ``` ### 示例 2: 复杂路径跟踪 ```cpp // 从 CSV 加载路径 PathCurve path = PathCurve::loadFromCSV("complex_path.csv"); // 使用 Stanley 以获得更高精度 tracker.generateControlSequence("stanley", 0.05, 15.0, 1.0); ``` --- ## 参考文献 1. **Pure Pursuit**: - Coulter, R. C. (1992). "Implementation of the Pure Pursuit Path Tracking Algorithm". Carnegie Mellon University. 2. **Stanley**: - Hoffmann, G. M., et al. (2007). "Autonomous Automobile Trajectory Tracking for Off-Road Driving: Controller Design, Experimental Validation and Racing". American Control Conference. 3. **阿克曼转向**: - Rajamani, R. (2011). "Vehicle Dynamics and Control". Springer. --- **最后更新**: 2025-11-27