解析百度Apollo自动驾驶平台

Posted on Jul 7, 2018


最近对百度的自动驾驶平台Apollo项目做了一些了解。下面将我所了解到的一些信息分享给大家。

Apollo项目介绍

阿波罗(Apollo)是百度发布的面向汽车行业及自动驾驶领域的合作伙伴提供的软件平台。发布时间是2017年4月19日,旨在向汽车行业及自动驾驶领域的合作伙伴提供一个开放、完整、安全的软件平台,帮助他们结合车辆和硬件系统,快速搭建一套属于自己的完整的自动驾驶系统。而将这个计划命名为“Apollo”计划,就是借用了阿波罗登月计划的含义。

可以在这里感受一下Apollo的实车驾车体验:CES 2018 百度Apollo 2.0无人车美国桑尼维尔试乘

SAE Level

对于自动驾驶,SAE(Society of Automotive Engineers,美国汽车工程师学会) International于2014年发布了从全手动系统到全自动系统六个不同级别的分类系统,这6个级别的描述如下:

SAE Level Name System capability Driver involvement
0 No Automation None The human at the wheel steers, brakes, accelerates, and negotiates traffic.
1 Drive Assistance Under certain conditions, the car controls either the steering or the vehicle speed, but not both simultaneously. The driver performs all other aspects of driving and has full responsibility for monitoring the road and taking over if the assistance system fails to act appropriately.
2 Partial Automation The car can steer, accelerate, and brake in certain circumstances. Tactical maneuvers such as responding to traffic signals or changing lanes largely fall to the driver, as does scanning for hazards. The driver may have to keep a hand on the wheel as a proxy for paying attention.
3 Conditional Automatio In the right conditions, the car can manage most aspects of driving, including monitoring the environment. The system prompts the driver to intervene when it encounters a scenario it can’t navigate. The driver must be available to take over at any time.
4 High Automation The car can operate without human input or oversight but only under select conditions defined by factors such as road type or geographic area. In a shared car restricted to a defined area, there may not be any. But in a privately owned Level 4 car, the driver might manage all driving duties on surface streets then become a passenger as the car enters a highway.
5 Full Automation The driverless car can operate on any road and in any conditions a human driver could negotiate. Entering a destination.

阿波罗项目的官网地址如下:http://apollo.auto

在阿波罗项目的官网,介绍了该项目有如下特点:

  • 开放能力:Apollo(阿波罗)是一个开放的、完整的、安全的平台,将帮助汽车行业及自动驾驶领域的合作伙伴结合车辆和硬件系统,快速搭建一套属于自己的自动驾驶系统。
  • 共享资源、加速创新:Apollo开放平台,为你提供技术领先、覆盖广、高自动化的高精地图服务;全球唯一开放,拥有海量数据的仿真引擎;全球开放数据量第一,基于深度学习自动驾驶算法End-to-End。
  • 持续共赢:Apollo开放平台,你可以更快地研发、测试和部署自动驾驶车辆。参与者越多,积累的行驶数据就越多。与封闭的系统相比,Apollo能以更快的速度成熟,让每个参与者得到更多的受益,同时Apollo平台也将在你的参与之下变得更好!

目前,其官网上列出的合作伙伴已经接近100家。

阿波罗项目的蓝图如下:

  • 2017-07:封闭场地的自动驾驶能力
  • 2017-12:在城市简单路况下的自动驾驶能力
  • 2020-12:高速公路和普通城市道路上的全自动驾驶

最新发布的Apollo 2.5版本主要目标是L2级自动驾驶。

详细的Apollo版本演进信息如下图所示:

源码

可以在这里获取到阿波罗项目的源码:https://github.com/ApolloAuto。这个路径中包含了5个开源项目:

  • apollo:Apollo自动驾驶平台的源码。
  • apollo-platform:Apollo项目基于Robot Operating System (ROS),这里是相关代码。目前发布的源码基于ROS Indigo。
  • apollo-DuerOS:Apollo-DuerOS是一套与Apollo相关的远程信息处理产品,这其中包含了几个开源产品。关于DuerOS,请看这里:DuerOS
  • apollo-kernel:Apollo项目的Linux内核。
  • ApolloAuto.github.io:Apollo相关文档,可以访通过https://apolloauto.github.io访问这些文档。

编译和运行

关于如何编译和运行阿波罗项目请参见这里:https://apolloauto.github.io

执行该任务需要Ubuntu和Docker环境。

编译完成之后,可以在电脑上通过该项目提供的Dreamview功能来熟悉环境,Dreamview通过浏览器访问,其界面看起来是这个样子:

关于Dreamview的更多说明,请参见这里:Dreamview Usage Table

开发

阿布罗平台的开发包含下面几个步骤:

  1. 了解离线模拟引擎Dreamviewer和ApolloAuto核心软件模块
    • 了解算法如何在汽车上运作
    • 不需要使用真正的汽车或硬件,就立即开始开发
  2. 核心模块集成
    • Location模块
    • Perception模块:(支持第三方解决方案,如基于Mobileye ES4芯片的摄像头,用于L2开发)处理来自Lidar的点云数据,并根据请求返回分段对象信息。
    • Planning模块:计算微调路径,为路径服务的路径段提供汽车动态控制信息。
    • Routine模块:通过Navigator接口查找路径段的本地实现。
  3. 高清地图。L4级别的自动驾驶需要高清地图。由于自动驾驶汽车需要在系统中重建3D世界,因此参考对象坐标在重新定位地图和现实世界中的自动驾驶方面发挥着重要作用。
  4. 基于云的在线仿真驱动场景引擎和数据中心。
    • 作为百度的合作伙伴,将被授予Docker证书来提交新图像并重播你在云上开发的算法。
    • 创建和管理复杂的场景以模拟真实世界的驾驶体验

Apollo与ROS

ROS全称是Robot Operating System。它包含了一套开源的软件库和工具,专门用来构建机器人应用。其官网地址在这里:http://www.ros.org

在一个ROS系统中,包含了一系列的独立节点(nodes)。这些节点之间,通过发布/订阅的消息模型进行通信。例如,某个传感器的驱动可以实现为一个节点,然后以发布消息的形式对外发送传感器数据。这些数据可以被多个其他节点接收,例如:过滤器,日志系统等等。

ROS系统中的节点可能位于不同的主机上,例如:在一个Arduino设备上发布消息,一台笔记本电脑订阅这些消息,一个Android手机也监测这些消息。

ROS系统中包含了一个主(Master)节点。主节点使得其他节点可以查询彼此以进行通讯。所有节点都需要在主节点上进行注册,然后就可以与其他节点通讯了。如下图所示:

熟悉Android系统的人可能很容易发现,这和Binder中的ServiceManager的作用是类似的。

节点之间通过发布和订阅主题(Topics)进行通讯。例如,在某个机器人系统中,位于机器人上有一个相机模块可以获取图像数据。另外在机器人上有一个图像处理模块需要获取图像数据,与此同时还有另外一个位于个人PC上的模块也需要这些图像数据。那么,相机模块可以发布/image_data这个主题供其他两个模块来订阅。其结构如下图所示:

Apollo项目基于ROS,但是对其进行了改造,主要包括下面三个方面:

  1. 通信性能优化
  2. 去中心化网络拓扑
  3. 数据兼容性扩展

通信性能优化

自动驾驶车辆中包含了大量的传感器,这些传感器可能以非常高频的速度产生数据,所以整个系统对于数据传输效率要求很高。在ROS系统中,从数据的发布到订阅节点之间需要进行数据的拷贝。在数据量很大的情况下,很显然这会影响数据的传输效率。所以Apollo项目对于ROS第一个改造就是将通过共享内存来减少数据拷贝,以提升通信性能。如下图所示:

去中心化网络拓扑

前文我们提到,ROS系统中包含了一个通信的主节点,所有其他节点都要借助于这个节点来进行通信。所以,很显然的,假如这个节点发生了通信故障,就会影响整个系统的通信。并且,整个结构还缺乏异常恢复机制。

所以Apollo项目对于ROS的第二个改造就是去除这种中心化的网络结构。Apollo使用RTPS(Real-Time Publish-Subscribe)服务发现协议实现完全的P2P网络拓扑。整个通信过程包含下面四个步骤:

关于RTPS详见这里:Real-Time Publish-Subscribe

数据兼容性扩展

Apollo项目对于ROS最后一个较大的改进就是对于数据格式的调整。

在ROS系统中,使用msg描述文件定义模块间的消息接口。但不幸的是,接口升级之后不同的版本的模块难以兼容。

因此,Apollo选择了Google的Protocol Buffers格式数据来解决这个问题。

Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。现阶段官方支持C++、JAVA、Python三种编程语言,但可以找到大量的几乎涵盖所有语言的第三方拓展包。

注:如果你查看了Apollo项目的源码,可以看到很多名称为“proto”的文件夹,这些文件夹中包含的就是Protocol Buffers(简称protobuf)格式的数据结构。

硬件架构

Apollo 2.5上必须的硬件如下表所示:

外设包括下面这些:

硬件架构如下图所示:

软件架构

Apollo平台的软件架构如下图所示:

在Apollo上,运行的核心软件模块包括:

  • Perception:感知模块识别自动车辆周围的世界。在Perception模块中有两个重要的子模块:障碍物检测和交通灯检测。
  • Prediction:Prediction模块预测未来的感知障碍物的运动轨迹。
  • Routing:Routing模块告诉自动车辆如何通过一系列车道或道路到达目的地。
  • Planning:Planning模块计划自主车辆的时空轨迹。
  • Control:Control模块通过生成诸如节流阀,制动器和转向的控制命令来执行计划的时空轨迹。
  • CanBus:CanBus将控制命令传递给车辆硬件的接口。它还将机架信息传递给软件系统。
  • HD-Map:提供有关道路特定结构化的信息。
  • Localization:该模块利用各种信息来源,例如GPS,LiDAR和IMU来估计自动车辆所在的位置。

这些模块的交互结构如下图所示:

每个模块都作为独立的基于CarOS的ROS节点运行。每个模块节点都会发布和订阅某些主题。订阅的主题用作数据输入,而发布的主题用作数据输出。

关于Apollo平台的系统架构可以阅读这篇文档:HOW TO UNDERSTAND ARCHITECTURE AND WORKFLOW

从这篇文档中我们看到:

  • 自动驾驶车辆由规划引擎通过CAN总线(Controller Area Network bus)来进行控制。
  • 为了计算效率,Location模块,Perception模块,Planning模块作为独立的输入源和输出源通过P2P一起工作。
  • 通过阅读源码${MODULE_NAME}/conf目录下的配置文件,我们可以获得有关模块订阅和发布的主题的基本信息。
  • 每个模块通过触发 Init 接口和注册回调开始。
  • 所有模块都会在下面这个点上注册:AdapterManager::Init。该函数部分代码片段如下:
void AdapterManager::Init(const AdapterManagerConfig &configs) {
  if (Initialized()) {
    return;
  }

  instance()->initialized_ = true;
  if (configs.is_ros()) {
    instance()->node_handle_.reset(new ros::NodeHandle());
  }

  for (const auto &config : configs.config()) {
    switch (config.type()) {
      case AdapterConfig::POINT_CLOUD:
        EnablePointCloud(FLAGS_pointcloud_topic, config);
        break;
      case AdapterConfig::GPS:
        EnableGps(FLAGS_gps_topic, config);
        break;
      case AdapterConfig::IMU:
        EnableImu(FLAGS_imu_topic, config);
        break;
      case AdapterConfig::RAW_IMU:
        EnableRawImu(FLAGS_raw_imu_topic, config);
        break;
      case AdapterConfig::CHASSIS:
        EnableChassis(FLAGS_chassis_topic, config);
        break;
      case AdapterConfig::LOCALIZATION:
        EnableLocalization(FLAGS_localization_topic, config);
        break;
      case AdapterConfig::PERCEPTION_OBSTACLES:
        EnablePerceptionObstacles(FLAGS_perception_obstacle_topic, config);
        break;
      case AdapterConfig::TRAFFIC_LIGHT_DETECTION:
        EnableTrafficLightDetection(FLAGS_traffic_light_detection_topic,
                                    config);
     ...

下面是对系统中主要的核心模块的一些解析。

核心模块

apollo/modules/中包含了系统中的各个模块的源码。

阅读这些源码会发现,这些核心模块的类都继承自一个公共基类ApolloApp,相关结构如下图所示:

ApolloApp类的结构如下图所示:

该类中的主要函数说明如下:

函数名 说明
std::string Name() const 返回模块名称
apollo::common::Status Init() 模块的初始化函数,模块启动的第一个函数
apollo::common::Status Start() 模块的启动函数
int Spin() 模块的入口点
void Stop() 模块的停止函数

apollo_app.h这个头文件中,还包含了一个宏以方便每个模块声明main函数,相关代码如下:

#define APOLLO_MAIN(APP)                                       \
  int main(int argc, char **argv) {                            \
    google::InitGoogleLogging(argv[0]);                        \
    google::ParseCommandLineFlags(&argc, &argv, true);         \
    signal(SIGINT, apollo::common::apollo_app_sigint_handler); \
    APP apollo_app_;                                           \
    ros::init(argc, argv, apollo_app_.Name());                 \
    apollo_app_.Spin();                                        \
    return 0;                                                  \
  }

每个模块的根目录都包含了一个README.md文件,是对这个模块的说明。我们可以以此为入口来了解模块的实现。

Perception(感知)

模块介绍

自动驾驶车辆通过前置摄像头和雷达与最近的车辆(closest in-path vehicle,简称CIPV)保持距离。子模块还预测障碍物运动和位置信息(例如,航向和速度)。Apollo 2.5支持高速公路上的高速自动驾驶,无需任何地图。深度网络算法已经学会处理图像数据。随着收集更多数据,深度网络的性能将随着时间的推移而提高。

模块输入:

  • 雷达数据
  • 图像数据
  • 雷达传感器校准的外部参数(来自YAML文件)
  • 前置相机校准的外部和内部参数(来自YAML文件)
  • 车辆的速度和角速度

模块输出:

  • 3D障碍物跟踪航向,速度和分类信息
  • 带有拟合曲线参数的车道标记信息,空间信息以及语义信息

模块解析

Perception模块需要根据输入信息快速的解析出两类信息,即:道路和物体。其中的深入网络基于YOLO算法[1][2]

Apollo 2.5不支持高曲率,没有车道标志的道路,包括当地道路和交叉路口。感知模块基于使用具有有限数据的深度网络的视觉检测。因此,在发布更好的网络之前,驾驶员在驾驶时应小心谨慎,并始终准备好通过将车轮转向正确的方向来解除自主驾驶。

  • 推荐道路
    • 两侧清晰的白色车道线
  • 不推荐道路
    • 高曲率的道路
    • 没有车道线标记的道路
    • 路口
    • 对接点或虚线车道线
    • 公共道路

而对于物体来说,又分为静态物体和动态物体。静态物体包括道路和交通灯等。动态物体包括机动车,自行车,行人,动物等。

为了保持车辆在车道上,需要一系列模块的配合,相关流程图如下所示:

Perception模块在Init函数中会注册一系列类以完成模块启动后的正常工作,相关代码如下:

void Perception::RegistAllOnboardClass() {
  /// regist sharedata
  RegisterFactoryLidarObjectData();
  RegisterFactoryRadarObjectData();
  RegisterFactoryCameraObjectData();
  RegisterFactoryCameraSharedData();
  RegisterFactoryCIPVObjectData();
  RegisterFactoryLaneSharedData();
  RegisterFactoryFusionSharedData();
  traffic_light::RegisterFactoryTLPreprocessingData();

  /// regist subnode
  RegisterFactoryLidarProcessSubnode();
  RegisterFactoryRadarProcessSubnode();
  RegisterFactoryCameraProcessSubnode();
  RegisterFactoryCIPVSubnode();
  RegisterFactoryLanePostProcessingSubnode();
  RegisterFactoryAsyncFusionSubnode();
  RegisterFactoryFusionSubnode();
  RegisterFactoryMotionService();
  lowcostvisualizer::RegisterFactoryVisualizationSubnode();
  traffic_light::RegisterFactoryTLPreprocessorSubnode();
  traffic_light::RegisterFactoryTLProcSubnode();
}

我们可以以这里为入口了解各个子模块的逻辑。

RegisterFactoryLidarProcessSubnode为例。

代码中其实并不存在RegisterFactoryLidarProcessSubnode这个函数,该函数的定义其实是由宏完成的。相关代码如下:

Lidar(也称之为LIDAR,LiDAR,或LADAR)的全称是Light Detection And Ranging,即激光探测与测量。


// /modules/perception/onboard/subnode.h
#define REGISTER_SUBNODE(name) REGISTER_CLASS(Subnode, name)


// /modules/perception/lib/base/registerer.h
#define REGISTER_CLASS(clazz, name)                                           \
  class ObjectFactory##name : public apollo::perception::ObjectFactory {      \
   public:                                                                    \
    virtual ~ObjectFactory##name() {}                                         \
    virtual perception::Any NewInstance() {                                   \
      return perception::Any(new name());                                     \
    }                                                                         \
  };                                                                          \
  inline void RegisterFactory##name() {                                       \
    perception::FactoryMap &map = perception::GlobalFactoryMap()[#clazz];     \
    if (map.find(#name) == map.end()) map[#name] = new ObjectFactory##name(); \
  }

而在lidar_process_subnode.h中使用了上面这个宏。

REGISTER_SUBNODE(LidarProcessSubnode);

于是就会生成一个名称为ObjectFactoryLidarProcessSubnode的类,该类继承自apollo::perception::ObjectFactory,并且其中包含了名称为RegisterFactoryLidarProcessSubnode的函数。

Prediction(预测)

模块介绍

Prediction模块从Perception模块接受障碍物信息。该模块需要的信息包括位置,航向,速度,加速度,并产生具有障碍概率的预测轨迹。

模块输入:

  • 来自Prediction模块的障碍物信息
  • 来自Localizaton模块的位置信息

模块输出:

  • 障碍物的预测轨迹

模块解析

Prediction的Init函数中添加了三个回调用来从其他模块获取信息的更新:

AdapterManager::AddLocalizationCallback(&Prediction::OnLocalization, this);
AdapterManager::AddPlanningCallback(&Prediction::OnPlanning, this);
AdapterManager::AddPerceptionObstaclesCallback(&Prediction::RunOnce, this);

这里最重要的就是Prediction::RunOnce这个函数。这个函数中包含了Prediction模块的主要逻辑,它会在接收到一个新的障碍物消息时触发。

Prediction模块中有三类重要的子模块。

第一类是Container,用来存储从订阅频道获取的数据。包括:

  • 感到到的障碍物信息
  • 车辆位置信息
  • 车辆计划信息

第二类是Evaluator,用来针对指定的障碍物预测路线和速度。目前有三类Evaluator,包括:

  • Cost evaluator:通过一组代价函数来计算可能性
  • MLP evaluator:通过MLP模型来计算可能性
  • RNN evaluator:通过RNN模型来计算可能性

Evaluator通过EvaluatorManager类管理,Evaluator类结构如下图所示:

Prediction模块中第三类重要的子模块就是Predictor。它用来预测障碍物的轨迹。

不同的障碍物运动的轨迹会不一样,因此实现中包含了很多个类型的Predictor,它们的结构如下图所示。

类似的,会有一个PredictorManager来管理Predictor。

Routing(路由)

模块介绍

Routing模块根据请求生成导航信息。

模块输入:

  • 地图数据
  • 请求,包括:开始和结束位置

模块输出:

  • 路由导航信息

模块解析

Routing模块的内部结构如下图所示:

Routing模块的输入是地图数据和导航请求,因此其Init函数就是围绕这个逻辑的:

apollo::common::Status Routing::Init() {
  const auto routing_map_file = apollo::hdmap::RoutingMapFile();
  AINFO << "Use routing topology graph path: " << routing_map_file;
  navigator_ptr_.reset(new Navigator(routing_map_file));
  CHECK(common::util::GetProtoFromFile(FLAGS_routing_conf_file, &routing_conf_))
      << "Unable to load routing conf file: " + FLAGS_routing_conf_file;

  AINFO << "Conf file: " << FLAGS_routing_conf_file << " is loaded.";

  hdmap_ = apollo::hdmap::HDMapUtil::BaseMapPtr();
  CHECK(hdmap_) << "Failed to load map file:" << apollo::hdmap::BaseMapFile();

  AdapterManager::Init(FLAGS_routing_adapter_config_filename);
  AdapterManager::AddRoutingRequestCallback(&Routing::OnRoutingRequest, this);
  return apollo::common::Status::OK();
}

这段代码的重点是下面三个地方:

  • apollo::hdmap::RoutingMapFile()包含了HD地图数据。
  • Navigator负责导航,我们很容易想到这个类应当是该模块的核心。
  • Routing::OnRoutingRequest是接收导航请求的回调函数。

Routing::OnRoutingRequest中,最主要的就是通过Navigator::SearchRoute来搜索导航路径。

void Routing::OnRoutingRequest(const RoutingRequest& routing_request) {
  AINFO << "Get new routing request:" << routing_request.DebugString();
  RoutingResponse routing_response;
  apollo::common::monitor::MonitorLogBuffer buffer(&monitor_logger_);
  const auto& fixed_request = FillLaneInfoIfMissing(routing_request);
  if (!navigator_ptr_->SearchRoute(fixed_request, &routing_response)) {
    AERROR << "Failed to search route with navigator.";

    buffer.WARN("Routing failed! " + routing_response.status().msg());
    return;
  }
  buffer.INFO("Routing success!");
  AdapterManager::PublishRoutingResponse(routing_response);
  return;
}

目前,Apollo 2.5版本中的导航基于A*算法。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。该算法综合了Best-First Search和Dijkstra算法的优点:在进行启发式搜索提高算法效率的同时,可以保证找到一条最优路径(基于评估函数)。

在A*算法计算的过程中,会尝试多条路径。一旦遇到障碍物,便将该路径上的点标记为不需要继续探索(图中的实心点)。继续以剩下的空心点为基础探索。最终求得最优路径。

下图动态描述了A*算法查找目标路径的算法过程。

Planing(计划)

模块介绍

Planing模块根据定位信息,车辆状态(位置,速度,加速度,底盘),地图,路线,感知和预测,计算出安全和舒适的形式线路让控制器执行。

目前的系统实现中包含了四种计划器:

  • RTKReplayPlanner(自Apollo 1.0以来):RTK重放计划器首先在初始化时加载记录的轨迹,并根据当前系统时间和车辆位置发送适当的轨迹段。
  • EMPlanner(自Apollo1.5以来。EM是Expectation Maximization的缩写):EM计划器,会根据地图,路线和障碍物计算驾驶决策和线路。基于动态规划(Dynamic programming,简称DP)的方法首先用于确定原始路径和速度曲线,然后使用基于二次规划(Quadratic programming,简称QP)的方法来进一步优化路径和速度曲线以获得平滑的轨迹。
  • LatticePlanner:网格计划器
  • NaviPlanner:这是一个基于实时相对地图的计划器。它使用车辆的FLU(Front-Left-Up)坐标系来完成巡航,跟随,超车,接近,变道和停车任务。

模块输入:

  • RTK重放计划器:
    • Localization
    • 记录的RTK轨迹
  • EM计划器:
    • Localization
    • Perception
    • Prediction
    • HD Map
    • Routing

模块输出:

  • 无碰撞和舒适的轨迹让控制模块的执行

模块解析

Planing模块在初始化的Init函数中,这里面会注册所有的计划器,然后根据配置文件中的配置确定当前所使用的计划器。目前,配置文件中配置的是EM计划器。

Planing模块在Start函数中设定了一个Timer用来完成定时任务:

Status Planning::Start() {
  timer_ = AdapterManager::CreateTimer(
      ros::Duration(1.0 / FLAGS_planning_loop_rate), &Planning::OnTimer, this);
...

Planning::OnTimer最主要的就是调用RunOnce(),而后者包含了Planing模块的核心逻辑。目前,FLAGS_planning_loop_rate值是10。也就是说,Planing模块运行的频度是每秒钟10次。

在Planning::Plan函数中,更通过配置的计划器进行路线的计算,然后将结果对外发布。关键代码如下:

Status Planning::Plan(const double current_time_stamp,
                      const std::vector<TrajectoryPoint>& stitching_trajectory,
                      ADCTrajectory* trajectory_pb) {
  auto* ptr_debug = trajectory_pb->mutable_debug();
  if (FLAGS_enable_record_debug) {
    ptr_debug->mutable_planning_data()->mutable_init_point()->CopyFrom(
        stitching_trajectory.back());
  }

  auto status = planner_->Plan(stitching_trajectory.back(), frame_.get());

  ExportReferenceLineDebug(ptr_debug);

  const auto* best_ref_info = frame_->FindDriveReferenceLineInfo();
  if (!best_ref_info) {
    std::string msg("planner failed to make a driving plan");
    AERROR << msg;
    if (last_publishable_trajectory_) {
      last_publishable_trajectory_->Clear();
    }
    return Status(ErrorCode::PLANNING_ERROR, msg);
  }
  ptr_debug->MergeFrom(best_ref_info->debug());
  trajectory_pb->mutable_latency_stats()->MergeFrom(
      best_ref_info->latency_stats());
  // set right of way status
  trajectory_pb->set_right_of_way_status(best_ref_info->GetRightOfWayStatus());
  for (const auto& id : best_ref_info->TargetLaneId()) {
    trajectory_pb->add_lane_id()->CopyFrom(id);
  }

  best_ref_info->ExportDecision(trajectory_pb->mutable_decision());

  ...
  
  last_publishable_trajectory_->PrependTrajectoryPoints(
      stitching_trajectory.begin(), stitching_trajectory.end() - 1);

  for (size_t i = 0; i < last_publishable_trajectory_->NumOfPoints(); ++i) {
    if (last_publishable_trajectory_->TrajectoryPointAt(i).relative_time() >
        FLAGS_trajectory_time_high_density_period) {
      break;
    }
    ADEBUG << last_publishable_trajectory_->TrajectoryPointAt(i)
                  .ShortDebugString();
  }

  last_publishable_trajectory_->PopulateTrajectoryProtobuf(trajectory_pb);

  best_ref_info->ExportEngageAdvice(trajectory_pb->mutable_engage_advice());

  return status;
}

Control(控制)

模块介绍

控制模块根据计划和当前的汽车状态,使用不同的控制算法来生成舒适的驾驶体验。控制模块可以在正常模式和导航模式下工作。

模块输入:

  • 计划的线路
  • 车辆状态
  • 定位信息
  • Dreamview AUTO模式更改请求

模块输出:

  • 控制命令(转向,油门,刹车)到底盘

模块解析

Control模块的主体逻辑也是通过Timer定时执行的。在定时触发的函数Control::OnTimer中,会生成命令然后派发出去:

void Control::OnTimer(const ros::TimerEvent &) {
  double start_timestamp = Clock::NowInSeconds();

  if (FLAGS_is_control_test_mode && FLAGS_control_test_duration > 0 &&
      (start_timestamp - init_time_) > FLAGS_control_test_duration) {
    AERROR << "Control finished testing. exit";
    ros::shutdown();
  }

  ControlCommand control_command;

  Status status = ProduceControlCommand(&control_command);
  AERROR_IF(!status.ok()) << "Failed to produce control command:"
                          << status.error_message();

  double end_timestamp = Clock::NowInSeconds();

  if (pad_received_) {
    control_command.mutable_pad_msg()->CopyFrom(pad_msg_);
    pad_received_ = false;
  }

  const double time_diff_ms = (end_timestamp - start_timestamp) * 1000;
  control_command.mutable_latency_stats()->set_total_time_ms(time_diff_ms);
  control_command.mutable_latency_stats()->set_total_time_exceeded(
      time_diff_ms < control_conf_.control_period());
  ADEBUG << "control cycle time is: " << time_diff_ms << " ms.";
  status.Save(control_command.mutable_header()->mutable_status());

  SendCmd(&control_command);
}

Control模块内置了三个控制器,它们的结构和说明如下:

  • LatController:基于LQR的横向控制器,计算转向目标。详见:Vehicle Dynamics and Control
  • LonController:纵向控制器,计算制动/油门值。
  • MPCController:MPC控制器,组合了横向和纵向控制器。

结束语

本文主要以Apollo项目2.5版本为基础做了一些调查分析。

上文中我们也提到,目前的2.5版本仅仅是针对L2级自动驾驶的,而百度计划在2019年实现L3级自动驾驶,2021年实现L4级自动驾驶。可见,这个项目接下来的时间里将会非常高速的发展。

另外,今年我刚好有机会参加了上海的CES展。在展会上也看到了百度展出的两款自动驾驶车型:

一款是小型巴士。

还有一款是小型物流车。

今后我也会继续保持对该项目的关注,如果有更多的信息会继续分享给大家。

参考资料与推荐读物


如果你喜欢我写的文章,说不定我写的书: 《深入剖析Android新特性》也会对你有帮助。
 Contents