영상 속 안경을 클릭할 수 있게 만들기
브랜드 영상을 보다가 마음에 드는 안경을 발견했다고 해보자. 영상 안에서 그 안경을 누르면 상품 정보를 볼 수 있을까?
영상 아래에 상품 목록을 붙이는 방법도 있지만, 이번에는 영상 속 제품 자체가 반응하는 경험을 만들어보고 싶었다. 안경에 마우스를 올리면 영상이 잠시 멈추고, 제품 정보를 살펴본 뒤 다시 감상하는 흐름이다.
이 아이디어를 Interactive Film Runtime이라는 이름의 POC로 구현했다. SAM 2.1로 안경 영역을 추적하고, React 플레이어 위에 인터랙션을 연결했다. 상품 정보는 데모 데이터이며 실제 구매 기능은 아직 붙이지 않았다.
실제 브라우저 동작을 캡처한 GIF. 재생 → 호버와 일시정지 → 상품 미리보기 → 이탈 후 재생 순서다. 잠시 화면이 멈추는 구간은 호버 동작이다. 화면의 상품명과 가격은 데모 데이터다.
처음에는 안경을 잘 분리해내면 나머지는 간단할 줄 알았다. 막상 만들다 보니 분리한 영역을 어떻게 누르게 할지, 어디까지 강조할지, 멈춘 영상을 언제 다시 재생할지에 더 많은 판단이 필요했다.
먼저 클릭할 장면부터 골랐다
샘플 영상은 약 14.85초, 1920×990 해상도에 356프레임으로 구성되어 있었다. 컷 전환을 기준으로 나누면 여섯 장면이다.
모든 장면에 인터랙션을 넣지는 않았다. 전신 장면에서는 안경이 너무 작았고, 테이블이나 책이 보이는 장면에는 선택할 안경이 없었다. 안경이 충분히 크게 보이는 세 구간을 대상으로 잡았다.
| 구간 | 처리한 프레임 수 | 적용 범위 |
|---|---|---|
| 0–2.50초 | 60 | 첫 번째 클로즈업 |
| 6.55–10.26초 | 89 | 중간 클로즈업 |
| 12.10–14.85초 | 66 | 마지막 클로즈업 |
총 215프레임에 안경 영역을 만들고, 나머지 141프레임에는 클릭 영역을 두지 않았다. 컷이 바뀌면 이전 장면의 영역도 끝난다.
왼쪽 위부터 시간순으로 나눈 여섯 장면. 안경이 크게 보이는 1·4·6번 장면에만 인터랙션을 적용했다. 각 이미지 아래에는 해당 컷의 시간 구간과 대표 프레임 번호가 표시되어 있다.
이렇게 범위를 좁히니 처음 확인할 것이 명확해졌다. 화면 속 모든 제품을 자동으로 찾아내기보다, 선택한 클로즈업에서 안경을 안정적으로 누를 수 있는지를 먼저 보았다.
추론은 미리 하고, 브라우저는 결과를 재생한다
이번 POC에서는 SAM 2.1 Hiera Small을 로컬 Mac의 MPS에서 실행했다. SAM 2는 이미지와 영상에서 프롬프트로 지정한 대상을 분리할 수 있고, 영상에서는 지정한 대상을 다른 프레임으로 전파할 수 있다. 모델 사용 방식은 Meta의 공식 SAM 2 저장소를 참고했다.
처리 흐름은 다음과 같다.
1
2
3
4
5
6
7
영상과 컷 구간
→ 얼굴 주변 ROI 추출
→ 안경을 지정하는 프롬프트
→ 컷 내부에서 마스크를 앞뒤로 전파
→ 마스크 정리와 검수
→ 프레임별 윤곽·클릭 영역 내보내기
→ 웹 플레이어에서 시간에 맞춰 표시
ROI는 추론할 관심 영역이다. 이번에는 얼굴 주변을 원본 해상도로 잘라 사용했다. 첫 번째와 중간 클로즈업은 이미지 예측 마스크를 시작점으로 삼았다. 마지막 클로즈업은 이미지 마스크만으로 시작했을 때 추적이 얼굴 쪽으로 번져, 박스와 포함할 점·제외할 점을 지정하는 방식으로 보정했다.
따라서 이 결과는 영상을 넣기만 하면 완성되는 자동 파이프라인은 아니다. 사람이 대상을 지정하고 결과를 검수하는 과정이 들어간다.
브라우저에는 모델 대신 영상과 생성된 데이터를 전달한다. 재생 중에는 현재 프레임에 해당하는 영역을 읽어 표시한다. 웹 실행 환경에 Python이나 추론용 GPU를 요구하지 않는 구조다.
장면과 상품의 연결도 따로 관리했다. 안경의 위치를 찾았다는 사실만으로 실제 상품 SKU까지 알 수는 없다. 이번에는 장면마다 데모 상품을 명시적으로 연결했고, 이름·컬러·가격은 별도 카탈로그에 두었다.
보이는 영역과 누르는 영역은 달라야 했다
안경은 클릭 영역을 만들기에 까다로운 물체였다. 테는 얇고 렌즈는 투명하다. 분리된 테만 클릭 대상으로 삼으면 사용자가 렌즈 중앙을 눌렀을 때 반응하지 않을 수 있다.
그래서 프레임마다 두 종류의 데이터를 만들었다.
| 데이터 | 역할 |
|---|---|
mask | SAM 결과를 정리한 윤곽. 원래 분리된 영역을 표시할 때 사용 |
hit | 외곽 윤곽을 이용해 렌즈 안쪽을 채운 영역. 클릭 판정에 사용 |
마스크의 작은 연결 요소를 정리하고, 클릭 영역은 외곽선을 기준으로 만들었다. 프레임 사이를 수동 보간한 대체 마스크는 사용하지 않았다.
실제로 내보낸 200번 프레임의 데이터를 원본 위에 겹친 비교 이미지. 연두색은 mask, 파란색은 hit이며, 설명을 위해 색을 입혔다. 이 프레임에서는 렌즈 상당 부분이 이미 마스크에 포함되어 두 영역의 차이가 작은 내부 구멍에 나타난다. 모든 프레임에서 테만 분리되는 것은 아니다.
화면에 나타나는 부드러운 강조 윤곽도 hit을 사용한다. 사용자가 하나의 안경으로 인식하는 범위와 반응하는 범위를 가깝게 맞추려는 선택이다. 원래 마스크를 옅게 표시하는 레이어는 별도로 남겼다.
이 작업에서 세그멘테이션 결과는 인터페이스의 재료였다. 그 결과를 사용자가 누르기 좋은 영역으로 만드는 단계가 한 번 더 필요했다.
영상과 영역은 같은 시간을 바라봐야 한다
안경 위치가 정확해도 영상보다 늦게 움직이면 어색하다. 특히 컷이 바뀌는 순간 이전 안경의 윤곽이 남아 있으면 바로 눈에 띈다.
플레이어는 requestVideoFrameCallback()이 전달하는 mediaTime을 사용한다. 이 값은 영상 프레임의 미디어 타임스탬프를 알려준다. API의 동작과 콜백 타이밍에 관한 제약은 MDN 문서에 정리되어 있다.
내보낸 데이터에는 각 프레임의 PTS, 즉 표시 타임스탬프를 저장했다. 런타임은 이 목록을 이진 탐색해 재생 시점에 해당하는 프레임을 고른다. 평균 FPS에 시간을 곱해서 반올림하는 방식은 컷 경계에서 다른 프레임을 선택할 수 있어 피했다.
지원하지 않는 환경에서는 requestAnimationFrame()과 currentTime을 이용하는 대체 경로를 두었다. 재생 위치를 옮기거나 일시정지했을 때도 영역을 갱신한다. 다만 이 구조를 만들었다고 해서 모든 기기에서 동기화 오차가 일정하게 보장되는 것은 아니다. 실제 오차 측정은 별도 과제다.
좌표 변환도 필요했다. 영상은 object-fit: contain으로 표시하므로 화면 비율에 따라 여백이 생긴다. 컨테이너 전체를 영상으로 생각하면 클릭 위치가 어긋난다.
1
2
3
4
5
6
scale = min(표시 너비 / 원본 너비, 표시 높이 / 원본 높이)
offsetX = (표시 너비 - 원본 너비 × scale) / 2
offsetY = (표시 높이 - 원본 높이 × scale) / 2
원본 x = (컨테이너 안의 포인터 x - offsetX) / scale
원본 y = (컨테이너 안의 포인터 y - offsetY) / scale
포인터를 원본 영상 좌표로 되돌린 뒤 클릭 영역 안에 있는지 판정한다. 영상 바깥의 여백은 제외한다.
클릭할 수 있다는 것을 먼저 알려줘야 했다
안경을 따라 클릭 영역이 움직여도, 사용자에게는 그냥 영상일 수 있다. 실제로 첫 화면에서는 인터랙션이 가능하다는 표현이 부족했다.
안경 옆에 + 안경 보기 표시를 넣고 옅은 윤곽을 더했다. 이어서 강조에 탄성을 넣고, 호버하면 상품 미리보기가 나오도록 바꿨다.
여기서 애니메이션을 적용할 대상은 구분했다. 안경을 따라가는 좌표는 프레임 데이터로 즉시 갱신하고, 빛의 세기와 선 두께, 표시 요소의 반응에 애니메이션을 적용했다. 좌표까지 느긋하게 따라오면 윤곽이 실제 안경에서 뒤처지기 때문이다.
움직임 감소 설정에서는 불필요한 애니메이션을 줄였다. 탄성은 선택에 대한 반응을 전달하는 데 사용하고, 영상 속 위치를 맞추는 일과 섞지 않았다.
호버로 멈춘 영상은 언제 다시 재생해야 할까
다음으로 안경에 마우스를 올리면 영상이 멈추고 상품 정보가 나타나게 했다. 움직이는 대상을 쫓아 클릭하지 않아도 제품을 살펴볼 수 있는 흐름이다.
단순히 진입 시 pause(), 이탈 시 play()를 호출하면 문제가 생긴다. 사용자가 원래 멈춰둔 영상도 마우스를 떼는 순간 재생되기 때문이다.
호버를 시작할 때 재생 중이었는지를 저장하고, 끝날 때 그 상태를 복원했다.
| 진입 전 상태 | 호버 중 | 호버 종료 후 |
|---|---|---|
| 재생 중 | 일시정지와 상품 미리보기 | 재생 복귀 |
| 일시정지 | 상품 미리보기 | 일시정지 유지 |
안경에서 미리보기 카드로 포인터를 옮길 때도 바로 닫히면 안 된다. 카드에서도 호버를 유지하고, 이탈 처리는 180ms 지연해 이동할 여유를 두었다.
미리보기에서 상세 패널을 열 때는 호버 이전의 재생 상태를 패널에 넘긴다. 이미 호버 때문에 멈춰 있는 영상을 보고 ‘원래 일시정지 상태였다’고 판단하지 않도록 하기 위해서다.
모바일에서는 호버 대신 탭으로 상품 패널을 연다. 이번 모바일 확인은 Chromium 터치 에뮬레이션 범위이며 실제 iOS Safari에서의 검증은 남아 있다.
390px 너비의 터치 에뮬레이션 화면. 탭하면 상품 패널이 열리고, 정보를 확인한 뒤 필름으로 돌아갈 수 있다. 컬러 선택은 상품 정보에만 적용된다.
선을 더 잘 보이게 했더니 오히려 어색해졌다
처음에는 안경 주변의 선을 더 눈에 띄게 만들고 싶었다. 그런데 경계가 선명해질수록 제품 위에 별도의 그래픽을 붙인 느낌이 강해졌다.
호버할 때 주변을 조금 어둡게 하는 방식으로 강조를 조정했다. 최종 구현에서는 어두운 레이어의 불투명도를 0.2로 두고, 안경 주변은 SVG 마스크로 비워두었다. 비워지는 경계에는 블러를 적용해 밝기 차이가 부드럽게 이어지도록 했다.
윤곽을 완전히 없애지는 않았다. 강한 흰 테두리를 낮은 불투명도의 부드러운 빛으로 바꾸고, 선 자체도 흐리게 처리했다. 호버가 끝나면 어두운 레이어와 강조가 함께 사라진다.
호버 상태의 실제 화면. 안경 주변의 부드러운 빛과 어두워진 배경으로 시선을 모으고, 옆에는 상품 미리보기를 표시했다.
이 조정은 인식률을 높이는 작업과 달랐다. 대상이 어디인지 알려주면서도 영상의 분위기를 얼마나 유지할지 결정하는 일이었다. 선명하게 드러내는 것만이 항상 좋은 강조는 아니었다.
이번 POC에서 확인한 것과 남은 것
구현 작업에서 빌드와 다섯 개의 단위 테스트를 통과했다. 테스트는 생성 데이터의 215개 마스크와 컷 경계, 렌즈와 얼굴의 클릭 판정, 모바일 비율의 좌표 변환, 잘못된 데이터 거부를 다룬다. 이 글의 예시를 촬영하면서 브라우저의 호버 시 멈춤과 미리보기, 이탈 후 재생 복귀를 다시 확인했고, 모바일 상품 패널과 최종 강조 화면도 캡처했다.
215프레임의 마스크를 생성했다는 것은 215프레임 모두의 정확도를 정답 데이터로 평가했다는 뜻은 아니다. 이번에는 수동 정답과 비교한 정량 정확도 평가를 하지 않았다. 모델이 반환하는 품질 점수도 그런 평가를 대신하지 않는다.
실제 상품 API, 재고, 장바구니와 결제는 연결하지 않았다. 다른 영상에도 같은 수준으로 적용하려면 프롬프트 보정과 상품 연결을 검수하는 도구가 필요하다. 전환율이나 상품 탐색에 도움이 되는지는 사용자 검증으로 확인해야 한다.
이번에 만든 것은 영상 속 제품을 발견하고, 잠시 멈춰 정보를 살핀 뒤, 보던 장면으로 돌아가는 흐름이다. 세그멘테이션으로 위치를 얻은 다음에도 클릭 영역과 재생 상태, 강조 표현을 각각 설계해야 했다. 다음 실험에서는 이 흐름을 처음 접하는 사람이 안내 없이 알아보고 사용할 수 있는지부터 확인해보고 싶다.