ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Apollo规划模块场景驱动决策引擎深度解析:Scenario机制与实战

Apollo规划模块场景驱动决策引擎深度解析:Scenario机制与实战 做自动驾驶规划的同学早晚都会碰上Apollo。不管你是做量产、做预研还是写论文Apollo规划模块里那套场景Scenario驱动的决策框架都是绕不开的设计。我第一次打开modules/planning源码的时候第一反应是头大。目录多、类多、一层套一层光是看着scenarios底下那一堆文件夹就有点劝退。但真正跑起来、翻过几次问题之后你会发现这套东西的思想其实特别直白——把无限复杂的道路环境拆成有限个能打的场景每个场景再配一套固定的打法。这就是场景驱动决策引擎的核心。这篇文章我想从决策引擎的角度把Apollo规划模块里的Scenario机制整个拆开聊一遍。包括它为什么存在、三层结构怎么工作、场景切换实际怎么判定以及我调试过程中踩过的坑。适合两类人看一类是刚上手Apollo源码、想搞懂规划模块主线的同学另一类是已经在用Apollo做二次开发但被场景不切换、阶段卡死这类问题折磨过的朋友。相信我理清Scenario这一条线后面读任何Task的实现都会轻松很多。1. 为什么Apollo规划要引入Scenario机制1.1 规划问题的本质一个不可能写完的分支树先说一个很多人刚接触自动驾驶规划时的直觉想法规划不就是根据当前环境算出一条安全轨迹吗为什么要搞这么复杂问题在于“当前环境”这四个字。真实道路上的情况是无限组合的——你可能在直行道上跟前车、可能在路口等红灯、可能在Stop Sign前停车、可能被旁边车道的车挡住、可能前面有个事故车需要绕行这些情况还会互相叠加。如果不用场景管理最直接的做法就是写一个巨大的if-else分支树if前方有障碍物走这个逻辑if路口有红绿灯走那个逻辑if同时有障碍物和红绿灯再写一套组合逻辑。我见过一些初创团队早期就是这么干的前三个月挺爽规则越加越多后面就崩了。因为每条规则之间会互相干扰改一个判断条件可能让另一个完全不相关的路况表现异常。这就像你让一个新手司机死记硬背一百条“遇到情况怎么办”的规则一旦遇到两条规则同时触发的情况他就懵了。Apollo的解法是把道路环境先做一次“分治”。在任何一个瞬间车辆面对的核心问题其实是有限的。你不可能一边等红绿灯一边做高速公路变道超车当然极端场景不排除但那是后续要处理的问题。所以完全可以先把“现在是什么场景”判断出来然后整个规划模块只围绕这个场景调用对应的策略。这就是Scenario机制存在的根本原因把无限映射到有限。1.2 没有场景管理会怎样一段混沌规划的脑补为了把这个事情说得更具体咱们脑补一下没有Scenario机制的规划模块会是什么样。假设自动驾驶车正在最右侧车道直行前方50米有一个路口红灯刚亮起同时右侧有辆车正在并入本车道而后方有一辆车快速接近。这四件事在真实道路上完全可能同时发生。如果没有场景管理规划模块要同时处理停车线约束、信号灯约束、侧向碰撞风险、纵向加速度限制所有逻辑全部平铺在一个大池子里互相竞争输出。最终结果往往是轨迹算出来了但是既没有在停止线前稳稳停住也没有给右侧并线车辆让出足够空间甚至可能因为过度保守停在路中间。有了场景管理之后处理方式完全不同。规划模块先做一个顶层判断当前和下一个Routing路径段上有没有路口有的话信号灯状态是什么如果距离停止线还有一段距离就直接进入“路口场景-接近阶段”在这个阶段里右侧汇入车辆和后方来车都只是这个场景下的障碍物约束规划器只需要把车平稳地开到停止线前停下来。等信号灯变绿再切到“通行阶段”把注意力放在怎么安全通过路口上。你看同一个时间段处理的复杂度一下子降下来了。1.3 Scenario在Apollo整体链路里的位置说回Apollo代码本身。Apollo整体的链路是感知Perception识别障碍物、交通灯等预测Prediction给出障碍物未来的运动轨迹路由Routing规划出一条从起点到终点的宏观行车路径最后规划Planning在这条宏观路径上以10Hz甚至更高的频率实时计算车辆的局部行驶轨迹。Scenario机制就活在Planning模块内部。当Routing给出了一条长路径之后这条路径会经过许多“子任务”直行、转弯、等灯、停车、靠边……每一个子任务就是决策引擎眼里的一个Scenario。Planning模块在每个周期要做的第一件大事不是马上开始算轨迹而是先搞清楚“我现在到底在哪个子任务里”这个判断做对了后面的轨迹规划才有意义。所以如果你把Apollo Planning整体看成一个决策引擎那么Scenario就是这个引擎的“挡位”。挂错挡油门踩得再准也没用。2. Scenario、Stage、Task三层结构深度拆解2.1 三层结构各自负责什么打开Apollo的scenarios目录你会看到一堆场景文件夹比如lane_follow、traffic_light、stop_sign、change_lane、pull_over、parking等。每个场景文件夹内部又有几个场景类文件、stage文件、config目录。但很多人不知道的是Scenario并不是最细的粒度。它下面还分了Stage阶段Stage下面才是具体干活的Task任务。我画个表给大家看这个分层关系一定要牢牢记住层级粒度生命周期典型例子Scenario场景粗持续数秒到数分钟LaneFollow、ChangeLane、TrafficLight、PullOverStage阶段中持续数百毫秒到数秒ApproachStopSign接近停止牌、IntersectionPass通过路口Task任务细一个规划周期内执行PathDecider、OptimizePath、SpeedDecider、TrajectoryFallback既然拆了三层每层自然有它的意义。Scenario是最上层的策略选择它决定了“我当前用什么策略模板”。Stage解决的是场景内部的状态推进问题同一个场景必须按顺序走完几个阶段才能完成。Task则是最底层的一个个具体算法负责真正做路径规划、速度规划、避障决策。用老司机开车的经验来类比就很好懂。Scenario是“我现在要跑一段山路”整体策略选型Stage是“先入弯、再弯中、再出弯”阶段推进Task是“这时候我要减速、打方向盘多少度”具体动作。三层各管各的互不越权这让代码的复用性特别高。2.2 ScenarioManager决策引擎的中枢神经场景的管理逻辑集中在scenario_manager.cc里。这个类在OnLanePlanning中扮演决策引擎角色每个规划周期都会调用它的Update方法再通过Dispatch方法决定上一帧和这一帧之间场景是否需要切换。核心流程大致是这样bool ScenarioManager::Update(const common::TrajectoryPoint ego_state, const ReferenceLineInfo reference_line_info) { // 每帧执行 // 1. 判断当前场景是否还能继续执行 // 2. 如果当前场景需要退出或者更优的场景满足进入条件 // 则把current_scenario_切到新场景 // 3. 调用当前场景的Process推进内部Stage }关键点在于Apollo不是“每帧从所有场景里重新挑一个”而是有一定惯性。它会先看当前场景能不能继续用只有当前场景无法满足条件时才会触发场景切换。这个设计说白了就是滞回控制防止场景在边界处来回震荡。ResourceManager在部分版本里叫ScenarioManager的辅助类还会统一管理所有场景的注册。你在代码里看到SCENARIO_CREATE、SCENARIO_REGISTER这类宏就是在注册场景。新加一个场景就是在这个清单里注册一下再实现对应的Enter条件和Stage逻辑。2.3 Stage与Task场景内部的作战单元场景类本身不直接做规划它只是“编导演”。真正干活的是Stage而Stage里面挂着一串Task。我拿traffic_light场景举个例子。一个简单的红绿灯路口车从接近到通过至少会经历“减速接近停止线”和“安全通过路口”两个阶段。在“减速接近”这个阶段里Stage里挂的Task通常是PathBoundedDecider先把路径范围框出来PathDecider基于障碍物决策出一个初步路径SpeedDecider根据信号灯状态和前方车辆算出一个速度曲线最后OptimizeTrajectory做轨迹平滑。这一串Task按顺序执行前一个的输出是后一个的输入。这就是Apollo规划里面Task链的典型工作方式。所以如果你看一个Stage的Process方法你会发现它做的事情非常聚焦遍历自己的Task列表逐个调用Execute每个Task处理完就把结果写回Frame。Stage自己只管一件事检查所有Task跑完之后要不要切到下一个Stage。这种设计的优雅之处在于不同场景完全可以共享同一个Task。比如ChangeLane场景里有PathDecision相关的TaskLaneFollow里也有不需要为每个场景单独写一套。Stage负责按需组装Task列表这就是“策略模板”的体现。2.4 场景注册与生命周期管理场景不是凭空出现的它的生命周期分三步注册、激活、退出。注册发生在程序初始化阶段所有场景通过静态注册的方式进入场景管理器的候选列表。这里要注意候选列表不是字典结构而是一个有序的、带优先级的数组。不同版本的Apollo可能用map或vector实现但核心思想一致存在一个能让每个场景自己判断“我能不能被激活”的接口。进入条件EnterCondition通常看这几样东西当前参考线在Routing里的角色比如是不是变道参考线前方有没有交通信号灯、停止标志障碍物的相对位置和类型车辆自身的状态速度、挡位、是否停车路径终点是否靠近场景所关注的位置点。退出条件ExitCondition相对简单通常是场景内部的Stage走到了最后一个阶段且成功完成或者外部条件已经不满足。一旦退出场景管理器会回到默认场景LaneFollow车道保持然后下一帧再根据条件决定是否进入新场景。这里有个非常实用的小经验排查问题时第一步永远是确认当前活跃场景是什么。很多人花半天查轨迹问题结果发现是场景切错了方向完全跑偏。从Log里找到当前scenario名称十秒钟定位问题源头。3. 从代码层面看场景的识别与切换3.1 常见场景与触发条件Apollo里的场景很多但核心的就这么几类我把它们的触发条件整理成一张表场景触发条件典型阶段LaneFollow默认兜底无特殊条件正常跟车巡航ChangeLane参考线切换需结合变道路径线判断目标车道有障碍或过慢前车时触发变道准备、执行变道TrafficLight前方存在激活的信号灯且车辆在停止线影响范围内接近停止线、通过路口、清理路口StopSign前方存在停止标志预测路径与停止点冲突接近停止牌、确认安全、启动PullOver用户指令或安全策略要求靠边停车寻找位置、靠边、停车EmergencyStop紧急障碍物或上游安全模块干预立即停车、请求外部援助注意上表只是大白话版。真实代码里每个场景的Enter条件都封装在对应的scenario类里通常要读reference_line_info、frame和perception信息做综合判断。比如TrafficLight场景的激活要满足reference line上的停止线在车前方一定距离内而且信号灯是当前车道有关的灯不是路口对向的车。实践经验来看最容易被忽视的是ChangeLane场景。它依赖Routing模块给出的变道路径。如果Routing没下发变道参考线哪怕相邻车道空得能跑飞机规划模块也不会进入ChangeLane场景。所以排查变道问题先确认Routing的输出。3.2 切换的优先级与仲裁同一帧可能存在多个场景同时满足进入条件。比如你在接近一个路口前方有红绿灯同时导航提示你要左转而左转车道上有一辆慢车挡着。这时候TrafficLight和ChangeLane条件可能同时满足切哪个Apollo的做法是给每个场景分优先级。整体原则可以概括成紧急程度越高的场景优先级越高兜底场景优先级最低。EmergencyStop这类安全相关场景优先级最高再往下是StopSign、TrafficLight这类法规强制场景然后才是ChangeLane、PullOver这类策略型场景。如果连条件都不满足就停在LaneFollow。这个仲裁逻辑藏在场景注册表里具体在线程调度时按顺序遍历。了解优先级之后再回头看你遇到的问题就豁然开朗了。比如“明明旁边车道没车为什么不变道”很可能是因为前方红绿灯场景激活优先级更高决策引擎认为当前执行的是过路口任务不允许同时变道。还有一个细节有些版本里场景切换并不是直接切换而是先让当前场景的Stage完成当前阶段的“收尾”再启动新场景。比如你在StopSign场景已经停车了突然感知到后方有紧急车辆这时候一般会先让当前场景的当前阶段进入Fallback状态再切入EmergencyStop。这个过程如果处理不好车会出现“顿一下再走”的体感。3.3 实操修改一个阈值观察场景切换的实际行为理论知识聊得差不多了来点实操。假设你发现车辆在高速上跟车时变道触发得太晚跟车跟到很近才愿意变道体验很差。这个行为大概率受ChangeLane场景内决策阈值影响。我以Apollo 5.0/6.0版本为例说说大体思路。先找到本车所在参考线是否有换道路径再找ChangeLane场景内部决策的地方。关键参数有下面几个// 常见参数不同版本位置不同 change_lane_min_length // 变道最小路径长度 change_lane_success_frames // 判定变道成功的帧数 min_overtake_distance // 超车/变道的最小安全距离要定位这些参数最直接的办法是在代码目录里搜关键词grep -rn change_lane modules/planning/conf/ modules/planning/scenarios/ | grep -i min\|distance\|length找到对应配置文件后改小或调大阈值重新编译planning模块然后用录好的数据包cyber_recorder录的record文件重放观察变道触发时机变化。具体命令大致长这样# 重新编译plannning模块在Apollo Docker环境内 build.sh planning # 重放数据 cyber_recorder play -f your_record_file.record然后打开Dreamview在规划模块可视化面板里观察当前scenario状态。如果参数改完毫无变化多半是你改的参数根本没被读到这就是我要说的第四部分。3.4 配置结构这几个文件就够了很多人想调参但被Apollo庞杂的配置目录吓到了。我帮大家画个最小地图。规划模块的核心配置入口是modules/planning/conf/planning_config.pb.txt这是planning模块的主配置里面定义了默认规划器、默认场景配置、以及各场景参数文件的引用。然后每个场景目录下又会有一个或者多个.pb.txt比如traffic_light目录下的traffic_light_scenario.pb.txt里面配置了这个场景的各个Stage参数。再往下Task级别的配置通常在modules/planning/conf/task_config.pb.txt里或者某些场景内部有独立的task配置引用。规则是越具体的配置越靠后覆盖场景级配置会覆盖全局同名字段。一个小技巧不确定哪个配置生效时直接看二进制文件包里有没有同名的.pb.txt。Apollo编译后的配置路径和源码路径不一定完全一致很多时候你改了源码里的配置编译后用的是另一份。先确认文件位置再改不然白费劲。4. 实战避坑场景驱动决策引擎的典型问题4.1 场景切不过去或切错了的排查套路先讲一个最常见的坑该变道不变道。我见过一个case车在高架上跟在一辆慢车后面旁边车道一辆车都没有但规划就是不出变道轨迹。日志一看当前场景一直是LaneFollowChangeLane场景压根没激活。排查步骤是这样走的看Routing决策确认参考线里是否有换道参考线。没有换道路径线的话ChangeLane场景永远不可能进。结果一查Routing下发的是主路路径没有变道请求。看ChangeLane场景的Enter条件确认目标车道障碍物判断逻辑。最后发现问题出在Routing层面而不是Planning的Scenario决策。这个case给我的启示是场景不切换先查更上游的条件不要一上来就改规划参数。另一个case是红绿灯路口完全不减速。日志显示当前场景跑到了TrafficLight但Stage一直停在ApproachStopLine阶段。后来定位到是信号灯信息没有正确写入Frame导致场景内部一直等信号灯状态。排查这类问题重点是看planning的输入里signal信息是否完整以及reference_line上停止线是否被正确投影。4.2 Stage卡死与Task执行失败的经典表现场景激活了但车停在路口不动或者一直在重复某个动作这种时候大概率是Stage推进逻辑出了问题。Stage推进依赖两个条件当前Stage所有Task执行成功且达到了Stage自身的完成条件。如果一个Task抛了异常或者输出不满足下一个Task的输入要求Stage就会卡在当前阶段不动。我举一个实际例子。处理靠边停车PullOver时Stage在寻找可停车位置阶段卡住车不断往前试探又拉回来。后来发现是PathBoundsDecider输出的path bounds在停车区域附近收得太窄导致优化器算不出可行路径于是Stage以为任务没完成反复重试。这种问题光看场景状态看不出来得往Task层深挖。我的建议是开启Task的详细日志# 在launch或mainboard启动时增加vlog级别 --v2VLOG输出里能看到每个Task的执行状态、失败原因和耗时能省下大量猜的时间。4.3 配置不生效与老版本硬编码问题这个我不想再踩第二次了。Apollo早期版本里大量阈值不是读配置而是直接写死在代码里。哪怕你在.pb.txt里改得再漂亮代码根本不吃这一套结果就是调参半天一点反应都没有。遇到参数没生效第一时间确认两点。第一代码里有没有FLAGS_开头的gflags定义如果有优先通过启动参数或配置文件传入。第二搜索这个参数名下有没有Init函数被调用。很多场景和Stage在初始化时会读一次配置后续不再刷新所以动态改配置不一定能在运行中生效。经验改完参数必须重启planning模块不要指望热加载。Apollo某些版本支持RuntimeConfig但绝大多数情况下老老老实实重启一次最稳妥。4.4 调试工具链Dreamview、cyber_recorder和日志三板斧最后分享一套我个人觉得最顺手的调试组合姑且叫“三板斧”。第一板斧是Dreamview可视化。跑起来之后在Dreamview的Planning面板里能看到当前active scenario、当前stage、参考线、路径和速度曲线。最实用的一个操作是打开Planning的决策模块可视化直接把场景切换的瞬间可视化出来你会看到车辆行为在切换前的纠结。第二板斧是cyber_recorder。真实车辆或仿真环境里的问题录一段record下来在本地反复重放。重放时配合gflags调整参数比每次上车测试效率高十倍。重放时注意时间同步把planning、perception、prediction三个模块都正常启动否则输入不完整很容易复现不出来。第三板斧是日志。Apollo的日志体系很完善AINFO、ADEBUG、AVLOG层层分级。排查问题先打开DEBUG级别的planning日志找到场景切换的关键行。日志里会明确打印出新场景名称和切换原因比看代码猜快多了。我一直觉得读Apollo规划代码最忌讳的就是一头扎进某个Task的实现细节里出不来。真正的主线逻辑就是Scenario这条线每一帧先判断“现在是什么场景”再根据场景调用对应的Stage和Task。搞清楚这条线规划模块的大框架就立住了后面再去看任何一个细节你都能知道它属于哪个环节、服务于什么目标。以我个人的体会场景驱动这套设计的厉害之处不在于某个算法有多精妙而在于工程层面的优雅给系统划清了边界新增场景不用动老场景的代码排查问题也始终有一条清晰的路径可循。如果你后面要自己扩展一个场景比如自定义的园区接驳场景顺着Scenario注册、Enter条件、Stage编排这三步走基本不会走偏。希望这篇能把你在Apollo规划模块的探索路上往前推一步。
返回列表