自動駕駛平臺Apollo 2.5閱讀手記:perception模塊之camera detector

原文地址: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()中被調用。對於其它模塊也是類似的。上面這幾個結構關係如圖:

這裏寫圖片描述

創建好YoloCameraDetector對象然後調用其初始化函數Init()。首先通過GetProtoFromFile()函數將detector的config文件(yolo_camera_detector_config.pb.txt)讀入到成員變量config_中。從中讀出yolo_root(/apollo/modules/perception/model/yolo_camera_detector)保存到yolo_root變量中。yolo_config爲yolo_root目錄下的配置文件config.pt。config.pt中的信息被讀入並寫到成員變量 yolo_param_中。之後依次調用以下函數初始化:

  1. load_intrinsic()函數根據配置文件計算ROI區域寬高。原始輸入尺寸爲1920x1080。默認配置下會截掉上面的312像素。留下下面的1920 x 768的區域作爲ROI。而實際處理時會按比例resize成960 x 384分辨率。變量min_2d_height_爲960 x 384分辨率下的檢測物體高度閥值,也就是如果在960 x 384分辨率下高度小於17像素的物體會被忽略。min_3d_height也是類似的,3D邊界框物體高度小於0.5米的也會被忽略。
    這裏寫圖片描述
  2. init_cnn()函數用於初始化CNN執行環境。這裏用的深度學習推理框架是CUDA加速的Caffe。如之前所說,網絡結構描述位於deploy.pt,權重信息位於deploy.md。feature.pt是特徵描述文件,它配置特徵抽取方式,這些特徵可以被後面處理任務所用。總得來說,網絡輸入結點只有一個,名爲data。輸出分三部分:seg_prob爲語義分割結果;loc_pred, obj_pred, cls_pred, ori_pred, dim_pred, lof_pred, lor_pred爲物體檢測結果。conv3_3爲圖像的中間特徵層,用於ROIPooling處理生成物體特徵。obj_pred的輸出的寬高是output_width_和output_height_(60 x 24)。seg_prob輸出的寬高和輸入的一樣,爲lane_output_width_和lane_output_height_(960 x 384)。用於特徵提取的conv3_3層輸出結點的寬高爲240 x 96。本函數主要會創建CNNCaffe,它是CNNAdapter的實現類,主要封裝Caffe相關操作。之後分別調用它的init()方法進行初始化。然後通過get_blob_by_name()函數根據配置文件中指定的輸出blob名得到相應的blob對象,並得到相應的寬高信息。然後通過reshape_input()函數指定網絡輸入維度,也就是960 x 384。接着構造特徵提取層,首先根據特徵配置文件feature.pt中指定的將conv3_3這個blob拿出來,然後創建ROIPoolingFeatureExtractor對象並調用其init()函數初始化。這個初始化函數中會構建ROIPooling特徵提取層。輸入有兩個:一個是conv3_3結點輸出;另一個是檢測到的物體座標信息。輸出爲ROIPooling操作後的結果。變量feat_dim爲生成的單個物體的特徵維度,這裏是576。
    這裏寫圖片描述
  3. 我們知道當前流行的物體檢測網絡通常會產生很多物體框的candidate,按國際慣例最後一般都要經過非極大值抑制(Non-Maximum Suppression)將多餘的檢測框去除。而該算法的一些參數就是在load_nms_params()函數基於配置文件config.pt中的值進行設置。
  4. 自從Faster R-CNN之後,流行的物體檢測網絡通常使用anchor box(如SSD, Yolo之類),即一些預定的框,作爲檢測物體的heuristic。這裏的init_anchor()函數就是初始化anchor box信息。它從anchors.txt文件中讀入相關信息。預定義的anchor box總共有16個,讀入後寫到anchor_這個Caffe數據塊中。整個image分成60 x 24的格子,每個格式會根據16個anchor box給出相應的預測,則總共產生物體預測框的個數爲obj_size_(60 x 24 x 16 = 23040)個。然後讀入檢測類型文件types.txt,信息記入types_變量。默認就檢測車輛,自行車,行人,和未知固定障礙物。然後定義了幾個Caffe數據塊,如res_box_tensor_, res_cls_tensor_, overlapped_, idx_sm_。它們會在NMS中用到,而NMS也是用GPU加速的。res_box_tensor_用來存放物體檢測位置信息。其中元素0-3爲2d圖像中bbox座標。元素4爲朝向。元素5-7爲3d大小。元素8-11和12-15分別爲3d bbox的前和後面的座標。res_cls_tensor_用來存放檢測類別。overlapped_用來存放top k檢測框(按confidence排序)間是否重疊。idx_sm_爲top k檢測框的index。

到此,初始化過程基本結束,大體流程圖如下:

這裏寫圖片描述

接下來回到測試程序,下一步通過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的結果放到裏面。這個函數稍稍有些複雜:

  1. 一些準備工作,如將相應的輸出blob通過CNNCaffe的get_blob_by_name()函數取出來。然後判斷網絡是否輸出了ori, dim, lof, lor等信息。其中的obj_batch,obj_height和 obj_width分別爲1, 24, 60。
  2. 調用GetObjectsGPU()函數將網絡各輸出結點信息放到之前定義的用於存放結果的Caffe數據塊res_box_tensor_和res_cls_tensor_中。該函數實現位於network.cu文件。主要實現在get_object_kernel()函數中。網絡中物體檢測部分沿用自Yolo,而Yolo的輸出並不是物體框的頂點座標,因此需要做一些轉化。轉化公式可參見論文《YOLO9000:
    Better, Faster, Stronger》的Figure 3。其它的輸出也按需做轉化。
  3. 對於每一種物體類別,調用apply_nms_gpu()函數去除多餘的檢測框。該函數實現在region_output.cu文件。這個函數首先先用閥值過濾下,把confidence小於0.8的幹掉,剩下的框的index和confidence放在idx和confidence兩個vector中。然後把剩下的元素按confidence排個序。接着調用compute_overlapped_by_idx_gpu()函數計算這些框間的重疊關係。當兩個框間的IoU(即Jaccard overlap)大於0.4時,算兩者重疊。基於這個結果,就可以調用apply_nms()執行NMS算法。結果放在indeces這個成員中。indices是類別到物體框index數組的映射。
  4. 將上面處理後的輸出填到要返回的變量temp_objects中。對於之前NMS算法中倖存下來的每個物體框,創建VisualObject對象。該結構中幾個關鍵成員:type代表該物體類別,如是車輛還是行人等;type_probs是一個數組,代表該物體框爲每種類別的confidence;score爲type_probs中的最大值,它是objectnesss的confidence。像upper_left/lower_right/alpha/height/width/length等這一坨都是代表該物體在2d和3d中的位位置信息,來自之前填的cpu_box_data結構;object_feature爲之前提到的ROIPooling後輸出的特徵,這個現在置空,一會會在Extract()函數中填。id爲這一幀中物體框的編號。

這裏寫圖片描述

到此爲止,檢測出來的物體信息都放在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()函數的輸出變量。最後,返回到測試程序中,將語義分割結果以圖片形式保存。

檢測部分的流程大體如下圖:

這裏寫圖片描述

爲了看起來更加直觀,用官方自帶的測試圖片檢測的結果數據可以圖形化如下:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章