hello, robot

청소로봇이 테이블 아래로 들어갔다가 못 나오는 영상을 봤어요. 다리 사이에 몸이 낀 것도 아닌데 같은 자리를 계속 맴돌더라고요.

커뮤니티에서는 세 가지 추측이 나왔어요. 인플레이션 레이어 크기가 절묘하게 길을 막는 것 같다, 로봇이 커서 그렇다, 장애물 크기가 왔다 갔다 한다. 셋 다 그럴듯한데 어느 쪽이 맞는지 가릴 만큼 코스트맵을 알지는 못했어요. 공개된 내비게이션 스택 소스를 열어 세 추측을 하나씩 대조한 기록이에요.

대상영상 관찰 (제품 스택은 비공개) 참조 구현Nav2 · 소스 공개 확인한 것비용 상수 · 인플레이션 계산식 확인 못 한 것실기체 재현 · 조치 효과
먼저 짚어둘 전제
영상 속 로봇이 어떤 소프트웨어를 쓰는지는 알 수 없어요. 상용 청소로봇은 대부분 자체 내비게이션 스택을 쓰고 내부를 공개하지 않아요. 그래서 아래는 "저 제품이 이렇게 동작한다"가 아니라, 코스트맵 기반으로 경로를 짜는 로봇이라면 같은 증상이 어떤 경로로 만들어지는지에 대한 설명이에요. 근거는 소스가 공개된 Nav2에서 가져왔어요.

253이라는 값

경로를 계산하는 쪽에서 로봇은 크기가 없는 점이에요. 대신 장애물을 로봇 크기만큼 미리 부풀려 두고, 부풀린 격자 위에서 점을 움직여요. 점이 안전한 경로가 곧 실제 로봇이 안전한 경로가 되는 방식이에요.

코스트맵 비용 값
nav2_costmap_2d/include/nav2_costmap_2d/cost_values.hpp에 정의돼 있어요.
255NO_INFORMATION모르는 칸254LETHAL_OBSTACLE장애물 자체253INSCRIBED_INFLATED_OBSTACLE중심이 오면 몸이 반드시 겹침252MAX_NON_OBSTACLE여기부터 아래는 통과 가능0FREE_SPACE자유 공간경로 계획에는 "못 감"253과 254는 값이 다르지만길을 찾는 입장에서는 똑같이 막힌 칸이에요"가급적 피해라"inflation_radius 안쪽에 지수적으로 깔리는그라데이션이라 통로를 닫지는 않아요오래된 문서에는 253이 254로 적힌 것이 있어요. 지금 소스에서 254는 LETHAL_OBSTACLE이라 두 값이 겹쳐 보이면 한참 헤매요.

비용을 매기는 규칙은 inflation_layer.hppcomputeCost에 있어요. 거리에 따라 세 갈래로 나뉘어요.

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.cppupdateInscribedRadiuslayered_costmap_->getInscribedRadius()를 가져다 쓰는 구조예요.

즉 통로를 닫는 건 인플레이션 반경이 아니라 로봇 치수예요. 두 번째 추측인 “로봇이 커서 그렇다”가 정확히 이 지점을 짚은 거였어요.

1cm가 0이 되는 순간

여유가 근소하면 격자가 먹어요
다리 두 개 사이가 열리려면 간격이 내접 반경의 두 배를 넘어야 해요.
간격 0.60 m남는 폭 0.16 m0.60 m격자 한 칸 0.05 m한 칸이 들어가요간격 0.45 m남는 폭 0.01 m0.45 m격자 한 칸 0.05 m한 칸도 안 남아요통로가 열릴 조건간격 > 2 × 내접반경반경 0.22 m면 0.44 m를 넘어야 해요여유가 근소하면0.45 m에서 남는 폭은 0.01 m인데코스트맵 한 칸은 보통 0.05 m예요.물리적으로는 1 cm가 있는데계산상으로는 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_radiusinscribed_radius보다 작게 잡으면 안 된다는 거예요. 253 판정은 inscribed_radius 기준이라 그대로 남는데 바깥 그라데이션이 사라져서, 경로가 벽에 딱 붙게 돼요. 두 값은 항상 같이 봐야 해요.

아직 확인하지 못한 것

이번에 처음 뜯어본 영역이라 한계를 같이 적어둘게요. 이 글은 영상 관찰과 공개 소스를 대조해 세운 설명이지, 직접 로봇을 테이블 밑에 넣고 코스트맵을 띄워 253 영역을 눈으로 확인한 단계는 아니에요. 위 표의 조치들도 문서상 근거로 방향을 잡은 것이지 실측으로 효과를 잰 건 아니고요.

그래도 하나 분명해진 것
이 현상에는 버그가 없어요. LiDAR는 자기가 볼 수 있는 평면만 정직하게 봤고, 인플레이션은 로봇 크기만큼만 정확히 부풀렸고, 경로 계획은 253을 피하라는 규칙을 그대로 지켰어요. 각 단계가 설계대로 동작한 결과가 끼임이고, 그래서 로그에 아무것도 안 남아요.

로봇 치수를 적는 파일에 숫자 하나를 크게 적으면 주행이 안 될 수 있다는 것도 같이 알게 됐어요. 나중에 직접 띄워보면서 그 숫자를 바꿔가며 253 영역이 어떻게 변하는지 확인해볼 생각이에요.

관찰 대상 영상은 테이블에 걸린 청소로봇(Monkey Code)이에요. 코드 근거로 참고한 소스는 nav2_costmap_2d의 cost_values.hpp, inflation_layer.hpp, inflation_layer.cpp이고, 파라미터 기본값은 Nav2 Inflation Layer 문서Costmap 2D 문서에서 확인했어요.