Nav2 代價地圖與路徑規劃

2026-07-18
  • ros2
  • nav2
  • costmap
  • path-planning

問題定義

上一節建好的地圖,本質上只是一張標示「這裡是牆、這裡是空地」的靜態影像,路徑規劃演算法沒辦法直接拿它做決策——演算法需要知道的是「每個位置的可通行代價有多高」,包括要離牆面保持多遠、附近突然出現的障礙物有多危險。代價地圖(costmap)就是把原始地圖與即時感測資料,轉換成規劃演算法可以直接運算的代價數值網格。

核心概念說明

Global Costmap 與 Local Costmap 的分工

延續上一節提到的兩層時間尺度概念,Nav2 用兩張獨立的代價地圖分別服務不同目的:

  • Global Costmap:涵蓋整張已知地圖範圍,主要基於 SLAM 建好的靜態地圖產生,更新頻率較低,供 planner_server 規劃一條從起點到終點的全域路徑。
  • Local Costmap:只涵蓋機器人周圍一個固定大小的視窗(例如 3 公尺見方),完全基於即時感測資料(例如當下的雷射掃描),更新頻率很高,供 controller_server 即時避開規劃當下才出現、地圖上原本沒有的障礙物。

Layer:代價地圖是多層疊加出來的結果

無論是 global 還是 local costmap,實際數值都是由多個 layer 插件依序疊加計算出來的,這是一種類似圖層合成的設計,讓每種「代價來源」可以獨立開關、調整,互不影響彼此的邏輯:

Layer作用
static_layer讀取 SLAM 建好的靜態地圖,牆面標記為高代價
obstacle_layer讀取即時感測資料(雷射掃描),動態標記當下偵測到的障礙物
inflation_layer把每個障礙物周圍的代價往外「膨脹」,離障礙物越近代價越高,形成漸層

inflation_layer 特別值得展開說明:它解決的問題是「機器人不是一個點」——單純知道障礙物的確切輪廓不夠,規劃器還需要知道機器人本體的實際尺寸,離障礙物太近的路徑即使技術上沒有真的壓到障礙物,也應該被視為高風險而避免。膨脹半徑通常會設定成略大於機器人本體半徑,確保規劃出的路徑留有安全餘裕。

實作範例:設定代價地圖並觀察規劃結果

1. 代價地圖參數設定

檔案位置:~/ros2_ws/src/my_package/config/nav2_params.yaml

yaml
global_costmap:
  global_costmap:
    ros__parameters:
      update_frequency: 1.0
      publish_frequency: 1.0
      global_frame: map
      robot_base_frame: base_link
      resolution: 0.05
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
      static_layer:
        plugin: "nav2_costmap_2d::StaticLayer"
        map_topic: /map
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        inflation_radius: 0.3
        cost_scaling_factor: 3.0

local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      rolling_window: true
      width: 3
      height: 3
      resolution: 0.05
      plugins: ["obstacle_layer", "inflation_layer"]
      obstacle_layer:
        plugin: "nav2_costmap_2d::ObstacleLayer"
        observation_sources: scan
        scan:
          topic: /scan
          max_obstacle_height: 2.0
      inflation_layer:
        plugin: "nav2_costmap_2d::InflationLayer"
        inflation_radius: 0.3
        cost_scaling_factor: 3.0

留意兩個地方跟第 6.1 節提到的座標系概念直接對應:global_costmapglobal_frame 設成 map(全域、精準),local_costmapglobal_frame 設成 odom(局部、平滑不跳動),這不是隨意選擇,而是刻意讓即時避障運算建立在不會突然跳動的座標系上,避免 local costmap 因為定位修正的瞬間跳動而產生錯誤的避障行為。

2. 啟動 costmap 節點並載入參數

bash
ros2 run nav2_costmap_2d nav2_costmap_2d --ros-args \
  --params-file ~/ros2_ws/src/my_package/config/nav2_params.yaml \
  -r __ns:=/global_costmap

3. 用 RViz2 觀察代價地圖

在 RViz2 加入 Map Display,Topic 選 /global_costmap/costmap

預期畫面

牆面附近會出現從深色(高代價,緊鄰障礙物)到淺色(低代價,遠離障礙物)的漸層色帶,這條漸層帶的寬度大約對應 inflation_radius 設定的 0.3 公尺——這是驗證 inflation_layer 有正確運作最直觀的方式。

4. 發送一個規劃請求,觀察路徑繞行障礙物膨脹區域

bash
ros2 action send_goal /compute_path_to_pose nav2_msgs/action/ComputePathToPose \
  "{goal: {pose: {position: {x: 2.0, y: 1.0, z: 0.0}}}}"

預期輸出

text
Waiting for an action server to become available...
Sending goal:
     goal:
  pose:
    header:
      frame_id: ''
    pose:
      position:
        x: 2.0
        y: 1.0
        z: 0.0
...
Result:
    path:
  poses: [...]

在 RViz2 裡加入 Path Display 觀察這條路徑,會看到路徑刻意繞開了膨脹層的深色高代價區域,即使技術上直線穿過那個區域也不會真的撞到障礙物本體。

常見錯誤與除錯技巧

錯誤一:規劃請求回報找不到可行路徑

text
Result:
    error_code: 202
    error_msg: 'No valid path found'

原因:常見原因有兩種:目標點本身落在高代價(甚至無法通行)區域內,或整張代價地圖上,起點跟目標點之間被連續的高代價區域完全阻隔(例如 inflation_radius 設太大,把一條原本能通過的窄通道也判定為不可通行)。

排除方式:先用 RViz2 目視確認目標點座標是否真的落在合理的可通行區域,再檢查是否有場景整體被過度膨脹的問題——如果窄通道確實存在但規劃不出路徑,嘗試降低 inflation_radiuscost_scaling_factor 做測試,確認是不是膨脹設定過度保守導致的。

錯誤二:Local costmap 裡出現「幽靈障礙物」,明明現場已經沒有東西了

現象:實際場景中的障礙物已經移開,但 local costmap 上原本的位置依然標記著高代價區域,遲遲沒有清除。

原因obstacle_layer 需要靠新的感測資料主動「觀察到那個位置現在是空的」才會清除舊的標記;如果感測器視角有死角(例如障礙物移到了雷射掃描不到的角度),代價地圖沒有機會被更新,會一直保留舊資料。

排除方式:確認感測器的視野範圍涵蓋需要清除代價的區域,必要時調整感測器安裝角度或加裝額外感測器覆蓋死角;也可以用 ros2 topic echo /local_costmap/costmap_updates 確認代價地圖確實有在持續接收更新,而不是完全沒有在運作。

小結

代價地圖是連接「靜態地圖/即時感測資料」與「路徑規劃演算法」之間的橋樑,global/local 兩張地圖分別服務全域規劃與即時避障,多個 layer 插件疊加計算出最終代價,inflation_layer 確保規劃結果考慮到機器人本體實際尺寸的安全餘裕。有了代價地圖,下一節要介紹 Nav2 怎麼用行為樹,把規劃、避障、失敗重試這些行為,組織成一套完整、可應付各種異常狀況的自主導航邏輯。

延伸閱讀

常見問題

faq_01.log
為什麼需要兩張代價地圖,一張不夠嗎?
global costmap 涵蓋整張地圖,更新頻率較低,負責全域路徑規劃;local costmap 只涵蓋機器人周圍一小塊範圍,但更新頻率很高,負責即時避開規劃當下才出現的障礙物(例如突然有人走過)。如果只用一張全域地圖做即時避障,範圍太大導致更新太慢,來不及反應新出現的障礙物;如果只用一張區域地圖做全域規劃,機器人看不到範圍外的整體地形,容易規劃出繞遠路甚至走不通的路徑。兩者分工,各自針對不同的時間尺度與空間範圍優化。
faq_02.log
inflation layer 的膨脹半徑要設多大?
至少要大於機器人本體的半徑加上一些安全餘裕,否則規劃出來的路徑理論上『沒有壓到障礙物代價』,但機器人本體的實際尺寸還是可能撞上障礙物邊緣。膨脹半徑設太大則會讓機器人在狹窄通道(例如門框)誤判為無法通過,实務上需要根據機器人實際尺寸與場景寬度做權衡調整。
faq_03.log
規劃器規劃出的路徑,跟機器人實際走的路線會完全一樣嗎?
不會,也不應該期待完全一樣。全域路徑規劃器(planner_server)產出的是一條理論上可行的路徑,實際執行時交給 controller_server 即時根據 local costmap 與當下感測資料微調路線,尤其是遇到規劃當時不存在、之後才出現的障礙物時,會偏離原始全域路徑做局部避讓,這是正常且必要的行為,不是規劃器算錯。