PinkRab - 전체 목차2장

PinkRab - UMI 그리퍼 착수, 계획 수립과 첫 실측

들어가며

지난 편에서 팔을 만들었다. 이제 그 팔에 먹일 데이터가 필요한데, 여기서 문제가 하나 생긴다. 데이터를 모으려면 로봇이 있어야 하고, 로봇이 있어야 하는 순간 데이터를 모을 수 있는 장소는 내 책상 앞으로 줄어든다. 벽에 볼트로 박아 둔 팔을 들고 부엌이나 창고에 가서 시연을 모을 수는 없으니까.

UMI(Universal Manipulation Interface)가 끊는 고리가 정확히 이거다. 사람이 집게처럼 생긴 물건을 손에 들고 그냥 일을 하면, 그게 로봇 데이터가 된다. 원리 자체는 논문 해석 - UMI 에 자세히 풀어 뒀으니 여기선 다루지 않는다. 이 글은 그 방식을 내 로봇에 어떻게 붙일지 결정한 하루의 기록이다.

정한 것, 잰 것, 걸려 넘어진 것 순으로 간다.


1. 무엇을 기록해야 하나

UMI 시연 하나는 결국 세 갈래 신호로 이루어진다. 이 셋만 있으면 정책을 학습시킬 수 있고 하나라도 빠지면 못 쓴다.

기록할 것왜 필요한가PinkRab 의 답
그리퍼가 본 화면정책의 관측 입력광각(fisheye) 카메라 모듈
그리퍼의 6-DoF pose정책의 행동 출력마커 큐브 + 관찰자 카메라
jaw 벌림행동 출력의 나머지 절반남는 리더암 서보를 트리거 encoder 로

카메라는 같은 모듈을 두 군데에

가장 먼저 못 박은 규칙이다. 손에 드는 handheld 쪽과 로봇 손목 쪽에 똑같은 카메라 모듈을 단다.

이유는 관측 분포다. 사람 손으로 모은 영상으로 학습한 정책이 로봇에서 돌아가려면, 로봇이 보는 화면이 학습할 때 본 화면과 같은 종류여야 한다. 화각이 다르고 왜곡 곡선이 다르면 정책 입장에서는 다른 세상이다. 렌즈를 맞추는 게 도메인 갭을 줄이는 가장 싸고 확실한 방법이다.

jaw 는 서보로 잰다

원조 UMI 는 손가락 사이 폭을 영상에서 읽는다. 집게 몸통에 마커를 붙이고 카메라가 그걸 보게 해서 폭을 복원한다. 처음엔 이게 우아해 보였는데, 뜯어 보니 그건 우아함이라기보다 제약의 산물이었다. GoPro 하나로 모든 걸 해결해야 하니 폭도 그 한 대의 눈으로 읽을 수밖에 없었던 것.

나에게는 그 제약이 없다. 지난 편에서 리더암을 포기했으니 서보가 남는다. 트리거에 서보를 하나 물려 두면 손가락 폭이 그냥 각도로 나온다. 영상 조건과 무관하고, 조명이 어두워도 나오고, 마커가 가려져도 나온다. 남는 부품으로 문제 하나를 통째로 지우는 셈이라 안 할 이유가 없다.

2. 카메라 무게를 먼저 쟀다

"광각 카메라를 단다"까지는 쉬운데, 어떤 카메라를 다느냐는 로봇 쪽이 결정한다. 손목 끝에 매달리는 무게라서 그렇다.

그래서 카메라를 고르기 전에 MuJoCo 모델의 손목에 가짜 질량을 달아 보고 중력 토크를 뽑았다. 30g, 60g, 100g 세 가지로.

결과가 좀 서늘했다. 아무것도 안 달린 기본 상태에서 이미 서보 권장 토크 한계 근처였다. 여기에 60g 을 더하면 중력 토크가 약 17% 증가한다. 100g 이면 더하다.

그러니까 GoPro(150g)는 애초에 후보가 아니다. 로봇 손목에는 물리적으로 못 단다. 서보가 버티더라도 상시 한계 근처에서 돌아가는 관절은 발열과 마모로 값을 치른다.

여기서 앞의 규칙이 발목을 잡는다. 두 곳에 같은 모듈을 달기로 했으니, 로봇이 못 다는 카메라는 handheld 에도 못 단다. handheld 만 보면 GoPro 도 괜찮은데 말이다. 결론은 경량 광각 모듈로 확정. 계산 한 번으로 후보 목록의 절반이 날아갔는데, 이런 계산은 부품을 사기 전에 하는 게 훨씬 싸게 먹힌다. 사고 나서 재면 그건 계산이 아니라 후회다

3. pose 는 어떻게 잴 것인가

세 신호 중 제일 어려운 게 pose 다. 그리퍼가 방 안에서 어디에 어떤 방향으로 있었는지를 알아야 하는데, 방에는 좌표계를 알려 주는 장치가 없다.

원조 UMI 는 그리퍼에 달린 카메라 영상으로 SLAM 을 돌려 카메라 자신의 궤적을 뽑는다. 나는 구조를 뒤집기로 했다. 그리퍼는 보이는 쪽, 관찰자가 보는 쪽이다.

  • handheld 등판에 마커 큐브를 하나 세운다.
  • 그걸 보는 관찰자 카메라를 머리에 쓴다. 헤드캠이다.

그럼 관찰자 카메라 자신이 움직이는 건 어떻게 하냐고? 그건 SLAM 이 배경 풍경으로 상쇄한다. 기차에 앉아 창밖의 옆 기차를 볼 때를 떠올리면 된다. 옆 기차만 보고 있으면 누가 움직이는지 알 수 없지만, 플랫폼을 한 번 힐끗 보면 즉시 정리된다. 방의 정지된 배경이 그 플랫폼 역할을 한다.

그래서 pose 는 두 조각을 곱해서 만든다.

world_T_cube(t)  =  world_T_cam(t)  @  cam_T_cube(t)
                    스테레오 적외선     마커 큐브에
                    영상 위의 SLAM      ArUco 로 푼 자세

앞 조각은 ORB-SLAM3 가 오프라인으로 만들어 주는 파일이고 뒤 조각이 이 패키지가 직접 하는 일이다. SLAM 을 라이브러리 의존이 아니라 파일 계약으로 둔 건 일부러다. C++ 빌드와 vocabulary 파일이 필요한 물건이라 기하를 다루는 패키지 안에 들어올 이유가 없다.

카메라 구매 검토 여담

관찰자 카메라를 뭘로 할지가 다음 문제였다. 처음엔 사는 쪽을 봤다.

  • 상용 VIO 모듈(OAK 계열): 자세 추정을 통째로 해 주는 물건이라 매력적인데, 정확도를 뒷받침할 근거를 찾기 어려웠다. 데이터시트의 숫자와 내가 필요한 조건(40~60cm, 손이 빠르게 움직임)에서의 실측은 다른 얘기다.
  • Quest 컨트롤러: 추적 성능은 좋은데 데이터를 밖으로 빼는 경로가 라이선스에 묶여 있다. 오픈소스로 공개할 프로젝트에서 이건 결격이다.
  • 그리고 둘 다 비쌌다.

한참 뒤지다가 서랍에서 RealSense D435 를 찾았다. 예전에 사 두고 안 쓰던 물건. 이게 답이었다.

  • 스테레오 + 글로벌 셔터: 두 눈 사이 간격(49.85mm 로 실측됐다)이 곧 자다. IMU 없이도 metric scale 이 나온다는 뜻이고 단안 SLAM 이 늘 앓는 스케일 모호성 문제가 통째로 없다.
  • 공장 캘리브레이션: 장치가 자기 intrinsic 을 들고 있다. 표에서 가져온 초점거리는 1~2% 씩 틀리는데, 초점거리 1% 오차는 보고되는 모든 거리의 1% 오차다.

한 가지 반직관적인 설정이 있다. 적외선 프로젝터를 끈다. D435 가 실내에서 깊이를 보는 이유가 그 프로젝터인데, 여기선 그게 독이다. 점 패턴이 마커 종이 위에 떨어지면 pose 정확도가 기대는 코너 리파인먼트를 망가뜨리고, 방 안에 떨어지면 카메라에 딱 붙어 따라다니는 텍스처가 된다. SLAM 이 구조로 착각하기 딱 좋은, 그러나 구조가 아닌 바로 그것.

4. 마커 큐브

pinkrab-umi marker ./print 하나로 프린트할 것들이 나온다. 벡터 PDF, 같은 페이지의 600dpi PNG, 그리고 큐브 STL.

큐브 한 변50 mm
종이 타일48 mm, 0.4mm 깊이의 48.4mm 포켓에
마커 검은 사각형36 mm (모듈 6mm, quiet zone 6mm)
면 id+Z 0, +X 1, -X 2, +Y 3, -Y 4
스톡 소켓8 x 8 mm, 깊이 15 mm, -Z 면

solvePnP 에 들어가는 건 36mm 뿐이고 나머지는 전부 조립 치수다.

왜 한 면이 아니라 큐브인가

평면 마커 한 장으로도 pose 는 나온다. 문제는 그 답이 둘이라는 것.

정사각형 하나를 카메라가 본 모습만으로는 "약간 왼쪽으로 기울었다"와 "약간 오른쪽으로 기울었다"가 거의 같은 이미지를 만든다. 이걸 twofold ambiguity 라고 하는데, 노이즈가 두 해 사이를 왔다 갔다 뒤집는다. 특히 마커를 정면으로 볼 때가 최악이다. 두 해가 가장 비슷해져서 solver 가 사실상 동전을 던진다.

큐브의 두 면은 같은 평면 위에 있지 않다. 두 면이 동시에 보이면 모호성이 그냥 사라진다. 다섯 면에 붙이는 건 어느 각도에서 봐도 최소 두 면이 보이게 하려는 배치다.

프린트 관련해서 세 가지가 큐브 파일에는 안 적혀 있다.

  • 100% 로 인쇄한다. 맞춤 인쇄, 페이지에 맞춤 전부 안 된다. 3% 축소된 페이지는 3% 작은 마커를 주고, 3% 작은 마커는 큐브를 3% 더 멀리 보낸다. 모든 프레임에서, 매끄럽게, 완벽하게 그럴듯해 보이는 궤적으로. 시트에 100mm 자 막대를 같이 찍어 두니 자르기 전에 재 보면 된다.
  • 무광지로. 적외선 이미저는 광택 인쇄를 뒤쪽 조명의 거울로 본다. 마커 위에 하이라이트가 터지면 딱 그 각도에서 디코딩이 멈춘다.
  • 타일 위쪽 막대가 방향 표시다. PnP 는 마커 자신의 프레임을 돌려주므로, 타일을 90도 돌려 붙이면 그 면이 보일 때마다 pose 가 직각만큼 틀린다. 그런데 잔차는 완벽하다. 맞춤 자체는 완벽하고 그 뒤의 기하가 틀렸으니까. 풀 마르기 전에 막대를 확인하자.

5. 트러블슈팅 — IR 에서 마커가 사라졌다

큐브를 조립하고, 타일을 붙이고, 카메라를 켰다. 그리고 아무것도 검출되지 않았다.

처음엔 검출기를 의심했다. 파라미터를 만지고, 임계값을 바꾸고, 디버그 이미지를 찍어 봤다. 그런데 디버그 이미지가 이상했다. 마커가 있어야 할 자리에 아무 무늬 없는 하얀 종이가 있었다.

내 눈에는 새까맣다. RGB 스트림에서도 새까맣고 디코딩까지 잘 된다. 그런데 적외선 이미저에게는 그 자리가 백지였다.

원인 확정을 위해 IR 과 RGB 를 나란히 띄우는 실시간 뷰어를 하나 만들었다. 같은 큐브, 같은 순간, 왼쪽에는 또렷한 마커, 오른쪽에는 텅 빈 종이. 이 화면을 보고 나서야 검출기가 무죄라는 걸 알았다.

범인은 프린터였다.

염료 잉크젯 잉크는 근적외선에서 투명하다. 가시광에서는 빛을 흡수해 검게 보이지만 850nm 근처에서는 그냥 통과시킨다. 적외선 이미저에게 그 잉크는 존재하지 않는다.

해법은 레이저 프린터다. 레이저 토너는 카본이고 카본은 이 카메라가 보는 모든 파장에서 검다. 그리고 편의점 복합기가 레이저다. 결국 이 문제는 편의점에 한 번 다녀와서 해결됐다. 반나절을 태우고 나서

이걸 README 맨 앞에 박아 뒀다. 오픈소스로 공개할 물건이라 남들도 똑같이 밟을 지뢰이기 때문이다. 게다가 이 실패는 조용하다. 에러가 안 난다. 그냥 아무것도 검출이 안 될 뿐이고 그러면 누구나 자기 코드부터 의심하게 된다.

6. 첫 실측

고정 카메라(삼각대) 단계부터 시작한다. 카메라가 안 움직이면 world_T_cam 은 항등행렬이고 SLAM 을 돌릴 게 없다. 나머지 전부, 그러니까 큐브와 검출과 합성과 closure 지표는 나중과 똑같이 굴러간다.

RGB 스트림으로 진행했다. 이 시점엔 아직 레이저로 다시 뽑은 타일이 손에 없었으니까.

거리별 자세 노이즈는 이렇게 나왔다.

거리xyz
34 cm0.11 mm0.17 mm0.51 mm
61 cm1.6 mm1.3 mm4.0 mm
124 cm3.9 mm4.0 mm12.4 mm

눈에 띄는 건 z 축(깊이)이 항상 제일 나쁘고, 거리의 제곱에 가깝게 커진다는 점이다. 단안 PnP 가 깊이를 마커의 겉보기 크기로 추정하기 때문인데, 멀어질수록 픽셀 몇 개 차이가 큰 거리 차이로 번역된다.

그리고 진짜 성적표인 closure.

closure 5.01 mm / 3.31°

closure 는 이런 지표다. 그리퍼를 고정 지그에서 떼어 내고, 30초 동안 아무렇게나 움직이고, 다시 같은 지그에 꽂는다. 그때 나오는 자세 차이가 파이프라인 전체의 오차 예산이다. 잔차(reprojection error)는 "마커가 코너에 잘 맞았다"까지만 말해 준다. 초점거리가 1% 틀렸다거나 SLAM 이 분당 1cm 씩 흐른다거나 하는 건 잔차를 아름답게 유지한 채로 궤적을 통째로 망친다. closure 는 움직이지 않은 플라스틱 덩어리 하나만 있으면 되는, 끝에서 끝까지의 측정이다.

원조 UMI 가 보고한 값이 6.1mm 다. 5.01mm 면 그보다 낫고 무엇보다 내가 실제로 쓸 헤드캠 거리인 40~60cm 구간에서 여유 있게 통과한다. 첫 측정치로는 충분히 갈 만하다는 뜻이라 여기서 하루를 마무리했다.

남은 문제도 있다. 회전이 가끔 뒤집힌다. 앞에서 말한 평면 마커의 twofold ambiguity 가 완전히 사라지지 않았다. 큐브가 두 면을 보여 주지 못하는 순간, 그러니까 거의 정면으로 한 면만 보이는 각도에서 튄다. 여기에 시간 연속성 필터를 걸 생각이다. 손은 프레임 사이에 90도씩 뒤집히지 않으니까, 직전 자세와 너무 먼 해는 그냥 버리면 된다.

정리

하루 동안 정한 것들.

  • 카메라는 로봇 손목이 먼저 결정한다. 기본 상태가 이미 서보 한계 근처라 60g 추가에 중력 토크가 17% 오른다. GoPro 는 로봇 쪽에서 불가능하고 두 곳에 같은 모듈을 쓰기로 했으니 handheld 도 경량으로 간다.
  • jaw 는 영상이 아니라 서보로 읽는다. 원조의 vision 방식은 GoPro 한 대라는 제약에서 나온 답이고 리더암을 안 쓰는 나에게는 서보가 남는다.
  • pose 는 관찰자 구조로 만든다. 큐브는 보이는 쪽, 헤드캠이 보는 쪽. 관찰자 자신의 움직임은 배경 풍경 위의 SLAM 이 상쇄한다.
  • 큐브는 다섯 면이어야 한다. 평면 마커 한 장의 pose 는 두 갈래로 갈리고 노이즈가 그 사이를 뒤집는다.
  • 레이저 프린터로 뽑는다. 잉크젯 염료는 근적외선에서 투명하다. 눈에도 RGB 에도 멀쩡한 마커가 IR 에서는 백지다.
  • 첫 성적은 closure 5.01mm / 3.31°. 쓸 거리에서는 통과, 회전 플립은 숙제.

다음 편은 카메라를 머리에 얹는 이야기다. 헤드 마운트를 설계해 붙이고, 실제로 걸어 다니면서 찍은 프레임으로 ORB-SLAM3 를 돌려 world_T_cam 을 채운다. 고정 카메라에서 잘 나온 숫자가 머리 위에서도 버티는지 보면 된다. 안 버티면 그것도 그대로 적을 생각이다.