阿波罗(Apollo)是百度发布的面向汽车行业及自动驾驶领域的合作伙伴提供的软件平台。发布时间是2017年4月19日,旨在向汽车行业及自动驾驶领域的合作伙伴提供一个开放、完整、安全的软件平台,帮助他们结合车辆和硬件系统,快速搭建一套属于自己的完整的自动驾驶系统。而将这个计划命名为“Apollo”计划,就是借用了阿波罗登月计划的含义。
可以在这里感受一下Apollo的实车驾车体验:CES 2018 百度Apollo 2.0无人车美国桑尼维尔试乘。
对于自动驾驶,SAE(Society of Automotive Engineers,美国汽车工程师学会) International于2014年发布了从全手动系统到全自动系统六个不同级别的分类系统,这6个级别的描述如下:
SAE Level | Name | System capability | Driver involvement |
---|
阿波罗项目的官网地址如下:http://apollo.auto
在阿波罗项目的官网,介绍了该项目有如下特点:
目前,其官网上列出的合作伙伴已经接近100家。
阿波罗项目的蓝图如下:
最新发布的Apollo 2.5版本主要目标是L2级自动驾驶。
详细的Apollo版本演进信息如下图所示:
可以在这里获取到阿波罗项目的源码:https://github.com/ApolloAuto。这个路径中包含了5个开源项目:
关于如何编译和运行阿波罗项目请参见这里:https://apolloauto.github.io。
执行该任务需要Ubuntu和Docker环境。
编译完成之后,可以在电脑上通过该项目提供的Dreamview功能来熟悉环境,Dreamview通过浏览器访问,其界面看起来是这个样子:
关于Dreamview的更多说明,请参见这里: Dreamview Usage Table
阿布罗平台的开发包含下面几个步骤:
ROS全称是Robot Operating System。它包含了一套开源的软件库和工具,专门用来构建机器人应用。其官网地址在这里:http://www.ros.org。
在一个ROS系统中,包含了一系列的独立节点(nodes)。这些节点之间,通过发布/订阅的消息模型进行通信。例如,某个传感器的驱动可以实现为一个节点,然后以发布消息的形式对外发送传感器数据。这些数据可以被多个其他节点接收,例如:过滤器,日志系统等等。
ROS系统中的节点可能位于不同的主机上,例如:在一个Arduino设备上发布消息,一台笔记本电脑订阅这些消息,一个Android手机也监测这些消息。
ROS系统中包含了一个主(Master)节点。主节点使得其他节点可以查询彼此以进行通讯。所有节点都需要在主节点上进行注册,然后就可以与其他节点通讯了。如下图所示:
熟悉Android系统的人可能很容易发现,这和Binder中的ServiceManager的作用是类似的。
节点之间通过发布和订阅主题(Topics)进行通讯。例如,在某个机器人系统中,位于机器人上有一个相机模块可以获取图像数据。另外在机器人上有一个图像处理模块需要获取图像数据,与此同时还有另外一个位于个人PC上的模块也需要这些图像数据。那么,相机模块可以发布/image_data
这个主题供其他两个模块来订阅。其结构如下图所示:
Apollo项目基于ROS,但是对其进行了改造,主要包括下面三个方面:
自动驾驶车辆中包含了大量的传感器,这些传感器可能以非常高频的速度产生数据,所以整个系统对于数据传输效率要求很高。在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上,运行的核心软件模块包括:
这些模块的交互结构如下图所示:
每个模块都作为独立的基于CarOS的ROS节点运行。每个模块节点都会发布和订阅某些主题。订阅的主题用作数据输入,而发布的主题用作数据输出。
关于Apollo平台的系统架构可以阅读这篇文档:HOW TO UNDERSTAND ARCHITECTURE AND WORKFLOW。
从这篇文档中我们看到:
${MODULE_NAME}/conf
目录下的配置文件,我们可以获得有关模块订阅和发布的主题的基本信息。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类的结构如下图所示:
该类中的主要函数说明如下:
函数名 | 说明 |
---|
在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
文件,是对这个模块的说明。我们可以以此为入口来了解模块的实现。
自动驾驶车辆通过前置摄像头和雷达与最近的车辆(closest in-path vehicle,简称CIPV)保持距离。子模块还预测障碍物运动和位置信息(例如,航向和速度)。Apollo 2.5支持高速公路上的高速自动驾驶,无需任何地图。深度网络算法已经学会处理图像数据。随着收集更多数据,深度网络的性能将随着时间的推移而提高。
模块输入:
模块输出:
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模块从Perception模块接受障碍物信息。该模块需要的信息包括位置,航向,速度,加速度,并产生具有障碍概率的预测轨迹。
模块输入:
模块输出:
Prediction的Init
函数中添加了三个回调用来从其他模块获取信息的更新:
AdapterManager::AddLocalizationCallback(&Prediction::OnLocalization, this);
AdapterManager::AddPlanningCallback(&Prediction::OnPlanning, this);
AdapterManager::AddPerceptionObstaclesCallback(&Prediction::RunOnce, this);
这里最重要的就是Prediction::RunOnce
这个函数。这个函数中包含了Prediction模块的主要逻辑,它会在接收到一个新的障碍物消息时触发。
Prediction模块中有三类重要的子模块。
第一类是Container,用来存储从订阅频道获取的数据。包括:
第二类是Evaluator,用来针对指定的障碍物预测路线和速度。目前有三类Evaluator,包括:
Evaluator通过EvaluatorManager
类管理,Evaluator类结构如下图所示:
Prediction模块中第三类重要的子模块就是Predictor。它用来预测障碍物的轨迹。
不同的障碍物运动的轨迹会不一样,因此实现中包含了很多个类型的Predictor,它们的结构如下图所示。
类似的,会有一个PredictorManager
来管理Predictor。
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模块在初始化的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& 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模块的主体逻辑也是通过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模块内置了三个控制器,它们的结构和说明如下:
本文主要以Apollo项目2.5版本为基础做了一些调查分析。
上文中我们也提到,目前的2.5版本仅仅是针对L2级自动驾驶的,而百度计划在2019年实现L3级自动驾驶,2021年实现L4级自动驾驶。可见,这个项目接下来的时间里将会非常高速的发展。
另外,今年我刚好有机会参加了上海的CES展。在展会上也看到了百度展出的两款自动驾驶车型:
一款是小型巴士。
还有一款是小型物流车。
今后我也会继续保持对该项目的关注,如果有更多的信息会继续分享给大家。