← BlogAMR 내비게이션 입문· 3/12

Costmap 2D 완전 해부 — Static · Obstacle · Inflation 세 층의 합성

Nav2 costmap이 단일 지도가 아니라 static·obstacle·inflation 여러 레이어를 쌓아 만들어지는 구조와 각 층의 역할, 그리고 '장애물 층을 빼자 로봇끼리 서로 통과한' 실제 버그.

약 3분
Nav2costmaplayersobstacle-layerAMR

Costmap 2D 완전 해부

많은 사람이 costmap을 "장애물이 그려진 한 장의 지도"로 생각합니다. 실제로는 다릅니다. Nav2의 costmap은 여러 레이어(layer)를 쌓아 만든 합성물입니다 — 사진 편집 프로그램의 레이어처럼요. 이 구조를 이해하면 내비게이션 디버깅의 절반이 풀립니다.

세 개의 층

가장 기본적인 costmap은 세 층으로 구성됩니다.

도식 렌더링 중…
  • Static Layer — 변하지 않는 지도(앞 글의 점유 격자에서 옴). 벽, 기둥, 선반.
  • Obstacle Layer — LiDAR가 지금 보는 장애물. 사람, 다른 로봇, 새로 놓인 상자. 매 스캔마다 갱신.
  • Inflation Layer — 장애물 주변에 cost 그라데이션을 둘러 로봇 크기를 보상(다음 글 주제).

각 셀의 최종 cost는 0(자유)부터 254(lethal, 절대 통과 불가)까지의 값으로, 세 층의 정보가 합쳐진 결과입니다. 경로 계획기는 이 cost를 "통행료"처럼 보고 합이 가장 작은 길을 찾습니다.

세 층을 합치는 규칙은 한 줄입니다 — 각 셀에서 최댓값.

# 마스터 costmap = 각 셀에서 모든 층의 최댓값
def master_cost(cell, layers):
    return max(layer.cost(cell) for layer in layers)
    # static(벽) · obstacle(센서·타 로봇) · inflation(여유) 를 한 값으로 합성

층을 잘못 빼면 — 로봇끼리 서로 통과한 버그

레이어 구조는 강력하지만, 잘못 조합하면 미묘한 버그가 납니다. 우리가 겪은 사례입니다.

정적 지도(static layer)가 이미 벽을 다 알고 있으면, global costmap에서는 LiDAR obstacle layer를 빼는 최적화를 흔히 합니다. LiDAR 노이즈가 전역 경로를 흔들리게(oscillation) 만드는 걸 막기 위해서죠. 합리적인 선택입니다.

그런데 다중 로봇 환경에서 이게 함정이 됐습니다.

도식 렌더링 중…

다른 로봇은 정적 지도엔 없습니다. 오직 obstacle layer(실시간 센서)에만 잡힙니다. 그래서 obstacle layer를 빼자, 로봇들이 서로를 못 보고 전역 경로가 겹쳐 버렸습니다.

올바른 처방은 "전부 빼기"가 아니라 선택적 합성 — 로봇 간 충돌 정보는 local과 global costmap 양쪽에 다시 넣고, 흔들림의 원인인 raw LiDAR 노이즈만 global에서 거릅니다. "층을 하나 빼면 그 층에만 있던 정보가 통째로 사라진다"는 게 핵심 교훈입니다.

costmap 초기화의 또 다른 함정 — Off Grid

costmap은 origin(기준점)과 size(크기)로 월드의 어느 영역을 덮는지 결정됩니다. 이게 잘못 잡히면 로봇이나 목표가 격자 바깥에 놓여 "Pose Goes Off Grid" 오류가 납니다. 경로 계획기는 자기가 덮지 못하는 좌표의 경로를 만들 수 없으니까요. costmap의 범위가 환경 전체(특히 양수 사분면 너머)를 덮는지 항상 확인해야 합니다.

한 줄 정리

📌 Costmap은 한 장의 지도가 아니라 static(고정) + obstacle(실시간) + inflation(안전여유) 의 레이어 합성물이다. 층을 빼는 최적화는 그 층에만 있던 정보(예: 다른 로봇)를 통째로 지운다 — 빼는 게 아니라 선택적으로 합성해야 한다.