原文地址:https://blog.csdn.net/jinzhuojun/article/details/80875264
我们知道,无人驾驶系统中,感知(perception)模块是重中之重,而且一般都会是个大部头,子模块众多。为了保证准确率和鲁棒性,系统多会采用多传感器(如camera,LIDAR, RADAR等)融合。因为它们各有优劣,相互融合可以优势互补。其中,camera以其数据处理方便,算法相对丰富成熟,价格便宜,能提供纹理信息等诸多优点,成为了最基本的硬件配置。
在百度的Apollo无人驾驶平台(源码地址https://github.com/ApolloAuto/apollo)中,camera会用来检测车道线及场景中物体(车辆,自行车,行人等)。这是通过一个多任务网络来完成的。其中的encoder部分是Yolo的darknet,decoder分两部分:一部分是语义分割,用于车道线区域检测;另一部分为物体检测,用于物体检测。物体检测部分基于Yolo,同时还会输出物体的方向等3D信息,因此网络称为yolo3d。这些信息被输出后,就可以送到后续模块(如cc_lane_post_processor)中进一步处理。另外,比较巧妙的是,CNN网络中的某一层被拿来用作生成特征供后续模块(如tracker)使用。网络结构如下图:
其中框1部分为物体检测输出;框2部分为车道线的语义分割输出;框3部分为用作特征提取的其中一个卷积层输出。为了更好地理解如何使用这个模型进行道路的场景感知,我们通过官方自带的例子大体走一下流程(如何搭建环境和运行例子在之前的文章自动驾驶平台Apollo 2.5环境搭建已有描述)。yolo_camera_detector_test这个测试程序中有三个test。第一个是测试初始化;第二个测试物体检测和特征提取;第三个会检测车道线和场景中物体。第三个测试几乎综合了所有功能,所以我们主要看multi_task_test这个测试。首先看下涉及到的几个文件:在/apollo/models/perception/model/yolo_camera_detector目录下有两个配置文件config.pt和feature.pt,分别是camera detector模块和特征提取的配置。模型文件位于/apollo/modules/perception/model/yolo_camera_detector/yolo3d_1128目录下。其中的deploy.pt和deploy.md分别为网络的结构描述文件和权重文件。它们分别对应典型Caffe模型的prototxt和caffemodel文件。
例子中首先通过BaseCameraDetectorRegisterer::GetInstanceByName()函数创建YoloCameraDetector对象,它是BaseCameraDetector的实现类。Apollo中的模块实现类的工厂函数组织在类型为BaseClassMap的静态变量factory_map中。它是string到FactoryMap的映射;FactoryMap又是string到ObjectFactory指针的映射。以camera detector模块为例,首先在基类声明文件base_camera_detector.h中有:
REGISTER_REGISTERER(BaseCameraDetector);
#define REGISTER_CAMERA_DETECTOR(name) REGISTER_CLASS(BaseCameraDetector, name)
宏REGISTER_REGISTERER(BaseCameraDetector)定义了BaseCameraDetectorRegisterer类。该宏定义于registerer.h文件中。之后实现类就可以通过REGISTER_CAMERA_DETECTOR进行注册,比如yolo_camera_detector.h中:
REGISTER_CAMERA_DETECTOR(YoloCameraDetector);
该宏会定义ObjectFactoryYoloCameraDetector,它是ObjectFactory的继承类。同时定义RegisterFactoryYoloCameraDetector()函数用于注册相关工厂函数,这个函数会在camera_process_subnode.cc文件的模块初始化函数InitModules()中被调用。对于其它模块也是类似的。上面这几个结构关系如图:
到此,初始化过程基本结束,大体流程图如下:
接下来回到测试程序,下一步通过OpenCV的imread()函数读入图片,然后就调用YoloCameraDetector的Multitask()函数进行检测。它输出两个结构。变量lane_map为语义分割结果,为车道线信息。变量objects为VisualObject的vector,每个检测到的物体用一个VisualObject结构表示。接下来我们看看核心函数Multitask()。其中主要调用Detect()函数进行检测。这个函数进去后首先通过CNNCaffe的get_blob_by_name()函数将input blob取出,然后通过resize()函数将原始输入图片取ROI(下面的1920 x 768区域),再resize成神经网络input blob指定大小。resize()函数实现在cuda_util/util.cu文件中。
然后就是通过CNNCaffe的forward()函数做一把inference。这一步最为耗时,在笔者的穷人GPU上耗时~40ms。之后就是拿结果了。先拿检测物体信息。定义临时变量temp_objects为VisualObject的vector,然后通过get_objects_gpu()函数将网络inference的结果放到里面。这个函数稍稍有些复杂:
到此为止,检测出来的物体信息都放在temp_objects这个VisualObject数组中。但是,筛选还木有结束,这些物体检测框还要经过最后一道考验,就是前面提到的2d和3d的最小高度检测。如果小于之前指定的阀值,那也被干掉。这一部后幸存下来的物体框放在objects变量中。
接下来调用Extract()函数提取这些物体框的特征。它对于extractors_变量中的每个特征提取器(也就是之前创建的ROIPoolingFeatureExtractor),调用其extract()函数。这个函数就是执行一下之前在ROIPoolingFeatureExtractor的init()函数中创建的ROIPooling层。因为是要提取检测物体的特征,所以该函数需要传入检测框信息objects。ROIPooling层输出的特征经过L2 normalization后存于对应VisualObject结构中的object_feature成员。
Extract()函数后调用yolo::recover_bbox()函数进行坐标的转换。因为之前VisualObject中upper_left/uppper_right中填的是相对于ROI(图片下面1920 x 768)中并且归一化到[0,1]范围内的。比如xmin/ymin/xmax/ymax为(0.552336, 0.27967, 0.583794, 0.344488)。这个坐标会转换到ROI中的像素坐标。转换后x/y/w/h为(1060, 526, 60, 49)。之后还会和图像的大小做一个并,把那些超出图像的部分切掉。最后如果框的边界是在图像的边上的,会设置VisualObject的trunc_width/trunc_height成员。
Detect()函数返回后,接下来处理用于车道线的语义分割结果。通过get_blob_by_name()函数得到seg_prob这个blob。然后把它里边的数据拷贝到类型为cv::Mat的mask变量中。这个变量也是Multitask()函数的输出变量。最后,返回到测试程序中,将语义分割结果以图片形式保存。
检测部分的流程大体如下图:
为了看起来更加直观,用官方自带的测试图片检测的结果数据可以图形化如下: