1. 程式人生 > >自動駕駛平臺Apollo 3.0閱讀手記:perception模組之lane post processing

自動駕駛平臺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]); } } }

將結果視覺化後得到: 在這裡插入圖片描述 黃色為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()函式,這裡是演算法的主要部分,大體流程圖如下: 在這裡插入圖片描述

主要可分為以下幾步:

  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得到車道線的語義分割結果,根據預定閥值轉為二值圖: 在這裡插入圖片描述 然後通過連通圖檢測和邊緣檢測等處理,得到車道線的內側邊緣。 在這裡插入圖片描述 接著得到Marker資訊,再將這些Marker聚類分組,形成Group。這些資訊會通過Graph組織起來。 在這裡插入圖片描述 之後,基於這些資訊生成每條車道線對應的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的官方文件中提到,前一種方式用作視覺定位,後一種用於車道保持。