ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自动泊车运动学建模:从转弯半径到S型速度规划与碰撞检测

自动泊车运动学建模:从转弯半径到S型速度规划与碰撞检测

简介:面向高校数学建模参赛者与自动驾驶入门学习者的赛题解析文档,聚焦2022年MathorCup挑战赛C题“自动泊车问题”。内容围绕阿克曼转向车辆参数展开,逐问梳理最小转弯半径与直线加速距离的计算、从车库入口到垂直、平行及45°倾斜车位的泊车轨迹建模,以及车位随机占用条件下的最优车位选择与实时路径仿真,涉及动力学约束、路径规划、碰撞规避和算法复杂度分析。压缩包仅含1个PDF文件,大小约2.07MB,便于在电脑或平板上直接查阅,适合赛前研读与专题复盘。目前已有1230人学习,可作为参赛队理解题意、搭建模型框架和核对关键参数的参考资料。

1. 停车这件事,最后卡住的不是算法而是几何

把车塞进一个只比车身长一米的平行车位,人类司机平均要来回调整两把;换成数学语言,这道题的难度不在“找一条路”,而在“车根本转不了那么急”。给定轴距 2.8 米、方向盘最大转角 470°、方向盘与前轮 16:1 的传动比,前轮的最大摆角就只有 29.375°,最小转弯半径因此被限制在 4.97 米。这个数字一出来,停车场的几何可行性就基本确定了:有些车位不是算法不够聪明进不去,而是物理上就进不去。一份数学建模赛题把自动泊车拆成四个递进的问题,问的其实是同一件事——几何给出的边界在哪,规划能在边界内争取到多少余量,以及在车位随机被占的现实里,这份余量还够不够用。搞懂这条链条,比背十个路径规划算法名有用得多。

2. 转向几何:4.97 米的转弯半径是怎么算出来的,为什么它决定了一切

2.1 为什么控制点一定要选在后轴中心

运动学建模第一步是选控制点,也就是“用哪个点代表整辆车”。赛题里明确假设控制点位于车身对称轴上的后轴中心,这不是为了简化而简化,而是因为只有这个点的速度方向恒等于车头朝向。换成质心或前轴,速度方向与车身纵轴之间会多出一个角度项,倒车时符号还要再翻一次,稍不留神整条轨迹就跑偏了。

把后轴中心记作 $(x, y)$,车头朝向记作 $\theta$,前轮转角记作 $\delta$,车速记作 $v$(带符号,前进为正、倒车为负),运动学方程就是标准的自行车模型:

$$ \dot{x} = v\cos\theta,\quad \dot{y} = v\sin\theta,\quad \dot{\theta} = \frac{v\tan\delta}{L} $$

其中 $L=2.8,\text{m}$ 是轴距。第三个式子是全篇的地基:车头转得多快,只取决于车速、前轮转角和轴距三个量。它同时告诉你一件反直觉的事——车停着不动时($v=0$),方向盘打多少度车身朝向都不会变。所有“原地打方向”的操作在运动学模型里都是无效动作,必须先动起来才有转向。

提示:倒车时 $v<0$,$\dot\theta$ 会自动反号,这正是倒车打方向“车尾跟着方向盘走”的数学来源。如果代码里把 $v$ 取了绝对值,倒车段的轨迹会画成车在横着平移。

2.2 把方向盘转角翻译成曲率:4.97 米的完整推导

方向盘最大转角 470°,传动比 16:1,前轮最大摆角就是 $470/16 = 29.375°$。由自行车模型,后轴中心走过的圆弧半径 $R = L/\tan\delta$,把 $\delta$ 取到最大值即可:

$$ R_{\min} = \frac{2.8}{\tan 29.375°} \approx 4.97\ \text{m},\qquad \kappa_{\max} = \frac{1}{R_{\min}} \approx 0.201\ \text{m}^{-1} $$

这段计算本身就是问题 1 的第一问,但真正有用的是它衍生出的两个量:车身外廓扫掠半径和内后轮的转弯半径。前者决定通道要多宽,后者决定贴着障碍物转弯时车尾会不会蹭上去。

import math L = 2.8 # 轴距 m W = 1.8 # 车宽 m LEN = 4.9 # 车长 m TRACK = 1.7 # 轮间距 m SW_MAX = 470.0 # 方向盘最大转角 deg RATIO = 16.0 # 方向盘 : 前轮 = 16 : 1 SW_RATE = 400.0 # 方向盘最大转速 deg/s V_MAX = 20.0 / 3.6 # 前进限速 20 km/h -> m/s delta_max = math.radians(SW_MAX / RATIO) # 前轮最大转角 rad R_min = L / math.tan(delta_max) # 后轴中心最小转弯半径 m kappa_max = math.tan(delta_max) / L # 最大曲率 1/m # 外廓扫掠半径:后轴中心距转弯圆心 R_min,车身最远角点再叠加 # 横向 W/2 与纵向前悬 lf(题目未给前悬,此处按 1.0 m 估计) lf = 1.0 R_out = math.hypot(R_min + W / 2, lf) R_in_rear = R_min - TRACK / 2 # 后内轮的转弯半径,贴边判断用它 print(f"前轮最大转角 {math.degrees(delta_max):.3f} deg") print(f"最小转弯半径 {R_min:.3f} m,最大曲率 {kappa_max:.4f} 1/m") print(f"外廓扫掠半径 {R_out:.3f} m,后内轮半径 {R_in_rear:.3f} m")

跑出来是:前轮最大转角 29.375°,最小转弯半径 4.973 m,最大曲率 0.2011 m⁻¹,外廓扫掠半径约 5.96 m,后内轮半径约 4.12 m。外廓扫掠半径 5.96 米这个数,直接换算成“垂直车位前的通道至少要多宽”——通道比它窄,一把方向打满也会蹭到对面车位里的车,只能靠多把来回。这就是几何给规划器定的先天上限。

2.3 曲率对弧长的变化率:为什么高速转弯不能猛打方向

问题 1 的第三问最容易读漏:车速 20 km/h 时开始转弯,路径曲率相对路径长度的变化率有什么限制。这不是文字游戏,它问的是 $|\mathrm{d}\kappa/\mathrm{d}s|$ 的上界,而这个上界由方向盘转速反向推导出来。

方向盘最大转速 400°/s,除以传动比,前轮转速上限是 25°/s,即 $0.4363\ \text{rad/s}$。曲率 $\kappa = \tan\delta / L$,对 $\delta$ 求导得 $\mathrm{d}\kappa/\mathrm{d}\delta = \sec^2\delta / L$,于是

$$ \left|\frac{\mathrm{d}\kappa}{\mathrm{d}t}\right| = \frac{\sec^2\delta}{L}\cdot\left|\frac{\mathrm{d}\delta}{\mathrm{d}t}\right|, \qquad \left|\frac{\mathrm{d}\kappa}{\mathrm{d}s}\right| = \frac{1}{v}\left|\frac{\mathrm{d}\kappa}{\mathrm{d}t}\right| $$

代入 $\delta = 0$ 得 0.1558 m⁻¹·s⁻¹,代入 $\delta = 29.375°$ 得 0.2053 m⁻¹·s⁻¹。在 $v = 5.556\ \text{m/s}$ 下,$|\mathrm{d}\kappa/\mathrm{d}s|$ 的上界落在 0.028 到 0.037 m⁻² 之间,取最保守的满舵值0.037 m⁻²。

这个式子最有价值的地方在于那个 $1/v$:车速越低,允许的曲率变化率越大。泊车时倒车限速 10 km/h(2.778 m/s),同样的方向盘转速能换来两倍的 $\mathrm{d}\kappa/\mathrm{d}s$,这就是为什么低速时车看起来可以“贴着弯走”,而高速变道必须提前几十米缓慢打方向。很多规划器只看曲率上界不看变化率上界,生成的轨迹在纸面上无碰撞,实际跟踪时方向盘根本转不过来,误差越积越大,最后贴着车位柱子停下。

3. 加加速度:速度规划里最容易被当成“软约束”放过去的那条线

3.1 从 0 加速到 20 km/h 到底要几米

问题 1 的第二问是纯动力学:加速度上限 3.0 m/s²,加加速度上限 20 m/s³ 时,直线加速到 20 km/h 的最短距离。如果完全无视 jerk,直接套 $s = v^2/(2a)$:

$$ s = \frac{5.556^2}{2\times 3.0} \approx 5.14\ \text{m} $$

加上 jerk 约束后要分三段:加速度从 0 以 20 m/s³ 爬升到 3.0(用时 0.15 s,这段只走 0.011 m)、恒加速段、再以 $-20$ m/s³ 把加速度降回 0(又 0.15 s)。三段拼起来总距离约 5.56 m,比无视 jerk 多出约 0.42 m,增幅 8%。

如果只要求起点加速度为 0、终点不管,那就只有前两段,距离约 5.15 m,和无约束几乎没差。**这个对比说明一件事:jerk 约束的代价不在加速段本身,而在于你是否要求加速度连续回到零。**要求了,每个加速段的末尾都要多付一小段“收油”的距离和时间;不要求,规划器给出的速度曲线在段与段衔接处就会出现加速度阶跃,实车上表现为一顿一冲。

3.2 S 型速度廓形:把 jerk 写进约束里再求解

梯形速度廓形(加速-匀速-减速)是绝大部分人第一版会写的东西,它在加速度层面连续、在 jerk 层面是无穷大脉冲。想满足 jerk 约束,就得换成 S 型廓形:加速度本身也是一个梯形。

下面这段代码给定总位移和各项限值,反解出七段式廓形的时间参数,是赛题里生成速度曲线的通用骨架:

def s_curve_times(dist, v_max, a_max, j_max): """七段式 S 型廓形:jerk 上升/恒 a/ jerk 下降 各两次,中间可插匀速段""" t_j = a_max / j_max # jerk 段时间 v_j = 0.5 * a_max * t_j # 单个 jerk 段的速度增量 s_j = a_max * t_j ** 2 / 6 # 单个 jerk 段的位移 # 达到 v_max 所需的最短位移(不含匀速段) v_peak = min(v_max, 2 * v_j + a_max * 0) # 先按纯 S 型估峰值 s_min = 2 * s_j + a_max * t_j ** 2 # 约等于升速到位所需位移 if dist <= s_min: # 距离太短,达不到 v_max,需反解峰值速度 return None t_a = (v_max - 2 * v_j) / a_max # 恒加速段时长 s_a = v_max * t_a - 0.5 * a_max * t_a ** 2 + 2 * v_j * t_a return t_j, t_a def speed_profile(dist, v_max, a_max, j_max, dt=0.01): """按时间步长采样,返回 t / s / v / a / jerk 五条曲线""" t_j = a_max / j_max v_hold = 0.5 * a_max * t_j # jerk 段带来的速度增量 # 三段加速 + 匀速 + 三段减速,总时间由距离反解 t_acc = (v_max - 2 * v_hold) / a_max # 恒加速段 s_acc = 2 * (a_max * t_j ** 2 / 6) + v_hold * t_acc + 0.5 * a_max * t_acc ** 2 t_flat = max(0.0, (dist - 2 * s_acc) / v_max) return t_j, t_acc, t_flat # 后续按这五个参数分段积分即可

两个函数里关键是t_j = a_max / j_max:jerk 越大、加速度越小,这段“爬坡”越短。给定 3.0 m/s² 和 20 m/s³,它就是 0.15 s,正好对应上面那段手算。参数改动时这个比例关系要保持——把 a_max 提到 4.0 而 j_max 不变,t_j 变成 0.2 s,单个 jerk 段的位移按 $jt^3/6$ 涨到 0.027 m,收油距离翻了一倍多。

注意:赛题原文写的是“加加速度不超过 20 m/s³ 为宜”,属于软约束。但速度平滑性本身是评分项,软约束在结果里翻车的概率一点不比硬约束低。

3.3 减速带与限速区间:点约束和区间约束的区别

停车场里最容易被做错的一条是“黄色减速带前后 5 m,车速不超过 10 km/h”。很多人把它当成了一个点:减速带中心处速度降到 2.778 m/s。可题目说的是前后各 5 m 的区间,一共 10 m 的走廊里全程限速。

正确做法是把限速约束写成沿路径弧长的分段函数,速度规划器在每个采样点上检查区间归属:

$$ v_{\max}(s) = \begin{cases} 2.778 & s \in [s_b - 5,\ s_b + 5] \ 5.556 & \text{其它(前进)} \ 2.778 & \text{倒车段} \end{cases} $$

这里有个容易被忽略的耦合:减速需要距离。从 5.556 m/s 以 $-6.0$ m/s² 刹到 2.778 m/s,需要 $(5.556^2-2.778^2)/(2\times6.0) \approx 1.93$ m。这 1.93 m 必须落在减速带前 5 m 的缓冲区里,否则就会出现“刚进限速区还在刹车”的违规。如果车位离减速带很近,缓冲距离不够,就得在更靠前的位置提前降速,甚至牺牲总时间。这类约束冲突在手工拼曲线时几乎必然出现,用分段优化求解反而更快收敛。

4. 车位几何建模:垂直、平行、45° 倾斜三种车位为什么难度完全不同

4.1 车体矩形与位姿变换:控制点不是车

轨迹规划给出的是控制点(后轴中心)的序列,但碰撞检测必须拿整个车身矩形去判断。这两个概念混在一起,是“仿真通过、实车蹭漆”的头号原因。

车身在局部坐标系里是一个以控制点为参考的矩形:横向 $\pm W/2 = \pm 0.9$ m,纵向从前悬到后悬。题目给车长 4.9 m、轴距 2.8 m,前后悬合计 2.1 m,具体怎么分配原文没写,取前悬 1.0 m、后悬 1.1 m 是个合理估计。给定位姿 $(x, y, \theta)$,四个角点就是局部坐标乘旋转矩阵再平移到世界系:

import numpy as np LEN, WID, LF = 4.9, 1.8, 1.0 # 车长、车宽、前悬(估) def body_corners(x, y, theta): """后轴中心位姿 -> 车体四角点,局部系 x 向前、y 向左""" local = np.array([ [ LF, WID / 2], # 右前 [ LF, -WID / 2], # 左前 [-(LEN - LF), -WID / 2], # 左后 [-(LEN - LF), WID / 2], # 右后 ]) c, s = np.cos(theta), np.sin(theta) R = np.array([[c, -s], [s, c]]) return local @ R.T + np.array([x, y])

local @ R.T的写法等价于先把每个点做旋转再平移,注意旋转矩阵用的是转置(右乘)而不是左乘,因为这里是以行向量批量处理四个点。写错成R @ local时,前两个角点还能对上,后面两个会镜像到车身另一侧,碰撞检测看起来“时好时坏”,非常难查。

4.2 分离轴定理:矩形对矩形碰撞检测的最小实现

停车场里的障碍物(被占车位上的车、禁行区、围墙)可以全部近似成矩形,矩形对矩形的相交判定用分离轴定理(SAT)就够了,四个轴:两个矩形的各两条边法向。只要在任意一个轴上两个矩形的投影区间不重叠,就判定不相交。

def sat_overlap(poly_a, poly_b, eps=1e-9): """两个凸多边形是否相交;矩形只需检查自身两条边法向即可""" for poly in (poly_a, poly_b): for i in range(len(poly)): edge = poly[(i + 1) % len(poly)] - poly[i] axis = np.array([-edge[1], edge[0]]) n = np.linalg.norm(axis) if n < eps: continue axis /= n pa, pb = poly_a @ axis, poly_b @ axis if pa.max() < pb.min() - eps or pb.max() < pa.min() - eps: return False # 找到分离轴,立即判不相交 return True

这段代码只做相交判定,实际规划时还要留安全余量。常见的两种做法:一是把障碍矩形按车身外接圆半径向外膨胀,保守但简单,代价是窄车位会被判成不可通行;二是把车体矩形缩小一圈(每边缩 0.1 m)再判定,等价于给轨迹留 10 cm 余量。前者安全但会误杀可行解,后者可行但对控制误差容忍度低,取舍要看定位精度。

还有一件事 SAT 本身不解决:离散采样导致的穿透。轨迹按 0.2 m 步长采样做碰撞检测,两个采样点之间车身可能刚好扫过墙角。圆弧段的扫掠面积最大,步长要压到 0.05 m 以下,或者对相邻位姿之间做连续碰撞检测。

4.3 三种车位的进入结构:单圆弧、两段圆弧、斜向切入

题目指定了 10 号垂直车位、82 号平行车位、31 号 45° 倾斜车位三种场景,它们的难度差异全在几何上。

垂直车位是最简单的:从通道驶过车位,车头对准车位中线后倒车进入,路径基本是一段圆弧接一段直线。约束主要来自通道宽度——前面算出的外廓扫掠半径 5.96 m 意味着通道窄于这个量级时,一把倒不进去,必须前进调整一次再倒。车宽 1.8 m、车位宽按常见的 2.5 m 算,单侧余量 0.35 m,这个余量对控制精度要求很高,轨迹末端最好让车身与车位中线夹角小于 5°。

平行车位是最难的:车位长按 6.0 m 算,车长 4.9 m,前后总共只有 1.1 m 的操作空间。可行路径的典型结构是“前进到前车侧方 → 打满方向倒车走第一段圆弧 → 反向打满走第二段圆弧 → 直线微调”。两段圆弧相切,切点处曲率从 $+0.201$ 直接跳到 $-0.201$,跳变幅度 0.402 m⁻¹——这是全篇所有轨迹里最猛烈的一次曲率突变。实车在这里必须停车换挡、原地打方向,或者插入一段回旋线把曲率过渡开。后者的代价是每段回旋线要多吃掉十厘米量级的纵向空间,车位余量只有 1.1 m 时几乎塞不下。

45° 倾斜车位介于两者之间:车位与通道成 45°,泊入后车头朝向也必须是 45°,路径是单段圆弧接斜向直线。因为有效进入角比垂直车位小了 45°,所需通道宽度明显下降,一次倒入的概率高很多,但它对末端的角度误差更敏感——垂直车位停歪一点还能靠倒车修正,斜车位停歪了车头会顶出车位线。

5. 避坑:泊车建模里最容易出问题的五个地方

5.1 五条典型问题的现场记录

第一条:仿真图看着能过,实车贴边。

现象是把轨迹画在停车场平面图上,视觉上留了不少空隙,但按车宽一算,最窄处只剩几厘米。原因是碰撞检测只拿了后轴中心这一个点去和障碍物比距离。解决办法是把检测对象换成body_corners返回的四个角点构成的矩形,配合 SAT,并且把采样步长压到 0.05 m 以内,圆弧段再加密。

第二条:轨迹曲率突变,方向盘跟不上。

现象是路径由直线段和圆弧段硬拼接而成,曲率在连接点从 0 直接跳到 0.201 m⁻¹。这违反了第 2 章推出的 $|\mathrm{d}\kappa/\mathrm{d}s| \le 0.037\ \text{m}^{-2}$。原因在于规划器只约束了曲率幅值,没约束变化率。解决办法是在曲率不连续处插入回旋线,或者干脆用五次多项式参数化整条路径,让曲率本身连续可导;验证阶段则要沿弧长数值微分 $\kappa(s)$,逐点检查跳变幅度。

第三条:减速带限速区间被当成了点。

现象是速度曲线只在减速带中心那一瞬间降到 10 km/h,两侧立刻弹回 20 km/h。原因在于把“前后 5 m”这一区间约束退化成了单点约束。解决办法是把限速写成沿弧长的分段函数,并预留刹车距离——从 20 km/h 降到 10 km/h 至少需要 1.93 m,这段必须落在缓冲区内部。

第四条:倒车段轨迹画出来车在横着走。

现象是倒车段的朝向曲线和位移曲线对不上,车头朝北但车在往东平移。原因是把速度取了绝对值,或者把 $\dot\theta$ 里的 $v$ 符号丢了。在自行车模型里 $v$ 必须是带符号的,倒车时 $v<0$,$\dot\theta$ 自动反向。绘图和插值时统一用有符号速度,就不会出现这种自相矛盾的图。

第五条:最优车位选出来需要来回调整五把。

现象是按“离我最近”排序,排第一的车位实际要揉五把才能进去。原因是代价函数只有路径长度一项,缺了泊入难度的度量。解决办法是至少在代价里加三项:路径长度、预估车辆换向次数、车位类型偏好(垂直比平行容易)。这三项的权重怎么定,见最后一章。

5.2 提交前的一组自检

上面五条对应的检查可以固化成一个清单,每次生成轨迹后机械地跑一遍,比肉眼盯图靠谱得多:

检查项判据不通过时的处置
曲率变化率$\max|\mathrm{d}\kappa/\mathrm{d}s| \le 0.037$插回旋线或降低车速重规划
速度上限前进 ≤ 5.556,倒车 ≤ 2.778 m/s裁剪速度廓形峰值
减速带区间前后 5 m 内 $v \le 2.778$前移减速起点,重算刹车距离
碰撞沿轨迹每 0.05 m 做一次 SAT收缩搜索步长或换路径族
加速度连续性$\max|a(t_{i+1}) - a(t_i)| / \Delta t \le 20$检查是否漏了 jerk 收油段

这张表的每一项都能直接从第 2、3 章的公式抄下来,不用另设标准。真正容易忽略的是最后一项:加速度连续性检查要用规划器输出的原始时间序列去算,而不是拿规划器的内部参数自证——参数写对了但插值实现错了的情况并不少见。

6. 代价函数与复杂度:为什么最后要主动放弃一部分最优性

问题 3 和问题 4 把静态几何变成了一个搜索问题:N 个候选车位,每个都要算一条可行轨迹,还要在车位被随机占用释放时重新决策。这里的设计取舍比几何计算微妙得多。

按“每个车位完整规划一次再比较”这套做法,N 取 100、单次规划 0.2 s,一轮决策要 20 s,车早开过入口了。而按每小时进出各 30 辆估计,全库的事件到达率约 0.0167 次/秒,10 秒窗口内目标车位状态发生变化的概率约 15%——也就是说重规划窗口本来就只有十来秒,根本容不下全量搜索。

所以合理的结构是分层的:先用欧氏距离加车位类型偏好粗筛出前 5 个候选(这一步是 O(N) 的标量运算,几十微秒),再对候选做完整规划,最后用代价函数定序。

$$ J = w_1 L_{\text{path}} + w_2 T_{\text{est}} + w_3 n_{\text{shift}} + w_4 p_{\text{type}} + w_5 (1 - p_{\text{free}}) $$

策略决策耗时(N=100)最优性适用场景
全量规划比较≈ 20 s全局最优离线验证、评测
距离粗筛 + 前 k 精规划≈ 1 s近似最优在线决策主力
只取最近一个≈ 0.2 s常常局部最优车位宽松、不拥堵

我一般的取舍是:$w_1$ 和 $w_3$ 给大权重,因为多倒一把的时间成本远高于多开十米;$w_5$ 用泊松模型实时更新——单个车位在 $\Delta t$ 内被占用的概率约 $1-e^{-\lambda \Delta t}$,$\lambda$ 取 1/120 每秒,规划窗口 10 s 时这个概率约 8%,一旦目标车位落入这个风险区间就提前触发重规划。多加一次重规划只花 1 s,赌它不被占用却可能让车白跑一趟。

真正需要接受的现实是:在线泊车系统的最优性是买来的,不是算出来的。把搜索半径从 100 个车位砍到 5 个,理论上会丢掉全局最优解,但换回来的是 20 倍的时间余量,这笔交易在任何实时系统里都划算。我头几年做这类题总想把每个车位的完整轨迹都算出来再比,结果仿真跑得漂亮、一加上随时间变化的占用状态就彻底跑不动。从那以后我每次动手前都先把决策时间预算写死在最上面,再倒推能搜几个车位、规划器允许迭代多少次,宁可少搜几个,也不让单帧决策超过预算。希望这套思路能帮到你——这份赛题资源里的原始模型和参数表,值得逐条对着算一遍,尤其是那几个约束边界,自己手推过和只看结论,理解深度完全不是一回事。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进