天天看點

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

背景

之前寫過一篇雜文《自動駕駛平台Apollo 2.5閱讀手記:perception子產品之camera detector》,介紹到用于camera輸入的DNN模型不僅會輸出物體檢測結果,還會輸出車道線的語義分割結果。但要得到最終的車道線資訊,還要經過後處理。項目中自帶一個例子cc_lane_post_processor_test,我們就以這個例子為線索,看下它的大體實作。

首先按《自動駕駛平台Apollo 2.5環境搭建》中介紹的進入到Apollo的docker運作環境中,編譯完後,執行以下指令就可以跑了:

# 加GLOG_v=4是為了輸出更多資訊便于分析
GLOG_v=4 ./bazel-bin/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test
           

可能會碰到下面錯誤:The width of input lane map does not match。這是因為modules/perception/data/cc_lane_post_processor_test目錄中的lane_map.jpg是全尺寸的,而配置檔案lane_post_process_config.pb.txt中指定的是縮放後的ROI區域,大小為960/384。是以隻要将lane_map.jpg換成yolo_camera_detector_test輸出的這個:modules/perception/data/yolo_camera_detector_test/lane_map.jpg就行。如果要将結果畫在原尺寸原圖上,測試程式中也要做相應修改。另外改了一些參數,包括 1) 坐标空間類型,因為這裡我們的輸入是圖檔。2) 對lane map的閥值,預設的閥值太高,一被過濾就不剩什麼了。3) 預設指定的模型目錄是空的,是以換了個目錄。具體修改如下:

diff --git a/modules/perception/model/camera/lane_post_process_config.pb.txt b/modules/perception/model/camera/lane_post_process_config.pb.txt
index c958661..8712672 100644
--- a/modules/perception/model/camera/lane_post_process_config.pb.txt
+++ b/modules/perception/model/camera/lane_post_process_config.pb.txt
@@ -1,6 +1,6 @@
 name: "CCLanePostProcessor"
 version: "0.1.0"
-space_type: "vehicle"
+space_type: "image"
 image_width: 1920
 image_height: 1080
 roi: 0
@@ -14,7 +14,7 @@ non_mask: 1260
 non_mask: 1079
 non_mask: 660
 non_mask: 1079
-lane_map_confidence_thresh: 0.97
+lane_map_confidence_thresh: 0.5
 cc_split_siz: 50.0
 cc_split_len: 15
 min_cc_pixel_num: 50
diff --git a/modules/perception/model/yolo_camera_detector/lane.pt b/modules/perception/model/yolo_camera_detector/lane.pt
index 6ed2033..a15a1a3 100644
--- a/modules/perception/model/yolo_camera_detector/lane.pt
+++ b/modules/perception/model/yolo_camera_detector/lane.pt
@@ -1,5 +1,5 @@
 model_param {
-  model_name: "lane2d_0612"
+  model_name: "lane2d_0627"
   proto_file: "deploy_lane_high.pt"
   weight_file: "deploy_lane_high.md"
   offset_ratio: 0.28843
diff --git a/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc b/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
index 880d478..88292c2 100644
--- a/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
+++ b/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
@@ -96,12 +96,14 @@ TEST_F(CCLanePostProcessorTest, test_lane_post_process_vis) {
   for (int h = 0; h < lane_map.rows; ++h) {
     for (int w = 0; w < lane_map.cols; ++w) {
       if (lane_map.at<float>(h, w) >= lane_map_conf_thresh) {
-        vis_lane_objects_image.at<cv::Vec3b>(h, w)[0] =
-            static_cast<unsigned char>(lane_mask_color[0]);
-        vis_lane_objects_image.at<cv::Vec3b>(h, w)[1] =
-            static_cast<unsigned char>(lane_mask_color[1]);
-        vis_lane_objects_image.at<cv::Vec3b>(h, w)[2] =
-            static_cast<unsigned char>(lane_mask_color[2]);
+        int orig_h = h * 2 + config_.start_y_pos();
+        int orig_w = w * 2;
+        vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[0] =
+          static_cast<unsigned char>(lane_mask_color[0]);
+        vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[1] =
+          static_cast<unsigned char>(lane_mask_color[1]);
+        vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[2] =
+          static_cast<unsigned char>(lane_mask_color[2]);
       }
     }
   }
           

将結果可視化後得到:

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

黃色為lane map結果;紅色代表L_0車道線(最靠近目前車左車道線),藍色代表R_0車道線(最靠近目前車右車道線)。判斷L_0和R_0的邏輯小改了下,因為按預設的邏輯貌似會找到外邊的車道線上去。

流程

車道線後處理代碼主要位于perception/obstacle/camera/lane_post_process目錄。測試程式代碼為cc_lane_post_processor_test.cc,先看下它的大體流程。

# 讀入配置檔案。
GetProtoFromFile(FLAGS_cc_lane_post_processor_config_file, &config_);
...
# 初始化CCLanePostProcessor類。
cc_lane_post_processor_->Init();
...
# 讀入原圖test_image.jpg。
cv::Mat test_image_ori = cv::imread(input_test_image_file, CV_LOAD_IMAGE_COLOR);
# 讀入車道線語義分割結果lane_map.jpg。
cv::Mat lane_map_ori = cv::imread(input_lane_map_file, CV_LOAD_IMAGE_GRAYSCALE)
...
# 将上面讀入的lane map元素轉為FLOAT32。
cv::Mat lane_map;
lane_map_ori.convertTo(lane_map, CV_32FC1, 1.0f / 255.0f, 0.0f);
...
# 主處理過程,結果放于lane_objects中。
cc_lane_post_processor->Process(lane_map, lane_post_process_options, &lane_objects);

# 按預定閥值(lane_map_conf_thresh)對lane map進行過濾,并可視化。
cv::Mat vis_lane_objects_image;
for (int h = 0; h < lane_map.rows; ++h)
    for (int w = 0; w < lane_map.cols, ++w) {
        if (lane_map.at<float>(h, w) >= lane_map_conf_thresh)
            ...
    }
    
# 可視化後處理得到的車道線對象。
for (size_t k = 0; k < lane_objects->size(); ++k) {
    ...
    # 畫車道線辨別。
    for (auto p = lane_objects->at(k).image_pos.begin(), p != lane_objects->at(k).image_pos.end(); ++p) {
            cv::circle(vis_lane_objects_image, ...)
        }
    }
    
    # 得到拟合出的多項式曲線上的點,并可視化。
    ...
    for(float l = start; l <=end; l++) {
        cv::circle(vis_lane_objects_image, ...)
    }
}

# 将可視化結果存到圖檔中。
cv::imwrite(lane_objects_image_file, vis_lane_objects_image);
           

該測試程式主要包含以下幾步:

  1. 調用GetProtoFromFile()函數讀入config檔案存于變量config_中。根據cc_lane_post_processor_config_file這個option(見perception_gflags.cc),車道線後處理子產品的config檔案預設在modules/perception/model/camera/lane_post_process_config.pb.txt。
  2. 在測試初始化時已經建立了類型為CCLanePostProcessor的變量cc_lane_post_processor_。接下來調用其Init()函數進行初始化。
  3. 接下去讀入lane_map.jpg檔案,并進行資料格式轉化,放于變量lane_map中。這是車道線檢測模型的輸出結果,是一張關于車道線的語義分割圖。每個像素中的值越大,代表這個點為車道線上的點的機率越大。
  4. 接下去調用cc_lane_post_processor_的Process()函數,并将剛才讀入的lane_map作為參數傳入,輸出會放在變量lane_objects中。
  5. 将lane_map,以及前面的處理結果(車道線辨別及拟合的多項式曲線)進行可視化。

可見主要的算法處理在CCLanePostProcessor類中的Init()和Process()兩個函數中。下面分别看下這兩個函數。它們的實作在檔案cc_lane_post_processor.cc中。Init()函數中主要的步驟如下:

  1. 讀config檔案,存入變量config_。然後把其中的值賦到成員變量options_中,友善後面使用。其中包括後面算法中要用到的各種閥值參數。
  2. 如果坐标空間是大地坐标的話,建立Projector類,用于從圖像坐标到大地坐标的轉換。這個測試中最後我們想将結果畫于圖像上,是以這裡用的圖像坐标。
  3. 建立ConnectedComponentGenerator類,用于産生連通區域。如果CUDA_CC為true,用的是GPU實作版本,否則是CPU實作版本。預設為CPU版本。
  4. 建立LaneFrame類。這個後面再細看。

再來看下Process()函數,這裡是算法的主要部分,大體流程圖如下:

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

主要可分為以下幾步:

  1. 如果指定使用曆史資訊,調用InitLaneHistory()初始化。這個測試中隻有單張圖,是以這條路不執行。
  2. cur_lane_instances_為LaneInstance對象的vector。調用GenerateLaneInstances()函數生成車道線執行個體,然後對其按size進行排序。
  3. 生成LaneObject,放入變量lane_objects中。對于前面給出的每一個LaneInstance,調用AddInstanceIntoLaneObjectImage()函數将LaneInstance轉為圖像中的LaneObject對象。從graphs_中拿出對應的graph(即lane cluster),然後周遊其中的元素,将對應的marker資訊放入LaneObject對象。然後是調用PolyFit()對LaneObject中的點進行多項式拟合。如果小于4個點的就用直線,否則用二次曲線。最後是用PolyEval()函數計算lateral distance。根據lateral distance與x中心坐标,就可以判斷該車道線是左還是右車道線。
  4. 調用EnrichLaneInfo()函數,對于車道線點的圖像坐标進行多項式曲線拟合。不過因為這個測試中輸入就是圖像坐标,是以其實這裡和上一步中的多項式拟合差不多。
  5. 如果指定要用到曆史資訊話,這裡會利用曆史資訊來修正目前車道線檢測結果。

其中第二步中的GenerateLaneInstance()函數步驟稍微複雜一些,再細化看下其中主要步驟:

  1. 通過對lane_map限定閥值得到二值的mask,存于變量lane_mask。
  2. 通過ConnectedComponentGenerator的FindConnectedComponents()函數從lane_mask找到連通圖,結果放到類型為ConnectedComponentPtr的變量cc_list中。這裡用到的資料結構是并查集。裡邊主要是兩個循環:
    1. 第一個循環從左上到右下方向周遊ROI區域。對于每一個像素點,如果為背景點,将變量frame_label_中對應位置設為-1;如果為前景點(即為車道線所在像素),分四種情況:
      1. 左邊和上面沒有鄰接點,則在并查集中加入新的元素,代表新的連通區域。變量frame_label_的相應元素指派為連通區域編号;變量root_map_加入元素-1。
      2. 左邊有鄰接點,在變量frame_label_中将左鄰接點的連通區域編号賦到目前點。
      3. 上方有鄰接點,在變量frame_label_中将上鄰接點的連通區域編号賦到目前點。
      4. 左邊和上方都有鄰接點,則将連通區域編号小的賦到目前點,同時在并查集中合并這兩個集合。
    2. 第二個循環,還是從左上到右下周遊ROI區域,對于每個連通區域,生成ConnectedComponent結構,放于變量cc中。前一個循環中,對于每個左邊和上面沒有鄰接點的前景像素點,都加入了root_map_中,但其中有些區域已經在後面被連接配接起來了,是以,這裡對于每個像素點,通過并查集的Find()函數找到其root,然後看root_map_中如果為-1,就建立新ConnectedComponent對象,放入傳回變量cc中,交将連通區域序号存于root_map_中;如果不為-1,代表目前位置對應的root已經建立ConnectedComponent對象,将目前點通過AddPixel()對象放入該對象即可。
  3. 找内邊(靠車這側的邊)和分離輪廓。周遊上一步中找到的存于變量cc_list中的所有連通區域元素,作以下操作:
    1. 調用函數FindBboxPixels()找到邊界框上的像素點。周遊該連通區域内的所有像素,找出在bbox上的那些點,将它們的index存入成員bbox_.bbox_pixel_idx中。
    2. 調用函數FindVertices()找到頂點。這裡的政策是先把bbox邊界上的點分别加入到left_boundary/up_boundary/right_boundary/down_boundary中,然後周遊上一步中找到的bbox上的點,結合邊界資訊判斷是否為頂點,是的話就加入成員vertices_。
    3. 調用函數FindEdges()根據前面計算出的頂點計算出内邊。
    4. 調用DetermineSplit()函數判斷是否要進入split流程。當bbox對角線長度超過指定閥值(預設為50)時傳回真,如高度大于5則縱向split,否則橫向。如DetermineSplit()傳回真,先調用FindCountourForSplit()函數找到輪廓像素。然後調用SplitContour()進行輪廓的split。
  4. 聯接車道線辨別,确定車道線執行個體标簽。
    1. 建立LaneFrame對象,然後調用其Init()函數初始化。根據連通區域的每條内邊段建立一個Marker對象,放到成員markers_中。
    2. 調用LaneFrame的Process()函數。
      1. 先通過GreedyGroupConnectAssociation()函數做marker association。
      2. 調用ComputeBbox()函數計算graph的邊界框。
      3. 建立LaneInstance對象,存入instances變量中。

其中的GreedyGroupConnectAssociation()函數主要步驟:

  1. 根據連通區域産生marker組。将一個連通區域内的marker分一組,由Group類表示。
  2. 周遊所有的組,估計方向。主要在Group::ComputeOrientation()函數中計算。
  3. 計算marker組間的距離矩陣。距離通過Group::ComputeDistance()函數計算。這個距離會考慮幾方面的因素,并進行權重。如果有兩個組的距離大于0,代表可以連接配接。資訊記錄在connect_mat,edges,to_group_idx和from_group_idx結構中。
  4. 将上面的的group間距離排序,從小到大進行組間的連接配接。
  5. 生成lane cluster。對于沒有可連接配接的組,建立Graph對象(表示lane cluster),并放入變量groups_中。然後把相連接配接的組加到這個Graph中。

小結

可以看到,Apollo中車道線後處理的流程還是比較長的。大體來說,首先會通過DNN得到車道線的語義分割結果,根據預定閥值轉為二值圖:

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

然後通過連通圖檢測和邊緣檢測等處理,得到車道線的内側邊緣。

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

接着得到Marker資訊,再将這些Marker聚類分組,形成Group。這些資訊會通過Graph組織起來。

自動駕駛平台Apollo 3.0閱讀手記:perception子產品之lane post processing

之後,基于這些資訊生成每條車道線對應的LaneInstance結構。最後,LaneInstance經過篩選存到LaneObject中。

這個過程相對比較長的一個原因是前面語義分割網絡是對車道線所占像素進行分割,是以像虛線的各個部分在lane map中是分離的,需要在後進行中進行一系列操作将它們組合連接配接起來。在Apollo 3.0中加入了另一種方式,稱為"Whole lane line"。看結構像有可能是位于modules/perception/model/yolo_camera_detector/lane13d_0716/目錄下這個模型。看關鍵字可能是類似AAAI 2008上的論文《Spatial As Deep: Spatial CNN for Traffic Scene Understanding》(TuSimple車道線檢測競賽的第一名)。這種方案是在訓練标注中就以整條車道線為機關,這樣訓練得到的網絡也能輸出相對完整的線,是以後處理就可以簡單化很多。在Apollo的官方文檔中提到,前一種方式用作視覺定位,後一種用于車道保持。

繼續閱讀