B样条曲线轨迹优化(控制点优化) - 基于二阶差分的曲率限制

B样条曲线轨迹优化(控制点优化) - 基于二阶差分的曲率限制

这篇文章介绍了B样条曲线轨迹优化(控制点优化)技术,基于二阶差分的曲率限制方法。通过引入二阶差分曲率代价项,控制曲线局部变化,优化过程自动平滑高曲率段,提升轨迹质量,提高无人系统路径规划的可控性与执行安全性。文章详细阐述了曲率代价函数的设计、可导性与梯度推导,并提供了C++实现和Python动画可视化代码。

1️⃣ 背景与目标

B样条轨迹因其光滑、连续、可微等特性,广泛应用于无人系统路径规划中。然而,若控制点分布不合理,曲线可能出现剧烈弯折(高曲率),影响可控性与执行安全性。

我们将引入一个 “二阶差分曲率代价项”,控制曲线局部变化,优化过程自动平滑高曲率段,提升轨迹质量。

2️⃣ 二阶差分与曲率

在离散控制点层面,曲率近似由二阶差分向量表达:

💡 二阶差分定义:

\mathbf{d}_2^{(i)} = \mathbf{P}_i - 2\mathbf{P}_{i+1} + \mathbf{P}_{i+2}
  • 表示相邻三个控制点的“折线弯曲程度”
  • 二阶差分越大,说明控制点形成更尖锐的弯折,对应曲线曲率大

⚠️ 注意事项:

该二阶差分项并不是真正的几何曲率(实际上是加速度),而是一种简化的近似。在以下假设成立时,它才能较好地反映曲率趋势:

  • 控制点之间 间距较小且均匀
  • B 样条参数 t 与弧长 近似线性关系
  • 曲线采样点分布 近似等距

但是依然有一定的误差,如下图所示:

6d1881906d8de742c495ec6ab9b7d27.png

3️⃣ 曲率代价函数设计

设定最大允许曲率(差分范数)为 ​\kappa_{\text{max}},一旦超限,则施加惩罚代价。

📦 代价项定义:

对于每组三个控制点:

\phi(\mathbf{d}_2) = \begin{cases} (\|\mathbf{d}_2\| - \kappa_{\text{max}})^2, & \text{if } \|\mathbf{d}_2\| > \kappa_{\text{max}} \\ 0, & \text{otherwise} \end{cases}

最终总曲率代价为:

J_{\text{curv}} = \sum_{i=0}^{n-3} \phi(\mathbf{d}_2^{(i)})

4️⃣ 可导性与梯度推导

✅ 由于我们使用的是范数 + 二次惩罚项,整体函数连续可导,适用于主流优化器。

✏️ 梯度推导(链式法则):

​\mathbf{d}_2 = \mathbf{P}_i - 2\mathbf{P}_{i+1} + \mathbf{P}_{i+2}

\frac{\partial \phi}{\partial \mathbf{d}_2} = 2(\|\mathbf{d}_2\| - \kappa_{\text{max}}) \cdot \frac{\mathbf{d}_2}{\|\mathbf{d}_2\|}

再对控制点求偏导(线性关系):

\frac{\partial \phi}{\partial \mathbf{P}_i} = \frac{\partial \phi}{\partial \mathbf{d}_2} \cdot \frac{\partial \mathbf{d}_2}{\partial \mathbf{P}_i} = \mathbf{g} \\ \frac{\partial \phi}{\partial \mathbf{P}_{i+1}} = -2\mathbf{g} \\ \frac{\partial \phi}{\partial \mathbf{P}_{i+2}} = \mathbf{g}

其中:

\boxed{ \mathbf{g} = \frac{2(\|\mathbf{d}_2\| - \kappa_{\text{max}})}{\|\mathbf{d}_2\|} \cdot \mathbf{d}_2 }

5️⃣ C++ 实现代码

// 计算曲率成本及其梯度(二维)
// 若某个二阶差分超出阈值,则添加代价,并更新梯度
void BsplineOptimizer::calcCurvatureCost(
    const Eigen::MatrixXd& ctrl_points,
    double& cost,
    std::vector<double>& grad) 
{
    cost = 0.0;
    int num_ctrl_points = ctrl_points.cols();

    for (int i = 0; i < num_ctrl_points - 2; ++i) {
        Eigen::Vector2d d2 = ctrl_points.col(i) 
                           - 2 * ctrl_points.col(i + 1) 
                           + ctrl_points.col(i + 2);
        double norm_d2 = d2.norm();

        if (norm_d2 > max_curvature_) {
            double penalty = norm_d2 - max_curvature_;
            cost += penalty * penalty;

            Eigen::Vector2d grad_term = (2.0 * penalty / norm_d2) * d2;

            grad[2 * i + 0]     += grad_term.x();
            grad[2 * i + 1]     += grad_term.y();
            grad[2 * (i + 1) + 0] += -2.0 * grad_term.x();
            grad[2 * (i + 1) + 1] += -2.0 * grad_term.y();
            grad[2 * (i + 2) + 0] += grad_term.x();
            grad[2 * (i + 2) + 1] += grad_term.y();
        }
    }
}

6️⃣ Python 动画可视化

曲率代价.gif

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from scipy.interpolate import BSpline

# 控制参数
max_curvature = 1.0
num_iters = 100
alpha = 0.01  # 梯度步长

# 初始化控制点
ctrl_pts = np.array([
    [0, 0],
    [1, 2],
    [2, 0],
    [3, 2],
    [4, 0],
    [5, 2],
    [6, 0]
], dtype=float)

# B样条阶数(这里取3阶)
k = 3
n = len(ctrl_pts)
t = np.concatenate((
    np.zeros(k),
    np.linspace(0, 1, n - k + 1),
    np.ones(k)
))

# 计算B样条点
def get_bspline(ctrl_pts):
    spline = BSpline(t, ctrl_pts, k)
    x = np.linspace(0, 1, 200)
    pts = spline(x)
    return pts[:, 0], pts[:, 1]

# 计算二阶差分曲率代价和梯度
def curvature_cost_and_grad(ctrl_pts, max_curvature):
    cost = 0.0
    grad = np.zeros_like(ctrl_pts)
    for i in range(len(ctrl_pts) - 2):
        d2 = ctrl_pts[i] - 2 * ctrl_pts[i+1] + ctrl_pts[i+2]
        norm_d2 = np.linalg.norm(d2)
        if norm_d2 > max_curvature:
            penalty = norm_d2 - max_curvature
            cost += penalty ** 2
            grad_term = (2.0 * penalty / norm_d2) * d2
            grad[i]     += grad_term
            grad[i+1]   += -2.0 * grad_term
            grad[i+2]   += grad_term
    return cost, grad

# 初始化画布:左图B样条 + 右图代价图
fig, (ax_curve, ax_cost) = plt.subplots(1, 2, figsize=(12, 5))

spline_line, = ax_curve.plot([], [], 'b-', lw=2, label='B-Spline')
ctrl_line, = ax_curve.plot([], [], 'ro--', label='Control Points')
ax_curve.set_xlim(-1, 7)
ax_curve.set_ylim(-2, 4)
ax_curve.set_title("B-Spline Optimization")
ax_curve.legend()

# cost 曲线初始化
cost_values = []
cost_line, = ax_cost.plot([], [], 'g-', label='Curvature Cost')
ax_cost.set_xlim(0, num_iters)
ax_cost.set_ylim(0, 10)  # 初始范围,动态调整可加
ax_cost.set_title("Curvature Cost Over Iterations")
ax_cost.set_xlabel("Iteration")
ax_cost.set_ylabel("Cost")
ax_cost.grid(True)
ax_cost.legend()

def init():
    return spline_line, ctrl_line, cost_line

def animate(i):
    global ctrl_pts, cost_values
    cost, grad = curvature_cost_and_grad(ctrl_pts, max_curvature)
    ctrl_pts -= alpha * grad  # 梯度下降

    x_spline, y_spline = get_bspline(ctrl_pts)
    spline_line.set_data(x_spline, y_spline)
    ctrl_line.set_data(ctrl_pts[:, 0], ctrl_pts[:, 1])

    # 更新 cost 曲线
    cost_values.append(cost)
    cost_line.set_data(range(len(cost_values)), cost_values)
    ax_cost.set_ylim(0, max(1.0, max(cost_values)) * 1.1)

    return spline_line, ctrl_line, cost_line

ani = FuncAnimation(fig, animate, frames=num_iters, init_func=init, blit=True, interval=500)
plt.tight_layout()
plt.show()
Comment