0biglife.

[자율주행] 어노테이션 툴 만들기 ① 에디터 — 라벨러의 12초를 설계하는 일

자율주행/어노테이션 툴

· 2026-09-07

[자율주행] 어노테이션 툴 만들기 ① 에디터 — 라벨러의 12초를 설계하는 일

본문의 점선 밑줄 친 전문용어를 누르면 설명이 열립니다.

어노테이션 데이터에서 정답이 무엇이고 어떻게 관리되는지를 다뤘다. 이 글은 그 반대편이다. 그 정답을 사람이 실제로 만드는 도구를 어떻게 만드는가.

먼저 왜 이게 중요한지부터. 산수 하나면 충분하다.

3D 큐보이드 하나에 30초  →  10만 개면 833 사람-시간
3D 큐보이드 하나에 12초  →  10만 개면 333 사람-시간

같은 데이터셋에 예산이 2.5배 차이 난다. 그리고 그 18초는 대부분 알고리즘이 아니라 UI에서 나온다 — 회전을 몇 번 시도했는지, 뷰를 몇 번 돌렸는지, 실수를 몇 번 되돌렸는지.

그래서 툴은 "내부용 도구니까 대충"이 가장 비싼 영역이다. 도구의 품질이 곧 데이터 예산이다.

구조 한 장

┌─ 데이터 층 ──────────────────────────────────┐
│  프레임 로더 · 점군/이미지 디코딩 · 프리페치     │
└──────────────┬───────────────────────────────┘
               ▼
┌─ 뷰어 층 ────────────────────────────────────┐
│  3D 뷰 · BEV 뷰 · 카메라 뷰   (WebGL)          │
└──────────────┬───────────────────────────────┘
               ▼
┌─ 편집 층 ────────────────────────────────────┐
│  히트 테스트 · 핸들 · 제약 · 스냅 · 단축키       │
└──────────────┬───────────────────────────────┘
               ▼
┌─ 상태 층 ────────────────────────────────────┐
│  라벨 모델 · 커맨드 스택(undo) · 더티 추적       │
└──────────────┬───────────────────────────────┘
               ▼
┌─ 동기화 층 ──────────────────────────────────┐
│  자동 저장 · 낙관적 잠금 · 충돌 처리            │
└──────────────────────────────────────────────┘

이 글은 뷰어~상태 층을 다룬다. 동기화 층과 백엔드는 다음 글에서.

좌표 왕복 — 에디터의 심장

에디터가 하는 일을 한 문장으로 줄이면 "마우스 커서를 3D 공간의 무언가로 바꾸는 것" 이다. 이 왕복이 정확하지 않으면 나머지는 의미가 없다.

스크린 → 월드

마우스는 2D인데 공간은 3D다. 클릭 하나로는 깊이가 정해지지 않는다. 그래서 광선(ray) 을 만들어 무언가와 교차시킨다 — 이 과정을 라고 부른다.

1// NDC(-1..1) 로 정규화
2const ndc = {
3  x: (px / width) * 2 - 1,
4  y: -((py / height) * 2 - 1),   // Y 뒤집기 — 여기서 자주 틀린다
5};
6
7// 카메라 역행렬로 광선 방향
8const dir = unproject(ndc, invViewProj);
9
10// 무엇과 교차시킬 것인가가 결정이다
11const hit = intersectGroundPlane(camPos, dir);  // 지면 z=0
12// 또는
13const hit = pickFromIdBuffer(px, py);           // GPU 피킹

교차 대상을 무엇으로 잡느냐가 UX를 좌우한다.

  • 지면 평면 — 새 박스를 놓을 때. 도로 위에 놓는다는 가정이 자연스럽다.
  • 기존 객체() — 선택할 때. id 버퍼를 함께 그려두고 클릭 지점 픽셀을 읽는다.
  • 의 가장 가까운 점 — 벽이나 지형 위에 찍을 때.

월드 → 스크린

반대 방향도 계속 필요하다. 핸들을 어디에 그릴지, 라벨 텍스트를 어디에 띄울지, 박스가 화면 밖으로 나갔는지.

여기서 자주 하는 실수 하나. 카메라 뒤의 점을 투영하면 화면 안에 유령처럼 나타난다. z > 0 검사를 빼먹으면 뒤쪽 객체의 라벨이 화면에 떠다닌다.

3D 큐보이드 편집이 어려운 진짜 이유

2D 박스는 쉽다. 드래그해서 사각형을 그리면 된다. 3D 는 자유도가 7개다 — 위치 3, 크기 3, yaw 1. 그런데 입력 장치는 2차원이다.

자유롭게 3D 조작을 허용하면 라벨러는 매번 "지금 이게 어느 축으로 움직이는 거지"를 고민하게 되고, 그 고민이 초 단위로 쌓인다. 해법은 자유도를 줄여주는 것이다.

① BEV 뷰에서 편집한다

가장 큰 한 방이다. 위에서 내려다보면 차량 큐보이드의 조작이 2D 문제로 축소된다 — 중심 (x, y), 크기 (w, l), 회전 yaw. 높이와 z는 대부분 자동으로 채워도 된다.

도로 위 객체는 거의 모두 지면에 붙어 있고 롤·피치가 0에 가깝다. 이 도메인 지식이 UI 설계에 그대로 반영된다.

② 축 제약

3D 뷰에서 조작할 때는 한 번에 한 축만 움직이게 한다. 핸들을 축별로 색을 달리해 두면(x=빨강, y=초록, z=파랑) 학습이 필요 없다.

③ 지면 스냅

박스 바닥을 자동으로 지면에 붙인다. 라벨러가 z를 만질 일이 거의 없어진다. 지면 높이는 점군의 지면 추정에서 가져온다.

④ 점군 자동 맞춤 — 처리량을 가장 크게 올리는 기능

대충 감싸기만 하면 나머지를 맞춰준다.

박스 안에 들어온 점들을 모아 주성분 분석(PCA)으로 주축을 찾고, 그 방향으로 최소 경계 상자를 다시 계산한다. 라벨러는 대략적인 영역만 지정하면 되고, 정밀한 크기와 yaw는 도구가 낸다.

라벨러: 대충 드래그  →  도구: 안쪽 점군에 맞춰 크기·yaw 자동 조정  →  라벨러: 필요하면 미세 조정

실무에서 큐보이드 작업 시간을 가장 크게 줄이는 단일 기능이 보통 이것이다. 다만 점이 너무 적으면(원거리) 오히려 방해가 되므로 점 개수 임계값 아래에서는 끄는 게 낫다.

⑤ 다중 뷰 동기화

3D 뷰 · 뷰 · 카메라 투영 뷰를 나란히 두고, 한 곳에서 움직이면 세 곳이 동시에 갱신되게 한다.

카메라 뷰가 특히 중요하다. 3D 박스를 이미지에 투영해 겹쳐 보면 크기와 위치가 맞는지 사람 눈이 즉시 판단한다. 라이다 만 보고는 "이게 승합차인지 소형 트럭인지" 판단이 안 되는 경우가 많은데, 이미지를 같이 보면 1초다.

여기서 캘리브레이션과 왜곡이 정확해야 한다. 투영이 어긋난 상태로 라벨러에게 보여주면, 라벨러가 그 어긋남을 보정하려고 박스를 옮긴다. 도구의 버그가 라벨의 오차로 굳는다.

편집 상태 모델 — 실행취소가 공짜가 아닌 이유

라벨러는 실수한다. 그리고 실수를 되돌릴 수 없으면 조심스러워지고, 조심스러워지면 느려진다. undo는 편의 기능이 아니라 속도 기능이다.

커맨드 패턴

모든 편집을 "실행할 수 있고 되돌릴 수 있는 객체"로 만든다.

1class MoveBoxCommand {
2  constructor(boxId, from, to) { ... }
3  do(state)   { state.boxes[this.boxId].center = this.to; }
4  undo(state) { state.boxes[this.boxId].center = this.from; }
5}

스택 두 개(undo/redo)로 관리한다. 구현이 단순하고 메모리가 적게 든다.

스냅샷 방식도 있다. 편집 전 상태를 통째로 복사해 쌓는 것이다. 구현이 훨씬 쉽지만 프레임당 객체가 수백 개면 메모리가 빠르게 는다. 절충은 불변 자료구조 + 구조적 공유다 — 바뀐 부분만 새로 만들고 나머지는 참조를 공유한다.

어디까지가 하나의 실행취소인가

이게 설계 결정이다. 드래그 중 매 프레임마다 커맨드를 쌓으면 undo를 100번 눌러야 한 번의 드래그가 취소된다. 드래그 시작~끝을 하나의 커맨드로 묶는다.

반대로 "박스 10개 일괄 삭제"는 하나로 묶는 게 맞다. 원칙은 라벨러가 "한 동작"이라고 인식하는 단위다.

시간축 — 트랙 편집

여기서 툴이 일반적인 그래픽 편집기와 갈린다. 객체가 프레임을 넘어 이어진다.

키프레임과 보간

모든 프레임에 박스를 그리는 건 불가능하다. 몇 개의 만 찍고 나머지는 채운다.

  • 위치·크기 — 선형 보간으로 대체로 충분하다.
  • yaw — 각도를 그냥 선형 보간하면 179° → -179°에서 한 바퀴 돈다. 최단 경로로 감싸거나(shortest-arc) 쿼터니언 slerp를 써야 한다. 3D 기하에서 이야기한 그 문제가 여기서 나온다.
  • 정지 물체 — 에고가 움직이므로 센서 좌표계에서는 계속 움직인다. 월드 좌표계에 고정해두고 보간하면 키프레임이 훨씬 적게 든다. 에고 포즈가 여기서 필요하다.

트랙 편집 도구

  • 분리(split) — 잘못 이어진 트랙을 특정 프레임에서 자른다
  • 병합(merge) — 가림 때문에 끊긴 두 트랙을 잇는다 (id switch 수정)
  • 일괄 속성 변경 — 트랙 전체의 클래스를 한 번에

타임라인 UI가 이 작업의 중심이 된다. 트랙별로 가로줄을 그리고, 키프레임을 점으로, 가림 구간을 다른 색으로 표시한다. 여기서 트랙이 끊긴 곳이 눈에 띄어야 한다.

단축키 — 초를 줄이는 곳

라벨러는 하루 종일 같은 동작을 반복한다. 손을 마우스에서 떼는 횟수가 곧 시간이다.

에는 몇 가지 원칙이 있다.

  • 왼손은 키보드 홈 근처, 오른손은 마우스. 자주 쓰는 키를 Q W E A S D 근처에 둔다.
  • 클래스 전환은 숫자 키. 드롭다운을 열게 하지 않는다.
  • 모드 전환보다 즉시 실행. "회전 모드로 들어가서 회전하고 나오기"보다 "회전 키를 누른 채 드래그"가 빠르다.
  • 프레임 이동은 한 손으로. 이전/다음 프레임, 다음 , 다음 미완성 객체.
  • 되돌리기는 반드시 표준 키로. 여기서 창의성을 발휘하면 안 된다.

그리고 측정하라. 어떤 기능이 몇 번 쓰이는지, 한 객체당 클릭이 몇 번인지 도구가 스스로 기록하면, 다음에 무엇을 고쳐야 할지가 추측이 아니라 데이터가 된다. 이 이야기는 세 번째 글에서 이어진다.

성능 — 60fps를 지키는 예산

편집기가 버벅이면 정밀 작업이 불가능하다. 그리고 자율주행 데이터는 기본이 무겁다.

  • 프레임 로딩 — 스크럽할 때마다 수십 MB를 새로 받으면 못 쓴다. 앞뒤 N프레임 프리페치가 필수다. 라벨러는 대부분 순차로 이동한다.
  • 디코딩은 워커에서 파싱과 이미지 디코딩을 메인 스레드에서 하면 UI가 멈춘다.
  • 누적 점군의 LOD — 여러 프레임을 겹쳐 보여줄 때 전부 그리면 수백만 점이다. 거리·밀도 기반으로 줄인다.
  • 편집 중에는 덜 그린다 — 드래그하는 동안 점군 밀도를 낮추고, 놓는 순간 원래대로. 게임의 고전적 수법이고 여기서도 통한다.
  • 타임라인 가상화 — 트랙이 수백 개면 화면에 보이는 것만 렌더한다.

렌더링 자체의 설계는 WebGL2로 뷰어를 직접 짓기에서 다뤘다. 에디터는 그 위에 얹히는 층이다.

실수를 막는 설계

라벨 오류의 상당수는 라벨러의 부주의가 아니라 도구가 실수를 허용해서 생긴다.

  • 자동 저장 — 브라우저가 죽어도 잃지 않는다. 로컬에 먼저, 서버에 주기적으로.
  • 미완성 표시 — 클래스가 안 정해졌거나 속성이 빈 객체를 눈에 띄게 표시하고, 제출을 막는다.
  • 경고, 차단 아님 — "박스 안에 점이 0개입니다" 같은 검사는 경고로 띄우되 막지는 않는다. 정당한 예외가 늘 있다.
  • 삭제 되돌리기 — 확인 대화상자보다 undo 가능한 삭제가 낫다. 대화상자는 무시하게 되고, undo는 실제로 구제한다.

정리

  • 라벨 예산은 라벨러의 이고, 처리량을 정하는 건 대개 도구다. 30초 vs 12초가 2.5배의 예산 차이다.
  • 에디터의 심장은 스크린 ↔ 월드 좌표 왕복이다. 무엇과 교차시킬지가 UX를 결정한다.
  • 3D 가 어려운 건 2D 입력으로 자유도 7을 다루기 때문이다. 해법은 자유도를 줄이는 것 — 편집, 축 제약, 지면 스냅, .
  • 다중 뷰 동기화, 특히 카메라 투영 겹쳐 보기가 판단 속도를 크게 올린다. 대신 이 틀리면 도구의 오차가 라벨에 굳는다.
  • undo는 편의가 아니라 속도 기능이다. 으로 만들고, "한 동작" 단위로 묶는다.
  • 시간축 편집은 + 보간이고, yaw 보간과 정지물의 좌표계 선택에 함정이 있다.
  • 단축키는 손이 마우스를 떠나는 횟수로 설계하고, 사용 빈도를 측정한다.
  • 성능은 프리페치·워커 디코딩·LOD·편집 중 저품질로 지킨다.

이어서 볼 글: 저장·동시성·워크플로는 어노테이션 툴 만들기 ② 백엔드, 도구와 데이터가 서로를 먹이는 지점은 ③ 휴먼인더루프, 렌더링 바닥은 WebGL2로 뷰어를 직접 짓기에서 다룬다.

Index

구조 한 장좌표 왕복 — 에디터의 심장스크린 → 월드월드 → 스크린3D 큐보이드 편집이 어려운 진짜 이유① BEV 뷰에서 편집한다② 축 제약③ 지면 스냅④ 점군 자동 맞춤 — 처리량을 가장 크게 올리는 기능⑤ 다중 뷰 동기화편집 상태 모델 — 실행취소가 공짜가 아닌 이유커맨드 패턴어디까지가 하나의 실행취소인가시간축 — 트랙 편집키프레임과 보간트랙 편집 도구단축키 — 초를 줄이는 곳성능 — 60fps를 지키는 예산실수를 막는 설계정리