Apollo 2.0 软硬件框架初探(二)

本文原创作者:冀渊
地址:https://mp.weixin.qq.com/s/GccST3xJ1QRIVM7cFgsn3A


本篇开始将会与大家一起探讨 Apollo 的感知模块 (Perception) 。我会试着从整体框架开始,慢慢深入,与大家一同探讨。Perception 模块里代码较多、涉及面较广,我在理解代码的过程中有许多感到疑惑的地方。在叙述中,我会阐述我理解的部分,并列出一些我的疑惑点,希望可以与大家共同学习。

出于个人专业背景和内容重要性的考虑,我会把重点放在感知模块的障碍物感知部分。由于内容较多,所以本篇只会重点叙述 Lidar 的部分。Radar 和 fusion 及其他部分将会放在下一篇。

 Apollo 2.0 框架及源码分析(零) | 引言[1] 中,有介绍过一些笔者资料的参考来源,这次感知的部分也有参考 Apollo 开发者社区中分享的相关资料。对 Apollo 的感知及相关概念不是十分熟悉的同学,建议可以先看下这两份官方资料。同时,在本篇中也会用到其中的部分 PPT [2][3]。

Apollo 自动驾驶感知技术分享

PPT资料 | Apollo 自动驾驶感知技术


Apollo 感知模块 (Perception) 实现框架

无人驾驶系统车端的部分,大致可分为3块内容,感知,决策,控制。其中,感知是其余两者的基础,是无人驾驶中极为重要的一个部分。

无人驾驶中所用到的传感器各有长处和短板,单一的传感器难以满足复杂场景的需求,因此使用多种传感器进行相互融合(sensor funsion)就显得十分有必要了。

下图是 Apollo 公开课对无人驾驶中常见传感器的介绍,从图中看出,各个传感器擅长的情况不一,而依靠传感器的融合则可以应对大多数情况。

图片来源:PPT资料 | Apollo 自动驾驶感知技术

 

Apollo 的这张图整体表述不错,十分清晰。

但笔者的 mentor 认为图中对 Lidar 在 Lane Tracking 上的描述有点欠妥。他认为可以根据路面和车道线对 Lidar 射线反射强度的不同来追踪车道。因此 Lidar 在车道线追踪的能力应该可以算得上是 Fair 而不是和 Radar 一样的 Poor

另外,雷锋网的这篇文章中也提到了基于激光雷达回波信号检测车道线的可能性[4]。

专栏 | 如何利用激光雷达检测车道线?这里提供了4种方法

https://www.leiphone.com/news/201712/iG2xBYren1q9faI9.html?utm_source=debugrun&utm_medium=referral&viewType=weixin

Apollo 2.0 框架及源码分析(一) | 软硬件框架 中有介绍过,Apollo 2.0 感知的整体框架如下图所示,分为 3D障碍物感知 和 交通灯感知 两大部分 [5]。

Perception 框架,图片来源: Apollo 2.0 框架及源码分析,地址:https://zhuanlan.zhihu.com/p/33059132

 

其实这两者在 Apollo 官方文档和讲座中,已经有了比较清晰的讲解。笔者十分建议大家先阅读官方提供的材料。当然为了方便诸位阅读,本篇也会摘取其中部分重要内容,以期读者能够对感知部分的工作流程有个大体的了解[6][7]。

 


www.51apollo.com 无人驾驶,百度apollo,交流论坛


 

3D 障碍物感知

https://github.com/ApolloAuto/apollo/blob/master/docs/specs/3d_obstacle_perception_cn.md

交通灯感知

https://github.com/ApolloAuto/apollo/blob/master/docs/specs/traffic_light.md

Apollo 官方公开课中的对感知部分的功能是用下图描述的。

从图中可知,在 Apollo 中感知模块有以下几个职能:

  1. 探测物体 (是否有障碍物)
  2. 对物体分类 (障碍物是什么)
  3. 语义解析 (障碍物从背景中分割)
  4. 物体追踪 (障碍物追踪)

本篇接下来会叙述 Apollo 3D障碍物感知部分是如何实现上述职能的。

障碍物感知部分的框架如下图所示。障碍物感知主要依靠的是 Lidar 和 Radar 两者的感知结果的相互融合。该部分输入为传感器的原始数据,根据接收到的数据的来源的不同,进行不同的处理,最终输出融合后的结果。

Apollo 社区也提供了视频为其障碍物感知能力进行了演示,十分精彩。

观看地址:https://mp.weixin.qq.com/s/GccST3xJ1QRIVM7cFgsn3A


从代码上看,障碍物感知由3个主体部分组成: LidarRadar fusion。接下来,本篇将重点描述其中的 Lidar 部分的实现原理。

官方对 Lidar 部分的原理解释得极其清楚。大家结合上文列出的参考文档和PPT资料 ,应该就会对 Lidar 部分的整体工作原理会有个清晰的认识了。

如上图 PPT 所示,Lidar 部分简单来说是这样工作的:

  1. 输入为 Lidar 得到的点云数据,输出为检测到的障碍物
  2. 由 HDmap 来确定 ROI (region of interest),过滤 ROI 区域外的点
  3. 处理点云数据,探测并识别障碍物 (由 AI 完成)   AI => 深度学习
  4. 障碍物追踪

障碍物感知的各个部分的具体原理及细节,大家可以参考这篇官方文档 3D 障碍物感知[6]。

总得来说,Apollo 团队在 Lidar 部分投入了极大的精力。在Apollo 自己的演示视频中,Lidar 的感知能力被展现的淋漓尽致,十分精彩。但十分遗憾的是,Lidar 的最核心部分,障碍物探测和分类部分(Obstacle Detection & Classification),涉及到的 caffe 的源码是不开源的[8]。

caffe in docker is different from origin version and is not open source in Apollo 2.0. If you want to use outside docker please copy caffe’s *.h and *.so to your environment. Or wait for Apollo 2.5

由于笔者在深度学习上的积累有限,并且 Lidar 部分官方文档也已经十分清楚,因此本篇在此只再简单提炼下其中的一些要点,并加以一些细节以飨读者。

笔者和 mentor 在阅读代码时遇到的第一个问题是如何寻找模块的函数入口。很遗憾,我们直到最后对 Lidar 这一部分的函数入口的选择也有所疑惑,最终只能凭经验选取下面的函数,作为 Lidar 的入口来阅读代码。

入口函数位置: [https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/onboard/lidar_process_subnode.cc]


void LidarProcessSubnode::OnPointCloud(
      const sensor_msgs::PointCloud2& message);

关于程序的结构和各部分的函数入口的问题,笔者会在下一篇中说到。

根据代码的注释,这里分成7步对 Lidar 的流程进行叙述。

1. 坐标及格式转换 (get velodyne2world transform)

Apollo 使用了开源库 Eigen 进行高效的矩阵计算,使用了 PCL 点云库对点云进行处理。

该部分中,Apollo 首先计算转换矩阵 velodyne_trans,用于将 Velodyne 坐标转化为世界坐标。之后将 Velodyne 点云转为 PCL 点云库格式,便于之后的计算。

2. 获取ROI区域 (call hdmap to get ROI)

核心函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/onboard/hdmap_input.cc


bool HDMapInput::GetROI(const PointD& pointd, const double& map_radius,
                        HdmapStructPtr* mapptr);

查询 HDmap, 根据 Velodyne 的世界坐标以及预设的半径 (FLAG_map_radius) 来获取 ROI 区域。

首先获取指定范围内的道路以及道路交叉点的边界,将两者进行融合后的结果存入 ROI 多边形中。该区域中所有的点都位于世界坐标系。

3. 调用ROI过滤器 (call roi_filter)

核心函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/roi_filter/hdmap_roi_filter/hdmap_roi_filter.cc


bool HdmapROIFilter::Filter(const pcl_util::PointCloudPtr& cloud,
                            const ROIFilterOptions& roi_filter_options,
                            pcl_util::PointIndices* roi_indices);

官方文档对该部分是这么描述的:

高精地图 ROI 过滤器(往下简称“过滤器”)处理在ROI之外的激光雷达点,去除背景对象,如路边建筑物和树木等,剩余的点云留待后续处理。

一般来说,Apollo 高精地图 ROI过滤器有以下三步:
1. 坐标转换
2. ROI LUT构造
3. ROI LUT点查询

蓝色线条标出了高精地图ROI的边界,包含路表与路口。红色加粗点表示对应于激光雷达传感器位置的地方坐标系原始位置。2D网格由8*8个绿色正方形组成,在ROI中的单元格,为蓝色填充的正方形,而之外的是黄色填充的正方形。

ROI 过滤器部分涉及到了 扫描线法 和 位图编码 两个技术。具体来看,该部分分以下几步:

a. 坐标转换

将地图 ROI 多边形和点云转换至激光雷达传感器位置的地方坐标系。

b. 确定地图多边形主方向

比较所有点的 x、y 方向的区间范围,取区间范围较小的方向为主方向。并将地图多边形 (map_polygons) 转换为待加工的多边形 (raw polygons)。

c. 建立位图

将 raw polygons 转化为位图 (bitmap) 中的格点,位图有以下特点:

  • 位图范围, 以 Lidar 为原点的一片区域 (-range, range)*(-range, range) 内,range 默认 70米
  • 位图用于以格点 (grid) 的方式存储 ROI 信息。若某格点值为真,代表此格点属于 ROI。
  • 默认的格点大小为 cell_size 0.25米。
  • 在列方向上,1bit 代表 1grid。为了加速操作,Apollo 使用 uint64_t 来一次操纵64个grids。

为了在位图中画出一个多边形,以下3个步骤需要被完成:

i. 获得主方向有效范围

ii. 将多边形转换为扫描线法所需的扫描间隔:将多变形在主方向上分解为线(多边形->片段->线),计算每条线的扫描间隔。

iii. 基于扫描间隔在位图中画格点

关于扫描线,这里推荐笔者参考的一篇解析 扫描线算法完全解析 [9]。

d. ROI 点查询

通过检查 grid 的值,确定在位图中得每一个 grid 是否属于 ROI。

4. 调用分割器 (segmentor)

入口函数所在文件:cnn_segmentation.cc


bool CNNSegmentation::Segment(const pcl_util::PointCloudPtr& pc_ptr,
                              const pcl_util::PointIndices& valid_indices,
                              const SegmentationOptions& options,
                              vector<ObjectPtr>* objects)

 

分割器采用了 caffe 框架的深度完全卷积神经网络(FCNN) 对障碍物进行分割,简单来说有以下四步:


www.51apollo.com 无人驾驶,百度apollo,交流论坛


 

a. 通道特征提取

计算以 Lidar 传感器某一范围内的各个单元格 (grid) 中与点有关的8个统计量,将其作为通道特征(channel feature)输入到 FCNN。

1. 单元格中点的最大高度
2. 单元格中最高点的强度
3. 单元格中点的平均高度
4. 单元格中点的平均强度
5. 单元格中的点数
6. 单元格中心相对于原点的角度
7. 单元格中心与原点之间的距离
8. 二进制值标示单元格是空还是被占用如

计算时默认只使用 ROI 区域内的点,也可使用整个 Lidar 范围内的点,使用标志位 use_full_cloud_ 作为开关。

b. 基于卷积神经网络的障碍物预测

  • 与 caffe 相关的 FCNN 源码貌似是不开源
  • Apllo 官方叙述了其工作原理,摘录如下

完全卷积神经网络由三层构成:下游编码层(特征编码器)、上游解码层(特征解码器)、障碍物属性预测层(预测器)

特征编码器将通道特征图像作为输入,并且随着特征抽取的增加而连续下采样其空间分辨率。 然后特征解码器逐渐对特征图像 上采样到输入2D网格的空间分辨率,可以恢复特征图像的空间细节,以促进单元格方向的障碍物位置、速度属性预测。 根据具有非线性激活(即ReLu)层的堆叠卷积/分散层来实现 下采样和 上采样操作。

c. 障碍物集群 (Cluster2D)

核心函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/segmentation/cnnseg/cluster2d.h

#void Cluster(const caffe::Blob<float>& category_pt_blob,
               const caffe::Blob<float>& instance_pt_blob,
               const apollo::perception::pcl_util::PointCloudPtr& pc_ptr,
               const apollo::perception::pcl_util::PointIndices& valid_indices,
               float objectness_thresh, bool use_all_grids_for_clustering);

Apollo基于单元格中心偏移预测构建有向图,采用压缩的联合查找算法(Union Find algorithm )基于对象性预测有效查找连接组件,构建障碍物集群。

https://github.com/ApolloAuto/apollo/blob/master/docs/specs/3d_obstacle_perception_cn.md(a)红色箭头表示每个单元格对象中心偏移预测;蓝色填充对应于物体概率不小于0.5的对象单元。(b)固体红色多边形内的单元格组成候选对象集群。

d. 后期处理

涉及的函数:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/segmentation/cnnseg/cluster2d.h

void Filter(const caffe::Blob<float>& confidence_pt_blob,
              const caffe::Blob<float>& height_pt_blob);void Classify(const caffe::Blob<float>& classify_pt_blob);void GetObjects(const float confidence_thresh, const float height_thresh,
                  const int min_pts_num, std::vector<ObjectPtr>* objects);

  • 聚类后,Apollo获得一组包括若干单元格的候选对象集,每个候选对象集包括若干单元格。根据每个候选群体的检测置信度分数物体高度,来确定最终输出的障碍物集/分段。
  • 从代码中可以看到 CNN分割器最终识别的物体类型有三种:小机动车、大机动车、非机动车和行人。

在obstacle/lidar/segmentation/cnnseg/cluster2d.h中

enum MetaType {
  META_UNKNOWN,
  META_SMALLMOT,
  META_BIGMOT,
  META_NONMOT,
  META_PEDESTRIAN,
  MAX_META_TYPE
};

5. 障碍物边框构建

入口函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/object_builder/min_box/min_box.cc

void BuildObject(ObjectBuilderOptions options, ObjectPtr object)

边界框的主要目的还是预估障碍物(例如,车辆)的方向。同样地,边框也用于可视化障碍物。

如图,Apollo确定了一个6边界边框,将选择具有最小面积的方案作为最终的边界框。

 

6. 障碍物追踪

入口函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/tracker/hm_tracker/hm_tracker.cc

// @brief track detected objects over consecutive frames
  // @params[IN] objects: recently detected objects
  // @params[IN] timestamp: timestamp of recently detected objects
  // @params[IN] options: tracker options with necessary information
  // @params[OUT] tracked_objects: tracked objects with tracking information
  // @return true if track successfully, otherwise return false
  bool Track(const std::vector<ObjectPtr>& objects, double timestamp,
             const TrackerOptions& options,
             std::vector<ObjectPtr>* tracked_objects); 

障碍物追踪可分两大部分,即 数据关联 和 跟踪动态预估。Apollo 使用了名为 HM tracker的对象跟踪器。实现原理:

在HM对象跟踪器中,匈牙利算法(Hungarian algorithm)用于检测到跟踪关联,并采用 鲁棒卡尔曼滤波器(Robust Kalman Filter) 进行运动估计。

数据关联

数据关联的过程是确定传感器接收到的量测信息和目标源对应关系的过程,是多传感多目标跟踪系统最核心且最重要的过程[10]。

Apollo 首先建立关联距离矩阵,用于计算每个对象 (object ) 和 每个轨迹 (track )之间的关联距离。之后使用 匈牙利算法 为 object和 track 进行最优分配。

计算关联距离时,Apollo 考虑了以下5个关联特征,来评估 object 和 track 的运动及外观一致性,并为其分配了不同的权重。


关联特征                             一致性评估         默认权重

location_distance                运动                    0.6

direction_distance              运动                     0.2

bbox_size_distance           外观                      0.1

point_num_distance           外观                      0.1

histogram_distance           外观                      0.5


由上表可以看出,Apollo 在计算关联距离时,重点考虑的还是几何距离和两者的形状相似度。计算得到类似下图的关联距离矩阵后,使用匈牙利算法将 Object 与 Track 做匹配。


Object             track1       track2      track3

Object A           2               3               4

Object B           3               4               5

Object C           2               4               5

关联距离矩阵实例


关于匈牙利算法的实现原理,这里有篇很有趣的解释 趣写算法系列之–匈牙利算法 – CSDN博客[11]。

跟踪动态预估 (Track Motion Estimation)

使用卡尔曼滤波来对 track 的状态进行估计,使用鲁棒统计技术来剔除异常数据带来的影响。

不了解卡尔曼滤波原理的同学请参考:卡尔曼滤波器的原理以及在matlab中的实现 [12]。这一部分的滤波整体看来是一个标准的卡尔曼滤波。在此基础上,Apollo 团队加入了一些修改,根据官方文档,Apollo 的跟踪动态预估有以下三个亮点 :

  • 观察冗余

在一系列重复观测中选择速度测量,即滤波算法的输入,包括锚点移位、边界框中心偏移、边界框角点移位等。冗余观测将为滤波测量带来额外的鲁棒性, 因为所有观察失败的概率远远小于单次观察失败的概率。

卡尔曼更新的观测值为速度。每次观测三个速度值 :

锚点移位速度、边界框中心偏移速度 和 边界框角点位移速度。

从三个速度中,根据运动的一致性,选出与之前观测速度偏差最小的速度为最终的观测值。

根据最近3次的速度观测值,计算出加速度的观测值。

  • 分解

高斯滤波算法 (Gaussian Filter algorithms)总是假设它们的高斯分布产生噪声。 然而,这种假设可能在运动预估问题中失败,因为其测量的噪声可能来自直方分布。 为了克服更新增益的过度估计,在过滤过程中使用故障阈值。

这里的故障阈值应该对应着程序中的 breakdown_threshold_。

该参数被用于以下两个函数中,当更新的增益过大时,它被用来克服增益的过度估计:

  • KalmanFilter::UpdateVelocity
  • KalmanFilter::UpdateAcceleration

两者的区别在于:

速度的故障阈值是动态计算的,与速度误差协方差矩阵有关

velocity_gain *= breakdown_threshold_; 

加速度的故障阈值是定值,默认为2

acceleration_gain *= breakdown_threshold;  
  • 更新关联质量 (UpdateQuality)

原始卡尔曼滤波器更新其状态不区分其测量的质量。 然而,质量是滤波噪声的有益提示,可以估计。 例如,在关联步骤中计算的距离可以是一个合理的测量质量估计。 根据关联质量更新过滤算法的状态,增强了运动估计问题的鲁棒性和平滑度。

更新关联质量 update_quality 默认为 1.0,当开启适应功能时 (s_use_adaptive ==true)Apollo 使用以下两种策略来计算更新关联质量:

  1. 根据 object 自身的属性 — 关联分数 (association_score) 来计算
  2. 根据新旧两个 object 点云数量的变化

首先根据这两种策略分别计算更新关联质量,之后取得分小的结果来控制滤波器噪声。

7. 障碍物类型融合 (call type fuser)

入口函数位置:

https://github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/type_fuser/sequence_type_fuser/sequence_type_fuser.cc

/**
   * @brief Fuse type over the sequence for each object
   * @param options Some algorithm options declared in BaseTypeFuser
   * @param objects The objects with initial object type
   * @return True if fuse type successfully, false otherwise
   */
  bool FuseType(const TypeFuserOptions& options,
                std::vector<ObjectPtr>* objects) override;

该部分负责对 object 序列 (object sequence) 进行类型 (type) 的融合。

object 的type 如下代码所示:

enum ObjectType {
  UNKNOWN = 0,
  UNKNOWN_MOVABLE = 1,
  UNKNOWN_UNMOVABLE = 2,
  PEDESTRIAN = 3,
  BICYCLE = 4,
  VEHICLE = 5,
  MAX_OBJECT_TYPE = 6,
};

Apollo 将被追踪的objects 视为序列。

当 object 为 background 时,其类型为 “UNKNOW_UNMOVABLE”。

当 object 为 foreground 时,使用线性链条件随机场(Linear chain Conditional Random Fields) 和 维特比(Viterbi)算法对 object sequence 进行 object 的类型的融合。

由于笔者对统计学习方法上的积累不足,为避免误导,这部分就不过多叙述了。由于该部分没有任何文档,大家如需更多的细节,请自行浏览代码,查看注释。

 

相关链接:

Apollo 2.0 框架及源码分析(零) | 引言

【http://link.zhihu.com/?target=https%3A//github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/type_fuser/sequence_type_fuser/sequence_type_fuser.cc】

参考来源:

[1] Apollo 2.0 框架及源码分析(零) | 引言

【http://link.zhihu.com/?target=https%3A//github.com/ApolloAuto/apollo/blob/master/modules/perception/obstacle/lidar/type_fuser/sequence_type_fuser/sequence_type_fuser.cc】

[2] Apollo 自动驾驶感知技术分享

[3] PPT资料 | Apollo 自动驾驶感知技术

[4] 专栏 | 如何利用激光雷达检测车道线?这里提供了4种方法

【https://www.leiphone.com/news/201712/iG2xBYren1q9faI9.html?utm_source=debugrun&utm_medium=referral&viewType=weixin】

[5] Apollo 2.0 框架及源码分析(一) | 软硬件框架

【http://link.zhihu.com/?target=https%3A//www.leiphone.com/news/201712/iG2xBYren1q9faI9.html%3Futm_source%3Ddebugrun%26utm_medium%3Dreferral】

[6] 3D 障碍物感知

【https://github.com/ApolloAuto/apollo/blob/master/docs/specs/3d_obstacle_perception_cn.md】

[7] 交通灯感知

【https://github.com/ApolloAuto/apollo/blob/master/docs/specs/traffic_light.md】

[8] 交通信号灯识别模块caffe net的输入与输出层问题 · Issue #2547 · ApolloAuto/apollo

【https://github.com/ApolloAuto/apollo/issues/2547】

[9] 扫描线算法完全解析

【https://www.jianshu.com/p/d9be99077c2b】

[10][多源信息融合(第二版)]韩崇昭等著 清华大学出版社

[11] 趣写算法系列之–匈牙利算法 – CSDN博客

【http://blog.csdn.net/dark_scope/article/details/8880547】

[12] 卡尔曼滤波器的原理以及在matlab中的实现

【https://v.qq.com/x/page/o03766f94ru.html】


欢迎加入交流QQ群: 519 034 368

(非常欢迎您关注无人驾驶论坛的微信公众号)




(非常欢迎您关注Apollo官方公众号)



发表评论