1. 先把“合集”当成一个系统来看:20期内容为什么不能简单堆在一起
做自动驾驶合集做到第20期,最直观的感受是:单篇讲一个模块,读者看得懂;把20篇摞在一起,反而没人看得懂。原因不复杂,前19期大多是“点状突破”——这一期聊激光雷达点云怎么滤波,下一期聊卡尔曼滤波怎么调Q和R,再下一期聊仿真场景怎么造。每一篇单独看都有用,但拼不成一条完整的链路。所以第20期合集真正要解决的不是“再讲一个新知识点”,而是把散落的东西按一条真实的数据流重新串起来。这也是我做这个自动驾驶合集20时最先定下的原则:按数据在车端和云端怎么流动来组织,而不是按教科书里的感知、决策、控制三大块来组织。
这个合集适合谁看?我的判断是三类人。第一类是刚进这个方向、能看懂论文但没跑通过完整链路的同学,合集能帮你把“论文里的模块”和“车上跑的进程”对上号。第二类是做单一模块做久了、想往系统层走的人,比如一直做检测模型训练,想知道自己的模型输出到下游规划手里会发生什么。第三类是带小团队的技术负责人,需要一份可以拿去对齐讨论的清单,看看自己项目里哪些环节其实一直是个黑盒。不适合谁?指望靠一篇文章学会写自动驾驶全栈的人,这里没有那种东西,任何一个环节单独拎出来都够写一本书。
“合集”这个词还有一层隐含意思:它是可以反复翻的。所以我在组织内容时尽量让每一节都能独立成立,同时又在关键处留了交叉引用——讲标定的地方会提示你去看时间同步那一节,讲规划抖动的地方会把你引回控制周期。这种写法看起来啰嗦,但实际用起来省事:出了问题时你不需要从头读,直接跳到对应小节就能对上号。
另外要提前说清楚一点:下面涉及的所有具体数值、参数、代码片段,都是我在模拟项目和几个小规模验证环境里跑过的常见配置,不代表任何特定量产方案。不同传感器、不同算力平台、不同车型,参数一定要重算,直接抄数字是最容易翻车的行为。
2. 整体设计与思路拆解:一条链路、两个闭环、三层工具
2.1 为什么按“数据流”组织,而不是按“模块”组织
按模块组织最省事:感知一章、预测一章、规划一章、控制一章,写完收工。但真到了车上出问题,故障从来不按模块出现。我印象很深的一次排查:车辆在某个路口反复出现轻微画龙,一开始所有人都在看控制参数,查了两天没结果,最后发现是感知输出的目标航向角在低速时有约1.5度的系统性偏差,规划拿这个航向去拟合曲线,控制再跟着追,抖动被放大了一倍。这种问题只有在数据流视角下才能看见——你得知道一个数字从哪个进程产生、经过几次坐标变换、在哪个节点被平滑过。
所以合集的骨架是这条链路:
传感器原始数据 → 时间同步与运动补偿 → 标定与外参校验 → 感知(检测/分割/占用)→ 目标跟踪与状态估计 → 预测(轨迹/意图)→ 规划(路径+速度)→ 控制(转向/驱动/制动)→ 执行器反馈
以及挂在旁边的两条闭环:车端实时闭环和云端数据闭环。车端闭环要求确定性,一切以“这一帧必须在100毫秒内算完”为约束;云端闭环追求覆盖度,关心的是“这一个月采集的数据里,有多少种长尾场景被真正喂进了训练集”。这两条闭环的节奏、工具、评价标准完全不同,混在一起谈必然乱。
2.2 20期内容的分层:哪些是“必须自己做”,哪些可以“买+改”
合集里我反复强调一个分层思路,按“是否构成差异化”来切:
| 层级 | 内容 | 建议策略 | 理由 |
|---|---|---|---|
| 基础层 | 中间件通信、时间同步、日志回放、标定工具 | 优先用成熟开源方案,少造轮子 | 这些做不出差异化,但做不好会拖垮整条链路 |
| 感知层 | 检测、分割、占用栅格、跟踪 | 自研为主,开源模型做基线 | 直接决定安全边界,且与自家传感器布置强绑定 |
| 预测层 | 轨迹预测、交互建模 | 自研+大量数据验证 | 场景强相关,通用模型落地效果普遍打折 |
| 规划控制 | 路径优化、速度规划、跟踪控制 | 自研,规则与优化混合 | 直接关联乘坐体感和安全兜底 |
| 工具链 | 数据标注、仿真、回放、评测 | 能买则买,自研只做胶水层 | 投入大、迭代慢,容易变成无底洞 |
这张表的价值不在于结论多新鲜,而在于它逼着团队回答一个问题:我们的钱和人,到底该压在哪儿。我见过的小团队最常见的失误是工具链自研上瘾——花半年做了一个标注平台,功能还不如现成的,等想追感知进度时人已经散了。反过来也有另一种失误:感知全靠外部方案,等到要针对自己的传感器布置做优化时,发现连中间特征都拿不到。
2.3 方案选型:从“堆传感器”到“重感知、轻地图”的取舍逻辑
这几年最明显的变化是地图依赖在下降。早期方案高度依赖高精地图提供先验车道线,好处是规划省事,坏处是地图更新成本高、覆盖范围受限,一旦进入未采集区域就退化。转向“重感知”之后,车道线、路口拓扑、可行驶区域都从传感器实时推断,地图退化成一层薄薄的先验(大致走向、路口存在性)。
这个转向不是审美选择,是成本账。高精地图的单公里采集与维护成本,在城区规模铺开时是数量级问题;而感知侧靠数据闭环迭代,边际成本是递减的。但它带来的代价也很实在:感知一旦出错,没有任何兜底。以前地图能告诉你“这里应该是三车道”,现在只能靠模型自己判断。所以采用轻地图方案时,我在合集里一直强调两件事必须同步做:一是感知的不确定性输出要显式建模,不要只给一个类别和置信度就完事;二是规划必须能接受“拓扑不确定”这种输入,而不是假设输入永远干净。
3. 核心细节解析:五个最容易被低估的环节
3.1 时间同步:所有诡异问题的第一嫌疑人
如果让我给刚入行的人一句忠告,那就是:车上出现无法解释的行为,先查时间戳,再查别的。多传感器融合的本质是“把同一时刻的不同观测对齐”,一旦时间戳有偏差,融合结果就是错的,而且错得很隐蔽——它不会报错,只会让跟踪目标的位置轻微跳动,让规划以为前车在犹豫。
常见的时间源有三种精度档次:软件打时间戳(毫秒到十几毫秒抖动,够用但别指望精确)、网络时间同步协议(局域网内可以做到亚毫秒到毫秒级)、硬件触发同步(传感器曝光时刻由同一路信号触发,误差在微秒级)。做融合,尤其是相机和激光雷达融合,我建议至少做到硬件触发或严格的事件对齐,否则后面所有运动补偿都在给时间误差打工。
判断同步做得好不好,有个很土但有效的办法:让车以固定速度直线行驶,正前方放一块垂直于地面的标定板,同时录相机和点云。把点云投影到图像上,如果边缘出现与速度成正比的错位,基本就是时间不同步或外参有问题。速度越快错位越明显,这个特征很好辨认。
3.2 标定与外参校验:一次标定管不了一辈子
外参标定不是“出厂做一次”的事。悬挂高度变化、碰撞、温度导致的支架微变形、甚至换胎,都会让外参漂移。漂移量可能只有0.2度,但投射到50米外就是17厘米的横向误差,足以让融合把两个目标判成一个。
我的做法是分三层:
- 静态标定:在受控场地用标定板或标定场做,得到基准外参。
- 在线校验:每一帧用可行驶地面的平面拟合、车道线重合度、静态目标的重投影残差做健康度打分,超过阈值就报警降级。
- 定期复标:按里程或时间周期强制复标,哪怕健康度看起来没问题。
在线校验里最实用的是重投影残差:把点云里稳定存在的静态结构(杆子、墙面、地面边缘)投影到图像上,算边缘距离的统计量。正常状态下残差中位数应该很小且稳定,一旦漂移,中位数会缓慢爬升。这个指标不需要额外硬件,纯软件就能算,性价比极高。
3.3 从2D检测到BEV再到占用栅格:每一层的动机
早期做2D检测,图像上框得很准,但要落到自车坐标系就得靠逆投影,深度信息全靠猜,误差大。BEV(鸟瞰图)的思路是把多视角图像特征统一变换到一个俯视平面,在这个平面上做检测和分割,输出天然就是自车坐标系下的位置。这一步解决了“相机看到的东西怎么变成车能用的坐标”这个根本问题。
BEV的分辨率选择是个实打实的取舍。取0.5米每像素、覆盖前后左右各50米,特征图就是200×200;如果取0.25米每像素,同样范围就是400×400,显存和算力直接翻四倍。城区低速场景0.25米有意义,高速场景0.5米足够。我自己在模拟项目里常用的配置是:城区0.2~0.25米,高速0.4~0.5米,按ODD切换两套模型而不是硬撑一套。
BEV再往上就是占用栅格(Occupancy)。BEV本质还是“在地面上铺一层”,对悬空物体、异形障碍物、施工围挡这类不在地面平面上的东西无能为力。占用把空间切成体素,每个体素判断是否被占据。体素分辨率0.2米、范围前后各40米、左右各40米、高度从-3米到+5米,算下来是400×400×40,六百多万个体素,直接做稠密预测不现实,所以工程上普遍用稀疏表示或者先降高度维度再恢复。
3.4 预测模块:别把它当成一个“轨迹回归”问题
新手最容易把预测理解成“输入历史轨迹,输出未来轨迹”,然后拿一个序列模型去拟合。这么做的结果通常是在简单场景(跟车、直行)看着还行,一进路口就崩。原因是路口的行为主要不是由自身历史决定的,而是由交互决定的:对向车让不让、旁边车加不加塞、行人有没有启动意图。
所以我更推荐把预测拆成三块:意图分类(直行/左转/右转/变道/掉头)、轨迹生成(每个意图下生成若干候选)、概率打分(结合交互和规则约束给每条候选打分)。这样拆的好处是可解释、可调试——出问题时你能明确知道是意图判错了还是轨迹形状不对。端到端直接出轨迹的方案当然也有,但调起来像开盲盒。
3.5 规划与控制:规则兜底与优化求解的分工
规划这一层,我的经验是不要追求纯优化。纯优化在约束完备时很优雅,但真实场景里总有一些没法写成光滑约束的东西,比如“这个路口不能停在斑马线上”。所以实际方案通常是分层:先有一个基于规则的决策层(走/停/让/绕),再由优化层在决策给出的可行域里求解平滑轨迹。规则层负责正确性,优化层负责舒适性。
控制侧最常见的组合是前馈加反馈:转向用纯跟踪或Stanley做前馈,叠加PID做反馈;纵向用MPC做速度跟踪,因为MPC能显式处理加速度约束和舒适性惩罚。这里有个容易忽略的点:控制周期必须和规划周期匹配。如果规划10Hz出轨迹,控制却是50Hz,那控制在这50毫秒里跟踪的是一条“上一帧的、已经稍微过期”的轨迹,高速时会产生滞后。常见做法是规划给出一条带时间参数化的轨迹,控制按当前时刻做插值,而不是死盯轨迹点。
4. 实操过程:从数据采集到闭环跑通的关键步骤
4.1 数据采集与标注闭环:先想清楚“要什么”,再决定“采什么”
数据闭环最容易犯的错是“先采了再说”。车跑了几百公里,盘里堆了几十TB,回头一看全是直道跟车,真正需要的学习场景一个没有。我的建议是先把触发规则写清楚,再出去跑。触发规则可以很朴素:
- 感知置信度低于阈值的帧,前后各截取若干秒
- 规划出现急刹、急打方向、人工接管的片段
- 与历史轨迹偏离超过阈值的场景
- 按场景标签做配额采样,比如路口、施工、夜间、雨天各占一定比例
标注环节要区分“训练标注”和“评测标注”。训练标注追求量大,评测标注追求准,两套人马、两套流程,千万不要混用。我在模拟项目里见过最坑的情况是:评测集里的标注是从训练集复用的,模型在评测集上分数漂亮得不像话,一上实车全线崩盘。
4.2 一个最小可跑通的闭环:先仿真,再实车
在新环境里我一般先搭一个最小闭环,只有三个进程:一个假数据源按固定频率吐目标,一个规划进程吃目标吐轨迹,一个控制进程吃轨迹吐控制量,控制量回灌给一个极简的车辆运动学模型,再生成下一帧状态。链路跑通、频率对得上、延迟可测,再往里换真模块。
下面这段是车辆运动学模型的简化实现,用来在仿真里闭环。参数都是常见量级,直接能跑:
import math class KinematicBicycle: """简化运动学自行车模型,用于闭环验证,不追求精度""" def __init__(self, wheelbase=2.7): self.L = wheelbase # 轴距,单位米 self.x = 0.0 self.y = 0.0 self.yaw = 0.0 self.v = 0.0 def step(self, accel, steer, dt): # 加速度限幅,避免仿真里出现物理上不可能的跳变 accel = max(-4.0, min(2.0, accel)) # 低速时限制转角变化率,模拟转向执行器响应 max_steer = math.radians(35) steer = max(-max_steer, min(max_steer, steer)) self.v = max(0.0, self.v + accel * dt) # 自行车模型:yaw_rate = v * tan(steer) / L yaw_rate = self.v * math.tan(steer) / self.L self.yaw += yaw_rate * dt self.x += self.v * math.cos(self.yaw) * dt self.y += self.v * math.sin(self.yaw) * dt # 更新后立刻夹住角度,防止长时间仿真后数值漂移 self.yaw = math.atan2(math.sin(self.yaw), math.cos(self.yaw)) return self.x, self.y, self.yaw, self.v这段代码看着简单,但它把几个容易忽略的工程点固化了:加速度限幅、转角限幅、角度归一化。少了任何一条,长时间仿真就会出现“车越跑越快”或者“航向角累积到几万度”这种假故障,白白浪费排查时间。
4.3 MPC参数整定:先算清楚“要多远”,再定“看几步”
速度跟踪用MPC,参数不是拍脑袋定的,先从物理量倒推。核心是预测时域:它必须覆盖“从当前速度刹停所需的时间”。制动距离近似为 v²/(2a),取v=15米每秒、a=3米每二次方秒,制动距离约37.5米,对应时间约2.5秒。如果采样周期取0.05秒,那预测步数至少要50步。
但50步的优化问题在车端算起来很吃力,尤其还带约束。所以实际做法是“预测长、控制短”:预测时域覆盖2.5秒(50步),控制时域只优化前10步,后面保持最后一步的控制量不变。这样既能看见足够远的后果,又不会让求解规模失控。
代价函数我常用的形式是:
J = Σ (w_v * (v_ref - v_k)^2 + w_a * a_k^2 + w_jerk * (a_k - a_{k-1})^2 + w_slack * slack_k^2)其中w_v管跟踪精度,w_a管舒适性(加速度惩罚),w_jerk管平顺性(冲击度惩罚),slack是约束松弛量,防止无解。权重比例比绝对值重要,我的经验起点是 w_v : w_a : w_jerk ≈ 10 : 1 : 5,先跑通再微调。w_jerk给得偏大时车会变得“肉”,加速慢腾腾;给得太小车会一顿一顿。这个只能在闭环里试,纸上算不出来。
4.4 端到端模型:训练集与验证集的切分要按“场景”切,不能按“帧”切
现在端到端很热,但落地时最容易糊弄自己的就是数据切分。按帧随机切分,相邻两帧几乎一模一样,一帧进训练一帧进验证,验证集分数虚高。正确做法是按行驶片段切分,甚至按场景簇切分:同一个路口的几百个片段只能全进训练或全进验证,否则模型等于背答案。
我在模拟项目里用的切分比例是 训练:验证:测试 = 7:1.5:1.5,且三个集合在地理区域上不重叠。这样测出来的分数会难看不少,但它更接近实车表现,早难看比晚难看强。
4.5 车端部署的算力预算:先列表,再砍功能
算力不够是常态,功能优先级得靠数字说话。下面这张表是我常用的预算模板,单位是按某主流车端推理芯片的等效算力折算的相对占比,具体数值要按自己平台重算:
| 模块 | 单帧计算占比 | 延迟预算 | 降级策略 |
|---|---|---|---|
| 图像预处理与BEV变换 | 15% | 10ms | 降分辨率、减视角 |
| 检测与分割主干 | 35% | 30ms | 换轻量主干、降通道 |
| 占用栅格 | 20% | 25ms | 降高度维度、稀疏推理 |
| 跟踪与预测 | 10% | 15ms | 减候选轨迹数 |
| 规划与控制 | 10% | 10ms | 缩短预测时域 |
| 兜底与监控 | 10% | 常驻 | 不降级 |
这张表最有用的不是数字,而是最后一列——它逼着每个模块负责人回答“如果算力砍一半,你砍哪里”。答不上来的模块,通常是没做过分级设计的模块,真到资源紧张时就是整车的瓶颈。
5. 常见问题与排查技巧实录
5.1 一份可以对着查的故障速查表
| 现象 | 高概率原因 | 快速验证方法 | 处理方向 |
|---|---|---|---|
| 融合目标位置轻微跳动 | 时间戳不同步 | 匀速直线+标定板,看点云投影错位是否随速度增大 | 改用硬件触发或严格事件对齐 |
| 远处目标横向偏移 | 外参漂移 | 看静态结构重投影残差中位数是否缓慢爬升 | 在线健康度报警+周期复标 |
| 低速画龙 | 感知航向角系统性偏差 | 对比感知航向与真值航向的均值差 | 修正感知输出或规划侧加死区 |
| 路口频繁急刹 | 预测把对向车当成本车道目标 | 回放看预测输入的目标关联是否正确 | 修目标关联逻辑,别急着调规划权重 |
| 规划轨迹忽快忽慢 | 规划周期与控制周期不匹配 | 打点看控制拿到的轨迹时间戳延迟 | 轨迹按时间参数化,控制做插值 |
| 模型离线指标好、实车差 | 数据切分按帧泄漏 | 改为按片段/场景切分重训 | 重建验证集,接受分数下降 |
| 夜间/雨天性能断崖 | 训练数据分布缺失 | 统计各场景样本占比 | 按场景配额补采,别只加数据量 |
5.2 三个我踩过的坑
第一个坑是过度相信离线指标。有段时间检测模型的mAP一路涨,实车却越来越差。后来发现问题出在标注规范:新加入的标注员把“被遮挡一半以上的车”也标成了正样本,模型学会了预测一堆看不见的车。这类问题离线指标完全看不出来,只有回放实车数据才能发现。解决办法是把标注规范变成可执行的检查脚本,比如遮挡比例超过阈值的目标自动打标记,而不是靠人盯。
第二个坑是日志打得太少。早期为了省存储,只记录关键结果不记录中间量,出问题时只能猜。后来改成关键节点全量打点,包括每帧的时间戳、坐标变换矩阵、各模块耗时、输入输出的关键字段,存储成本涨了三倍,但排查效率涨了不止三倍。我的经验值是:中间结果的价值在你不需要它的时候最低,在你需要它的时候最高,所以宁可多打。
第三个坑是仿真场景造得太理想。仿真里传感器没有噪声、标定永远准确、目标永远不遮挡,模型在仿真里表现完美,实车一塌糊涂。后来我在仿真链路里强制注入三类扰动:传感器噪声、外参小偏差、随机丢帧。这三样加上之后,仿真分数掉了不少,但仿真和实车的差距明显缩小了。
5.3 几个提高排查效率的习惯
第一个习惯是给每个模块加耗时打点并画成时序图。不是为了优化性能,而是为了定位“偶发延迟”。很多诡异问题都是某一帧某个模块突然耗时涨了十倍导致的,没有打点你根本想不到。
第二个习惯是保留原始数据。处理后的数据体积小,但一旦处理逻辑有bug,你连回退重算的机会都没有。原始数据存一段时间,处理链路的版本号一起记,出问题能重放。
第三个习惯是建立“已知问题清单”并定期复盘。同一个问题在不同项目里反复出现,说明它不是偶发,而是流程缺陷。清单不用复杂,一列现象、一列根因、一列是否已修,坚持记一年,价值远超任何文档。
6. 工程协作与验收:合集里最不性感但最要命的部分
6.1 版本与数据管理:模型、代码、数据三者必须能对上号
自动驾驶项目里最让人崩溃的场景是:三个月前跑出一个很好的结果,现在想复现,复现不出来。原因通常是三者没有绑定——代码有版本管理,模型有权重文件,但训练用的数据集是“当时那一版”,没人记得是哪一版。
我的做法是给每次训练生成一个唯一的实验标识,里面记录:代码提交号、数据集版本号、标注规范版本号、训练配置、随机种子、评估脚本版本。这六样缺一不可。听起来麻烦,但用脚本自动生成就是几行代码的事,手工记则一定会漏。
数据版本管理还有个细节:删除数据要留痕。有时候发现一批标注有问题就把它删了,过几个月发现另一批也有同样问题,想回溯影响范围却查不到。所以数据只做“标记失效”,不做物理删除,失效原因写清楚。
6.2 验收标准:不要用“感觉还行”作为交付条件
模块交付前必须有可量化的验收标准,而且要区分“功能验收”和“性能验收”。功能验收看行为对不对,性能验收看指标达没达标。我在模拟项目里用的验收清单大致是这样:
- 功能:规定场景集里通过率、人工接管次数、急刹次数
- 性能:单帧耗时P99、内存峰值、启动时间、异常恢复时间
- 鲁棒性:注入噪声/丢帧/外参偏差后的指标衰减幅度
- 可观测性:关键中间量是否可打点、日志是否可回放
最后一栏经常被忽略,但它决定了出问题时你能不能查。一个不可观测的模块,本质上是不可维护的模块,哪怕它现在跑得很好。
6.3 团队分工上的一个反直觉经验
早期我倾向于按模块切分:感知组、规划组、控制组,各管一摊。跑了一段时间发现,模块边界上的问题没人认领——融合输出的目标质量差,感知说是跟踪的问题,跟踪说是关联的问题,关联说是预测的问题,最后谁都不改。
后来改成“按链路切分+按问题认领”:日常开发还是按模块,但每个跨模块问题必须有一个明确的负责人,负责到底,哪怕最后发现根因在别人模块里,也由他推动。这个改动看着只是流程调整,实际效果比想象中大得多,因为跨模块问题的本质是“没人对端到端结果负责”。
7. 这套合集后续还能怎么用
如果你手上正好在搭一条类似的链路,我的建议是别从第1节顺序读,而是先跳到第3节看那五个容易被低估的环节,对照自己项目里哪些是黑盒,把黑盒列出来,再回头按第2节的分层表决定哪些自研哪些用现成的。这个顺序能省不少时间,因为大部分项目的瓶颈不在“缺一个新算法”,而在“某个基础环节一直没做扎实”。
参数部分,尤其是MPC权重、BEV分辨率、体素尺寸这些,千万别照抄。我在模拟项目里的配置换个传感器布置、换个算力平台,效果就会明显不同。正确的用法是把我给的推导过程拿去套自己的物理约束:先算清楚“必须看多远”,再倒推步数,再算算力够不够,不够就砍控制时域,一层层往下推。这个过程本身比最终那几个数字值钱得多。
最后说一句实操里的体会:自动驾驶这条链路,单点做深不难,难的是让二十个环节在同一帧里同时不出错。所以真正拉开差距的往往不是某个模块多先进,而是标定准不准、时间戳齐不齐、日志全不全、验收严不严这些“不性感”的事。我见过太多团队在算法上死磕半年,最后被一个外参漂移拖住整个进度。把基础环节做成可检查、可报警、可回滚的,比追新模型划算得多。