PinkRab - 전체 목차3장

PinkRab - 헤드캠 SLAM 이 무너진 이유, 그리고 마커로 만든 마스크

들어가며

지난 편에서 삼각대에 카메라를 세워 놓고 큐브를 흔들었더니 0.72mm 가 나왔다. 원조 UMI 가 보고한 6.1mm 보다 한 급 위였고, 나는 기분이 좋았다.

그런데 삼각대에 묶여 있으면 결국 책상 앞이다. UMI 를 하는 이유가 "아무 데서나 데이터를 모으는 것" 인데, 카메라가 삼각대에 있으면 그 삼각대가 새로운 책상이 된다. 그래서 계획은 처음부터 2단계였다. 1단계는 삼각대, 2단계는 같은 카메라를 머리에 얹는 것.

머리에 얹으면 카메라가 움직인다. 카메라가 움직이면 큐브의 세계 좌표를 알려면 카메라가 어디 있었는지부터 알아야 하고, 그걸 푸는 게 SLAM 이다. 그래서 이번 편은 SLAM 을 붙이는 이야기다.

붙였더니 무너졌다.

1. 무너진 모습

측정 방식은 지난 편과 같다. 큐브를 정해진 자리에 놓고 시작, 30초쯤 들고 돌아다니다가, 같은 자리에 정확히 되돌려 놓고 끝낸다. 시작 프레임과 끝 프레임에서 계산한 큐브의 세계 좌표가 같아야 한다. 그 차이가 파이프라인 오차다. closure 라고 부른다.

헤드캠으로 찍은 첫 세션에 SLAM 을 돌린 결과다. 같은 입력, 같은 설정, 네 번 실행.

실행closure회전
175.4 mm5.11°
2253.6 mm13.57°
344.5 mm4.49°
440.7 mm2.35°

게이트는 10mm 다. 전부 밖이고, 무엇보다 편차가 6배다. 오차가 크다는 것보다 이게 더 나쁜 소식이었다. 크기만 문제면 줄이면 되는데, 실행마다 답이 다르면 어느 게 참인지 알 방법이 없다.

2. SLAM 이 매번 다른 답을 냈다

숫자가 튀는 게 큐브를 되돌려 놓는 내 손 때문일 수도 있었다. 그래서 큐브를 빼고 궤적 자체를 비교했다. 네 번 실행이 그린 머리 궤적을 겹쳐 보는 것이다.

SLAM 은 실행할 때마다 자기 좌표계 원점을 새로 잡으니 그냥 빼면 안 되고, 강체 정렬을 먼저 해야 한다. 정렬해서 겹친 결과가 이렇다.

비교중앙값p95
run1 – run2108 mm179 mm
run1 – run3160 mm312 mm
run2 – run4115 mm197 mm

4m 남짓 걸어다닌 궤적에서 실행끼리 10cm 넘게 어긋난다. 내 손 문제가 아니었다. SLAM 이 같은 영상을 보고 매번 다른 길을 그리고 있었다.

로그를 보니 tracking 을 놓친 적은 한 번도 없었다. 끊긴 게 아니라, 끊김 없이 매끄럽게 틀린 길을 그리고 있었다.

3. 범인은 내 손에 있었다

ORB-SLAM3 은 세상이 가만히 있다고 가정한다. 화면에서 뭔가 움직이면 그건 세상이 움직인 게 아니라 카메라가 움직인 거라고 해석한다. 그게 SLAM 의 전제다.

그런데 내 영상에는 가만히 안 있는 게 둘 있다. 큐브와 그걸 쥔 손. 게다가 둘 다 카메라에서 40~60cm 거리라 화면을 크게 차지한다. feature 가 거기에 잔뜩 앉는다.

애니메이션 로딩 중...

왼쪽이 지금까지 벌어지던 일이다. 큐브 위 feature 가 배경보다 많아지는 순간, "이 점들이 오른쪽으로 흘렀으니 카메라가 왼쪽으로 갔군" 이 다수결에서 이긴다. 카메라는 가만히 있었는데도.

dynamic SLAM 논문들은 이 상황에 이름을 붙여 뒀다. consensus inversion — 정적 배경보다 움직이는 물체에서 더 많은 feature 를 쓰는 상태. Learning to Segment Dynamic Objects using SLAM Outliers 가 이걸 정의하고 "major SLAM failure 를 일으킨다" 고 적어 놨다. 내 로그가 딱 그 모습이었다.

4. 남들은 어떻게 푸나

이 문제는 10년 가까이 독립된 갈래로 다뤄져 왔고, 해법은 결국 하나로 모인다. 움직이는 것 위의 feature 를 추정기에 넘기지 않는다.

논문어떻게 찾나어떻게 빼나
DS-SLAMSegNet 의미 분할 + 기하 일관성 재확인분할된 영역의 feature 제외
DOTinstance 분할 후 추적해 실제로 움직인 것만마스크를 앞단에서 생성
VAR-SLAM아는 물체는 keypoint 필터, 모르는 건 robust lossORB-SLAM3 기반, 코드 공개

전부 "무엇이 어디서 움직이는지" 를 먼저 알아내느라 분할망이나 검출망을 돌린다. 그게 이 계열의 비용이다.

그런데 나는 그 비용을 낼 필요가 없다. 큐브가 마커로 자기 위치를 매 프레임 알려주기 때문이다. 어차피 pose 를 풀려고 ArUco 를 검출하고 있으니, 그 사각형 좌표가 곧 "여기 움직이는 게 있다" 는 선언이다. 신경망도, 학습 데이터도, GPU 도 필요 없다.

5. 첫 시도는 더 나빠졌다

사실 이건 예전에 한 번 시도했다가 접었던 방법이다. 그때는 큐브와 손 영역을 회색으로 칠한 영상을 만들어 SLAM 에 다시 넣었다. 결과는 31.9mm → 76.5mm 로 악화였다.

이유는 나중에 알았다.

  • 칠한 자국이 큐브를 따라 움직이면서 배경 feature 를 지웠다 살렸다 한다. 지도에 있던 점이 사라졌다 나타나니 추적이 흔들린다.
  • 칠한 영역의 경계선 자체가 훌륭한 코너다. extractor 가 거기에 새 feature 를 잡는다. 없애려던 것보다 더 나쁜 걸 만든 셈이다.

그래서 결론은 픽셀을 지우면 안 된다 였다. 지워야 할 건 픽셀이 아니라 keypoint 다.

6. 어디에 끼워 넣나

ORB-SLAM3 이 한 프레임을 처리하는 순서는 이렇다. 이미지에서 ORB keypoint 를 뽑고 → 좌우를 매칭하고 → 지도의 점들과 맞춰 자세를 푼다.

끼워 넣을 자리는 뽑은 직후, 매칭 이전 딱 한 곳이다. 그보다 앞이면 픽셀을 건드리는 게 되고, 그보다 뒤면 이미 오염된 매칭 위에서 지우는 게 된다.

내보내는 쪽은 파이썬이다. 세션의 좌·우 이미지에서 마커를 다시 검출하고, 잡힌 사각형들을 convex hull 로 묶고, 0.6배 팽창시킨다. 팽창분은 손 몫이다. 손은 큐브 손잡이를 쥐고 있으니 큐브 폭만큼 뒤를 따라온다.

받는 쪽은 컨테이너 안의 C++ 다. Frame 생성자에서 좌우 추출 스레드가 join 한 바로 다음 줄에 한 줄을 넣었다.

threadRight.join();
pinkrab::drop_masked(mvKeys, mDescriptors, 0);
pinkrab::drop_masked(mvKeysRight, mDescriptorsRight, 1);

descriptor 는 keypoint 와 행 번호로 묶여 있으니 같이 걸러야 한다. 이 두 줄이 실제로 지운 건 전체 keypoint 의 18~23% 였다.

7. 결과

같은 세션을 마스킹만 켜고 끄면서 돌렸다. 나머지는 전부 동일하다.

take머리 이동마스킹 O마스킹 X
A1.55 m / 15 cm1.2 ~ 1.7 mm89.1 mm
B5.00 m / 63 cm5.3 ~ 6.1 mm49.2 mm

마스킹 O 는 네 번 실행한 범위다. 회전 오차는 0.7~1.2° 로 게이트 3° 안이다.

같은 입력에서 마스크만 빼면 60배가 나빠진다. take 를 잘 찍어서가 아니라 이 필터가 한 일이라는 걸 이 대조가 말해 준다.

재현성도 같이 돌아왔다. 네 실행의 궤적을 정렬해 겹치면 중앙값 0.5~1.4mm 차이다. 아까 10cm 씩 어긋나던 그 비교다. ORB-SLAM3 자체의 비결정성이 문제가 아니라, 움직이는 feature 가 그 비결정성을 증폭하고 있었던 것이다.

덤으로 loop closure 는 네 번 다 한 번도 안 걸렸다. 걸리지 않아도 통과한다는 뜻이고, "출발점으로 돌아와야 한다" 는 제약이 사라졌다.

여담 — 디버깅에서 두 번 속았다

화면이 어두워졌다고 생각했다. USB 를 뽑았다 꽂았더니 영상이 어두워 보였다. 알고 보니 어두워진 게 아니라 멈춘 것이었다. 카메라 스레드는 죽었는데 웹서버는 살아서 마지막 프레임을 계속 띄우고 있었다. 정지 화면인지 실시간인지 구분할 표시가 없으면 사람은 밝기 탓을 한다.

자동 노출이 안 도는 줄 알았다. 노출값을 읽었더니 기본값 8500 에서 꼼짝을 안 했다. 그런데 RealSense 는 자동 노출이 켜져 있으면 그 값이 마지막으로 누가 써넣은 숫자를 돌려준다. 실제 사용값이 아니다. 수동으로 고정해 비교해 보고서야 자동 노출이 멀쩡히 돌고 있다는 걸 알았다. 센서에 물어본 값이 아니라 이미지 자체의 통계를 봐야 했다.

둘 다 "측정하지 않고 추측했다" 는 같은 실수였다.

정리

  • 헤드캠 SLAM 이 무너진 원인은 손에 든 큐브 였다. 화면을 크게 차지하는 움직이는 물체가 다수결을 뒤집는다.
  • 해법은 keypoint 단계 제외. 픽셀을 지우면 배경까지 흔들려서 오히려 나빠진다.
  • 남들은 분할망으로 찾는 영역을, 우리는 마커가 매 프레임 공짜로 알려준다.
  • 결과는 1.2~6.1mm, 실행 재현성 1mm 안쪽. 원조 UMI 의 6.1mm 안이다.

이걸로 삼각대를 접을 수 있게 됐다. 다음은 카메라를 그리퍼에 붙이는 쪽이다.