본문 바로가기
Video2Any
2026-08-25

0.2.0: 웹캠 화면이 슬라이드 검출을 망가뜨리는 방식

video-slide-extractor 0.2.0을 오늘 npm에 공개했습니다. 이 패키지는 화면을 블록 단위로 비교하는 검출기입니다. 영상을 일정 간격으로 샘플링한 다음, 한 프레임씩 화면의 내용이 다음 슬라이드로 넘어갔는지 판정합니다. Video2Any가 브라우저에서 쓰는 것이 바로 이 검출기이며, 코드는 전부 공개되어 있고 의존성이 없으며 MIT 라이선스로 브라우저와 Node 양쪽에서 동작합니다.

7월에 공개한 0.1.x는 7월 16일 시점의 제품 코드에서 떼어낸 사본이었습니다. 깨끗한 화면 녹화에서는 제대로 판정하지만, 사용자가 실제로 가지고 있는 녹화본에서는 상당수를 잘못 판정했습니다. 실제 영상에는 판정을 망가뜨리는 상황이 두 가지 있고, 제품 쪽은 그 뒤 한 달 동안 두 가지를 모두 처리했지만 패키지 쪽은 그러지 못했습니다. 0.2.0은 그 두 가지를 반영한 버전입니다.

아래에서는 사용자가 올린 영상이 실제로 어떤 것이었는지, 고치기 전에 벤치마크가 이미 내놓았던 결론이 무엇이었는지, 그리고 고친 뒤 측정한 수치가 어떤지를 순서대로 설명합니다.

이 페이지의 목차

사용자가 올린 영상은 실제로 어떤 것이었나

Video2Any는 7월 16일부터 변환 이벤트를 기록해 왔습니다. 오늘까지 96명이 411번 변환을 실행했고, 그중 354번이 끝까지 진행되어 파일 291개가 나왔습니다. PowerPoint 188개, PDF 95개, 이미지 묶음 8개입니다. 대부분이 로컬 파일이어서, 411번 중 398번은 탭이 열려 있던 바로 그 기기에 있는 영상을 골랐습니다. (저희가 자동 테스트를 돌린 이틀치는 이 글의 모든 수치에서 제외했습니다.)

길이 정보를 가진 파일은 106건이었고, 이 영상들의 평균 길이는 53분이었습니다. 5분짜리 제품 데모가 아닙니다. 강의, 회의, 강좌 녹화이며, 7월의 검출기는 이런 종류의 영상에서 한 번도 측정된 적이 없었습니다.

더 볼 만한 것은 한 번에 나오는 장수입니다. 완료된 변환의 평균은 64장이고, 이 평균값은 정상으로 보이지만 분포를 가려버립니다. 100장을 넘긴 것이 64번, 300장을 넘긴 것이 7번, 그리고 정확히 900장인 것이 네 번이었습니다. 900은 훑기 단계에서 뽑을 수 있는 샘플의 최대치입니다. 즉 이 네 번은 샘플 하나하나가 모두 새 슬라이드로 판정되어, 검출기가 한 장도 걸러내지 않았다는 뜻입니다. 그중 한 영상은 30분짜리였습니다.

출력 속도도 고정된 값이 아닙니다. 같은 설정에서 한 시간 미만인 영상은 분당 약 6.5장, 한 시간을 넘는 영상은 분당 약 1.0장이 나왔습니다. 이 차이에는 실제인 부분도 있습니다. 두 시간짜리 회의가 짧은 데모보다 분당 화면 전환이 적은 것은 당연합니다. 문제는 장수만 봐서는 어느 쪽이 맞는지 판단할 수 없다는 점입니다. 다만 상한에 닿은 네 번은 판단할 수 있습니다. 샘플을 전부 남겼다면 검출기는 판단을 하지 않은 것입니다.

벤치마크는 이미 이 결론을 내놓고 있었다

이 문제를 고치기 전에 0.2.0은 먼저 bench/ 디렉터리를 패키지에 넣었습니다. 안에 있는 테스트 영상은 이미 알고 있는 슬라이드 순서로 렌더링한 것이라, 각 전환이 몇 초에 일어나는지가 렌더링 시점에 확정됩니다. 사람이 라벨을 붙일 필요가 없습니다. 각 덱은 세 가지로 렌더링합니다. 깨끗한 버전, 압축 잡음을 보려고 crf 45까지 낮춘 버전, 그리고 화면 구석에 움직이는 웹캠 화면을 얹은 버전입니다.

MIT 덱(46장, 2초마다 160×90으로 샘플링)에서 깨끗한 버전의 F1은 0.966이었습니다. 같은 덱에 웹캠 화면을 얹으면 F1은 0.538까지 떨어집니다. 46장짜리 내용에서 125장을 잡아냈고, 정밀도는 0.368이었으며, 내놓은 결과의 63%가 이미 잡았던 것과 같은 슬라이드였습니다.

원인은 운이 아니라 산수입니다. 기본 트리거 조건은 "바뀐 블록이 2%를 넘으면"입니다. 웹캠 화면, 마우스 커서, 반복 재생되는 로고 애니메이션처럼 어떤 영역이 계속 변하고 있으면 그 영역만으로 2%를 넘깁니다. 그래서 슬라이드가 한 장도 넘어가지 않아도 샘플링한 프레임마다 임계값을 넘습니다. 결국 몇 장을 받는지는 덱이 몇 장인지가 아니라 발표자가 카메라를 켰는지에 따라 정해집니다.

이 줄은 그대로 공개했습니다. 앞에서 본 900장짜리 네 번이 이 줄이 실제 영상에서 드러난 모습입니다. 0.2.0이 고치는 것이 바로 이것입니다.

0.2.0에 추가한 것

새로 내보내는 함수 셋, 선택 옵션 하나, 그리고 그 전부에 대한 타입 선언입니다.

  • buildActivityMask

    이 함수는 샘플 프레임을 받아, 인접한 거의 모든 프레임 쌍에서 변하는 블록을 찾아냅니다. 웹캠 화면, 마우스 커서, 구석의 시계 같은 것들입니다. 그리고 그 블록들을 비교에서 완전히 빼는 마스크를 돌려줍니다. 마스크가 화면 대부분을 덮게 되는 경우에는 마스크 대신 null을 돌려줍니다. 이 분기가 중요합니다. 화면 대부분이 움직이고 있다면 움직이는 그 부분이 곧 봐야 할 내용이고, 그것을 빼는 것은 검출을 포기하는 것과 같기 때문입니다.

  • chooseThreshold

    고정된 임계값 하나로 슬라이드와 말하는 얼굴을 동시에 감당할 수는 없습니다. 슬라이드는 정지해 있어서 실제 전환이 잡음보다 뚜렷하게 큽니다. 반면 카메라 영상은 계속 움직이고, 거기서 0.02는 모든 프레임을 새 슬라이드로 판정해 버립니다. 이 함수는 프레임 간 변화량의 분포를 살펴본 뒤 { changedRatio, mode }를 돌려줍니다. mode는 일부러 드러낸 값으로, bimodal, static, motion, default 중 하나입니다. 검출기가 어떤 종류의 영상이라고 판단했는지를 나타내므로, 결과가 틀렸을 때 판단 근거를 그대로 확인할 수 있습니다.

  • analyzeSamples

    이 함수는 같은 프레임들에 대해 위의 두 단계를 차례로 실행하고 { mask, choice }를 돌려줍니다. 대부분의 호출자에게 필요한 것이 이것입니다. 샘플이 있고 어떤 값으로 검출할지 알고 싶을 뿐이지, "어떤 값으로 검출할지"가 사실 서로 독립된 두 개의 결정이라는 것까지 알아야 할 이유는 없습니다.

  • frameDiff({ collect: true })

    이 옵션을 주면 frameDiffflags도 함께 돌려줍니다. 블록마다 1바이트로, 그 프레임의 변화가 어디에서 일어났는지 표시합니다. 검출기가 변화로 본 영역을 그려보고 싶을 때 쓸 수 있습니다. collect를 줬을 때만 돌려주는데, 평소에는 아예 없는 필드가 평소에 늘 null인 필드보다 제대로 다루기 쉽기 때문입니다.

실제로 부르는 순서대로 늘어놓으면 아래가 전부입니다.

import { analyzeSamples, createSlideDetector } from 'video-slide-extractor';

// frames: RGBA 샘플, 약 2초에 하나씩, 160x90으로 줄인 것
const { mask, choice } = analyzeSamples(frames, 160, 90);
console.log(choice.mode);            // 'bimodal' | 'static' | 'motion' | 'default'

const detect = createSlideDetector(160, 90, {
  mask,
  changedRatio: choice.changedRatio
});

const kept = [];
frames.forEach((frame, i) => {
  if (detect(frame).keep) kept.push(i);   // 원본 해상도로 잡아낼 샘플 인덱스
});

측정한 효과

테스트 영상과 절차는 그대로입니다. MIT 덱 46장, 웹캠 화면 버전, 2초마다 160×90 샘플 하나, 검출 결과와 라벨이 붙은 전환의 차이가 ±2.5초 이내면 일치로 봅니다.

MIT 덱, 웹캠 화면 버전, 실제 슬라이드 46장.
검출잡은 수정밀도재현율F1중복률
0.1.x 기본값(changedRatio 0.02)1250.3681.0000.5380.632
활동 마스크 적용540.8150.9570.8800.185
마스크와 자동 임계값 적용351.0000.7610.8640.000

마스크만 더해도 잡은 수가 125장에서 54장으로 줄고, 중복률은 63%에서 19%로 내려가며, F1은 0.538에서 0.880으로 올라갑니다. 게다가 마스크가 필요 없는 영상에는 아무 영향도 주지 않습니다. 깨끗한 버전과 잡음 버전에서는 null을 돌려주므로, 그 두 줄의 결과는 0.1.1과 완전히 같습니다.

임계값 쪽은 얻는 것과 잃는 것이 함께 있습니다. 이를 정확히 전하려면 성적이 나쁜 줄부터 꺼내는 편이 빠릅니다. 깨끗한 MIT 덱에서는 자동으로 구한 임계값이 예전 고정값보다 보수적이어서, 재현율이 0.935에서 0.761로 떨어집니다. 이번 릴리스에서 DEFAULTS를 그대로 둔 이유가 바로 이것입니다. analyzeSamples는 건네받은 영상이 어떤 종류인지 모를 때 쓰는 함수입니다. 알고 있다면 기존 상수가 그대로 남아 있고, 정하는 쪽은 호출자입니다.

새로 들어간 두 줄은 모두 릴리스에 해당하는 커밋에서 벤치마크 도구로 측정했습니다. 공개된 RESULTS.md는 아직 0.1.x의 방법 집합만 보고합니다. 마스크를 정식 방법으로 거기에 넣는 것이 bench/에서 다음에 할 일입니다.

일부러 넣지 않은 것

전체 범위의 중복 병합과 전환 다듬기는 이 패키지에 없고, 앞으로도 넣지 않습니다. 둘 다 영상 전체를 한 번에 봐야 합니다. 어떤 장면이 나중에 다시 돌아왔는지, 세 번 잡은 것이 사실은 한 번의 디졸브였는지 같은 판단이기 때문입니다. 이는 파이프라인이 다룰 문제입니다. "화면이 바뀌었는가"에만 답하는 검출기가 영상 전체까지 들고 있을 이유는 없습니다.

슬라이드 밀도도 마찬가지입니다. 90분짜리 강의가 몇 장이 "되어야 하는지"는 제품의 방침이지 검출의 문제가 아닙니다. Video2Any에는 그 방침이 있고 조절할 수 있는 장치도 있습니다. 라이브러리가 그 판단을 대신할 필요는 없으며, 화면의 어디가 바뀌었는지만 알려주면 충분합니다.

함께 읽을 만한 세 편:

다음에 할 일

  • 머무름 시간 판정. 새로 나타난 내용이 몇 초 동안 그대로 있어야 한 장으로 셉니다. 900장이라는 결과를 실제로 없앨 수 있는 것이 이것입니다. 마스크는 웹캠 화면을 걷어내지만, 빠른 디졸브나 재생바를 끄는 동작은 여전히 트리거를 당깁니다. 이 판정은 Video2Any에서 이미 동작하고 있고, 인터페이스가 정리되면 패키지에 넣습니다. 머무름 시간은 샘플 간격 없이는 의미가 없는데, 이 패키지는 호출자가 얼마나 자주 샘플링했는지 일부러 모르기 때문입니다.
  • 합성 소재를 실제 웹캠 영상으로 교체. 지금 테스트 소재는 구석에 렌더링해 넣은 애니메이션입니다. 실제 사람을 찍은 카메라 영상이 더 어렵고 현실에 가까운 시험이며, 마스크는 그런 소재에서 측정해야 합니다.
  • 어느 방법으로도 풀리지 않는 실패. 이어지는 두 장이 글머리 한 줄만큼만 다르면, 160×90에서 그 차이가 트리거 하한에 붙어 있어 한 줄씩 쌓이는 빌드가 통째로 누락됩니다. 합성으로 다시 렌더링한 소재에서도 벤치마크에 이 현상이 보입니다. 이는 해상도와 채점 기준의 문제이고, docs/evaluation.md는 "빌드를 어떻게 셀지"를 반드시 밝히고 보고해야 하는 매개변수로 정의했습니다. 평가자마다 다르게 해석하지 않도록 하기 위해서입니다.
  • 보정을 어느 층에서 할 것인가. Video2Any는 영상을 워커 넷에 나누고, 각 워커가 자기 구간에서 임계값을 따로 보정합니다. 그래서 같은 파일이라도 코어 수가 다른 기기에서는 결과가 조금씩 달라집니다. 바람직한 성질이 아닙니다. 뻔한 해법은 전체에서 한 번만 보정하는 것입니다. 실제로 만들어 측정해 봤습니다. ffmpeg가 실제 컷을 일곱 번 세는 29분짜리 회의에서, 구간별 보정은 여섯 번을 찾았고 전역 보정은 네 번만 찾았습니다. 전역 보정이 반드시 더 낫지는 않으므로, 확실히 더 나은 버전이 나오기 전까지 이 패키지는 그것을 기본값으로 삼지 않습니다.

써보기

설치는 명령 한 줄이고 의존성이 없습니다. 손에 있는 어떤 녹화본이든 그대로 돌아갑니다.

npm i video-slide-extractor

이걸로 무언가를 만들었거나, 이걸 실패하게 만드는 영상을 가지고 있다면 저장소로 보내 주세요. 검출기를 통하지 않게 만드는 영상은 저희에게 별 하나보다 값집니다. 지금의 웹캠 테스트 소재도 그렇게 만들어졌습니다.

영상 변환하기블로그