손동작이나 시선, 자세를 카메라로 인식하는 기능을 만들려면 보통 두 가지 선택지가 생긴다. 영상을 서버에 올려 ML 모델로 처리하거나, 모델을 기기 안에서 직접 돌리거나. 서버 방식은 네트워크 지연이 생기고 영상이 외부로 나간다. 기기 내 방식은 지연이 없고 영상이 장치를 벗어나지 않는다. MediaPipe Tasks Vision은 이 두 번째 방식을 브라우저에서 구현한 라이브러리다.
배경 — MediaPipe의 두 세대
MediaPipe는 Google이 2019년에 공개한 온디바이스 ML 파이프라인 프레임워크다. 처음에는 C++로 작성된 저수준의 프레임워크였고, JavaScript 바인딩도 제공했지만 설정이 복잡했다. 여러 파일을 직접 내려받아 경로를 연결하고, 솔루션별로 다른 초기화 방식을 알아야 했다.
2023년에 Tasks Vision API(이하 Tasks Vision)가 등장하면서 크게 단순해졌다. 손 추적, 얼굴 랜드마크, 포즈 추정, 객체 감지, 이미지 분류 등 각 작업(task)마다 통일된 API 형태를 제공한다. 한 패키지(@mediapipe/tasks-vision)에 모든 작업이 들어 있고, 모델 파일(.task 포맷)과 초기화 방식이 일관된다.
어떻게 브라우저에서 ML 모델이 돌아가나
브라우저는 원래 Python이나 C++을 실행하지 못한다. ML 모델 추론을 브라우저 안에서 돌리려면 두 층이 필요하다.
첫 번째 층은 WebAssembly(WASM)다. WASM은 C나 C++로 짠 코드를 브라우저가 실행할 수 있는 이진 포맷으로 컴파일한 것이다. MediaPipe의 추론 엔진은 C++로 작성되어 WASM으로 빌드되었고, 브라우저는 이걸 거의 네이티브 속도로 실행한다.
두 번째 층은 GPU 가속이다. 신경망 추론은 행렬 곱셈이 대부분이라 GPU에 올리면 CPU보다 훨씬 빠르다. 브라우저에서 GPU를 쓰는 방법은 WebGL이다. Tasks Vision의 delegate: "GPU" 옵션은 가능한 연산을 WebGL로 위임한다. 지원이 안 되는 환경에서는 자동으로 CPU 경로로 내려간다.
초기화 코드에서는 이 WASM 바이너리의 위치를 먼저 알려줘야 한다.
const fileset = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm"
);
FilesetResolver는 이 경로에서 WASM 바이너리를 내려받고 초기화한다. 이후 각 Landmarker를 생성할 때 이 fileset을 넘기면 런타임을 공유한다. CDN 경로는 개발 중에는 편리하지만 프로덕션에서는 로컬에 두는 것이 안정적이다. CDN 장애나 버전 정책 변경에 영향을 받지 않고, 오프라인(Capacitor 앱 등)에서도 동작한다.
모델 파일 — .task 포맷
각 작업에는 모델 파일이 필요하다. Tasks Vision은 .task 포맷을 쓴다. 이 파일은 TFLite 모델 가중치와 전처리/후처리 파이프라인, 메타데이터가 하나의 FlatBuffer 컨테이너에 묶인 것이다.
모델 종류마다 파일을 따로 내려받는다. 예를 들어 손 추적(HandLandmarker), 얼굴 랜드마크(FaceLandmarker), 포즈 추정(PoseLandmarker)은 각각 별도의 .task 파일을 필요로 한다. createFromOptions 호출 시 이 파일 URL을 넘기면 첫 초기화 때 내려받아 캐시한다.
포즈 추정의 경우 pose_landmarker_lite와 pose_landmarker_full, pose_landmarker_heavy 세 가지 크기가 있다. 상반신 자세를 보는 용도라면 lite로 충분하고 용량과 속도에서 이득이 크다. 필요 이상으로 무거운 모델을 쓸 이유는 없다.
IMAGE 모드와 VIDEO 모드의 차이
Tasks Vision의 각 Landmarker는 두 가지 실행 모드를 지원한다. IMAGE 모드는 정지 이미지 한 장을 처리한다. VIDEO 모드는 연속적인 프레임 스트림을 처리한다.
둘의 결정적 차이는 시간 정보의 사용 여부다. VIDEO 모드에서는 각 detectForVideo(video, tMs) 호출에 타임스탬프(밀리초)를 함께 넘긴다. 모델은 이전 프레임과의 시간 간격을 보고 움직임을 추정하고, 이전에 감지된 위치를 기반으로 이번 프레임에서 탐색할 영역을 좁힌다. 결과적으로 빠른 움직임에서도 추적이 안정적이고, 프레임당 연산량이 줄어든다.
여기서 중요한 제약이 하나 있다. detectForVideo에 넘기는 타임스탬프는 반드시 이전 호출보다 커야 한다. 단조 증가 조건이다. 같은 값이나 작은 값이 들어오면 예외가 발생한다. performance.now()를 그대로 쓰면 이 조건을 자연스럽게 만족하지만, requestAnimationFrame 콜백이 예상보다 늦거나 타이머를 잘못 관리하면 어길 수 있다. 이 예외는 프레임을 건너뛰는 방어 코드로 처리하는 것이 좋다.
신뢰도 파라미터 3종
HandLandmarker는 세 가지 신뢰도 임계값을 받는다.
minHandDetectionConfidence: 새 손을 처음 감지할 때 최소 점수.minHandPresenceConfidence: 이미 추적 중인 손의 존재를 확인하는 최소 점수.minHandTrackingConfidence: 추적 자체의 연속성 임계값.
이 세 값을 하나로 통일해 쓰면 문제가 생기는 경우가 있다. 핀치(엄지와 검지가 맞닿는 동작)를 하면 두 손가락이 겹쳐 카메라에서 가려진다. 이때 모델이 손의 존재 자체에 대한 신뢰도를 낮게 줄 수 있다. minHandPresenceConfidence와 minHandTrackingConfidence가 높게 설정되어 있으면 핀치 도중에 손 추적이 끊긴다. 제스처 중간에 추적이 사라지는 현상이 생기는 원인이다.
감지는 높게(새 손을 찾을 때는 확실하게), 존재 확인과 추적은 낮게(잡은 손을 쉽게 놓지 않게) 설정하면 핀치·OK 사인 같이 가림이 생기는 제스처에서 안정적인 추적을 유지할 수 있다.
신호 설계 — 앱은 필요한 모델만 로드한다
한 패키지에 여러 모델이 들어 있지만, 앱마다 필요한 신호가 다르다. 손 입력만 필요한 앱이 얼굴 인식 모델까지 내려받을 이유는 없다. 이상적인 설계는 앱이 필요한 신호 목록을 선언하고, 그 신호에 필요한 모델만 초기화하는 것이다.
예를 들어 얼굴 랜드마크에서 나오는 주의 집중 신호, 머리 방향 신호, 혀 감지 신호는 모두 FaceLandmarker 하나를 공유한다. 앱이 이 중 어느 것을 요청하든 얼굴 모델은 한 번만 초기화된다. 포즈 추정이 필요한 경우에만 PoseLandmarker가 추가된다. 이렇게 하면 초기화 시간과 메모리를 신호 조합에 따라 최소로 유지할 수 있다.
이점과 트레이드오프
이점은 세 가지다. 첫째, 영상이 기기를 벗어나지 않는다. 서버 없이 추론하므로 프라이버시 문제가 없다. 둘째, 네트워크 지연이 없다. 30fps로 프레임마다 결과를 받아도 서버 왕복이 없으므로 실시간이다. 셋째, 오프라인에서도 동작한다. 모델과 런타임을 로컬에 두면 네트워크 없이 쓸 수 있다.
감수하는 것도 있다. 첫 로드가 느리다. WASM 바이너리와 모델 파일을 내려받고 초기화하는 시간이 몇 초 걸린다. 이후에는 브라우저가 캐시하므로 재방문 때는 빠르다. 또 정확도는 클라우드 서버의 대형 모델보다 낮다. Tasks Vision의 모델은 모바일 기기에서 실시간으로 돌아야 하므로 작게 설계되어 있다. 고정밀 분석이 필요하다면 서버 모델 쪽이 유리하다. 마지막으로 낡은 기기에서는 GPU delegate가 지원되지 않아 CPU 경로로 내려가고 속도가 크게 떨어질 수 있다.
'TIL' 카테고리의 다른 글
| Face Blendshapes — 52개 숫자로 표정을 기술하는 법 (0) | 2026.06.08 |
|---|---|
| One Euro Filter — 손끝 커서를 부드럽게 만드는 적응형 필터 (0) | 2026.06.08 |
| WASM 온디바이스 추론 — 서버 없이 폰 안에서 모델 돌리기 (0) | 2026.06.07 |
| LoRA — 모델 전체를 재학습하지 않고 일부만 바꾸는 법 (0) | 2026.06.07 |
| ONNX와 INT8 양자화 — 학습한 모델을 폰에 올리는 법 (0) | 2026.06.07 |