第三章:路径表示与平滑处理

各位同学,今天我们来聊聊路径的表示与平滑。说实话,这是无人车控制里最容易被忽视、却又最影响最终效果的一环。我见过太多团队在控制算法上死磕,结果发现路径本身就有问题——毛刺多、曲率突变,再好的控制器也救不回来。

3.1 路径点插值:从离散到连续

传感器采集的路径点,通常是离散的。比如GPS每隔0.1秒给一个点,或者规划层输出了一系列稀疏的路径点。但控制器需要连续的位置信息,怎么办?插值。

3.1.1 线性插值

最简单的方法,就是两点之间连直线。数学上就是:

// 线性插值公式
P(t) = (1 - t) * P0 + t * P1,  t ∈ [0, 1]

代码实现也很直接:

def linear_interpolate(p0, p1, num_points):
    """线性插值,在p0和p1之间生成num_points个点"""
    points = []
    for i in range(num_points):
        t = i / (num_points - 1)
        x = (1 - t) * p0[0] + t * p1[0]
        y = (1 - t) * p0[1] + t * p1[1]
        points.append((x, y))
    return points
注意:线性插值在连接处只有C0连续(位置连续),一阶导数不连续。这意味着曲率会突变,车辆转向时会有冲击感。我曾经在早期项目中用线性插值做路径平滑,结果实车测试时方向盘一直在抖——嗯,从那以后我再也不敢只用线性插值了。

3.1.2 样条插值

为了解决连续性问题,我们需要更高阶的插值方法。我个人最常用的是三次样条插值。它保证C2连续(位置、速度、加速度都连续),非常适合路径表示。

# 使用scipy的三次样条插值
from scipy.interpolate import CubicSpline
import numpy as np

# 原始路径点
x = np.array([0, 1, 2, 3, 4, 5])
y = np.array([0, 0.5, 2, 1.5, 3, 4])

# 以路径长度s为参数进行样条插值
s = np.cumsum(np.sqrt(np.diff(x)**2 + np.diff(y)**2))
s = np.insert(s, 0, 0)  # 第一个点s=0

cs_x = CubicSpline(s, x)
cs_y = CubicSpline(s, y)

# 在更密集的s上采样
s_dense = np.linspace(0, s[-1], 100)
x_dense = cs_x(s_dense)
y_dense = cs_y(s_dense)
我的经验:用路径长度s作为参数,比直接用x或y作为参数更稳定。因为路径可能有回环或垂直段,直接用x或y会导致多值问题。这个坑我踩过,分享给大家。

3.2 路径平滑算法

插值解决了连续性问题,但原始路径点本身可能就有噪声。这时候需要平滑算法。说白了,就是既要贴近原始路径,又要让路径足够光滑。

3.2.1 B样条曲线

B样条曲线是我在项目中用得最多的平滑方法。它的核心思想是:用一组控制点来定义曲线,但曲线不一定要通过所有控制点。

import numpy as np

def bspline_basis(i, k, t, knots):
    """计算B样条基函数"""
    if k == 0:
        return 1.0 if knots[i] <= t < knots[i+1] else 0.0
    
    # 递归计算
    denom1 = knots[i+k] - knots[i]
    denom2 = knots[i+k+1] - knots[i+1]
    
    term1 = 0
    if denom1 != 0:
        term1 = (t - knots[i]) / denom1 * bspline_basis(i, k-1, t, knots)
    
    term2 = 0
    if denom2 != 0:
        term2 = (knots[i+k+1] - t) / denom2 * bspline_basis(i+1, k-1, t, knots)
    
    return term1 + term2

def bspline_curve(control_points, num_samples=100, degree=3):
    """生成B样条曲线上的点"""
    n = len(control_points)
    knots = np.linspace(0, 1, n + degree + 1)
    
    curve_points = []
    for t in np.linspace(0, 1, num_samples):
        point = np.zeros(2)
        for i in range(n):
            basis = bspline_basis(i, degree, t, knots)
            point += basis * control_points[i]
        curve_points.append(point)
    
    return np.array(curve_points)
B样条的优势:
  • 局部性:移动一个控制点只影响附近区域,不会全局变形
  • 光滑性:三次B样条保证C2连续
  • 凸包性:曲线始终在控制点构成的凸包内

3.2.2 贝塞尔曲线

贝塞尔曲线是另一种经典方法。它的特点是:曲线始终通过第一个和最后一个控制点,中间控制点起牵引作用。

def bezier_curve(control_points, num_samples=100):
    """生成贝塞尔曲线上的点"""
    n = len(control_points) - 1
    curve_points = []
    
    for t in np.linspace(0, 1, num_samples):
        point = np.zeros(2)
        for i in range(n + 1):
            # 伯恩斯坦基函数
            coeff = np.math.comb(n, i) * (t ** i) * ((1 - t) ** (n - i))
            point += coeff * control_points[i]
        curve_points.append(point)
    
    return np.array(curve_points)
注意:贝塞尔曲线是全局的——移动一个控制点会影响整条曲线。对于长路径,我建议分段使用低阶贝塞尔曲线(3阶或4阶),而不是用高阶曲线。高阶曲线容易出现振荡,你想想看,一个10阶贝塞尔曲线,控制点稍微动一下,曲线可能就扭成麻花了。

3.3 曲率计算

曲率是路径平滑度的核心指标。对于无人车来说,曲率直接决定了最大通过速度。曲率越大,转弯越急,速度就得越低。

3.3.1 离散点曲率计算

对于离散路径点,我们用三点法估算曲率:

def compute_curvature(p1, p2, p3):
    """计算三点确定的圆的曲率"""
    # 向量
    v1 = p2 - p1
    v2 = p3 - p2
    
    # 叉积的模(2倍三角形面积)
    cross_product = np.cross(v1, v2)
    
    # 边长
    a = np.linalg.norm(v1)
    b = np.linalg.norm(v2)
    c = np.linalg.norm(p3 - p1)
    
    # 曲率 = 4 * 面积 / (a * b * c)
    if a * b * c == 0:
        return 0.0
    
    curvature = 2 * abs(cross_product) / (a * b * c)
    return curvature

# 计算整条路径的曲率
def path_curvature(path_points):
    """计算路径上每个点的曲率"""
    curvatures = []
    n = len(path_points)
    
    for i in range(1, n - 1):
        k = compute_curvature(path_points[i-1], 
                              path_points[i], 
                              path_points[i+1])
        curvatures.append(k)
    
    # 首尾点曲率设为0
    curvatures.insert(0, 0.0)
    curvatures.append(0.0)
    
    return np.array(curvatures)

3.3.2 曲率约束检查

在实际项目中,我通常会做曲率检查:

def check_curvature_constraint(path_points, max_curvature):
    """检查路径是否满足最大曲率约束"""
    curvatures = path_curvature(path_points)
    
    violations = np.where(curvatures > max_curvature)[0]
    
    if len(violations) > 0:
        print(f"警告:发现 {len(violations)} 个点曲率超标")
        print(f"最大曲率:{np.max(curvatures):.4f}")
        print(f"允许最大曲率:{max_curvature:.4f}")
        return False
    
    return True
避坑指南:我曾经在一个项目中,路径曲率计算一直偏大,排查了半天发现是坐标系单位搞错了——经纬度直接当米来算。曲率对尺度非常敏感,一定要确保单位统一。建议所有路径点先转换到笛卡尔坐标系(比如UTM坐标),再做曲率计算。

3.4 知识体系总览

下面这张图总结了本章的核心内容:

路径表示与平滑处理知识体系 路径表示 路径点插值 路径平滑算法 线性插值 样条插值 B样条曲线 贝塞尔曲线 关键指标:曲率计算与约束检查 应用:路径跟踪控制的前置处理

从这张图可以看出,路径表示是基础,插值和平滑是核心方法,曲率是评价指标,最终服务于路径跟踪控制。每一步都有坑,每一步也都有技巧。

本章小结:
  • 线性插值简单但连续性差,适合快速原型验证
  • 三次样条插值C2连续,是工程中最常用的插值方法
  • B样条曲线局部可控,适合长路径平滑
  • 贝塞尔曲线全局可控,适合短路径或曲线设计
  • 曲率计算要特别注意单位一致性

好了,关于路径表示与平滑处理就讲到这里。这些方法我在实际项目中反复用过,踩过坑也填过坑。希望你们能少走弯路,直接用到自己的项目里。

专注资料整理