本文整理自百度技术沙龙。本文讲解了百度自动驾驶团队对 ROS 框架在自动驾驶场景使用中的一些实践和探索,以及在系统稳定性、通信效率及数据兼容性方面的改进。 今天主要介绍 ROS 框架,以及 ROS 框架应用在自动驾驶过程中所遇到的问题和相应的改进,同时介绍 Apollo 框架如何下载、如何使用以及如何在本地获取最基本的版本。
自动驾驶是非常复杂的系统,包含感知、障碍物检测、决策、车辆控制等多个模块,同时自动驾驶还需要处理激光雷达、相机、GPS 等大量数据并且实时反馈。如何把这么多功能各异,非常复杂的模块集成到一起,同时及时扩大稳定性资源,组成一个完整的自动驾驶系统完成自动驾驶的任务,是一个非常大的挑战。
为什么要用框架,一是需要高效的开发支持。自动驾驶处于快速迭代和快速发展过程中,无论感知算法、决策算法还是整体技术开发,无论是以 2D 为主的视觉方案还是以 3D 为主的方案,都在快速迭代中,对快速地构建系统和功能验证有非常大的需求。通过使用框架开发希望算法工程师能够将更多精力放在算法功能的研发上,而诸如配置管理、部署运行、底层通信等功能由框架来统一提供,这样可以快速地构建系统原型,验证算法和功能。
二是希望框架能够让模块灵活配置。像感知、定位、决策模块,本身都是功能相对独立的模块,模块之间只有数据依赖关系,对于框架来说,需要能够在开发阶段减少各个模块之间的耦合,而在车上运行阶段能够将各个模块组合串联起来。
三是自动驾驶系统涉及大量图像、主体算法,对于各种可视化工具有非常强烈的需求,比如障碍物检测算法,需要看图像识别障碍物框得准不准,规划路径是否合理,定位算法也需要看调试时车是不是在正确位置上,所以调试过程中可视化工具的支持也是非常重要的一点。
以上三点是框架最需要满足的要求。
基于以上的问题选择了 ROS 作为现在开发和调试的框架。做机器人和无人车相关研究的同行一般都会比较了解 ROS 框架。ROS 前身是斯坦福的人工智能实验室开发的一个机器人集成开发框架,从软件构架的角度说,它是一种基于消息传递通信的分布式多进程框架。ROS 很早就被机器人行业所使用,很多知名的机器人开源库,比如基于四元数的坐标转换、3D 点云处理驱动、定位算法 SLAM 等都是开源贡献者基于 ROS 开发的。截止现在为止,ROS 框架大概有 3000 多个基础库,有这么丰富的基础库的支持,我们基于 ROS 的开发可以达到很高的效率。
因为 ROS 是基于消息机制的,开发者可以根据功能把软件拆分成为各个模块,每个模块只是负责读取和分发消息,模块间通过消息关联,并完成最终的功能。当模块间真正要上车的时候,通过我们的框架可以把各个模块快速地集成到一起。
此外,ROS 是学术界使用最广泛的框架,对于实验各种新算法也提供了便利。正是由于 ROS 的以上特性,我们在 Apollo1.0 选择 ROS 作为我们的研发和调试框架,用于快速验证技术方案和算法功能。
是否 ROS 就能够满足我们所有的工程需求,答案并不是。在我们实际调试过程中遇到很多 ROS 带来的问题。因为 ROS 本身的定位是基于学术研究、快速搭建原形的框架,所以在工程化考虑并不是很多,比如大数据传输时的性能问题,ROS 基于 Master 单中心拓扑的单点问题,以及数据格式缺乏兼容性问题,都对我们的使用造成一定影响。
我们对这些主要问题做了定制化的优化和改动,以适应 ROS 在无人车上的应用,下面具体介绍一下这三方面的问题。
自动驾驶系统为了能够感知复杂的道路场景并完成驾驶任务,需要多种传感器协同工作,以覆盖不同场景、不同路况的需求。比如在白天光照情况非常良好的时候,更多依赖相机做障碍物识别,到了夜晚光照条件比较差的时候,相机很难发挥作用,这时候更多依赖激光雷达。在雨雪天气环境比较恶劣的时候,激光雷达也受到一定影响,此时需要使用哈姆波雷达完成其他障碍物检测和功能。
多种传感器共同使用的一大问题就是会引入很大的传输带宽需求,如下图所示,简单列了车上一些常用的传感器及这些传感器的带宽需求,可以看到其中相机和激光雷达数据量是非常大的,分别达到了 180MB/S 和 70MB/S,而主流的多传感器融合方案至少会包含一个激光雷达和多个相机,如此大的数据量对通信的性能有很大的挑战。
多种传感器共同使用的一大问题就是会引入很大的传输带宽需求,因为每个传感器数据量巨大,多传感器同时使用会对车载系统造成很大压力。如图所示,相机和激光雷达是两个数据量最大的模块,其中相机数据量达到 120MB/S 到 180MB/S,如果使用压缩数据量会小一些,但是量级不会变化。激光雷达使用 64 线激光雷达,会达到 50MB/S 到 70MB/S 的数据量。而主流的多传感器融合方案至少会包含一个激光雷达和多个相机,如此大的数据量对通信的性能有很大的挑战。
如果系统出现通信问题,一般会出现高延迟和丢帧两个现象。对于自动驾驶来说,高延迟是车的反应很慢,这是非常危险的,而丢帧相当于车看不见红绿灯、障碍物,也会引起非常危险的行为,这是在自动驾驶系统中绝对不会被允许的。
目前同一个机器上的 ROS 节点之间通过 socket 进行进程间通信,中间存在多层协议栈以及多次用户空间和内核空间的数据拷贝,造成了很多不必要的资源占用和性能损耗。
这在普通的机器人系统数据量不大的时候并没有问题,而在数据量很大的无人车系统中,该问题就会显著地影响系统的性能。
针对这个问题,我们选择使用共享内存的方式来替代 socket 作为进程间通信的方式,通过减少不必要的内存拷贝,大大降低了系统的传输延时和资源占用。从这个图上可以看到,通过内存优化,数据拷贝、延迟和消耗都是原来的一半左右。
上面讲的是 1 对 1 的传输场景下的优化,现在我们来看下在单对多的传输场景。无人车系统中单个传感器数据经常会被多个算法或工具使用,如上图示例,对于 camera 的图像,同时会有障碍物检测、红绿灯识别、定位等等模块使用。
ROS 在底层并没有实现多播机制,在处理一对多的消息传输时,底层实现实际是多个点对点的连接,当把一份数据要发给四个节点时,相同的数据会传输四次,这造成了很大的资源浪费。
基于共享内存传输方式,共享内存本身的特性天然能够支持一次写入、多次读取功能,对于一对多的传输场景,相同的数据包只需要写入一次即可,成倍地提高了传输的效率。
下面是我们基于原生 socket 和共享内存传输的一些对比数据,可以看到在吞吐量、资源占用、传输延迟这三个关键指标上都有了很大的提升。
在一对一传输过程中,共享内存吞吐量能达到 socket 两倍,而在一对多传输过程中,随着接收者的增多,共享内存、传输量的优势会进一步扩大。
共享内存传输延迟基本会比原生 socket 节省 50%,现在系统整个传输延迟需要 100 毫秒,共享内存能节省 50 毫秒。当车行驶速度 70 公里时,1 秒走 50 米左右,50 毫秒就是 1 米多,对整个系统有很大帮助。
共享内存 CPU 资源占用也要比 socket 减少很多,车载计算硬件不可能无限扩容,在车上硬件资源的分配是非常重要的。如果能在传输过程中减少更多资源消耗,把更多计算资源留给算法,一定能够提升算法能力。
ROS 是以 Master 为中心构建的 Hybrid p2p 拓扑网络,有比较强的容错性,当某个算法出现异常导致崩溃的时候,不会引起整个系统的异常,为局部异常处理提供了便利。
ROS 系统并不是一个完整的 P2P 拓扑网络。ROS 拓扑网络完全基于中心节点,各个节点都需要通过 Master 注册,并且通过 Master 获得网络的拓扑状态。上图左边是 ROS 节点连接的过程示例,一个发布者启动时会发布数据,当订阅者启动时,会向 Master 注册要接收的信息,Master 对所有结点注册信息进行统一整理之后,会通知订阅者所订阅消息的发布情况,并且把发布者通信地址传给订阅者,订阅者会通过地址与发布者建立连接,建立连接之后,发布者和订阅者通过点对点传输方式进行真实的消息传播。真实的数据传播都是在发送者和接收者之间的点对点传输,数据不需要通过 Master,能减少数据的传播和 Master 的负载。但是也存在一个致命弱点,整个系统非常依赖 Master 这个单点,一旦 Master 崩溃,所有节点都不能发现其他节点,这样整个系统就不能正常工作。
虽然 Master 使用频率不是很高,但是通过加监控程序监控 Master 状态,如果挂掉立刻重启也不能解决这个问题。因为 ROS 并没有 Master 的异常恢复机制,Master 里所有节点的注册信息,只存在 Master 内存中,并没有持久化或其他存储方式,所以当 Master 崩溃后,Master 里所有消息随之消失,即使重启 Master,也无法使其恢复。所以 Master 这个单点在现有结构上是很难解决的。
Master 单点的问题在多机的方案中更加明显。一般自动驾驶系统中是多机方案,当一套自动驾驶系统由两台机器构成时,一般用于冗余备份,但由于 ROS Master 的存在,两台机器并不是对等的。
如上图所示,如果 Master 部署在 A 机上,当 CompuerA 出现宕机后,ComputerB 中的节点无法再访问 Master 获取拓扑信息,虽然已经建立过的点对点连接可以继续使用,但新加入的节点或者拓扑变化将不再能更新,系统处于一个功能不完整的状态,失去了冗余系统的意义。
为了解决这个问题,我们在 ROS 在里面加入了基于 RTPS 协议的服务发现功能。
整个网络拓扑不再以 master 为中心构建,而是通过域的概念进行划分。所有节点加入域中,会通过 RTPS 协议相互广播和其他节点建立点对点连接,建立连接后再通过单播进行订阅、发布话题等消息交换,以替代 Master 作为中央信息交换的功能。
网络中一个新节点加入时,会通过广播通知域内的所有节点,广播内容包括 NODE 的名字,以及 NODE 的通信地址。广播是基于不可抗协议的周期性心跳信息,每隔几秒钟会向所有的节点发送通信方式,保证消息不会被丢弃。
节点发现之后,所有节点间会两两建立 unicast 连接,并且基于这个连接做消息交换。比如节点需要发布或订阅一个消息,作为消息的交换。
基于消息交换的通道,已有节点会对新加入节点发送历史拓扑消息。比如系统发布一个消息,此时订阅者还没有加入网络,过了一段时间后订阅者加入网络,遗失了之前的拓扑信息,因此需要已有节点向新加入节点发布所有历史拓扑信息,这样就不会有历史消息丢失的问题。订阅者收到信息,会与对应发布者建立点对点连接,之后这两个节点就可以直接进行点对点通信。
通过这种方式,能够使 ROS 网络的拓扑发现不再依赖 Master 单点,完全实现网络拓扑。对于多机拓扑、冗余备份和仿真都有很大帮助。而且这个修改是完全基于 ROS 底层的修改,对上层应用完全透明,原有的 ROS 程序使用这套协议不需要对代码做任何修改就可以直接适配。
ROS 通信通过 message,message 是 ROS 中描述软件组件接口的一种语言。当两个节点之间需要建立连接时,通常需要满足两个条件,一是接收和发送的 Topic 属于同一个话题,而且两个模块定义的模式是完全一致的。就像打电话一样,Topic 是对方电话号码,message 是说话语言,如果使用不同语言,双方还是无法通信,所以 message 是组件的接口语言。
ROS 怎么描述 message?ROS 使用 msg 文件通过生成语言对应的接口满足不同语言的通信交流需求。通过 msg 文件描述每个字段的消息类型,以及消息名称可以编译出对应的.h、.py、.java 等,这样开发者就可以与其他现有系统进行无缝衔接和以及进行任务的替换。
ROS 系统为了保证收发双方的消息格式一致,会对 message 做 MD5 校验。只有 messageMD5 值完全一样的节点可以通信。这种严格的校验导致了节点的兼容性问题。比如一个接口增加了一个新的可有可无的字段,但是因为它的增加导致 MD5 变化,系统就识别为一种新的消息,当需要与其他模块进行通信时,就会遇到 MD5 mismatch 的问题。甚至只调整两三个字段的顺序都会有 MD5 的变化,会有兼容性的问题。在项目规模比较小时影响不大,但是对无人自动驾驶这样庞大的项目就有很大影响。当某一个模块接口升级了,需要把所有相关模块升级到最新版本之后,才能一起进行基础功能的联调。同时在线下仿真时,经常需要把某一个模块回溯到历史版本验证或定位某一个问题,这时候接口之间出现升级,就会出现不兼容问题,导致系统完全跑不起来。
接口兼容性问题会对历史数据使用造成更大影响,无人车系统中历史数据是非常宝贵和重要的资源,可以做离线训练。由于 ROS 系统对于 message 严格的校验机制,导致不同版本的数据只能用于对应版本的程序,进行运行和仿真,老版本数据不能用于新版本系统进行回归测试,新版本的数据也不能用于老版本的系统,这样对于历史数据的价值和使用造成非常大影响。
对于这种问题,虽然可以通过离线批量转换或运行时 adapter 来解决,但对于无人车这样每年产生的数据以 T,甚至以 P 计算的庞大系统,如果经常做数据转换迁移,无论在技术上还是效率上都是难度很大,很难实现的。
基于这个问题,我们决定使用 protobuf 来替代 ROSmessage。protobuf 是现在比较流行的接口语言,最大好处是丰富的类型系统,包括常规类型,可以完全覆盖 message 本身包含的类型,有利于把既有的 ROSmessage 迁移到 protobuf 格式下。此外 protobuf 最重要的一点是有非常良好的兼容性支持,我们只需要在使用的时候谨慎的使用 required 字段,就能带来非常好的前向兼容性和后向兼容性。
在使用 protobuf 过程中也会遇到一些问题,由于 ROS 本身并不原生支持 Protobuf 的消息,在开源社区对此的使用一般是先将 Protobuf 消息序列化为 String,再使用 ROS 的 String 方式进行传输。
但这种方案本身无论在使用的成本还是实际效果上,都存在一定的问题,而且由于消息本身都是以序列化后的 String 方式使用,在调试工具中无法支持,会出现乱码等问题。
基于这种情况,ApolloROS 做了一整套对 protobuf 的支持,做了相应的定制和修改,对于开发者来说可以完全沿用 ros message 的使用方式,比如在可以编译的时候,完整地编译出文件。在工程中不需要做任何其他转换,同时在使用的时候能够正确地解析出 protobuf 消息。这样既能够很好地解决兼容性问题,也不会有额外的学习成本和使用成本。
在 Apollo 目前开源代码中都是使用这种方式调用接口,大家如果感兴趣可以看一下详细是怎么使用的。
以上是我们在 ApolloROS 版本中做的更新和优化,但是是否 ROS 加上这些优化就可以胜任无人车的计算方案,最后做工程化和产品化落地,答案是否定的。在实际使用中,由于 ROS 的价格问题,缺乏实时性的支持和确定性保障,以及缺少统一的全局调度和资源管理,而且如果产品化发布,我们希望自动驾驶系统能够有更高的集成性和更强的鲁棒性,这些 ROS 系统很难实现和支持。对此,我们在内部也使用了新一代纯自然的计算框架,系统解决相关问题,目前在内部测试和使用中。后续如果比较稳定会随后面的 Apollo 计划开放出来给大家使用。
以上是关于 Apollo 中对于 ROS 的使用和改进。
目前 Apollo 所有代码已经提交到 Github 上,大家可以在上面下载相关的工具和文档,同时在百度开放平台和 Apollo 开放平台上了解最新资讯。
简单介绍一下代码拉下来之后怎么做,我们提供了快速开始的过程。第一步,安装 docker 系统,Apollo 发布的时候通过 docker 提供开发和运行环境,一共是两个 docker,主要面向开发者,下载下来后可以重新修改、编译自己的 Apollo 系统,并且在离线和在线进行使用。第一步需要用 install-docker 脚本安装和部署 docker 环境,包含下载、代码等一系列工作,安装完之后需要注销,并且用户重新登陆,里面涉及用户权限的改变,需要当前用户注销之后才能完全生效,这也是大家使用反馈比较多的问题,所以这里特殊讲解一下。
第二步,bash Apollo.sh build。
第三步,启动 Apollo。Apollo 代码自动映射在里面,进入之后直接就是 Apollo 的根目录。这时候需要对 Apollo 系统进行编译,这也是一键式的,能把系统所需的工具和应用全部编译出来。编译完成之后启动 Apollo,这里提供了一键启动版本,可以把后台的应用和前端显示全部启动起来。
HMI 界面可以自动映射到主机的系统中,在启动系统中只需要打开浏览器,就能够看到整个 HMI 截面,可以直接看到 Apollo 的 HMI 界面。
把 Dreamview 打开,这也是工程师看的最多的界面,这个界面会显示主车路径、刹车油门状态、方向盘状态信息,但由于离线显示没有数据,所以这个车暂时静止在这里不动。
我们也为大家离线使用提供了可靠的 demo,这时候再回到 HMI 界面,可以看到车沿着既定的路面运行,同时可以看到规划轨迹,右上角可以看到刹车油门和车的速度,以及方向盘的控制转角。至此 Apollo 离线 demo 就可以跑起来了。
以上只是简单的步骤,在 quickstart 文件中有详细的介绍,包括中文和英文的各种文档。相信大家通过文档都能在自己的机器上跑起来,或者是组装一套完整的 Apollo 车辆。
如果大家想对我们的 Apollo 做定制化修改,比如增加全新的驱动,或者是想自己调试一下参数,看看车还能不能像原来那样顺滑地跑过去。我们 howto 这里有一系列教程,告诉大家如何改造自己的自动驾驶系统。当大家有需要的时候可以直接在 howto 中看,都是非常详尽的教程。
以上就是我今天给大家分享的内容,谢谢大家!
何玮,百度自动驾驶资深架构师,专注于百度自动驾驶车载计算框架相关工作。
关注 AI 前线公众账号(直接识别下图二维码),点击自动回复中的链接,按照提示进行就可以啦!还可以在公众号主页点击下方菜单“加入社群”获得入群方法~AI 前线,期待你的加入!