스크린샷 두 장을 잇는 게 얼마나 어려울까? 몇 번이고 뒤집어 다시 만든 iOS 긴 스크린샷 합성 엔진
스크린샷 두 장을 잇는 게 얼마나 어려울까? 몇 번이고 뒤집어 다시 만든 iOS 긴 스크린샷 합성 엔진
저는 몇 달 동안 한 가지 일만 했습니다. 모바일에서 긴 스크린샷 합성이 ‘딱 맞게’ 정렬되도록 만드는 것이었습니다.
쉬워 보이지 않나요? 인접한 스크린샷 두 장에서 겹치는 부분을 찾아서 자르고 붙이면 끝. 하지만 실제로 코드를 쓰기 시작하면 ‘딱 맞게’라는 네 글자가 얼마나 깊은 엔지니어링 늪인지 금방 알게 됩니다.
이 글에서는 ScrollShot의 긴 스크린샷 합성 엔진을 분해해 봅니다. 제품을 팔거나 과대포장하지 않고, 알고리즘과 제가 설계를 거듭 뒤집어야 했던 엔지니어링 디테일만 이야기합니다. ScrollShot의 전체적인 포지셔닝과 사용 시나리오가 궁금하다면, 제품 개요: 왜 ScrollShot을 iOS 긴 스크린샷 솔루션으로 선택해야 하는가를 먼저 읽어보세요.
OpenCV 파노라마 합성을 왜 안 썼을까?
직관적으로 이미지 합성은 오래된 문제입니다. OpenCV의 Stitcher 클래스면 몇 줄 코드로 파노라마 사진을 만들 수 있죠.
하지만 긴 스크린샷과 파노라마 사진은 완전히 다른 영역입니다. 파노라마는 수평 회전 촬영에 투시 변환이 있고, 겹치는 영역이 보통 크지 않습니다. 긴 스크린샷은 수직 단방향 스크롤이며, 연속된 두 프레임이 5%–80%까지 완전히 동일한 내용을 공유합니다. 그리고 — 이게 핵심인데 — 2픽셀만 어긋나도 바로 보입니다. 합성하는 대상이 텍스트일 수 있으니까요.
OpenCV를 써본 사람이라면 비슷한 경험이 있을 겁니다. 피처 매칭이 채팅 화면에서 완전히 길을 잃습니다. 어떤 사용자는 메신저에 배경 이미지를 설정해서 화면 대부분이 똑같이 보이는데, 이전 프레임의 끝과 다음 프레임의 시작이 겹치는 게 아니라 SIFT 특징점이 온통 ‘비슷하지만 다른’ 매칭 쌍으로 뒤덮입니다.
그래서 저는 다른 길을 택했습니다. 첫 줄부터 템플릿 매칭 엔진을 직접 작성했고, ‘수직 스크롤 스크린샷’ 시나리오에 특화하여 최적화했습니다.
전체 파이프라인은 이렇습니다:

엔진은 Apple Vision Framework를 이용한 매칭도 지원합니다(빠르지만 거친). 다만 이 글에서는 자체 개발한 템플릿 매칭에 집중합니다. 진짜 일을 해내는 부분이니까요. 이 알고리즘이 최종적으로 한 번 녹화하면 긴 스크린샷이 완성되는 경험으로 어떻게 이어지는지 궁금하다면 제품 기능 소개를 참고하세요.
첫 번째 관문: 1,800프레임에서 유효한 30프레임을 골라내기
30초짜리 60fps 화면 녹화 = 1,800프레임. 이걸 전부 합성에 넣겠다고? 메모리가 먼저 터지고, 시간도 터집니다.
가장 직관적인 접근은 등간격 프레임 샘플링입니다. 초당 3프레임을 추출하는 거죠. 하지만 치명적인 단점이 있습니다. 사용자의 스크롤 속도가 일정하지 않다는 겁니다. 빠르게 스와이프하면 두 프레임 사이에 반 화면 차이가 나서 합성할 때 반드시 내용이 누락됩니다. 천천히 볼 때는 인접한 두 프레임이 거의 똑같아서 전부 쓰레기 프레임이 됩니다.
ScrollShot은 등거리 프레임 샘플링을 사용합니다. 시간이 아니라 ‘화면이 얼마나 스크롤되었는지’를 봅니다. 누적 변위가 임계값을 초과해야만 프레임을 추출할 가치가 있습니다.
변위 임계값은 얼마가 적당할까?
이 숫자 때문에 정말 오래 고민했습니다. 너무 작게 설정하면 천천히 스크롤할 때 미친 듯이 프레임을 뽑아내며 중복 투성이 됩니다. 너무 크게 설정하면 빠르게 스크롤할 때 내용이 누락됩니다.
최종 공식:
displacementThreshold = dsHeight × 0.60 × presetScale
여기서 dsHeight는 다운샘플링된 영상 프레임 높이(1080p를 3배 다운샘플링하면 약 360p, 즉 dsHeight ≈ 640), 0.60은 기본 비율, presetScale은 사용자가 선택 가능한 프리셋(공격적/균형/보수적)입니다.
즉, 균형 모드에서는 화면이 약 384픽셀(640 × 0.60) 스크롤될 때마다 한 프레임을 추출합니다. 화면 높이의 절반보다 조금 적은 정도입니다.
또 하나 쉽게 놓치는 노이즈 필터가 있습니다. 한 번의 변위가 반드시 3픽셀 이상이어야 누적 변위에 합산됩니다. 이걸 넣지 않으면 템플릿 매칭의 서브픽셀 노이즈가 서서히 쌓여서, 정지 화면에서도 ‘가짜 프레임 추출’이 발생합니다. 이 버그를 찾는 데 오후 내내 걸렸습니다.
두 번의 스캔 구현
첫 번째 스캔(Pass 1) — 저해상도 움직임 분석. 영상 프레임을 3배 다운샘플링(1080p → 360p, 속도 한 단계 향상)하고, 경량 템플릿 매칭으로 프레임별 수직 변위를 추적합니다. 여기에 엔지니어링 디테일 몇 가지가 숨어 있습니다:
- 적응형 스텝 사이즈: 변위가 클 때는 크게 건너뛰고(
nativeFPS / 6, 약 10프레임마다 확인), 변위가 작을 때는 프레임 단위 스캔으로 돌아갑니다(nativeFPS / 30). 남은 거리가 임계값의 25% 미만이면 강제로 최소 스텝으로 전환합니다. 그렇지 않으면 정확히 한 프레임을 건너뛰어 놓칠 수 있습니다. - 장면 전환 감지: 인접 프레임의 SAD per pixel이 40.0을 초과하면? 사용자가 앱을 전환했을 확률이 높습니다. 즉시 프레임을 추출합니다.
- 바운스백 절단: Vision의 광학 흐름으로 움직임 방향을 감지하여, 누적 변위가 -50px 미만(사용자가 위로 스크롤 중)이면 맨 위에 도달했다고 판단하고 이후 프레임을 모두 폐기합니다. 하지만 이 임계값을 너무 타이트하게 설정하면 안 됩니다. 처음에 -10으로 설정했는데, 손가락 미세한 떨림만으로도 절단이 발생해서 녹화가 절반으로 줄었습니다.
1.5번째 스캔(Pass 1.5) — 선명도 보정. 각 후보 프레임에 대해 전후 ±2 프레임의 작은 윈도우에서 가장 선명한 프레임을 고릅니다.
선명도를 어떻게 수치화할까요? Laplacian 에너지를 사용합니다. 그레이스케일 이미지에 3×3 Laplacian 커널을 합성곱한 뒤, 응답값의 제곱 평균을 계산합니다:
kernel = [-1, -1, -1,
-1, 8, -1,
-1, -1, -1]
sharpness = mean((convolve(gray, kernel) - 128)²)
128을 빼는 이유는 합성곱 출력에 128 바이어스가 있기 때문입니다(vImage 요구사항). 제곱 후 평균하면 고주파 에너지가 됩니다. 값이 클수록 더 선명한 프레임입니다.
Laplacian 분산이 아니라 Laplacian 에너지를 쓴 이유는? 스크린샷 시나리오에서는 두 방법이 선택하는 프레임이 거의 동일하지만, 전자 계산이 더 간단합니다. vImage의 vDSP_vsq + vDSP_meanv 두 줄이면 끝입니다.
이 단계의 해상도 상한은 720×1280으로 충분합니다. 현실적인 이유가 있습니다. 가장 선명한 프레임은 사용자가 손가락을 떼는 순간이고, 스와이프 중인 프레임은 모션 블러가 끼기 쉽습니다. Laplacian 에너지는 모션 블러에 매우 민감해서, 차이가 보통 2–3배 납니다.
핵심 알고리즘: 템플릿 매칭 엔진
엔진 전체에서 코드량이 가장 많은 부분(단일 파일 3,000줄 이상)이자, 버그에 가장 오래 시달린 부분입니다.
역방향 매칭: 왜 새 프레임에서 옛 프레임을 찾을까?
첫 번째 버전은 ‘순방향 매칭’이었습니다. 이전 프레임 하단에서 템플릿을 추출하여 새 프레임에서 검색하는 방식입니다. 카카오톡 채팅 화면에서 바로 실패했습니다. 이전 프레임 하단에 ‘네’ 메시지 하나가 있었는데, 새 프레임에는 ‘네’ 메시지가 세 개나 나타났고, 매칭이 맨 위에 것을 잡아서 offset이 200픽셀이나 어긋났습니다.
그 후 방향을 바꿨습니다. 역방향 매칭(Reverse Matching). 새 프레임(img2)의 상단에서 템플릿을 추출하고, 이전 프레임(img1)의 하단에서 검색합니다.
왜 역방향이 더 나은가? 새 프레임 상단은 ‘방금 스크롤되어 들어온 새 내용’이고, 이 내용은 이전 프레임에 반드시 존재하며, 딱 한 번만 존재합니다(이전 프레임의 중하단에). 반대로 이전 프레임 하단의 ‘곧 사라질 기존 내용’은 새 프레임에서 약간만 보이거나, 이미 새 내용에 밀려 화면 밖으로 나갔을 수 있습니다.

검색 시 상태 표시줄 영역(상단 약 250px)과 하단 탭 바 영역(약 350px)을 자동으로 건너뜁니다. 하지만 여기서 또 하나의 함정이 있습니다. 어떤 앱은 하단 내비게이션 바 높이가 다르고, 어떤 페이지는 키보드가 있고, 어떤 것은 플로팅 버튼이 있습니다.
그래서 동적 푸터 감지를 추가했습니다. 구체적인 방법: 두 프레임 하단에서 한 줄씩 위로 스캔하며, 각 줄의 중앙 80% 픽셀을 추출하고(양쪽 10%씩 잘라내서 가장자리의 플로팅 버튼을 피함), 16픽셀마다 샘플링하여 두 프레임의 같은 줄에 대한 MAD(평균 절대 차)를 계산합니다. MAD ≤ 3.2이면 해당 줄이 두 프레임에서 거의 동일하다는 뜻이며, 정적 UI 요소일 가능성이 높습니다. 4줄 이상 연속으로 정적이 아닌 줄이 나오면 스캔을 중단하고(maxGapTolerance = 4), 누적 정적 줄이 24줄 미만이면 고정 하단 바가 없다고 판단하여 0을 반환합니다.
3.2라는 임계값은 경험치입니다. 순백 배경의 정적 영역은 MAD가 보통 0–1.5이고, JPEG 압축 노이즈에 의한 변동은 2–4 사이이며, 실제 내용 변화가 있는 줄은 최소 8 이상입니다. 3.2는 ‘압축 노이즈는 허용하면서 내용 변화는 거부하는’ 경계에 정확히 자리잡고 있습니다.
6개 템플릿 투표, ‘닮은 것들’에 대항하기
단일 템플릿 블록은 오매칭되기 쉽습니다. 이커머스 상품 목록 페이지를 상상해 보세요. 모든 상품 카드의 레이아웃이 거의 똑같아서, 단일 템플릿이 위아래 상품에 매칭될 수 있습니다.
엔진은 6개의 템플릿 블록(각각 높이 100px, 새 프레임 상단에서 간격을 두고 배치)을 동시에 추출하고, 각각 독립적으로 이전 프레임에서 최적 매칭 위치를 검색하여 6개의 offset 값을 얻습니다. 그런 다음 이 6개 offset을 클러스터링(허용 오차 5px)하여 가장 큰 클러스터의 중앙값을 취합니다.
6개 중 5개가 offset ≈ 320을 가리키고 1개가 800을 가리킨다면, 800은 거의 확실한 오매칭이므로 버리면 됩니다.
얼리 엑시트 최적화도 있습니다. 첫 3개 템플릿의 offset이 이미 일치하고(허용 오차 4px), 0에 가깝지 않다면(‘사용자가 아예 스크롤하지 않은 경우’ 제외), 나머지 3개는 계산할 필요가 없습니다.
여기서 제가 오래 고민한 디테일이 있습니다. 허용 오차를 얼마로 할까? 처음에 2px로 설정했는데, NCC 자체의 양자화 오차(뒤에서 설명) 때문에 같은 프레임 쌍의 다른 템플릿들 offset이 보통 2–3px 정도 차이가 나서 얼리 엑시트가 거의 발동되지 않았습니다. 4px로 완화한 후, 실제 테스트에서 약 60%의 프레임 쌍에서 얼리 엑시트가 발동되었고, 이로 인한 정확도 저하는 관찰되지 않았습니다.
피라미드 NCC: 거친 것에서 섬세하게 검색
각 템플릿 매칭은 **NCC(정규화 상호 상관)**로 유사도를 측정합니다. NCC 공식:
NCC의 장점은 전체 밝기 변화에 둔감하다는 것입니다(낮에 녹화한 화면과 다크 모드의 같은 페이지도 매칭 가능). 값의 범위는 [-1, 1]이고, 1 = 완전 일치입니다. 직관적으로 이해하면: NCC는 픽셀의 절대값을 비교하는 것이 아니라 ‘해당 영역의 밝고 어두운 패턴이 같은지’를 비교합니다.
하지만 이전 프레임 전체를 픽셀 단위로 슬라이드하며 검색하면 너무 느립니다. 엔진은 피라미드 3단계 검색을 사용합니다:
| 단계 | 스텝 | 검색 윈도우 | 후보 수 | 목적 |
|---|---|---|---|---|
| Level 1 (Pre) | 12px | 전체 범위 | ~125 | 대략적 위치 파악 |
| Level 2 (Coarse) | 4px | ±32px | ~16 | 행 단위 정확도 |
| Level 3 (Fine) | 1px | ±4px | 9 | 픽셀 단위 미세 조정 |
공유할 만한 수치가 있습니다. Level 1의 스텝 12는 후보 위치의 1/12만 검색한다는 뜻으로, 픽셀 단위 검색 대비 단일 템플릿 매칭 속도가 약 12배 향상됩니다. 세 단계를 모두 합한 총 계산량은 전체 범위 픽셀 단위 검색의 약 1/8입니다.
후보가 64개를 초과하면(보통 Level 1 전체 범위 검색에서 발생), GCD 병렬 계산을 자동으로 활성화(DispatchQueue.concurrentPerform)하여 검색 작업을 여러 CPU 코어에 분산시킵니다. 여기서 함정을 밟았습니다. 처음에 NSLock으로 공유 bestScore 변수를 보호했는데, 6코어에서의 락 경합이 오히려 속도를 떨어뜨렸습니다. 이후 스레드별 로컬 최적값을 기록하고 마지막에 병합하는 방식으로 변경하여, 락 경합을 O(n)에서 O(1)로 줄였습니다.
검색 윈도우: 역사가 길을 안내하게 하라
매번 이전 프레임의 전체 사용 가능 영역을 검색하면 느릴 뿐만 아니라 오매칭도 쉽습니다(페이지 상단과 하단에 유사한 레이아웃이 있을 수 있으니까요).
엔진은 두 가지 ‘사전 정보’를 활용하여 검색 윈도우를 좁힙니다:
- 시간적 사전 정보: 이전 프레임 쌍의 offset이 300px이었다면, 이번 쌍도 비슷할 확률이 높습니다. 사용자의 스크롤 속도가 갑자기 변하지 않으니까요. 검색 윈도우를 300 ± 120px로 축소합니다.
- Vision 사전 정보: 첫 프레임 쌍에서 과거 데이터가 없을 때, Apple Vision의 이미지 정렬 API로 대략적인 추정값을 얻고, 윈도우를 추정값 ± 180px로 축소합니다.
좁은 윈도우에서 최소 3개의 일관된 매칭을 찾지 못하면(사전 정보가 빗나갔다는 뜻), 자동으로 전체 범위 검색으로 돌아갑니다.
이 ‘최소 3개 일치’ 조건도 시행착오로 얻은 것입니다. 처음에는 ‘최소 1개’를 사용했는데, Vision 사전 정보가 크게 빗나갈 때(간혹 500px까지 틀림), 좁은 윈도우의 오매칭이 우연히 유효한 결과로 통과되었습니다. 3개로 변경한 후, 오매칭이 통과하려면 3개의 독립 템플릿을 동시에 속여야 해서 확률적으로 거의 불가능해졌습니다.
정상 매칭이 안 될 때: 3단계 폴백 전략
실전 환경은 실험실보다 훨씬 가혹합니다. 플로팅 버튼, 반투명 오버레이, 재생 중인 동영상, 깜빡이는 커서 — 이 모든 것이 NCC 점수를 끌어내립니다.
엔진은 3단계 폴백 전략을 구현했습니다. 실제 데이터로 각 단계의 발동 빈도를 보여드리겠습니다:
1단계: 강매칭(~80% 프레임 쌍). 대부분의 템플릿이 NCC ≥ 0.6이며 같은 offset을 가리킵니다. 가장 흔한 경우입니다.
2단계: 약매칭(~12% 프레임 쌍). 최적 템플릿의 NCC가 0.45~0.6에 불과합니다. 플로팅 UI가 일부 영역을 가렸을 수 있습니다. 이때 전체 중복 영역의 SAD(모든 픽셀의 밝기 차이 합)를 2차 검증으로 사용합니다. SAD per pixel ≤ 26.0이면 수락, 그렇지 않으면 거부합니다.
3단계: 1D Profile NCC(~5% 프레임 쌍). 2차원 이미지를 ‘납작하게’ 눌러 한 줄 평균 곡선으로 만듭니다. 각 행의 모든 픽셀을 평균하여 1차원 배열을 얻고, 두 1차원 곡선에 상호 상관을 수행합니다. 약 100배 빠르지만 위양성 위험이 높습니다.
전부 실패(~3% 프레임 쌍)? 이 프레임은 장면 전환이거나 중복이 전혀 없는 경우일 가능성이 높습니다. 합성 없이 마지막에 추가합니다.
2단계의 SAD 검증에는 섬세함이 필요합니다. 기본 임계값은 픽셀당 25.0이지만, 중복 비율이 80%를 초과하면 임계값이 적응적으로 조여집니다:
if overlapRatio > 0.8:
scale = max(minScale, 1.0 - (overlapRatio - 0.8) × 4.0)
threshold = 25.0 × scale
왜 조여지는가? 중복 영역이 클수록 SAD의 통계 샘플이 많아져서, 랜덤 노이즈가 평균으로 사라지고, 오매칭의 SAD도 낮아지기 쉽습니다. 조이지 않으면 고중복 시나리오에서 위양성이 증가합니다.
어떤 경로를 택하든, 마지막에는 항상 offset 미세 조정이 있습니다. 후보 offset의 ±6px 범위에서 픽셀 단위로 SAD를 계산하여 최소값을 취합니다.
왜 NCC가 내놓은 offset을 다시 미세 조정해야 할까? 이걸 이해하는 데 3일이 걸렸습니다. NCC는 최적값 근처에서 매우 ‘평탄’합니다. offset이 2px 달라도 NCC는 0.003밖에 변하지 않을 수 있습니다. 하지만 SAD는 ±1px 변위에 극도로 민감해서, 1px 차이로 SAD가 3–5 단위 변합니다. 두 가지가 상호 보완합니다. NCC가 대략적 위치를 잡고, SAD가 픽셀 단위로 교정합니다. 텍스트를 합성할 때 2px 차이는 ‘한 줄 텍스트가 반으로 잘린 것’과 ‘완벽한 정렬’의 차이입니다.
솔기선: 글자를 반으로 자르지 마라
offset을 찾았습니다. 두 프레임에 중복 영역이 있습니다. 어디서 ‘자를까’?
가장 직관적인 방법은 가운데서 자르는 것입니다. 하지만 가운데에 큰 텍스트 줄이 있으면, 잘린 윗부분과 아랫부분이 맞지 않고, 눈으로 바로 이상함을 감지합니다.
엔진의 방법: 중복 영역 내 각 행의 픽셀 차이를 계산하고, 슬라이딩 윈도우로 차이 총합이 최소인 위치를 찾습니다. 윈도우 높이는 적응적입니다. min(overlapH, max(100, adaptiveCutHeight))를 사용하며, adaptiveCutHeight는 overlap과 offset의 비율에 따라 동적으로 조정됩니다. 이 위치가 두 프레임의 내용이 가장 가까운 곳이고, 솔기를 여기에 놓으면 시각적으로 가장 자연스럽습니다.
성능 벤치마크
일반적인 30초 카카오톡 채팅 녹화(iPhone 15 Pro, 1080×2400, 60fps)를 실행한 엔진의 주요 지표:
| 지표 | 수치 |
|---|---|
| 총 프레임 수 | 1,800 |
| Pass 1 분석 시간 | 3.2s |
| 추출된 핵심 프레임 | 26프레임 |
| Pass 1.5 선명도 보정 | 0.8s |
| 템플릿 매칭 총 시간 | 4.1s (25개 프레임 쌍) |
| 쌍당 평균 매칭 | 164ms |
| 얼리 엑시트 발동률 | 58% (15/25 쌍) |
| 강매칭 비율 | 84% (21/25 쌍) |
| 약매칭 발동 | 3 쌍 |
| 1D Profile 폴백 | 1 쌍 |
| 전부 실패(추가) | 0 쌍 |
| 최종 이미지 크기 | 1,170 × 18,600 |
| 전체 소요 시간 | ~8.5s |
피라미드 검색의 가속 효과가 뚜렷합니다. Level 1 스텝 12가 후보 위치의 92%를 건너뛰고, Level 2는 ±32px 좁은 윈도우에서 ~16개 포인트만 계산하며, Level 3은 ±4px만 미세 조정합니다. 세 단계를 합한 유효 계산량은 전체 범위 브루트포스 검색의 약 1/8입니다.
놓칠 수 없는 엔지니어링 디테일
20,000픽셀 긴 이미지를 한 번에 로드할 수 없다
30프레임을 합성한 긴 이미지는 1,170 × 24,000픽셀에 이를 수 있습니다. 전체 해상도 RGBA 메모리 점유는 112MB(1,170 × 24,000 × 4 bytes) — iPhone에게는 너무 많습니다.
StreamingStitchingPlanner는 스트리밍 설계로 되어 있습니다. 매칭 중 메모리에는 ‘현재 프레임 + 이전 프레임’만 존재합니다. 합성 결과는 Piece 집합(원본 이미지 인덱스 + 크롭 영역 + 대상 위치)으로 기록되고, 최종 렌더링 시 타일 단위로 디코딩하고 그립니다.
스크롤바 제거 알고리즘
합성된 긴 이미지 오른쪽에는 거의 항상 스크롤바가 잔존합니다. 엔진은 그래디언트 투영법으로 자동 제거합니다:
오른쪽 가장자리 너비의 3% 띠를 추출 → 수평 그래디언트 누적 계산 → 스크롤바의 좌우 가장자리가 두 개의 그래디언트 피크를 형성(간격 3–20px) → 행 단위로 스크롤바 실제 위치 확인 → 왼쪽 인접 픽셀로 덮어씁니다. 하단 수평 스크롤바도 동일하게 처리합니다.
알고리즘이 실패할 때를 위한 폴백 UI
아무리 좋은 알고리즘도 때로는 실패합니다. ScrollShot은 모든 솔기선에 인터랙티브 미세 조정을 제공합니다. 사용자가 위아래로 드래그하여 합성 위치를 조정하고, 실시간 미리보기를 확인합니다. 내부의 FineTuneGeometryEngine이 기하학적 제약이 항상 유효하도록 보장합니다(한 조각을 다른 조각 위에 겹치게 끌 수 없습니다). 미리보기는 저해상도로 빠르게 렌더링하고, 내보낼 때 자동으로 원본 해상도로 전환합니다. 이러한 제어 가능성은 수동 합성 모드(Manual Stitch)의 핵심 상호작용이기도 합니다.
마지막으로 한마디
이 엔진을 만들며 가장 크게 느낀 점: 합성 알고리즘 자체는 어렵지 않습니다. 어려운 것은 온갖 ‘말도 안 되는’ 실제 페이지에서도 작동하게 만드는 것입니다.
카카오톡 채팅 배경은 다 비슷해 보이고, 쿠팡 상품 목록은 무한 반복되고, 시스템 설정 페이지는 순백 배경이 넓게 펼쳐져 있고, YouTube 동영상 썸네일은 움직이고… 이 모든 것이 교과서에 나오지 않고 논문에도 실리지 않는 엣지 케이스입니다.
특별히 기억에 남는 버그가 하나 있습니다. iOS 시스템 설정 페이지를 녹화할 때 NCC offset이 매번 약 40px 어긋났습니다. 오랫동안 디버깅한 결과, 설정 페이지 상단에 큰 순백 영역이 있고, 템플릿이 순백 영역에 걸리면 NCC가 모든 흰색 위치에서 1.0에 가까운 점수를 줘서 구분이 불가능했습니다. 해결 방법은 템플릿의 분산이 너무 낮으면 자동으로 해당 템플릿을 건너뛰는 것입니다. 순백 템플릿은 정보량이 없으므로, 안 쓰는 게 낫습니다.
한 번은 더 황당한 일도 있었습니다. YouTube 홈 화면을 녹화하는데, 어떤 동영상 썸네일에 움직이는 고양이가 있었습니다. 썸네일 영역이 두 프레임 사이에서 완전히 달랐고, NCC가 0.3까지 떨어졌습니다. 이런 케이스는 알고리즘 자체로는 구할 수 없습니다. 결국 6개 템플릿 투표 메커니즘이 해결했습니다. 나머지 5개 템플릿이 정적 영역을 추출해서, 투표 결과는 여전히 정확했습니다.
이런 문제를 해결하는 것은 더 우아한 수학 공식이 아니라, 하나하나의 ‘일단 이렇게 막아두자’라는 엔지니어링 트릭입니다. 비슷한 작업을 하고 계신다면, 이 글이 몇 가지 시행착오를 줄이는 데 도움이 되길 바랍니다.
더 읽을거리
- 왜 ScrollShot인가? iOS 긴 스크린샷을 깔끔하게 저장하는 방법 — 제품 관점에서 ScrollShot이 iOS 긴 스크린샷의 불편함을 어떻게 해결하는지 소개합니다. 화면 녹화 자동 합성, 수동 이어붙이기, 개인정보 보호, 고화질 내보내기까지.