청소로봇이 테이블 아래로 들어갔다가 못 나오는 영상을 봤어요. 다리 사이에 몸이 낀 것도 아닌데 같은 자리를 계속 맴돌더라고요.
커뮤니티에서는 세 가지 추측이 나왔어요. 인플레이션 레이어 크기가 절묘하게 길을 막는 것 같다, 로봇이 커서 그렇다, 장애물 크기가 왔다 갔다 한다. 셋 다 그럴듯한데 어느 쪽이 맞는지 가릴 만큼 코스트맵을 알지는 못했어요. 공개된 내비게이션 스택 소스를 열어 세 추측을 하나씩 대조한 기록이에요.
253이라는 값
경로를 계산하는 쪽에서 로봇은 크기가 없는 점이에요. 대신 장애물을 로봇 크기만큼 미리 부풀려 두고, 부풀린 격자 위에서 점을 움직여요. 점이 안전한 경로가 곧 실제 로봇이 안전한 경로가 되는 방식이에요.
nav2_costmap_2d/include/nav2_costmap_2d/cost_values.hpp에 정의돼 있어요.비용을 매기는 규칙은 inflation_layer.hpp의 computeCost에 있어요. 거리에 따라 세 갈래로 나뉘어요.
if (distance == 0) {
cost = LETHAL_OBSTACLE;
} else if (distance * resolution_ <= inscribed_radius_) {
cost = INSCRIBED_INFLATED_OBSTACLE;
} else {
double factor = exp(-1.0 * cost_scaling_factor_ *
(distance * resolution_ - inscribed_radius_));
cost = (INSCRIBED_INFLATED_OBSTACLE - 1) * factor;
}길을 막는 건 어느 파라미터인가
커뮤니티의 첫 번째 추측을 여기서 정정할 수 있어요. 방향은 맞는데 파라미터가 달라요.
| 파라미터 | 무엇을 정하나 | 통로를 닫나 |
|---|---|---|
inflation_radius (기본 0.55) | 그라데이션을 얼마나 멀리 칠할지 | 아니요 — 252 이하만 깔려요 |
inscribed_radius | 풋프린트에 내접하는 원의 반지름 | 예 — 이 안쪽은 조건 없이 253 |
inscribed_radius는 보통 직접 지정하지 않고 풋프린트에서 자동으로 계산돼요. inflation_layer.cpp의 updateInscribedRadius가 layered_costmap_->getInscribedRadius()를 가져다 쓰는 구조예요.
즉 통로를 닫는 건 인플레이션 반경이 아니라 로봇 치수예요. 두 번째 추측인 “로봇이 커서 그렇다”가 정확히 이 지점을 짚은 거였어요.
1cm가 0이 되는 순간
“절묘하게 막힌다”는 표현이 여기에 딱 맞아요. 조금만 넓었으면 지나갔을 곳이 이산화 때문에 사라져요.
관측도 흔들려요
세 번째 추측은 인플레이션이 아니라 obstacle layer 쪽 이야기예요. obstacle layer는 센서 광선이 무언가에 맞으면 그 끝점을 장애물로 표시하고, 광선이 통과한 경로는 비어 있다고 지워요. 지우는 건 광선이 실제로 통과했을 때만 일어나고요.
테이블 다리는 이 구조에서 까다로운 대상이에요.
| 조건 | 2D LiDAR가 보는 것 | 결과 |
|---|---|---|
| 멀 때 | 광선 간격이 벌어져 통째로 놓침 | 통로가 열려 보임 |
| 가까울 때 | 여러 칸에 걸쳐 잡힘 | 253 영역이 사방으로 자람 |
| 다리 뒤쪽 | 가려서 광선이 못 감 | 잘못 찍힌 칸이 안 지워짐 |
지름 3~5센티미터쯤 되는 얇은 기둥이라 광선이 몇 개나 맞을지가 거리와 각도에 따라 달라져요. 한 칸 크게 잡히면 253 영역이 사방으로 한 칸씩 넓어지고, 아슬아슬하던 통로는 그 순간 닫혀요.
들어갈 땐 열려 있었어요
이제 영상의 순서가 설명돼요.
멀리 있음 -> 다리가 잘 안 잡힘 -> 통로가 열려 보임 -> 테이블 아래로 경로가 남
로봇 진입
가까워짐 -> 다리가 제대로 잡힘 -> 253 영역이 자람 -> 로봇은 이미 안에 있음
안에서 -> 사방 다리가 근거리 관측 -> 둘러싼 칸이 전부 253 -> 나가는 경로 없음
경로 실패 -> 복구 동작이 코스트맵을 비움 -> 다시 열린 것처럼 보임
움직임 -> 센서가 다시 다리를 봄 -> 또 닫힘 (순환)밖에서는 이 순환이 서성이는 모습으로 보이는 거예요. 복구 동작이 근본 원인을 못 고친다는 게 여기서 드러나요. 관측을 지울 뿐 관측이 다시 들어오는 걸 막지는 못하니까요.
그럼 어떻게 푸나
원인이 셋으로 갈리니 손대는 곳도 달라요. 아래는 Nav2 문서와 소스를 근거로 정리한 방향이고, 실제 기체에서 검증한 값은 아니에요.
| 원인 | 조치 | 대가 |
|---|---|---|
| 풋프린트가 실제보다 큼 | robot_radius 또는 footprint를 실측값으로 | 너무 줄이면 실제로 부딪혀요. 범퍼 두께는 남겨야 해요 |
| 이산화로 통로 소멸 | 코스트맵 resolution을 0.05에서 0.025로 | 칸 수가 네 배라 연산량과 메모리가 늘어요 |
| 그라데이션이 과함 | inflation_radius 축소 (기본 0.55) | 좁은 곳은 통과하지만 벽에 붙어 다녀요 |
| 비용 경사가 가파름 | cost_scaling_factor 조정 (기본 10.0) | 253 영역은 안 줄어요. 그 바깥만 완만해져요 |
| 관측 요동 | 높이를 구분하는 voxel layer 도입 | 3D 센서가 필요해요 |
| 애초에 들어가면 안 되는 곳 | 진입 금지 영역 지정 | 근본 해결은 아니지만 가장 확실해요 |
하나 주의할 점은 inflation_radius를 inscribed_radius보다 작게 잡으면 안 된다는 거예요. 253 판정은 inscribed_radius 기준이라 그대로 남는데 바깥 그라데이션이 사라져서, 경로가 벽에 딱 붙게 돼요. 두 값은 항상 같이 봐야 해요.
아직 확인하지 못한 것
이번에 처음 뜯어본 영역이라 한계를 같이 적어둘게요. 이 글은 영상 관찰과 공개 소스를 대조해 세운 설명이지, 직접 로봇을 테이블 밑에 넣고 코스트맵을 띄워 253 영역을 눈으로 확인한 단계는 아니에요. 위 표의 조치들도 문서상 근거로 방향을 잡은 것이지 실측으로 효과를 잰 건 아니고요.
로봇 치수를 적는 파일에 숫자 하나를 크게 적으면 주행이 안 될 수 있다는 것도 같이 알게 됐어요. 나중에 직접 띄워보면서 그 숫자를 바꿔가며 253 영역이 어떻게 변하는지 확인해볼 생각이에요.
관찰 대상 영상은 테이블에 걸린 청소로봇(Monkey Code)이에요. 코드 근거로 참고한 소스는 nav2_costmap_2d의 cost_values.hpp, inflation_layer.hpp, inflation_layer.cpp이고, 파라미터 기본값은 Nav2 Inflation Layer 문서와 Costmap 2D 문서에서 확인했어요.