一、纯跟踪算法:从几何直觉到工程实现

各位同学好,我是你们的老朋友。今天咱们聊聊路径跟踪里一个非常经典的方法——纯跟踪算法(Pure Pursuit)。

说实话,我第一次接触这个算法是在五年前的一个园区物流项目里。当时车辆要在狭窄通道里走S弯,用PID控制总是 overshoot,后来换成纯跟踪,效果出奇的好。嗯,从那以后我就对这个算法情有独钟了。

1.1 基本原理:一个几何问题

纯跟踪算法的核心思想,说白了就是一句话:让车辆像追逐一个移动目标点一样去跟踪路径。

你想象一下:在车辆前方某处,有一个虚拟的“目标点”在路径上移动。车辆通过调整前轮转角,让自己始终朝着这个目标点前进。这就是“纯跟踪”这个名字的由来——纯粹地追逐那个点。

具体怎么算呢?我们来看几何关系:

核心公式:

δ = arctan( 2L·sin(α) / ld )

其中:

  • δ — 前轮转角(我们要算的东西)
  • L — 车辆轴距(固定值)
  • α — 车辆航向与目标点方向的夹角
  • ld — 前视距离(关键参数)

这个公式怎么来的?其实它基于一个简单的几何模型——阿克曼转向几何。车辆转弯时,后轴中心会画出一个圆弧。纯跟踪算法就是让这个圆弧恰好经过目标点。

我在项目中遇到过一个问题:公式里的α角怎么算?很多人直接用车辆朝向和目标点方向的夹角,但要注意坐标系转换。我习惯用车辆坐标系下的角度,这样更直观。

1.2 前视距离选择:最关键的调参

纯跟踪算法里,前视距离 ld 是最重要的参数。没有之一。

为什么这么说?你想想看:

  • ld 太大:车辆反应迟钝,过弯时容易切内线,甚至冲出道路
  • ld 太小:车辆频繁调整方向,出现“蛇形”振荡

我个人的经验是:前视距离应该与车速成正比。一个常用的公式是:

ld = k · v + ld_min

其中 k 是比例系数,v 是车速,ld_min 是最小前视距离。

我的调参建议:

先固定车速,从较大的 ld 开始试(比如 5 米),然后逐步减小,直到车辆出现轻微振荡,再回调 20%。这个“临界振荡法”我在三个项目里都用过,效果不错。

另外,不同曲率的弯道也需要不同的 ld。我记得有一次在高速匝道上测试,曲率半径只有 30 米,用固定 ld 怎么调都不行。后来改成根据路径曲率动态调整 ld,问题就解决了。

1.3 横向误差计算:怎么评价跟踪效果?

做控制,你得知道控制得好不好。横向误差就是最直观的指标。

横向误差的定义很简单:车辆当前位置到目标路径的最短距离。

计算方式有两种:

  1. 最近点法:找到路径上离车辆最近的点,计算距离
  2. 投影法:将车辆位置投影到路径上,计算垂直距离

我个人更推荐投影法,因为它能反映车辆相对于路径前进方向的偏差。最近点法在路径曲率变化大的地方容易跳变。

注意:横向误差有正负之分。我习惯定义:车辆在路径左侧为正,右侧为负。这样在调试时能直观看出车辆偏向哪边。

1.4 Python实现:从公式到代码

好了,理论讲完了,咱们来看看代码怎么写。下面是一个完整的纯跟踪算法实现:

import numpy as np
import math

class PurePursuitController:
    def __init__(self, wheelbase=2.5, k=0.5, ld_min=2.0):
        self.L = wheelbase      # 轴距(米)
        self.k = k              # 前视距离比例系数
        self.ld_min = ld_min    # 最小前视距离(米)
    
    def compute_steering_angle(self, vehicle_state, path_points, velocity):
        """
        计算前轮转角
        :param vehicle_state: [x, y, yaw] 车辆状态
        :param path_points: [[x1,y1], [x2,y2], ...] 路径点
        :param velocity: 当前车速(m/s)
        :return: 前轮转角(弧度)
        """
        # 1. 计算前视距离
        ld = self.k * velocity + self.ld_min
        
        # 2. 找到目标点
        target_point = self._find_target_point(
            vehicle_state, path_points, ld
        )
        
        if target_point is None:
            return 0.0
        
        # 3. 计算目标点在车辆坐标系下的角度
        dx = target_point[0] - vehicle_state[0]
        dy = target_point[1] - vehicle_state[1]
        
        # 转换到车辆坐标系
        alpha = math.atan2(dy, dx) - vehicle_state[2]
        
        # 4. 计算前轮转角
        steering_angle = math.atan2(
            2.0 * self.L * math.sin(alpha), ld
        )
        
        # 限制最大转角
        max_steer = math.radians(30)
        steering_angle = np.clip(
            steering_angle, -max_steer, max_steer
        )
        
        return steering_angle
    
    def _find_target_point(self, vehicle_state, path_points, ld):
        """找到前视距离处的目标点"""
        min_dist = float('inf')
        target_idx = 0
        
        # 找到最近点
        for i, point in enumerate(path_points):
            dist = math.hypot(
                point[0] - vehicle_state[0],
                point[1] - vehicle_state[1]
            )
            if dist < min_dist:
                min_dist = dist
                target_idx = i
        
        # 从最近点开始,找到距离为 ld 的点
        accumulated_dist = 0.0
        for i in range(target_idx, len(path_points) - 1):
            segment_dist = math.hypot(
                path_points[i+1][0] - path_points[i][0],
                path_points[i+1][1] - path_points[i][1]
            )
            accumulated_dist += segment_dist
            
            if accumulated_dist >= ld:
                # 线性插值
                ratio = (accumulated_dist - ld) / segment_dist
                x = path_points[i+1][0] - ratio * (
                    path_points[i+1][0] - path_points[i][0]
                )
                y = path_points[i+1][1] - ratio * (
                    path_points[i+1][1] - path_points[i][1]
                )
                return [x, y]
        
        # 如果路径不够长,返回最后一个点
        return path_points[-1]
    
    def compute_lateral_error(self, vehicle_state, path_points):
        """计算横向误差"""
        # 找到最近路径点
        min_dist = float('inf')
        nearest_point = None
        
        for point in path_points:
            dist = math.hypot(
                point[0] - vehicle_state[0],
                point[1] - vehicle_state[1]
            )
            if dist < min_dist:
                min_dist = dist
                nearest_point = point
        
        # 计算横向误差(带符号)
        dx = nearest_point[0] - vehicle_state[0]
        dy = nearest_point[1] - vehicle_state[1]
        
        # 路径方向角
        path_yaw = math.atan2(
            path_points[-1][1] - path_points[0][1],
            path_points[-1][0] - path_points[0][0]
        )
        
        # 横向误差 = 距离 × sin(相对角度)
        lateral_error = math.hypot(dx, dy) * math.sin(
            math.atan2(dy, dx) - path_yaw
        )
        
        return lateral_error

这段代码我用了好几年,基本框架没怎么变过。有几个细节我想强调一下:

  • 目标点查找:我用了累积距离法,比直接找最近点更稳定
  • 线性插值:路径点稀疏时,插值能避免转角跳变
  • 转角限幅:别忘了限制最大转角,否则实车会出问题

1.5 仿真验证:看看效果

光有代码不行,咱们得跑个仿真看看效果。下面是一个简单的仿真框架:

import matplotlib.pyplot as plt

def simulate_pure_pursuit():
    # 创建控制器
    controller = PurePursuitController(
        wheelbase=2.5, k=0.5, ld_min=2.0
    )
    
    # 生成参考路径(圆形)
    theta = np.linspace(0, 2*np.pi, 100)
    radius = 10.0
    path = np.column_stack([
        radius * np.cos(theta),
        radius * np.sin(theta)
    ])
    
    # 初始状态
    state = [radius, 0.0, math.pi/2]  # x, y, yaw
    velocity = 3.0  # 3 m/s
    
    # 记录轨迹
    trajectory = [state[:2]]
    lateral_errors = []
    
    # 仿真循环
    dt = 0.05  # 50ms
    for _ in range(500):
        # 计算控制量
        steer = controller.compute_steering_angle(
            state, path, velocity
        )
        
        # 更新车辆状态(简单运动学模型)
        state[0] += velocity * math.cos(state[2]) * dt
        state[1] += velocity * math.sin(state[2]) * dt
        state[2] += velocity / controller.L * math.tan(steer) * dt
        
        # 记录
        trajectory.append(state[:2])
        lateral_errors.append(
            controller.compute_lateral_error(state, path)
        )
    
    # 绘制结果
    plt.figure(figsize=(12, 4))
    
    plt.subplot(1, 2, 1)
    plt.plot(path[:,0], path[:,1], 'b--', label='参考路径')
    traj = np.array(trajectory)
    plt.plot(traj[:,0], traj[:,1], 'r-', label='实际轨迹')
    plt.axis('equal')
    plt.legend()
    plt.title('路径跟踪效果')
    
    plt.subplot(1, 2, 2)
    plt.plot(lateral_errors)
    plt.xlabel('时间步')
    plt.ylabel('横向误差 (m)')
    plt.title('横向误差变化')
    
    plt.tight_layout()
    plt.show()

# 运行仿真
simulate_pure_pursuit()

运行这段代码,你会看到车辆沿着圆形路径行驶,横向误差逐渐收敛到零附近。这就是纯跟踪算法的魅力——简单、直观、有效。

1.6 知识体系总览

为了让大家对纯跟踪算法有个整体认识,我画了一张结构图:

纯跟踪算法知识体系 纯跟踪算法 基本原理 前视距离选择 横向误差计算 阿克曼转向几何 目标点追逐原理 前轮转角计算公式 与车速成正比关系 动态调整策略 临界振荡调参法 最近点法 投影法(推荐) 带符号误差计算 Python实现与仿真验证

这张图把纯跟踪算法的知识体系梳理得很清楚。从基本原理出发,到前视距离和横向误差两个关键点,最后落地到代码实现。每个环节都环环相扣,缺一不可。

我的学习建议:

别急着调参,先把几何关系搞明白。我曾经带过一个实习生,上来就调 ld,调了两天没调好。后来我让他画了个几何图,五分钟就理解了问题出在哪。

好了,这一章的内容就到这里。纯跟踪算法虽然简单,但用好了威力很大。下一章我们会聊更高级的跟踪方法,到时候你会发现,很多复杂算法其实都是在纯跟踪的基础上改进的。

可可精品资料库