Apollo Planning 规划模块
Apollo Planning 模块
前言
本篇博客对 Apollo 的规划(Planning)模块做详细介绍。根据官方文档1,规划模块的主要功能是对自动驾驶车辆未来的时空轨迹进行规划。为了让读者对规划模块在整个 Apollo 系统中的位置有一个大致的印象,我特意从官网上截取此图👇。可以看到规划模块的数据上游是定位(Localization),预测(Prediction),感知(Perception)模块,而下游是控制(Control)模块。
场景
规划模块中,Apollo 设计团队引入了场景的概念,每个驾驶案例都被当做为一个不同的驾驶场景,这使得规划模块更具模块化,且更有针对性。更重要的是,对一个特定场景的修改不会影响其他场景。Apollo 5.5 主要注重于城市道路上的沿车道线自动驾驶,并引入了两种新的规划场景2。这里主要介绍 5 种驾驶场景,由于篇幅原因,我就在这里做简要介绍,详细内容可以点击参考文献 [2]。
沿车道线行驶
默认场景模式,该场景包括但不限于遵守基本交通规则,或者基本转弯的单车道(例如巡航)或变道。

会车
该场景包括了在有停:stop_sign: 标志的、有交通灯 :traffic_light: 的、什么都没有的十字路口上的会车情况。总的来说,自动驾驶车辆在遇到这些十字路口时,都会先抵达十字路口边,然后注意观察来往车辆并小心前进,最终通过十字路口

停车
停车场景中采用了一个令 Apollo 团队感到非常骄傲的 Open Space Planner 算法,该规划算法尤其适用于停车场景。文档中,一共介绍了两个子场景:
-
Valet 场景,用于将车辆停放在一个目标车位中
-
Pull Over 场景,用于到达目的地后,完成的路边停车任务。
即停即走(Park-and-go)
即停即走场景用于路边停车,并开始生成到达下一个目的地的新路径,有点像出租车放下一名乘客后的场景,对于路边交付,乘客接送或下车等情况非常有用。该场景将 Open Space Planner 算法与其他传统的轨迹规划算法结合在一起,以确保汽车不仅安全停车,而且还能够按照生成的轨迹驶出停车位。

紧急状况
紧急场景是为确保在给定触发事件的情况下(通过人工输入或由于一个或多个软硬件故障)让车辆能安全停下。 该场景对于城市驾驶极为重要,因为在道路上经常会遇到几种无法预料的情况,要求车辆完全停车。例如,有两种类型的紧急停车:
- 靠边停车,自动驾驶车辆使用 Open Space Planner 靠边停车
- 在车道停止,自动驾驶汽车在充分意识到周遭安全的情况下,在车道内停车

规划模块的体系结构
在 Apollo 5.5 版本中,Apollo 团队对规划模块的体系结构进行了修改,以纳入城市道路上的新驾驶方案。 如下图所示,因为新加入了有两种复杂方案:紧急情况和即停即走。为了有效地使用这些方案,团队引入了 2 个新的决策器—路径重用决策器(Path Reuse Decider)和限速决策器(Speed Bound Decider),并更新了现有决策器,使规划体系结构既强大又灵活,足以应付许多不同类型的城市道路驾驶方案。
每个驾驶场景都有其独特的驾驶参数集,使其更安全,高效,更易于自定义和调试,也更加灵活。

如果你认真了解了 Apollo Cyber RT 组件的相关知识,就明白模块的起点就是 Component
类,一切的模块都是继承 Component
类而来的。自然地,我把这它作为全面认识规划模块的切入点。
1 |
|
我们一条条地看,PlanningComponent
类继承的是 Component<prediction::PredictionObstacles, canbus::Chassis, localization::LocalizationEstimate>
类,简单地说,从名字上看,PlanningComponent
类需要处理三类消息:
prediction::PredictionObstacles
预测的障碍物行为canbus::Chassis
canbus 总线传来的底盘状态localization::LocalizationEstimate
来自定位模块的定位信息
但是,PlanningComponent
类内部有非常多的不同类型的订阅者 Reader
和发布者 Writer
。这些订阅者和发布者的用途,大都可以通过名称猜出个大概。值得注意的是,类的成员变量中,存在与 Reader
一一对应的变量,看起来它们是用于存放对应的 Reader
接收的消息,很快下文就证实了我的猜想:smiley:。
初始化
Init()
函数实现了模块初始化,在 modules/
中的代码不论取名、类型还是函数实现都非常长,这点不同于 Cyber RT,因此在大多数情况下,我选择部分截取代码,如果想要看完整的代码,移步 modules/planning/planning_component.cc::Init()
。
1 |
|
上面代码首先创建了一个 DependencyInjector
类的指针,然后根据 FLAGS_use_navigation_mode
模式的不同,选择创建不同的 PlanningBase
的子类,然后就是读取配置文件路径,再就是 PlanningBase
子类的初始化。先来说说这些规划子类的区别。
OpenSpacePlanning
主要的应用场景是自主泊车和狭窄路段的掉头,官方文档参考,但很奇怪的是,我没有找到相应的实现代码:confused:NaviPlanning
主要用于在确定了导航路线的情况下的自动驾驶OnLanePlanning
主要的应用于开放道路下沿车道线的自动驾驶。
1 |
|
紧接着,Init()
函数就用我之前博文中介绍的方式创建订阅者 Reader
和发布者 Writer
。仔细看上面给出的代码,CreateReader
函数的参数,一个是从配置文件中取得相应的配置信息,另一个是构建的回调函数。
咱们先来看一下后者:构建函数。根据我在这篇博客中所讲的流程,传入的回调函数是在 Reader
接收到消息后就会被调用的,从内容来看,Reader
在收到对应的消息后,调用 CopyFrom()
函数,把消息拷贝到自己的私有变量中。至于 Writer
就比较平凡了,就是获取了配置文件里相应的信息:man_shrugging:。
那么这个“配置文件中的相应信息”到底是什么信息呢:confused:?打开 modules/planning/conf/planning_config.pb.txt
,你就会恍然大悟:point_down:,原来上面代码中的 config_.topic_config().planning_trajectory_topic()
就是一个字符串,对应的就是信道的名字。
1 |
|
这就是初始化函数的全部内容了。
处理
再来看 PlanningComponent::Proc()
函数,之前的博客中我说过,Proc()
函数是可以由用户自己编写完成的,而之后,该函数会被封装处理,最终让对应的协程开始执行。不同于在 Cyber RT 中的精雕细琢,我这次采用不同的办法,先快速过一遍 Proc()
函数,然后找出自己感兴趣的,进而深入研究。那么先来看 Proc()
函数的第一部分:
1 |
|
首先,Proc()
函数调用 CheckRerouting()
检查并处理了可能的重新路由需求,然后就是构建自己对象内的成员 local_view_
,没错,为了防止多线程竞争,使用了非常多次的互斥锁:lock: 。该成员变量包含了所有规划模块需要的输入,这些输入信息非常重要,可能会经常使用,详细情况在下文的输入部分。
我们先把诸多疑问抛置脑后,直接来看 Proc()
函数的第二部分。
1 |
|
从注释中看出,MessageProcess
类有用于线上训练的功能,在 learning_mode
打开时,它会收集刚刚输入得到信息。之后,injector_
获得最新的学习数据帧(frame),并通过 planning_learning_data_writer_
发布者发布,用于强化学习(Reinforcement Learning)测试。
同样,跳过学习部分(这不是我所关心的重点),再来看第三部分代码:
1 |
|
在第三部分代码中,首先调用 RunOnce()
函数生成了自动驾驶轨迹,放在了 ADCTrajectory
类的消息中,看来这就是生产规划路径的关键逻辑,这是我比较关心的。然后,根据注释,我推断,程序修改了轨迹中所有点的相对时间,并通过发布者 planning_writer_
发送消息,最后进行历史记录。至此,PlanningComponent::Proc()
函数结束。
前文说到,Proc()
函数先检查路由情况,再收集输入数据到 local_view_
这个私有变量中。为了弄清模块的输入输出信息,进而了解整个 Apollo 系统的数据流通情况,我需要对输入数据做一个全面的解读。
1 |
|
- prediction_obstacles 从信道中获得的指针,类型为
PredictionObstacles
,代码文件在modules/prediction/proto/prediction_obstacle.proto
中,该类型的消息来自预测模块,包含了以下信息:Header
头结构:包含了这条消息发布的时刻(以秒为单位),目前位置的模块名,该消息的序列号(每个模块各自维护的)等,几乎每个消息都包含有这个头结构,下面就不提及了。PredictionObstacle
预测模块中的若干个障碍物行为:有感知模块中的障碍物(PerceptionObstacle
),它包括障碍物的 id,它的三维坐标、速度加速度、边界大小(长高宽)、特殊的形状、类型(行人、非机动车、机动车)、运动轨迹等;还有时间戳,记录 GPS 给出的时刻;还有预测的时间长度;多种可能的运动轨迹;障碍物的运动趋势、优先级等。- 开始的时间戳:记录预测开始的时刻
- 结束时间戳:预测结束的时刻
- 自动驾驶车辆的运动趋势,停止、正常行驶还是正在变道等
Scenery
现在的场景
- chassis 从信道获得的指针,类型为
Chassis
,这条消息直接来自总线 CanBus,代码文件在modules/canbus/proto/chassis.proto
中,该类包含了很多信息,主要是汽车底盘所给出的机械状态信息,比如:- 驾驶模式,有手动驾驶、自动驾驶、仅控制方向、仅控制速度以及紧急模式
- 档位情况、引擎转速、车辆速度、里程表、燃油情况、电池电量等
- 刹车、油门踏板的力度,方向盘的旋转角度,车轮转速
- 转向灯、雾灯、大灯的工作情况
- localization_estimate 从信道中获得的指针,类型为
LocalizationEstimate
,代码文件在modules/localization/proto/localization.proto
中,这条消息来自定位模块,该类主要包含了:Pose
位置姿势,包括车头朝向,在地图上的线速度、线加速度和相对位置,角速度、仰角度等- 测量上述姿势的时刻
- 车辆已经经过的轨迹点列
- MSF 定位状态与质量
上面的三条消息都是通过 Proc()
函数的参数传来的,回顾一下在通信里讲过的过程,这三条信息会被融合起来,随后程序会唤醒封装了 Component::Process()
的协程,让协程处于 Ready 状态,最后通过调度算法会让某个线程对它进行执行。而以下介绍的五条消息,与上三条不一样,这些消息,是通过 Reader
获得的,回顾在这篇博客里讲的订阅者内容,这些消息到来后,程序会唤醒封装了入队操作和 Reader
回调函数的协程,让它处于 Ready 状态,最后调度算法调度某个线程进行执行。接下来,我们就详细看一下这五条消息吧:
- traffic_light 获得现在的交通信号灯情况,类型为
TrafficLightDetection
,来自感知模块,该类在modules/perception/camera/lib/traffic_light/detector/detection/detection.h
中定义,主要是从摄像机拍摄的图片中获得目前的信号灯情况 - routing 导航路线,类型为
RoutingResponse
,来自路由模块,从RoutingComponent
类看出,RoutingResponse
就是该组件发布的消息类,类在modules/routing/proto/routing.proto
文件中定义,RoutingResponse
包含有:- 导航中要通过的道路,以及总路程等
RoutingRequest
可理解为要响应的路由请求- 地图版本和现在的状态等
- relative_map 相对地图,类型为
MapMsg
,是RelativeMapComponent
类发出的一个相对地图消息,在modules/map/relative_map/proto/navigation.proto
文件中定义,包含Header
头结构- 车辆的高清的三维坐标,定位信息
- 系统中的编号与对应的路名、车道表
- 从感知模块中获得的车道线标记
- pad_msg 便笺,类在
modules/planning/proto/pad_msg.proto
定义,除了一个头结构外,仅包含了驾驶动作,比如直行跟随,左右变道,靠边停车等。
生成轨迹
在初始化小节中,根据 FLAGS_use_navigation_mode
的设置, planning_base_
会指向不同的子类,或 NaviPlanning
类或是 OnLanePlanning
类,然后调用 RunOnce()
函数。
首先来看一下默认的规划类的 OnLanePlanning::RunOnce()
函数,回顾一下 Component::Proc()
函数的调用和组件的相关知识,你应该就会明白它是事件触发的,而不是时间触发的。该函数很长很复杂,很多地方我也并不关心(而且也看不懂),若有机会,可以细致地研究一下,大体上,它完成了以下步骤:
1 |
|
那么 Planner
又是什么东西呢?事实上才 OnLanePlanning::Init()
函数中就已经根据配置文件,由 PlannerDispatcher
指定了。我一共发现 4 种用于不同场景的 Planner
:
RTKReplayPlanner
根据录制的轨迹来规划行车路线PublicRoadPlanner
开放道路的轨迹规划器LatticePlanner
基于网格算法的轨迹规划器NaviPlanner
基于实时相对地图的规划器
未完待续……
参考
[1] Github Apollo