Apollo 系统传感器是以 Lidar 为主的,本文整理在 Apollo 6.0 中 Lidar 的基本代码流程。
1. 从 Component 出发
因为有了之前红绿灯检测代码分析的经验,我们自然能够知道感知任务先从它的 component 开始。
lidar 检测的 component 是
modules/perception/onboard/component/detection_component.cc
梳理代码可以得到基础的流程框架:
1.1 initConfig
initConfig 从本地目录中读取 proto 文件,然后配置变量。
那么,如何知道从哪里读取呢?
modules/perception/production/dag/dag_streaming_perception_lidar.dag
在 dag 目录下有配置。
我们只需找到 lane_detection_component.config 文件。
/apollo/modules/perception/production/conf/perception/lidar/velodyne128_detection_conf.pb.txt
配置文件说明了激活高精度地图,然后检测的结果存放在 DetectionObjects 这个 channel。
1.2 initAlgorithmPlugin
代码非常简单,创建一个 LidarObstacleDetection 对象,然后赋值给 detector_ 并初始化。 我们应该能够察觉到 LidarObstacleDetection 是算法核心实现类,后面我们将重点关注它。
1.3 Proc 和 InternalProc
Proc 方法中内部调用了 InternalProc 。
InternalProc 的逻辑非常简单,主要是做一个消息结构体的转换。
代码语言:javascript复制in_message --> out_message
实际上就是
代码语言:javascript复制PointCloud --> LidarFrameMessage
点云数据到 LidarFrame 数据的转换。
实际上通过 detector_->Process() 完成。
我们可以把目光移到 LidarObstacleDetection 这个类上
2. detector_->Process() 算法逻辑
路径是:
代码语言:javascript复制modules/perception/lidar/app/lidar_obstacle_detection.cc
我们阅读代码,可以得到这样的流程图:
其实整个过程非常的清晰,分 2 步走。
代码语言:javascript复制1. 点云预处理
2. 点云检测
2.1 点云 Preprocesses() 和 ProcessCommon()
点云预处理比较简单,就是将 PointCloud 中的点云过滤,然后复制到 LidarFrame 结构体对应的 cloud 中。 代码地址:
代码语言:javascript复制modules/perception/lidar/lib/pointcloud_preprocessor/pointcloud_preprocessor.cc
代码片断
那好,下面看看 ProcessCommon() 中发生了什么?
这里面又有一个 detector,代码调用了其 Detect() 方法,所有的真相应该可以在这里揭开。
查看变量声明,不难找出 detector 是个什么东西。
detector 是由 PointPillarsDetection 实现的。
2.2 PointPillarsDetection
代码路径:
代码语言:javascript复制modules/perception/lidar/lib/detection/lidar_point_pillars/point_pillars_detection.cc
Apollo 6.0 中激光雷达检测算法是由 PointPillar 模型实现的,我之前有文章介绍了其模型设计思想:自动驾驶激光点云 3D 目标检测 PointPillar 论文简述
在本文我关注于 Lidar 整体检测流程而不是 PointPillar 模型思想,所以不会过多介绍 PointPillar 本身,有兴趣的同学可以查看我上面的链接。
Detect() 方法代码比较多,这里没法直接张贴,根据代码注释绘制了流程图。
点云送到模型检测前要经过 downsample 步骤。 downsample 还经过 2 次:
- DownSamplePointCloudBeams
- DownSampleByVoxelGrid
在这里 downsample_factor 是大于 1 的整数,是下采样的因子,有点像卷积操作中的 stride,每隔 downsample_factor 取一点,最终减少了总体点云数据量。
下采样之后,要进入 fuse 阶段。 fuse 的是当前的点云和之前的点云。
融合也不代表所有的历史点云都参与融合。
代码语言:javascript复制// before fusing
while (!prev_world_clouds_.empty() &&
frame->timestamp - prev_world_clouds_.front()->get_timestamp() >
FLAGS_fuse_time_interval) {
prev_world_clouds_.pop_front();
}
超过 FLAGS_fuse_time_interval 就直接被剔除。
把过滤掉无效点云的历史数据全部加入当前点云当中。 问题:为什么点云需要前后数据的融合呢? 在代码中我直接看不明白,但我个人的猜测是:
无论是历史数据还是当前测量的数量,其实都存在误差,两者融合就有些像训练神经网络前处理中的数据增强,这样能够有效减少数据误差同时提升神经网络模型做点云检测的准确性和稳定性。
当然,这只是我个人的猜想,有不同理解的同学可以留言一起参与讨论。
数据融合之后,再做一些 shuffle 之类的操作就直接送到推断引擎中去做前向推断了,最后又通过神经网络输出的结果得到最终的检测目标。
推断引擎应用的是工厂模式,之前车道线检测的文章已经分析过,这里不再赘述。 倒是对 PointPillar 模型本身感兴趣的同学可以看下:
可以看到模型文件是 onnx 格式的,之前有的模型是 caffe2,有的是 libtorch,这说明 Apollo 框架确实强大。
最后的 GetObjects() 方法中代码比较长,就不张贴了。 大致内容是取得 Object 的方向、边框、类别。
到此,Lidar 检测的代码流程基本上介绍完毕。
总结
通过梳理代码可以发现
- Apollo 是个很厉害的自动驾驶框架,各个感知任务的 Component 工作流差不多一样,有固定的套路,这也让大家能够比较轻松地学习它,可以一个模块一个模块地学。
- Apollo 支持现有的神经网络模型导入并灵活配置,比如 Lidar 用 PointPillar,以后有新的模型也可以比较容易替换。
- 算法是自动驾驶一部分,我们经常看见论文中讲得模型有多厉害,但实际编码中还需要前处理、后处理过程,这其实就是传统的代码能力,所以,大家不要小看自动驾驶,在嵌入式平台上跑,算法模型重要,代码更加重要,两者是相辅相成的。不要轻代码重算法,也不要重代码轻算法。当然,如果团队够大,大家各司其职就好了,这一部分的压力是要系统架构师承担的。