← 모든 프로젝트

영상 속 악보 추출기

유튜브 악보 영상에서 악보만 뽑아 깔끔한 A4 PDF로 만들어 주는 윈도우 앱

기간
2026년 9월
대상
개인 의뢰인, 주로 드럼 악보 영상
역할
1인 작업: 컴퓨터 비전, 윈도우 앱, 빌드, 릴리스
기술
python, opencv, numpy, ffmpeg, yt-dlp, pymupdf, tkinter, pyinstaller
규모
3주 동안 커밋 246개, 앱 코드 8.4k줄, 테스트·진단 도구 10k줄
외주 작업, neoworks

다른 언어: english

어떤 앱인가요

유튜브의 드럼, 피아노 커버 영상 중에는 곡이 재생되는 동안 화면에 악보를 띄워 두는 게 많아요. 플레이헤드나 하이라이트가 악보 위를 지나가고, 몇 마디마다 페이지가 바뀌어요. 이 앱은 그런 영상 링크 목록을 받아서, 영상마다 흰 바탕에 검은 악보만 남긴 A4 PDF를 하나씩 만들어 줘요.

화면은 작은 한국어 윈도우 GUI예요. 링크를 붙여 넣고, 폴더를 고르고, 시작을 누르면 돼요.

the app after a conversion 빌드 스크립트가 실제 데스크톱에서 캡처한 윈도우 앱 화면

어떻게 동작하나요

영상은 두 번 디코딩해요. 첫 번째는 프레임 140개 정도를 보면서 악보가 어디 있는지 찾고, 두 번째는 4 fps로 샘플링하면서 그 영역만 잘라내요. 같은 악보 줄을 보여 주는 프레임끼리 묶고, 묶음마다 픽셀별 중앙값(median)을 구해서 한 줄짜리 스트립을 만들어요. 이렇게 하면 플레이헤드처럼 잠깐 악보 위를 지나가는 건 전부 지워져요. 그다음 스트립을 흰 바탕에 검은 글씨로 바꿔서 A4에 배치해요.

영상마다 따로 맞추는 설정은 없어요. 어느 영상의 어느 악보에나 공통으로 있는 게 하나 있는데, 바로 오선이에요. 길고 얇고 완전히 수평인 능선(ridge)이죠. 그래서 앱은 모든 프레임에서 밝은 쪽과 어두운 쪽 두 극성으로 능선을 찾고, 밝은 바탕에 어두운 선으로 읽은 결과와 어두운 바탕에 밝은 선으로 읽은 결과가 투표하게 해요.

어려웠던 부분

투표가 선이 아니라 행을 세고 있었어요. 한 채널의 영상 두 개에서 "악보를 찾지 못함"이 떴어요. 이 영상들은 오선 간격이 10 px였는데, 제가 테스트한 영상은 전부 14에서 24 사이였어요. 능선 검출기는 세로 9 px짜리 커널을 썼는데, 간격이 10 px이면 8 px짜리 흰 틈이 밝은 능선으로 읽혀요. 두꺼운 흰 틈 4개(32행)가 얇은 검은 선 5개(10행)를 이겨 버려서, 악보가 반전된 걸로 읽힌 거예요. 투표를 5 px 커널로 바꾸니 영상 20개 중 20개가 맞게 나왔어요.

중앙값도 늘 맞는 건 아니었어요. 최솟값을 쓰면 지나가는 어두운 것들이 다 남고, 플레이헤드를 하얗게 지우면 그 아래 음표까지 지워져요. 중앙값은 두 경우를 다 버텨요. 그런데 어떤 영상은 현재 마디 위에 반투명한 파란 박스를 칠하고 그 자리에 계속 세워 뒀어요. 묶음 안의 모든 프레임에서 박스가 같은 자리에 있으니 중앙값 자체가 그 부분에서 하얗게 나왔고, 마디가 통째로 사라졌어요. 처음 시도한 수정 두 개는 다른 영상에서도 작동해서 그 결과물까지 바꿔 버렸어요. 결국 통한 건 실제 실행 결과에서 오버레이의 모양을 재 보는 방법이었어요. 세워 둔 하이라이트는 꽉 찬 직사각형 하나(채움 비율 0.96에서 1.0)이고, 다른 영상의 색깔 있는 악보는 조각조각 나뉘어요(채움 비율 0.63에서 0.94). 그래서 마스크가 하나로 연결된 덩어리이고 자기 박스의 0.95 이상을 채울 때만 복원이 돌도록 했어요.

반복되는 마디는 똑같아 보여요. 프레임이 현재 줄과 달라 보이면 새 스트립을 시작하는데, 곡은 반복돼요. 한 영상에서는 다른 묶음이 전부 프레임 22개나 23개인데 한 묶음만 46개였어요. 음표가 거의 같은 서로 다른 두 시스템이 합쳐진 거고, 중앙값이 110마디와 114마디를 겹쳐 찍는 바람에 마지막 숫자가 0과 4가 포개진 글자 하나로 나왔어요. 해결책은 그림을 믿지 않는 거였어요. 앱은 헤더 맨 왼쪽 스트립에서 마디 번호를 읽고, 플레이헤드 자체도 추적해요. 플레이헤드는 왼쪽에서 오른쪽으로 움직이다가 새 시스템이 시작되면 폭의 0.9 정도만큼 뒤로 점프해요. 이 점프는 두 시스템이 똑같아 보이든 말든 상관없어요.

an intro printed twice 테스트 세트에서 나온 결함: 페이드인되는 인트로 사본(위)이 진짜 인트로 위에 찍힘

개수는 거짓말을 해요. 한 번은 수정 후에 시스템 개수가 정확히 34개 그대로였는데, 지워졌던 마디 하나가 돌아오고 중복 하나가 사라진 상태였어요. 그래서 출력된 모든 스트립에서 마디 번호를 OCR로 읽고, 그 순서가 악보대로 이어지는지 확인하는 검사를 만들었어요. 유사도 점수 대신 실제 페이지를 읽는 거예요.

회귀는 이렇게 막았어요

대부분의 수정은 다른 영상들이 기대고 있던 임계값을 건드렸어요. 그래서 모든 변경은 영상 18개짜리 게이트를 통과해야 했어요. 의도한 차이가 아니면 스트립은 바이트 단위로, 페이지는 픽셀 단위로 똑같아야 했어요. 더 넓게는 의뢰인이 변환해 둔 영상 108개를 의뢰인이 직접 돌린 결과의 개수와 비교해서 확인했어요.

이 게이트 때문에 거절한 수정도 있었어요. 잘린 아티큘레이션 기호 하나가 위쪽 트리밍 때문이었는데, 그 트리밍은 영상 69개의 스트립 458개에서 실제로 제 역할을 하고 있었어요. 기호 하나를 살리자고 트리밍을 느슨하게 하면 많은 페이지에 영상 이미지가 번져 들어갔을 거라서, 그대로 뒀어요.

지금은

3주 동안 1.0.0에서 1.7.16까지 갔어요. 릴리스마다 윈도우 VM에서 스크립트로 빌드하고, 거기 설치해서 실제 영상으로 돌려 본 다음에 의뢰인에게 보냈어요. 5분짜리 영상은 6분 정도 걸리는데, 대부분은 1080p AV1을 두 번 디코딩하는 시간이에요. 알려진 한계는 물리적인 거예요. 거의 흰 바탕 위의 흰 잉크는 신호가 거의 없어서, 반투명 오버레이는 끝까지 완전히 깨끗하게 나오지 않아요.