WebCodecs를 사용하여 브라우저에서 시맨틱 광학 흐름 추적(Semantic Optical Flow Tracking) 구현하기
요약
WebCodecs를 활용하여 브라우저 환경에서 객체 감지 기반의 시맨틱 광학 흐름 추적을 구현하는 방법을 소개합니다. 단순한 픽셀 변위를 넘어 사람이나 객체의 움직임을 궤적으로 추적하는 로컬 우선 AI 비디오 편집 기술을 다룹니다.
핵심 포인트
- WebCodecs를 이용한 브라우저 내 로컬 비디오 처리 파이프라인 구축
- NanoDet, MediaPipe 등을 활용한 시맨틱 영역 기반 광학 흐름 계산
- 카메라 흔들림이나 배경 변화를 제외한 실제 객체의 궤적 추출
- 서버 업로드 없는 로컬 우선(local-first) AI 비디오 편집 구현
프로젝트 링크
GitHub: https://github.com/MartinDelophy/ai-video-editor
라이브 데모: https://video-editor.ai-creator.top/
Timeline Studio는 브라우저에서 실행되는 로컬 우선(local-first) AI 비디오 편집기입니다. 최근 여기에 실험적인 광학 흐름 추적 (Optical Flow Tracking) 기능을 추가했습니다.
목표는 단순히 프레임을 인상적인 화살표로 채우는 것이 아니었습니다. 이 기능은 먼저 사람이나 객체를 감지하고, 해당 시맨틱(semantic) 영역 내부에서만 광학 흐름을 계산하며, 로컬 벡터들을 하나의 모션 코호트(motion cohort)로 집계한 뒤, 해당 코호트를 가시적인 궤적(trajectory)으로 누적합니다.
처리가 완료되면 브라우저는 재사용 가능한 WebM 결과물을 렌더링하여 **내 에셋 (My assets)**에 추가합니다. 생성된 결과물은 임시 분석 미리보기가 아니라 실제 편집 가능한 에셋입니다.
왜 원시 광학 흐름 (raw optical flow)만으로는 충분하지 않았는가
광학 흐름(Optical flow)은 인접한 프레임 사이의 픽셀 변위(displacement)를 설명합니다:
v = (deltaX, deltaY)
모든 벡터를 그리면 우리에게 익숙한 모션 필드(motion-field) 시각화가 나타나지만, 이러한 벡터들은 장면을 이해하지 못합니다.
벡터들은 다음과 같은 원인으로 발생할 수 있습니다:
- 카메라 흔들림 (camera shake);
- 움직이는 배경 텍스처 (moving background textures);
- 자막 변경 (subtitle changes);
- 조명 변화 (lighting variation);
- 압축 아티팩트 (compression artifacts);
- 머리카락 및 의복의 변형 (hair and clothing deformation).
이 모든 벡터를 클러스터링(clustering)하면 배경 텍스처가 쉽게 가상의 "모션 그룹"으로 변할 수 있습니다.
그래서 저는 질문을 다음과 같이 바꾸었습니다:
어떤 픽셀이 움직였는가?
에서:
어떤 사람이나 객체가, 어느 방향으로, 어떤 궤적을 따라 움직였는가?
로 말입니다.
처리 파이프라인 (The processing pipeline)
브라우저 로컬 파이프라인은 다음과 같습니다:
선택된 비디오 클립
↓
사람 및 객체 감지
...
프레임 추출(Frame extraction), 모델 추론(model inference), 흐름 계산(flow calculation), Canvas 렌더링, 그리고 비디오 인코딩이 모두 브라우저 내에서 이루어집니다. 소스 비디오를 분석 서버로 업로드할 필요가 없습니다.
흐름을 계산하기 전 피사체 감지하기
첫 번째 샘플링된 프레임은 시맨틱 앵커 (semantic anchor) 역할을 합니다. 구현에는 다음과 같은 제한된 로컬 폴백 (local fallbacks)이 사용됩니다:
- 가벼운 사람 및 객체 제안 (proposals)을 위한 NanoDet;
- 사람 영역을 확인하기 위한 MediaPipe segmentation;
- 또 다른 탐지 폴백으로서의 YOLOS;
- 유효한 비디오임에도 모델 런타임 (runtimes)이 실패할 경우를 대비한 보수적인 중앙 집중식 시맨틱 사전 정보 (semantic prior).
"그룹 (group)"이라고 해서 반드시 여러 명의 사람이 필요하지는 않습니다. 명확하게 보이는 한 명의 사람 또는 하나의 주요 객체만으로도 이미 유효한 시맨틱 모션 코호트 (semantic motion cohort)가 됩니다.
클로즈업 영상은 특별한 처리가 필요합니다. 사람이 프레임의 거의 전체를 차지할 수 있으므로, 모든 큰 탐지 박스 (detection box)를 거부한다면 일반적인 인물 사진 구도에서 잘못된 실패를 일으킬 수 있습니다.
시맨틱 모션 코호트 표현하기
탐지된 각 피사체는 시간을 통해 전파될 수 있는 큰 영역이 됩니다.
const cohort = {
id: "cohort-1",
label: "person",
...
박스는 0–1 범위로 정규화 (normalized)됩니다. 이를 통해 작은 분석 프레임에서 움직임을 계산한 후, 나중에 그 결과를 더 높은 해상도의 렌더링 캔버스 (rendering canvas)에 매핑하는 것이 가능해집니다.
주변의 사람들이 각자의 영역과 움직임이 일치할 경우 더 큰 코호트로 병합될 수 있습니다. 단일 주요 객체는 자체적인 코호트로 유지됩니다.
시맨틱 ROI 내부의 흐름 계산하기
샘플링된 비디오 프레임은 그레이스케일 (grayscale)로 변환됩니다. 로컬 블록 매칭 (block-matching) 패스가 현재 프레임과 다음 프레임 사이의 최적의 변위 (displacement)를 검색합니다.
후보 변위에 대해 매칭 오차 (matching error)는 다음과 같이 설명될 수 있습니다:
E(dx, dy) = average(abs(frameA(pixel) - frameB(pixel + displacement)))
오차가 가장 낮은 변위가 후보 모션 벡터 (motion vector)가 됩니다.
모든 후보가 수용되는 것은 아닙니다. 구현 단계에서 다음과 같은 벡터들은 거부됩니다:
- 의미 있는 움직임을 나타내기에 너무 작은 경우;
- 불확실성이 너무 높은 경우;
- 시맨틱 ROI (Region of Interest) 밖에 있는 경우;
- 코호트의 지배적인 방향에서 너무 벗어난 경우;
- 품질이 낮은 블록 매칭에 의해 생성된 경우.
중요한 규칙은 배경 벡터가 스스로 사람이나 객체 코호트를 생성할 수 없다는 점입니다.
로컬 벡터를 하나의 그룹 방향으로 집계하기 (Aggregating local vectors into one group direction)
시맨틱 영역 (semantic region) 내부에서, 수락된 로컬 벡터들은 신뢰도 (confidence)를 가중치로 사용하여 결합됩니다:
cohortVector = sum(confidence[i] * vector[i]) / sum(confidence[i])
단순화된 구현은 다음과 같습니다:
function aggregateVectors(vectors) {
let dx = 0;
let dy = 0;
...
또한 로컬 벡터들이 그룹 방향과 얼마나 일관되게 일치하는지도 비교합니다. 이것이 코호트 (cohort)의 안정성 점수 (stability score)가 됩니다.
UI는 다음과 같은 몇 가지 유용한 진단 정보를 제공합니다:
- 유효 벡터 수 (valid vector count);
- 모션 코호트 수 (motion cohort count);
- 지배적 방향 (dominant direction);
- 궤적 안정성 (trajectory stability);
- 활성 탐지기 (active detector);
- 처리된 프레임 수 (processed frame count);
- 분석 및 인코딩 진행 상황 (analysis and encoding progress).
이러한 지표들은 실험을 더 쉽게 이해할 수 있게 하며, 취약한 트랙 (weak tracks)을 식별하는 데 도움을 줍니다.
프레임 단위 모션을 궤적으로 전환하기 (Turning frame-by-frame motion into a trajectory)
단일 광학 흐름 (optical-flow) 단계는 해당 순간의 움직임만을 설명합니다. A 지점에서 B 지점까지의 움직임을 보여주기 위해, 각 샘플링된 프레임 이후에 코호트 중심 (cohort center)을 업데이트합니다.
const nextPoint = {
x: previousPoint.x + cohortVector.dx,
y: previousPoint.y + cohortVector.dy,
...
눈에 보이는 잔상 (trail)은 누적된 포인트들에 대한 설정 가능한 윈도우 (window)입니다:
const visiblePoints = track.points.slice(-trailLength);
이는 짧은 모션 꼬리 (motion tails)뿐만 아니라 더 긴 실험 스타일의 궤적 (trajectories)도 지원합니다.
시각화 렌더링하기 (Rendering the visualization)
결과 캔버스 (Canvas)에는 다음이 포함됩니다:
- 원본 비디오 프레임;
- 미세한 분석 그리드 (analysis grid);
- 청록색 (cyan) 로컬 흐름 화살표;
- 라벨이 없는 코호트 경계 (cohort bounds);
- 뚜렷한 누적 잔상 (accumulated trails);
- 주요 코호트 방향;
- 우측 상단의 실험 타임코드 (experiment timecode).
저는 의도적으로 코호트 경계에서 텍스트 라벨을 제거했습니다. 근접 촬영 영상의 경우, "Person · 1"과 같은 라벨이 피사체를 가려 생성된 비디오가 완성된 실험 결과물이라기보다 디버그 화면처럼 느껴지게 만들었기 때문입니다.
코호트 수와 탐지기 정보는 여전히 인스펙터 (inspector)에 표시되며, 이곳에서는 이미지를 가리지 않습니다.
간소화된 화살표 렌더러 (arrow renderer)는 Canvas 2D API를 사용합니다:
function drawArrow(ctx, startX, startY, endX, endY, color) {
const angle = Math.atan2(endY - startY, endX - startX);
const head = 5;
...
타임코드 (timecode) 추가하기
클립 상대적 타임코드 (clip-relative timecode)가 생성된 각 프레임의 우측 상단에 번인 (burned in) 됩니다.
const timecode = formatTime(frame.time);
ctx.font = "700 16px ui-monospace, monospace";
...
이는 가독성을 높일 뿐만 아니라, 결과물에 완성된 동작 분석 (motion-analysis) 실험과 같은 느낌을 부여합니다.
저해상도 분석, 고해상도 렌더링
첫 번째 버전은 192픽셀 너비의 분석 프레임을 저장하고 결과 생성 시 이를 확대했습니다. 모션 벡터 (motion vectors)는 정확해 보였지만, 기반이 되는 비디오가 눈에 띄게 흐릿해졌습니다.
해결책은 분석 경로 (analysis path)와 렌더링 경로 (rendering path)를 분리하는 것이었습니다.
분석 경로 (Analysis path)
- 프레임을 작은 작업 해상도로 크기 조정 (resize);
- 그레이스케일 (grayscale)로 변환;
- 로컬 흐름 (local flow) 계산;
- 코호트 (cohorts) 및 궤적 (trajectories) 업데이트.
렌더링 경로 (Rendering path)
- 소스 비디오에서 선명한 샘플 유지;
- 렌더링 너비를 1280픽셀로 제한;
- 분석 좌표를 렌더링 캔버스 (Canvas)에 매핑;
- 벡터, 경계 (bounds), 흔적 (trails) 및 타임코드 다시 그리기;
- 고해상도 결과물 인코딩 (encode).
좌표 매핑은 간단합니다:
const scaleX = renderWidth / analysisWidth;
const scaleY = renderHeight / analysisHeight;
...
이를 통해 생성된 에셋 (asset)의 선명도를 희생하지 않으면서도 광학 흐름 (optical-flow) 계산 비용을 저렴하게 유지할 수 있습니다.
WebCodecs를 사용한 결과 인코딩
분석 후, 모든 샘플링된 순간은 캔버스 프레임으로 렌더링됩니다. 그런 다음 WebCodecs를 사용하여 WebM으로 인코딩됩니다.
const resultBlob = await encodeFrames(
renderedFrameBlobs,
renderWidth,
...
프레임 렌더링 및 인코딩 중에 동일한 중단 신호 (abort signal)를 확인하므로, 취소 시 추가 처리가 중단되고 인코더 리소스가 해제됩니다.
실제 타임라인 프레임 생성하기
생성된 비디오가 타임라인에 배치되었을 때 빈 사각형이 되어서는 안 됩니다. 따라서 렌더링된 출력 프레임으로부터 압축된 타임라인 프레임 (compact timeline frames)을 생성합니다.
const trackFrames = await createVideoTrackFramesFromBlobs(
renderedFrameBlobs,
{
...
에셋 (asset)은 그에 상응하는 트랙 프레임 지속 시간 (track-frame duration)을 저장합니다:
const asset = {
id: crypto.randomUUID(),
type: "video",
...
내 에셋(My assets)에 결과 추가하기
인코딩이 완료되면 결과가 미디어 라이브러리에 추가되고 자동으로 선택됩니다.
setUserAssets(current => [asset, ...current]);
setSelectedLibraryAssetId(asset.id);
setActiveTool("media");
...
타임라인에 자동으로 삽입되지 않도록 의도적으로 설계되었습니다. 사용자는 결과를 먼저 검토한 다음, 이것이 메인 비주얼 트랙에 속할지 아니면 오버레이 (overlay)로 사용될지 결정할 수 있습니다.
간단한 브라우저 벤치마크
4 fps 샘플링 속도로 2초 길이의 852×480 비디오를 사용하여 파이프라인 (pipeline)을 테스트했습니다.
결과물에는 다음이 포함되었습니다:
- 9개의 샘플링된 분석 프레임 (analysis frames);
- 268개의 유효한 모션 벡터 (motion vectors);
- 1개의 시맨틱 모션 코호트 (semantic motion cohort);
- 약 235도의 지배적인 방향;
- 약 78%의 궤적 안정성 점수 (trajectory stability score);
- 852×480 WebM 출력물.
저해상도 분석과 클리어 프레임 렌더링 (clear-frame rendering)이 분리되어 있기 때문에, 분석 프레임을 확대하는 대신 소스 차원 (source dimensions)을 그대로 유지합니다.
브라우저 로컬 처리의 장점과 한계
전체 파이프라인은 로컬에서 실행됩니다:
- 비디오 프레임 추출 (video frame extraction);
- 사람 및 객체 탐지 (person and object detection);
- 사람 영역 추정 (person-region estimation);
- 광학 흐름 계산 (optical-flow calculation);
- 코호트 추적 (cohort tracking);
- Canvas 렌더링 (Canvas rendering);
- WebM 인코딩 (WebM encoding);
- 타임라인 썸네일 생성 (timeline thumbnail generation).
이는 다음과 같은 몇 가지 장점을 제공합니다:
- 소스 비디오를 업로드할 필요가 없습니다;
- 백엔드 추론 대기열 (backend inference queue)이 없습니다;
- 매개변수 (parameter) 변경 사항을 즉시 테스트할 수 있습니다;
- 결과물이 즉시 편집 에셋 (editing asset)이 됩니다;
- 서버 측 비디오 저장 및 처리 비용이 절감됩니다.
실제적인 한계점도 존재합니다. 긴 클립, 높은 샘플링 속도(sampling rates), 그리고 4K 소스는 메모리 사용량과 처리 시간을 증가시킵니다. 심한 모션 블러 (motion blur), 긴 폐쇄 (occlusion), 그리고 장면 전환 (scene cuts)은 추적 안정성을 저하시킬 수 있습니다. WebCodecs 및 로컬 모델 런타임 (model runtimes)에 대한 브라우저 지원 수준 또한 다양합니다.
결론
가장 중요한 변화는 더 복잡한 화살표 렌더러 (arrow renderer)를 만든 것이 아니었습니다. 그것은 광학 흐름 (optical flow)에 시맨틱 경계 (semantic boundary)와 완전한 제품 워크플로우 (product workflow)를 부여한 것이었습니다.
사람/객체 탐지 (Person/object detection)
+
시맨틱 ROI (Semantic ROI)
...
다음 세 가지 결정이 가장 큰 차이를 만들었습니다:
- 배경 벡터 (background vectors)로부터 사람이나 객체 그룹을 생성하지 마십시오.
- 저해상도 분석 (low-resolution analysis)과 고해상도 출력 렌더링 (high-resolution output rendering)을 분리하십시오.
- 분석이 재사용 가능한 비디오 에셋 (video asset)이 된 후에만 완료된 것으로 간주하십시오.
그 결과, 이는 단순한 원시 컴퓨터 비전 (computer-vision) 데모라기보다, 일반적인 편집 워크플로우를 통해 계속 이어질 수 있는 완성된 브라우저 기반 모션 실험에 더 가깝게 느껴집니다.
브라우저 AI, WebCodecs, 비디오 편집, 또는 컴퓨터 비전 (computer vision)에 관심이 있다면, 다음 저장소를 확인해 보세요:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기