本篇文章想和大家分享一下Apollo7.0最新的规划算法。由于Apollo的planning整体代码都相当庞大,一开始还是理清其框架,再分算法块逐个击破这样效果更好。
这篇文章希望能带领读者理清planning的整体框架,梳理数据流,以主要场景为例,一直分析到task (apollo planning 的主要算法所在处)逻辑前的准备工作、输入如何构造的,之后再深入看task内部的细节也更容易理解。
特别注意,本文中的流程图均为作者花费了大量时间梳理而来,以便读者能够更易理解planning的整体框架,抓住重点不被细节带跑偏。
读懂一个模块,首先必然是了解其的上下游,即输入输出是什么。熟悉Apollo CyberRT框架的小伙伴都知道在该框架下,输入输出由Reader和Writer构成,并定义在每个模块的component文件中。除此之外,CyberRT框架定义了两种模式,分别为消息触发和时间触发,而planning中采用的为消息触发,因此必须接到特定的上游消息后,才会进入内部主逻辑,而消息触发的上游消息,定义为component中Process()函数的入参。
因此总结来看,planning的上下游关系总结为下图:
这里再重复一下,planning的输入分为Reader和Process()入参的原因在于,planning依赖于Process()的三个上游输入,只有同时接到这三个输入,才会触发planning的主逻辑,即是planning正常启动的必要条件。而Reader则不是,其中部分上游还依赖于配置参数是否打开,具体可以查看Apollo的源码。
planning的输出就比较简单了,主要是给控制的ADCTrajectory数据,包含了一条带时间、速度的轨迹点集,具体的格式定义可以查看对应的proto文件。
上面两张流程图是我整理的Apollo规划算法的框架,整体框架和之前并无太大变化。主框架分为两个线程,子线程ReferenceLineProvider以20HZ的频率运行,用于计算planning中最重要的数据结构reference_line;主线程上还是基于场景划分的思路,多数场景下还是采用基于ReferenceLine的规划算法,对于泊车相关场景,则利用open space算法。目前Apollo的场景划分为了16种,在proto文件中可以查看到。在Apollo 7.0中,新增了deadend_turnaround场景,用于无人车遇到断头路时,采用openspace的方法进行调头的轨迹规划,后续我会详细看一下里面的算法实现细节。
Apollo中的规划里的一个重要思想就是基于场景划分来调用不同的task处理,而其中如何进行场景分配便是实现该思想的核心。从上面的配置参数可以看到目前Apollo设定了16种场景,而场景下的具体切换逻辑也比较复杂,scenario_manager的具体流程逻辑如下图所示:
在上图红框中的场景判断中,我只画了第一步的场景判断,红框内的5种场景为大类,剩余的场景在这5大类中再细分做出判断。另外需要注意的是,红框内的5种场景是有优先级顺序的,即如果判断为某种场景后,后续的场景也就不再判断。下面从这5种场景出发,介绍一下Apollo中的场景判断条件,以及每个大类场景下包含哪些细分的场景小类。
该场景的判断条件为车辆是否静止,并且距离终点10m以上,并且当前车辆已经off_lane或者不在城市道路上,在该场景下采用的是open_space相关的算法。个人感觉该场景在驶离目标车道并正常规划失败导致的停车时会触发,利用open_space方法使其重新回到正常道路上,因此也是场景判断中首先需要check的。
在该场景下,又可细分四个场景。首先,根据先前计算的地图中第一个遇到的overlap来确定大类型,是包含交通标识的交叉口,还是其他交叉口。其次,若是包含交通标识的交叉口,还细分为stop_sign、traffic_light以及yield_sign,具体结构图如下所示。
PullOver场景即靠边停车,需要满足以下条件才可切换到该场景:
-
不在change_line的时候,即reference_line只有一条
-
当前位置距离终点在一定范围内并且满足pullover可以执行的最短距离
-
地图中能够找到pullover的位置
-
终点的位置不在交叉路口附近
-
能查找到最右边车道的lane_type,并且该车道允许pullover
-
只有从lane_follow场景下才能切换到pullover
ValetParking场景即代客泊车,判断逻辑如下:
-
从routing中得到target_parking_spot_id
-
从地图中搜索是否存在path能够抵达该parking_spot
-
查询当前位置至parking_spot的距离,满足条件即可切换至该场景
Apollo 7.0中新增的断头路场景,增加了"三点掉头"功能,增加了驶入驶出的能力,扩展了城市路网运营边界。"三点掉头"功能基于open space planner框架,包含以下几个部分:断头路场景转换、开放空间ROI构建、掉头轨迹规划。下列图片展现了从进入DeadEnd到驶离DeadEnd的整个过程,后续我也会详细了解该算法实现逻辑。
上面就是Apollo中的场景切换及管理逻辑。在每个场景scenario下,还分为一个或多个stage,而每个stage下面,又划分了多个task来完成相应的规划任务。以用到最多的lane_follow场景为例,它就包含了一个stage——lane_follow_default_stage,而在这个stage下包含了多个task,如下图所示:
仔细查看lane_follow场景下的task,我们可以看出Apollo的规划思路也是横纵向解耦,先规划path,再规划speed。具体的,对于path来说,先做出是否需要lane_change或者lane_borrow的决策,再根据决策状态来生成凸空间,最终基于reference_line及凸空间求解一个二次优化问题,从而得到优化后的path。对于speed来说,是基于ST图进行DP+QP的优化方法,先利用DP来找到一个cost值最小的可行解,再利用QP对可行解进行平滑,得到最终平滑后的ST图点集。最终,基于s值对path和speed进行融合,得到一条平滑的轨迹。
至此,Apollo 7.0 planning的核心框架及核心算法的输入都已经解释清楚了,总体来看Apollo规划的整体思路非常清晰,但是细节部分真的需要花费大量时间来理解,我觉得如果没能将这套算法部署到实车上跑过的话,很多算法可能真的无法很好地理解。
我个人也陆陆续续看这套代码有两年多了,始终觉得很多细节都没有深入理解透,代码光看是肯定不行的,如果没有条件在实车,或者是仿真里实际运行,解决相应场景下遇到的问题的话,是始终不能转变为自己的知识的,希望与大家共勉,也欢迎大家有任何疑问在评论区留言交流。