自动驾驶合集第24期:按数据流重构感知定位规划控制工程笔记
1. 把“合集”当成一个工程项目来做做自动驾驶合集做到第 24 期最大的感受不是内容不够而是内容太多、太散。前二十几期里堆过标定笔记、堆过规划调参记录、堆过仿真环境搭建的踩坑清单也堆过一堆“看起来很高大上但半年没人点开”的论文摘要。到了第 24 期如果还按老办法继续加链接这份合集就会变成一个只增不减的杂物间——新读者进来找不到入口老读者回来找不到上次看的那一条。所以这一期我做的第一件事是重构而不是续写。自动驾驶合集这类长期更新的东西真正的价值不在“收了多少”而在“能不能被复用”。一份能被复用的合集至少得满足三个条件任何一条内容都能被一句话说清楚它解决什么问题任何一条内容都带可验证的输入输出任何一条内容都留了失败记录。缺了第三条合集就只是成功案例展览读者照着做一遍照样翻车。这份内容适合谁刚入行、被感知和规划的名词绕晕的同学可以把它当路线图用按章节顺序走一遍能建立起“数据从传感器进来、到车轮转出去”的完整链路认知已经在一线做模块的同学可以挑第 3 章和第 5 章看那里是参数怎么定、问题怎么查的实操部分。下面所有内容都按“为什么这么选、具体怎么做、哪里容易翻车”三段式展开能抄的直接抄不能抄的至少知道方向。1.1 第 24 期为什么要重构目录而不是继续往下堆信息堆到一定量级后检索成本会指数上升。我做过一个很粗糙的统计前 23 期里同一个话题比如时间同步在不同期里被重复讲了四次但每次的结论都不完全一致早期那版甚至是错的。读者如果按时间顺序读会先学到错的东西再学到对的东西中间没有任何标记告诉他“前面那条作废了”。这是合集类内容最隐蔽的坑——它不告诉你哪条过期了。重构的核心动作是把“按时间组织”改成“按问题组织”。我用的是一套三层索引索引层级回答的问题举例维护频率第一层问题域我遇到的是哪一类问题定位漂移、跟车顿挫、夜间误检半年一次第二层模块归属这个问题落在哪条链路上标定、时间同步、规划、控制季度一次第三层可复现条目具体怎么做、怎么验、怎么失败某条外参复核流程每次更新三层索引立起来之后每条内容都必须挂到第三层挂不上去的就直接删。这条规则很粗暴但极其有效——我删掉了将近三分之一的历史条目理由是“它只能挂在第一层说不清具体怎么做”。能被说清楚的才值得留说不清楚的不是知识是情绪。提示合集做久了一定要有“删除机制”。只增不减的合集三个月后自己都不想打开。1.2 四层主线拆解为什么按数据流分层而不是按算法名分层按算法名分层是新手最容易犯的错。目录写成“卡尔曼滤波”“卡尔曼滤波变体”“粒子滤波”“图优化”看起来很学术但实际工作中没人这么找东西。真实场景是车在路口停下来不动了你得判断是感知没看到红绿灯、定位飘了、规划找不到可行路径还是控制没收到指令。这时候你需要的是按数据流组织的目录。层级输入输出关键验收指标高频翻车点感知图像、点云、毫米波回波目标框、可行驶区域、车道线召回率、误检率、时延时间戳对不齐、标定漂移定位感知结果、惯导、地图位姿位置姿态横向误差、航向误差地图版本错配、退化场景决策规划位姿、目标、地图、交规轨迹点序列速度舒适性、安全性、通行效率边界条件没覆盖、参数过拟合控制执行轨迹、车辆状态油门、刹车、转向跟踪误差、超调量执行器延迟、标定不准按这条主线走好处是任何人拿到一个现象都能顺着箭头往回倒推。我在合集里给每一层都配了一张“入口表”表里只有三列这一层最常见的五个现象、最可能的原因、对应的排查章节号。这套东西听着笨但新人上手速度能快一倍以上。2. 感知与定位合集里最容易注水的两块感知和定位是自动驾驶里最容易被写“水”的部分因为这两块的外行门槛高、验证门槛也高写的人容易只写结论不写过程。一份负责任的合集在这两块必须把“数据怎么对齐”和“误差怎么量化”讲透否则读者拿到的只是一堆名词。2.1 相机与激光雷达的数据对齐本质是三件事很多人以为标定就是求一个外参矩阵实际上完整的数据对齐要拆成三件独立的事空间对齐外参、时间对齐同步、语义对齐谁是谁。三件事任何一件没做好融合结果都会飘而且表现完全不同必须分开排查。空间对齐的核心是外参。外参标定的方法大致分两类依赖标定物的标定板、靶标和不依赖标定物的基于场景特征、基于运动。前者精度高、可复现适合出厂和定期复核后者灵活适合在线监测漂移。我个人的建议是两条腿走路——用标定物方法建立基准用场景方法做日常监控两者差值超过阈值就触发复核。外参误差到底有多致命做一个简单估算一个 1 度的角度误差在 30 米处的横向偏移是 30 × tan(1°) ≈ 0.52 米。听起来不到半米但如果这个误差出现在窄路会车或者贴边停车场景半米就是蹭与不蹭的区别。俯仰角误差更麻烦它会让地面点云整体“翘起来”直接把可行驶区域算错。所以合集里我写了一条硬规则任何外参变更必须在三个距离段10 米、30 米、60 米分别复核投影偏差单段合格不算合格。时间对齐比空间对齐更容易被忽略。相机一帧 30 毫秒到 50 毫秒激光雷达一圈 100 毫秒两个传感器各自打时间戳中间只要差 20 毫秒在 60 公里/小时约 16.7 米/秒下就是 0.33 米的空间错位。解决思路无非三种硬件同步共用触发源、网络时间同步精度到亚毫秒、软件时间戳补偿用运动模型反推。前两种成本高但一劳永逸第三种成本低但只能缓解。下面这段是我常用来做时间戳体检的脚本思路很简单把两路数据的时间戳排序后求相邻差值看分布有没有异常尖峰和空洞。import numpy as np def stamp_health(cam_ts, lidar_ts, nominal_dt0.05, tol0.02): 检查两路传感器时间戳的健康度 cam_ts / lidar_ts: 秒为单位、单调递增的时间戳数组 nominal_dt: 名义上的对齐周期 tol: 允许的抖动上限 cam_ts np.asarray(cam_ts, dtypenp.float64) lidar_ts np.asarray(lidar_ts, dtypenp.float64) # 1) 各自的自洽性相邻帧间隔是否稳定 d_cam np.diff(cam_ts) d_lidar np.diff(lidar_ts) # 2) 交叉对齐误差每帧相机找最近的雷达帧 idx np.searchsorted(lidar_ts, cam_ts) idx np.clip(idx, 0, len(lidar_ts) - 1) left np.abs(cam_ts - lidar_ts[np.maximum(idx - 1, 0)]) right np.abs(lidar_ts[idx] - cam_ts) cross_err np.minimum(left, right) report { cam_dt_mean: float(d_cam.mean()), cam_dt_max_jitter: float(np.abs(d_cam - d_cam.mean()).max()), lidar_dt_mean: float(d_lidar.mean()), cross_err_mean: float(cross_err.mean()), cross_err_p99: float(np.percentile(cross_err, 99)), cross_err_over_tol: int((cross_err tol).sum()), } return report # 用法先跑一遍看 cross_err_p99超过一个相机帧周期就说明同步链路有问题这段脚本不解决同步问题它只负责告诉你“有没有问题、问题多大”。我在实际项目里用它筛过一批数据发现有一路相机的 p99 误差到了 80 毫秒追下去是采集端写了两次时间戳一次是驱动回调时间、一次是落盘时间混用了。这种问题靠肉眼是绝对看不出来的。2.2 三张表要一次立好坐标系、时间基准、标定版本合集里我反复强调一件事上车之前先把三张表立起来比调十个算法管用。三张表分别是坐标系表、时间基准表、标定版本表。坐标系表要写清楚每个坐标系的名称、父节点、轴向定义、单位、原点位置以及谁负责发布。常见坑是“同一辆车上有两套相机坐标系轴向反了 180 度”这种问题在单测里发现不了一上融合就表现为目标左右镜像。轴向定义一定要白纸黑字写下来别指望口头传承。时间基准表要写清楚全系统的时间源是谁、同步方式是什么、各节点的期望偏差是多少。这里最容易出的事是“某个节点用了本地时钟”在长时间跑数据时不明显一跑几百秒就累积出漂移。标定版本表是最容易被省略的一张。我的要求是每次标定都必须生成一个版本号包含标定日期、设备序列号、标定方法、复核偏差、操作人用代号即可。跑任何数据、复现任何问题时先报版本号。这一点看着官僚但它能省下大量“到底是不是标定变了”的扯皮时间。我遇到过最典型的一次同一段数据两次跑出不同结果查了两天最后发现是两次跑之间有人做了在线标定外参动了 0.3 度。现象最可能原因快速验证方法融合目标左右偏移外参平移分量或轴向错误拿静止标定物投影看偏差方向是否一致目标“忽前忽后”时间同步偏差或时间基准不一致跑上面的时间戳体检脚本看交叉误差分布地面点云起伏俯仰角外参错误拟合地面平面看法向量与理论值差多少同一数据两次结果不同标定版本或配置版本变更比对标定版本号与配置文件哈希远处目标检测断断续续外参角度误差放大 点云稀疏在 60 米处复核投影偏差3. 决策规划与控制从“能跑”到“跑得像人”感知定位解决“我在哪、周围有什么”规划控制解决“我该怎么动”。这一块最折磨人的地方在于它没有绝对的正确答案。同一段路十个人能给出十条都说得通的轨迹所以评价体系比算法本身更重要。3.1 规划层的四个可量化指标先把尺子立起来我在合集里把规划评价拆成四把尺子每一把都能落到数字上第一把是舒适性。核心是加速度和加加速度jerk。纵向加速度一般控制在 ±2 m/s² 以内比较舒服急刹可以放宽到 -4 到 -6 但必须短促纵向 jerk 建议不超过 2 到 5 m/s³横向 jerk 更敏感控制在 0.5 到 1 m/s³ 会明显减少“甩”的感觉。举一个实际计算从 0 加速到 36 公里/小时10 米/秒用时 2 秒平均加速度 5 m/s²这个值已经偏猛了乘客会有明显推背感如果同样的目标速度用 5 秒完成平均加速度 2 m/s²体感就柔和很多。手感上的差异本质就是这条加速度曲线的斜率。第二把是安全性。常用的是最小间距和碰撞时间TTC。跟车时距建议在 1.5 到 2.5 秒之间随速度调整低速拥堵可以压缩到 1 秒出头高速必须拉到 2 秒以上。TTC 低于 2 秒就该开始考虑减速低于 1 秒基本属于危险区间。第三把是通行效率。不能只看平均速度那样会鼓励激进策略。我一般看两个量单位距离的平均耗时以及“无谓停车次数”——就是在没有让行义务的情况下停下来的次数。后者特别能反映规划是不是过于保守。第四把是合法性。这一项是硬约束不能拿来做权衡。压线、闯灯、逆行这些在仿真里必须直接判失败不给任何分数补偿。指标推荐阈值测量方式超标后的处理方向纵向加速度±2 m/s²常规轨迹二阶差分平滑速度曲线、拉长规划时域纵向 jerk≤ 2~5 m/s³轨迹三阶差分加速度限幅 低通横向 jerk≤ 0.5~1 m/s³曲率变化率换算限制曲率变化率跟车时距1.5~2.5 s间距 / 自车速度调整期望时距参数无谓停车次数越少越好人工标注统计放宽让行判定条件3.2 控制层纯跟踪、PID、MPC 各自该在什么位置控制层的选型我见过很多争论其实答案取决于三个约束车速范围、执行器精度、算力预算。纯跟踪Pure Pursuit适合低速、路径平滑的场景。它的核心参数是前视距离经验公式是前视距离 k × 车速 最小前视距离k 一般取 0.5 到 1.5最小前视距离取 3 到 5 米。前视距离太小车会“抖”因为它盯着很近的点反复修正前视距离太大车会“抄近道”弯道内侧切得厉害。下面是最小实现import math def pure_pursuit(pose, path, k1.0, ld_min4.0, wheelbase2.7, max_steer0.6): pose: (x, y, yaw)path: [(x, y), ...] 按行驶方向排列 x, y, yaw pose v pose[3] if len(pose) 3 else 0.0 # 速度由外部传入 ld k * abs(v) ld_min # 前视距离随速度线性增长 # 在路径上找最近点再向前搜索 ld 距离处的目标点 dists [math.hypot(px - x, py - y) for px, py in path] i0 int(min(range(len(dists)), keylambda i: dists[i])) target path[-1] for i in range(i0, len(path)): if math.hypot(path[i][0] - x, path[i][1] - y) ld: target path[i] break dx, dy target[0] - x, target[1] - y alpha math.atan2(dy, dx) - yaw alpha math.atan2(math.sin(alpha), math.cos(alpha)) # 归一化到 [-pi, pi] curvature 2 * math.sin(alpha) / max(ld, 1e-6) steer math.atan(wheelbase * curvature) return max(-max_steer, min(max_steer, steer))PID 用在纵向速度控制上依然是性价比最高的选择。整定顺序我推荐“先 P 后 I 再 D最后加抗饱和”先把 P 加到有一点超调但不振荡再加 I 消除稳态误差最后用 D 压超调。积分饱和是低速蠕行场景的头号杀手——车堵在原地误差一直存在积分项越积越大一旦前车走了车会猛地窜出去。解决办法是积分限幅加上“误差小于阈值才积分”的门控。MPC 适合执行器冗余、需要处理约束的场景比如同时约束转向角速率和加速度。它的代价是算力和参数调试成本一个 20 步、状态维数 6 的线性 MPC在嵌入式平台上跑 10 毫秒一帧是可以做到的但前提是把 QP 求解器的规模压下来。不建议新手一上来就用 MPCPID 调不明白的人MPC 只会调得更乱因为 MPC 的权重矩阵更难解释。控制器适用车速关键参数主要问题纯跟踪0~30 km/h前视距离系数 k、最小前视距离高速下跟踪滞后、弯道切内后轮反馈0~20 km/h反馈增益倒车和低速抖PID纵向全速域Kp、Ki、Kd、积分限幅积分饱和、执行器延迟补偿MPC全速域预测步长、Q/R 权重算力、参数难解释4. 仿真、数据闭环与车载部署把“实验室能跑”变成“车上能跑”前两部分解决“算法对不对”这一部分解决“工程能不能落地”。我见过太多在回放数据上表现完美的模块一上闭环仿真就崩一上车就抖。原因几乎都在这三块。4.1 仿真要分三层建别指望一层打天下第一层是回放仿真也叫开环回放。把录制的传感器数据按时间戳重放给算法算法输出的结果只记录不反馈。这层成本最低、复现性最好适合做回归测试。它的致命缺陷是“不会因为你算错而改变世界”——你规划撞墙了回放里墙还在那儿继续走所以它测不出连锁反应。第二层是闭环仿真。算法输出的控制量会驱动虚拟车辆虚拟车辆的位置变化又会影响传感器看到的场景。这层能测出交互问题比如加塞应对、路口博弈。成本在于场景建模和物理引擎的保真度建得太真跑不动建得太假结论没用。第三层是故障注入仿真。人为制造传感器丢帧、时间戳跳变、定位跳变、通信延迟看系统会不会崩。这一层最不受重视但收益最高。我做过一组对比同一套决策规划代码在正常场景下通过率 92%注入 200 毫秒通信延迟后掉到 61%注入定位横向跳变 0.5 米后掉到 43%。这些数字不上车是绝对看不到的。仿真层级能发现的问题发现不了的问题单场景耗时参考开环回放单模块精度、时延、边界条件交互博弈、连锁失效秒级闭环仿真交互、轨迹可行性、控制稳定性真实传感器噪声分布分钟级故障注入鲁棒性、降级策略、超时处理真实硬件老化分钟级场景库的建设我建议用覆盖矩阵来管而不是攒数量。矩阵的行是场景类型跟车、变道、路口、环岛、窄路、施工、夜间、雨天列是难度等级简单、一般、困难、极端。每个格子至少要有 N 个场景缺格子的地方就是测试盲区。我在合集里给的建议是先把行铺满再逐个格子加深因为盲区的风险远大于难度不足的风险。4.2 车载部署的资源账得一笔一笔算车载部署最常见的误判是把“标称算力”当成“可用算力”。一块标称 100 TOPS 的加速卡实际能稳定跑出来的往往是标称值的一半到七成因为内存带宽、功耗墙、散热都会限制它。合集里我建议按“实测吞吐”而不是“标称算力”做预算并且留 30% 余量给峰值场景。时延预算是另一笔必须算清楚的账。以 10 Hz 的感知周期为例100 毫秒的预算要这样分环节典型耗时说明曝光与读出10~20 ms与分辨率、快门类型强相关传输与拷贝3~10 ms零拷贝能省掉大半预处理5~10 ms缩放、去畸变、归一化模型推理20~40 ms与输入分辨率强相关后处理NMS 等5~15 ms目标多时容易失控结果发布与同步2~5 ms跨进程通信开销把这张表填满你会发现余量其实很少。所以我在合集里给了一条很实用的规则任何一个环节的耗时波动超过均值的 30%就必须查。波动往往意味着内存分配、线程调度或者缓存未命中出了问题这类问题在负载低的时候看不出来一上量产车就集中爆发。内存这块也要单独说。嵌入式平台的内存分配最怕两件事运行中动态申请和碎片化。我的做法是在初始化阶段把大块内存一次性分配好运行期只做复用日志和缓存全部走预分配的环形缓冲。功耗和散热更不能忽略——很多车规平台的降频保护触发得比想象中早夏天跑半小时后算力掉两成是常事这直接改变了你的时延预算。注意部署阶段的性能问题八成不是算法太慢而是数据搬运太多。先看拷贝次数再看算子。5. 常见问题与排查技巧实录这一章是合集里我自己回看频率最高的部分因为它是纯经验不含任何理论。以下都是我或者合作的同学真实踩过、并且记录下来反复验证过的坑实体信息全部做了泛化处理。5.1 一份能救急的问题速查表排查的第一原则是缩小范围而不是猜原因。下面这张表是按“现象”组织的不是按“模块”组织的因为现场你只知道现象。现象第一怀疑对象排查动作常见结论车在原地小幅来回摆动控制死区或前视距离过小打印转向指令与反馈前视距离低于 3 米跟车忽快忽慢纵向 PID 增益过高或时距参数抖动记录速度误差曲线Kp 偏大、缺低通定位偶尔跳变十几厘米时间同步或地图匹配退化比对惯导原始输出时间戳补偿没做夜间误检突然增多曝光策略或训练数据分布分时段统计误检率长尾场景欠采样规划轨迹“贴边”障碍物膨胀半径过小检查膨胀参数膨胀值按静态设置未随速调整仿真通过、实车失败传感器噪声模型失真用实车数据回流验证仿真噪声过于理想长时间运行后算力下降散热或内存泄漏监控频率与内存曲线降频保护触发举一个我印象最深的例子。某次测试发现车辆在通过一段缓弯时方向盘会出现大约 2 赫兹的轻微抖动幅度不大但很规律。第一反应是控制器增益问题把 Kp 从 1.2 降到 0.8抖动幅度确实小了但过弯跟踪误差变大弯道内侧会压线。继续查打印了从感知到控制整条链路的时间戳发现规划模块输出的轨迹时间戳和控制模块的消费时间戳之间存在 40 到 90 毫秒的不规则抖动控制模块用了“最近一帧轨迹”但没有做时间对齐导致同一个轨迹点在两次控制周期里被算出不同的期望曲率。最后是在控制入口加了轨迹的时间戳插值抖动直接消失增益也调回了 1.2。这个案例给我的教训是先查数据的时序再查算法的参数。参数是果时序往往是因。5.2 合集维护与实操避坑的几条硬经验第一条任何参数都要带单位和生效范围。合集里我强制要求写成“前视距离系数 k 1.0适用车速 0~30 km/h低速段建议 1.2”。只写一个数字是最坑人的读者抄过去发现不灵还以为自己环境有问题。第二条实验记录必须包含失败的那一半。我现在的记录模板固定五栏假设、改动、结果、反例、结论。反例那一栏最重要——它逼着你去想“这个改动在什么情况下会变差”。大部分参数调整都不是单调变好的只是恰好在你测的场景里变好了。第三条别在同一个变量上同时改两处。这是新手最常犯的错一次改三个参数效果变好了也不知道是哪个起作用效果变差了也不知道是哪个拖后腿。每次只动一个跑够样本量再动下一个。样本量这块我也吃过亏早期拿三五个场景就下结论后来发现同一套参数在不同场景下方差极大至少要覆盖同一场景族的十几个变体才敢说有效。第四条降级策略要比主策略更早写完。传感器丢帧怎么办、定位多久没更新就切安全模式、通信超时阈值是多少这些必须在功能开发的同时就把逻辑写清楚。我见过很多项目主流程做得非常漂亮一到降级就靠“实在不行就急刹”这在低速无所谓在高速上是灾难。合理的做法是分级降级先降精度比如从多传感器融合退到单传感器再降功能比如自动变道关掉最后才是最小风险停车。第五条留一条“能回到昨天”的路。配置、标定、模型、参数全部要能一键回滚并且回滚之后能跑通。这条听着像运维的事但它决定了你敢不敢改东西。我在合集里见过太多“改完之后出问题但是回不去了”的惨案最后只能靠重新烧录一折腾就是半天。最后分享一个我最近才养成的小习惯给每条结论标上“置信度”和“最后验证日期”。置信度分三档——验证过多次、只验证过一次、纯推测。合集做到二十几期内容里必然混着这三类标出来之后读者自己就能判断该不该信。这个习惯看着很朴素但它把一份合集从“资料堆”变成了“有信誉的笔记”我觉得这是第 24 期最值得做的事。