Routing模塊之A*搜索算法

Routing模塊之A*搜索算法

Routing模塊的結果是Planning模塊生成參考線的基礎,也就是通常所說的全局路徑規劃,Routing模塊中除了對地圖的抽象處理爲可供搜索的topo地圖,以及通過KDtree生成黑名單地圖外核心算法是通過A* 算法生成全局路徑,本文這裏主要介紹Routing模塊中如何同過A*算法在topo地圖中搜索全局通路——全局路徑。

1.邏輯框架

Routing模塊的功能是從地圖中搜索一條從起點–途經點–終點的全局路徑,要完成這個功能,需要解決幾個問題:1) 地圖信息較多,看起來是連續的,如何進行抽象爲可搜索的離散地圖; 2) A*算法在抽象的地圖中是如何搜索的; 3) 搜索的結果是如何使用的;
這裏我們首先了解一下routing模塊大概的邏輯
Routing邏輯框架

通過上圖可知routing實際上先將高精度地圖轉換爲topo地圖,topo地圖是由一系列的節點和邊組成,在生成節點和邊的同時會對每個節點和邊賦予一定的cost,通常cost與車道的長度、轉彎等屬性有關。然後通過A*算法對topo圖進行搜索,最後對搜索的結果進行封裝。

2.Topo地圖

在高精度地圖中採集的map信息包含了road,信號燈,車道線,物體,信號標識等很多信息,在路徑搜索的過程中不能直接使用,而且主要關注的是lane及lane之間的關係,因此,routing模塊主要是先將高精度地圖中採集的base_map地圖信息通過topo_creator程序創建節點和邊進而轉化爲Topo地圖,其結果如下,在map模塊中定義的參數有很多,在使用過程中需要用到以下參數,然後通過A算法來搜索一條可行的路徑:
在"routing/topo_creator"建圖的代碼中主要包括,構建節點,構建邊,構建圖,其文件結構如下圖所示:
routing/topo_creator文件結構
對於topo圖中的節點和圖的詳細描述是在routing/graph目錄下,其主要的信息如下圖 Topo地圖的詳細結構:
Topo地圖的詳細結構
在構建topo圖之後通過A
算法搜尋路徑,A*算法是基於圖搜索的一種方式,該模塊的輸入爲構建的圖以及子圖、起始點和目標點,輸出爲路由導航信息,對於Topo地圖構建的信息可以這樣理解,對於下圖的道路信息將其抽象爲下圖的節點和邊的信息:
道路信息
節點和邊的抽象topo圖從圖中可以瞭解到一個road,包含了多個車道信息,每個車道是一個搜索的節點,該節點也是有一定長度的,前後車道之間通過predecessor_id和successor_id進行連接,左右車道之間是通過車道線的類型進行連接的。

3.A*算法的流程

1)把起始格添加到開啓列表(Open List)。
2)重複如下工作:
  a)尋找開啓列表中估量代價F值最低的格子,稱之爲當前格;
  b)把它切換到關閉列表中(Closed List);
  c)對相鄰的八個格中的每一個進行如下操作:
     i.如果它不可通過或者已經在關閉列表中,略過它;反之如下;
     ii.如果它不在開啓列表中,把它添加進去。把當前格作爲這一格的父節點。記錄這一格的F,G和H值;
     iii.如果它已經在開啓列表中,用G值作爲參考檢查新的路徑是否更好。更低的G值意味着更好的路徑。如果是這 樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值,並在改變後重新按照F值對開啓列表排序。
  d)當目標格被添加進了關閉列表中,這時候路徑被找到;或者沒有找到目標格,開啓列表已經空了,這時候路徑不存在。
3)從目標格開始,沿着每一格的父節點移動直到回到起始格,保存路徑。

其過程如下圖過程解釋:
A*搜索過程
目標:給定一個topo地圖,綠色爲起點位置,紅色爲目標位置,A*算法的目的是搜索一條從起始位置到目標位置的路徑點序列。
步驟:1將起始當前節點爲起始點,將起始點的鄰接點加入到open_list中,然後計算其鄰接點的cost f值,然後將最小的f值選出來,並把最小的節點從open_list中移除,然後加入到close_list中,並把它的父節點設置爲起始節點;
步驟2:將上次的節點設置爲當前節點,如果當前節點的鄰接點不在open_list中,那麼將其加入到其中,進行計算cost f,再次找到代價值最小的點,並把最小的節點從open_list中移除,然後加入到close_list中,並把它的父節點設置爲起始節點;
步驟3,依此進行,直到到達目標點,然後根據父節點來依次回溯路徑到起始點,或者待計算的open_list集合中都計算完了,還沒有到目標點,表示路徑搜索失敗。

4.A*算法在routing中的應用

Apollo代碼中的A*算法的實現過程主要是在a_star_strategy.cc中的search()方法:
代價函數爲F=h+g ,其中g表示當前節點到起始節點的代價,主要是通過計算節點以及連接的邊的代價值,h爲通過曼哈頓距離表示的啓發式函數:

double distance = fabs(src_point.x() - dest_point.x()) + fabs(src_point.y() - dest_point.y());

其代碼解釋如下:

/參數分別爲所有圖節點,部分圖節點,起始節點,目標節點,結果集
bool AStarStrategy::Search(const TopoGraph* graph,
                           const SubTopoGraph* sub_graph,
                           const TopoNode* src_node, const TopoNode* dest_node,
                           std::vector<NodeWithRange>* const result_nodes) {
  //清楚所有的參數
  Clear();
  //記錄日誌信息
  AINFO << "Start A* search algorithm.";
  //std::priority_queue是一種容器適配器,這裏再最初重載了<操作符,所以優先輸出最小的
  //作爲已經尋路過的節點的代價值
  std::priority_queue<SearchNode> open_set_detail;
  //把源節點node傳入struct中,作爲源SearchNode
  SearchNode src_search_node(src_node);
  //計算源節點的代價值f,啓發式距離採用曼哈頓距離
  src_search_node.f = HeuristicCost(src_node, dest_node);
   // 源SearchNode 傳入push進尋路節點集的優先隊列中
  open_set_detail.push(src_search_node);
  // 源節點也傳入open集中
  open_set_.insert(src_node);
  // g函數評分,key-value,計算源節點到自身的移動代價值
  g_score_[src_node] = 0.0;
  // 設置源節點進入時的S值,key-value
  enter_s_[src_node] = src_node->StartS();
  // 定義SearchNode變量
  SearchNode current_node;
  //定義next邊集
  std::unordered_set<const TopoEdge*> next_edge_set;
  //定義sub邊集
  std::unordered_set<const TopoEdge*> sub_edge_set;
  //當open集合不爲空時,不斷循環檢查
  while (!open_set_detail.empty()) {
    // 此時節點賦值爲open優先級中棧頂的元素,權重用f來比較
    current_node = open_set_detail.top();
    // 保存當前SearchNode的起始節點
    const auto* from_node = current_node.topo_node;
    // 若起始結點已抵達最終的目標結點,則反向回溯輸出完整的路由,返回。
    if (current_node.topo_node == dest_node) {
      if (!Reconstruct(came_from_, from_node, result_nodes)) {// 重構該路徑是否可行
        AERROR << "Failed to reconstruct route.";// 不可行說明中間錯誤
        return false;
      }
      return true;// 否則返回已經正確找到節點
    }
    open_set_.erase(from_node);// open集node節點刪除
    open_set_detail.pop();// open集優先隊列中將SearchNode節點刪除
     // 若起始結點from_node在CLOSED集中的計數不爲0,表明之前已被檢查過,直接跳過
    if (closed_set_.count(from_node) != 0) {
      // if showed before, just skip...
      continue;
    }
    // 將起始結點加入close集中
    closed_set_.emplace(from_node);

    // if residual_s is less than FLAGS_min_length_for_lane_change, only move
    // forward
    // 獲取起始結點from_node的所有相鄰邊
    // 若起始結點from_node到終點的剩餘距離s比FLAGS_min_length_for_lane_change要短,
    // 則不考慮變換車道,即只考慮前方結點而不考慮左右結點。反之,若s比
    // FLAGS_min_length_for_lane_change要長,則考慮前方及左右結點。
    const auto& neighbor_edges =
        (GetResidualS(from_node) > FLAGS_min_length_for_lane_change &&
         change_lane_enabled_)
            ? from_node->OutToAllEdge()
            : from_node->OutToSucEdge();
     // 當前測試的移動代價g評分,初始化指定0
    double tentative_g_score = 0.0;
    // 從相鄰邊neighbor_edges中獲取其內部包含的邊,將所有相鄰邊全部加入集合:next_edge_set
    next_edge_set.clear();
    for (const auto* edge : neighbor_edges) {// 對於所有的臨接邊
      sub_edge_set.clear();// 子臨接邊清空
      sub_graph->GetSubInEdgesIntoSubGraph(edge, &sub_edge_set);// sub_edge_set賦值爲edge
      next_edge_set.insert(sub_edge_set.begin(), sub_edge_set.end());// 把sub_edge_set匯入next_edge_set
    }
    // 所有相鄰邊的目標結點就是我們需要逐一測試的相鄰結點,對相結點點逐一測試,尋找
    // 總代價f = g + h最小的結點,該結點就是起始結點所需的相鄰目標結點。
    for (const auto* edge : next_edge_set) {// 循環next_edge_set
      const auto* to_node = edge->ToNode();
      // 相鄰結點to_node在CLOSED集中,表明之前已被檢查過,直接忽略。
      if (closed_set_.count(to_node) == 1) {
        continue;
      }
      // 若當前邊到相鄰結點to_node的距離小於FLAGS_min_length_for_lane_change,表明不能
      // 通過變換車道的方式從當前邊切換到相鄰結點to_node,直接忽略。
      if (GetResidualS(edge, to_node) < FLAGS_min_length_for_lane_change) { // 如果邊到到達節點node的距離小於限定值,再次循環
        continue;
      }
      // 更新當前結點的移動代價值g
      tentative_g_score =
          g_score_[current_node.topo_node] + GetCostToNeighbor(edge);
      
      // 如果邊類型不是前向,而是左向或右向,表示變換車道的情形,則更改移動代價值g
      // 的計算方式。
      if (edge->Type() != TopoEdgeType::TET_FORWARD) {
        tentative_g_score -=
            (edge->FromNode()->Cost() + edge->ToNode()->Cost()) / 2;
      }

      // 若相鄰結點to_node在OPEN集且當前總代價f大於源結點到相鄰結點to_node的移動代價g,表明現有情形下
      // 從當前結點到相鄰結點to_node的路徑不是最優,直接忽略。
      // 因爲相鄰結點to_node在OPEN集中,後續還會對該結點進行考察。
      if (open_set_.count(to_node) != 0 &&
          tentative_g_score >= g_score_[to_node]) {
        continue;
      }
      
      // if to_node is reached by forward, reset enter_s to start_s
      // 如果是以向前(而非向左或向右)的方式抵達相鄰結點to_node,則將to_node的進入距離更新爲
      // to_node的起始距離。
      if (edge->Type() == TopoEdgeType::TET_FORWARD) {
        enter_s_[to_node] = to_node->StartS();
      } else {

        // else, add enter_s with FLAGS_min_length_for_lane_change
        // 若是以向左或向右方式抵達相鄰結點to_node,則將to_node的進入距離更新爲
        // 當前結點from_node的進入距離加上最小換道長度,並乘以相鄰結點to_node長度
        // 與當前結點from_node長度的比值(這麼做的目的是爲了歸一化,以便最終的代價量綱一致)。
        double to_node_enter_s =
            (enter_s_[from_node] + FLAGS_min_length_for_lane_change) /
            from_node->Length() * to_node->Length();
        // enter s could be larger than end_s but should be less than length
        to_node_enter_s = std::min(to_node_enter_s, to_node->Length());
        // if enter_s is larger than end_s and to_node is dest_node
        if (to_node_enter_s > to_node->EndS() && to_node == dest_node) { // 如果滿足enter_s比end_s大而且下一個節點是終點,就再次循環
          continue;
        }
        enter_s_[to_node] = to_node_enter_s;
      }

      // 更新從源點移動到結點to_node的移動代價(因爲找到了一條代價更小的路徑,必須更新它)
      g_score_[to_node] = tentative_g_score;// to_node的g評分重新賦值
      // 將相鄰結點to_node設置爲下一個待考察結點
      SearchNode next_node(to_node);
      next_node.f = tentative_g_score + HeuristicCost(to_node, dest_node);// next_node的f值爲g評分加上啓發式距離(曼哈頓)
      
      // 當下一個待考察結點next_node加入到OPEN優先級隊列
      open_set_detail.push(next_node);

      // 將to_node的父結點設置爲from_node
      came_from_[to_node] = from_node;
      // 若相鄰結點不在OPEN集中,則將其加入OPEN集,以便後續考察
      if (open_set_.count(to_node) == 0) {// 如果開集中發現to_node沒有出現過,就添加該節點
        open_set_.insert(to_node);
      }
    }
  }
  // 整個循環結束後仍未正確返回,表明搜索失敗
  AERROR << "Failed to find goal lane with id: " << dest_node->LaneId();
  return false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章