自动驾驶 Apollo 源码分析系列,感知篇(四):将红绿灯检测和识别代码细致走读一遍

2021-03-02 14:34:12 浏览数 (1)

前一篇文章讲到了 Apollo 中红绿灯算法逻辑和各部分代码位置及粗略流程,本篇文章更进一步去分析具体代码实现。

1. 相关代码流程及核心类

Apollo 是一个开源的框架,这个框架非常的庞大,研究代码的时候很容易陷入进去,为此,我根据不同的任务梳理了脉络,在红绿灯检测模块,我根据自己感兴趣的线索得到了下图:

之前的文章有做过说明,Apollo 是讲究 Module 这个概念的,Module 里面有不同的 Component 实现。

所以,如果要追踪整个红绿灯识别的代码流程,那么就需要从 TrafficLightPerceptionComponent.cc 开始。

1.1 基础流程
代码语言:javascript复制
modules/perception/onboard/component/trafficlights_perception_component.cc

上面是 trafficlights_perception_component.cc 的路径。 阅读代码可以得到基础流程。

忽略一些细节,可以看到流程非常简单,component 进行了一系列的初始化,就 OK 了。

所以,我们有必要看看它到底进行了哪些内容的初始化。

1.2 InitConfig

这个是用来初始 Config 的也就配置文件。 流程如下:

配置文件以 proto 文件保存,定义在哪里呢?

我在文章最开始的思维导图当中有答案。

modules/perception/production/conf/perception/camera/trafficlights_perception_component.config

好,那看看里面定义了什么。

其实就是将一些变量抽出来,做成了配置文件形式,方便后续的灵活配置。

1.3 InitAlgorithmPlugin

这个按照名字的意思是初始化算法插件,既然是插件,那肯定支持动态加载。 而实际上,这一段非常重要,因为核心相关代码都在这里进行初始化。

我在前一篇文章当中有讲过,红绿灯检测分为 3 部分:

  • 前处理
  • 算法处理
  • 后处理

所以,在 InitAlgorithmPlugin 的时候,我们可以看到 TLPreprocessor 的身影,这个很容易想到,它就是用来进行前处理的。

另外,还初始化了:

  • SensorManager
  • TransformWrapper
  • TrafficLightCameraPerception

内容比较多,我们简化处理,重点关注这 2 个:

  • TLPreprocessor
  • TrafficLightCameraPerception
1.4 Init CameraFrame

Apollo 把 Camera 中相关的数据定义为 CameraFrame。 CameraFrame 是一个 Struct,也不单单只为 TrafficLight 服务,它是通用的,可以应对车道线检测、目标检测、红绿灯检测任务。

对于 TrafficLight 检测,本文只需要关注 CameraFrame 中的这几个:

  • timestamp
  • frameid
  • data_provider
  • std::vector<base::TrafficLightPtr> 根据名字猜测,data_provider 应该是用来存储原始的图像数据,TrafficLightPtr 应该是检测到的红绿灯对象指针。
1.5 initCameraListeners

代码在初始化 CameraFrame 之后,进行了关键的一步,就是初始化 CameraListeners。 因为 Camera 可能有几个,所以,Listener 也对应有几个。

实际上,这一段代码也并不长。

代码语言:javascript复制
int TrafficLightsPerceptionComponent::InitCameraListeners() {
  // init camera listeners
  for (size_t i = 0; i < camera_names_.size();   i) {
    const auto& camera_name = camera_names_[i];
    const auto& camera_channel_name = input_camera_channel_names_[i];
    const std::string camera_listener_name = "tl_"   camera_name   "_listener";

    typedef const std::shared_ptr<apollo::drivers::Image> ImageMsgType;
    std::function<void(const ImageMsgType&)> sub_camera_callback =
        std::bind(&TrafficLightsPerceptionComponent::OnReceiveImage, this,
                  std::placeholders::_1, camera_name);
    auto sub_camera_reader =
        node_->CreateReader(camera_channel_name, sub_camera_callback);
    last_sub_camera_image_ts_[camera_name] = 0.0;
  }

  return cyber::SUCC;
}

值得注意的地方是,运用了 C 11 中的 std::function 和 std::bind 模板,主要作用就是用来设置回调函数。 在这里,回调函数是 onReceiveImage,当 channel 中有数据来临时,camera reader 触发 onReceiveImage 函数,而红绿灯检测就自此开始。 所以,到这里,至少我们知道了图片从哪里来.

1.6 onReceiveImage

当图像来临时,代码会如何处理呢?

这一个函数应该就是红绿灯检测的核心代码部分。

updateCameraSelection 这段代码如果要展开将非常的长,我简单概括。 通过时间戳查询车辆的位姿。 通过给 hd map 输入位置和时间戳,来查询地图中的红绿灯。 需要注意的是 hd map 中查询得到的红绿灯用 Signal 表示。 算法将 Signal 加工得到 TrafficLight。 TrafficLight 会保存 ROI 信息。 ROI 是利用相机模型计算公式,将红绿灯距离车辆的距离通过内外参投影到对应的相机成像图片上。 camera 选择好后,需要进行信息同步。 SyncInformation

代码语言:javascript复制
if (image_timestamp < last_pub_img_ts_) {
    AWARN << "TLPreprocessor reject the image pub ts:" << image_timestamp
          << " which is earlier than last output ts:" << last_pub_img_ts_
          << ", image_camera_name: " << cam_name;
    return false;
  }

这个是同步我根据代码猜测应该是为了保证红绿灯的检测有序进行,过时的信息将不会重复处理,这可能是异步带来的问题。

fillImageData 这个不需要解释太多,将接收到的 image 信息存储到 CameraFrame 当中去。

之后调用 traffic_light_pipeline.Perception。

这个 traffic_light_pipeline 是什么呢? 就是一个 TrafficLightCameraPerception 对象。 我在前一篇文章当中就已经分析过它了。 它是算法的核心类。 Apollo 是一个大型的开源库,很多算法都是比较经典的论文复现。

TrafficLightCamera 里面 detector_,recognizer_,tracker_ 是 3 个核心算法对象,分别对应红绿灯的检测、识别、跟踪。 它们都是基于神经网络。 感知相关的模型都存在一个 Models 目录中,路径如下:

它们经 lib 中的检测器调用,最终通过 Inferrence 选择合适的推理引擎,然后输出结果。

在 camera/lib 目录下存放着各类检测器的代码实现。

在 Perception 目录下有一个 Inference 目录,有大家平时常用的各类推理引擎。

通过工厂方法模型可以针对模型名字选择合适的引擎。

某个神经网络模型是何种模型,通过在 models 目录下的 pt 文件当中定义。 RTNet 应该对应的是 TensorRT 的引擎文件。 所以,到这里 Perception 中的逻辑流程也通了。 检测好的红绿灯信息,最终通过 writer 发送到对应的 channel 中去。 大家可以看到,即使忽略很多代码,红绿灯检测的流程还是非常的长,如果有兴趣研究的同学可以自己深入研究。 当然,后面我会整理一张更详细的时序图,将更细致描述各个类对象之间的交互关系,后面完成后更更新本文。

2. 阅读代码的意义

有同学反映 Apollo 中的算法并不一定先进,是的,存在这种问题。 但我的个人观点是,自动驾驶最重要的不一定是算法,而是工程代码。 工程代码决定你是否能够落地,现在世面上的很多算法其实鲁棒性都不够,实际优秀的表现都是工程调参出来的。 自动驾驶也是一个系统性问题,需要平衡的东西太多了,算法、代码、硬件、通信协议等等,如何把这么多东西糅合在一起并且能正常运行实在是太重要了,这也是我学习并研究 Apollo 源码的目的之一。 和优秀的框架一起学习,你的视野也会更开阔,你的包容性也会越强。

0 人点赞