점자로 보는 뮤직 비디오
🎞
이 글의 목표는 동영상을 점자 패턴 스트림 (Braille Pattern Stream, ⠨⠎⠢⠨⠀⠙⠗⠓⠾⠀⠠⠪⠓⠪⠐⠕⠢)으로 변환하는 방법을 설명하는 것이다. 점묘화로 만드는 동영상이나 아스키 아트로 만드는 동영상이라고 생각해도 좋을 것이다 (물론 점자는 ASCII가 아닌 Unicode이다.) 아직 점자 동영상이 무슨 뜻인지 모르겠다면 완성본을 먼저 보자.
자막을 켜고 5-10초를 기다리면 된다. 자막이 10MB라 로딩이 느릴 수 있다. 만약 점자가 보이지 않는다면 다음 영상을 보자.
점자 패턴 스트림은 말 그대로 연속된 형태의 점자 패턴이기에 동영상으로 만들 수도 있고 아예 자막으로 모든 것을 넣어버릴 수도 있다. 자막이야말로 텍스트 스트림을 보여주는 수단이기 때문이다. 때문에 이 프로젝트의 목표를 동영상을 변환해서 YouTube 자막으로 넣어보는 것으로 잡았다. 기반 기술은 다음과 같다.
- OpenCV (C++ cv2) — 동영상을 연속된 이미지로 변환하기 위해 사용됨
- Python Image Library (Python 3 Pillow) — 이미지를 점자로 변환하기 위해 사용됨
- Python Standard Library (sys, os, pathlib) — 파일을 읽고 쓰기 위해 사용됨
- ffmpeg (optional) — 동영상을 편집하기 위해 사용됨
anaclumos/video-in-dots에 오픈소스로 공개되어 있다.
설계
목표를 위해서 다음의 기술들이 필요하다고 생각했다.
- 임의의 이미지를 모노크롬 이미지로 변환하는 기술
- 모노크롬 이미지를 임의 크기의 점자 배열로 변환하는 기술
- 동영상을 프레임의 연속으로 변환할 수 있는 기술
- 3번에서 얻은 프레임을 2번에서 얻은 기술을 이용해 텍스트 스트림으로 변환하여 정형화된 자막의 형태로 변환하는 기술
- (나중에 알게 됨) 텍스트 스트림을 특정 크기 이하로 압축하는 기술
- (나중에 알게 됨) Dithering 처리 기술
1. 임의의 이미지를 모노크롬 이미지로 변환
모노크롬 이미지는 1비트 깊이의 이미지로, #000000
의 완벽한 검정과 #FFFFFF
의 완벽한 하양으로만 이루어지는 이미지이다.
이 하양과 검정을 점자에서 각 점이 칠해진 (raised) 상태와 그렇지 않은 상태에 대응시킬 수 있다.
점자의 기본적인 역할은 경계선과 형체를 구분할 수 있도록 도와주는 것이다. 이미지를 흑백의 형태로 변환한 뒤 1비트 흑백 이미지로 변환한다. 여기서 중요한 점은 자막은 대개 시스템 기본값이 하얀색이기 때문에 우리의 1비트 흑백 이미지에서는 밝은 픽셀이 1이 되어야 한다는 것이다.
제일 왼쪽의 의미지는 256단계의 grayscale 이미지이고, 나머지 3개의 이미지는 각기 다른 알고리즘으로 나타낸 모노크롬 이미지이다. 이 프로젝트에서는 최종적으로 Floyd-Steinberg 알고리즘을 사용한다.
이미지를 모노크롬으로 변환하기
이미지를 1비트 흑백으로 바꾸는 방법은 굉장히 다양한데, 이 프로젝트는 sRGB 영역만 사용할 것이기에 CIE 1931 sRGB에 정의된 Luminance 기반 변환을 사용한다. 다음처럼 간단하게 표현할 수 있다. 참고
def grayscale(red: int, green: int, blue: int) -> int:
return int(0.2126 * red + 0.7152 * green + 0.0722 * blue)
여기에서 red, green, blue는 0-255의 int
이다.
이 합이 임의의 hex_threshold
를 넘으면 해당 픽셀의 값을 1로 설정한다.
이 코드를 모든 픽셀마다 실행해주면 된다. 물론 위의 grayscale 코드는 이론적인 부분을 이해하기 위함이고,
최종적으로는 아래와 같이 Python PIL
에 내장된 코드를 사용할 것이다.
후술하겠지만, 이 라이브러리는 기본적으로 디더링 처리도 해준다.
resized_image_bw = resized_image.convert("1") # apply dithering
2. 모노크롬 이미지를 임의 크기의 점자 배열로 변환
위 문장은 3가지 파트로 나눌 수 있다. ① 1비트 모노크롬 이미지 를 ② 임의 크기의 ③ 점자 배열로 변환하는 기술. 1번은 완료했으니 우선 2번부터 살펴보자.
PIL을 통해 이미지 크기 변환하기
이 프로젝트에서는 이미지를 PIL을 사용해 불러오기 때문에, 다음 코드를 통해 이미지를 임의 크기로 변경할 수 있다.
def resize(image: Image.Image, width: int, height: int) -> Image.Image:
if height == 0:
height = int(im.height / im.width * width)
if height % braille_config.height != 0:
height = int(braille_config.height * (height // braille_config.height))
if width % braille_config.width != 0:
width = int(braille_config.width * (width // braille_config.width))
return image.resize((width, height))
이 프로젝트에서 사용할 점자는 크기가 2 x 3 크기이기에 이미지의 너비, 높이가 여기에 완벽하게 나누어 떨어지도록 크기를 미세하게 조정한다.
이미지를 점자 배열로 변환하기
이건 사진으로 보는 것이 이해하기가 더 편하다. 왼쪽과 같이 6 x 6 이미지가 있을 경우 너비를 2픽셀, 높이를 3픽셀마다 잘라 2 x 3 이미지로 만든 뒤 이를 점자로 변환한다.
점자 변환 알고리즘의 핵심은 어떻게 픽셀 배열에 해당하는 점자를 정확하게 찾느냐는 것이다. 가장 단순하게 모든 픽셀 배열 조합을 점자와 매핑해놓는 방법도 있다. 특히나 2 x 3의 점자는 26개의 조합 밖에 없기 때문이다. 하지만 유니코드가 점자 규격이 제정될 때 점자가 어떻게 배치되었는지를 이해하면 더 간단하게 나타낼 수 있다.
간단한 유틸 코드를 작성해보았다.
이 코드의 경우 위의 로직을 이용해서 이미지를 리사이징한 뒤 점자로 변환하고 색을 입혀 terminal
에 점자 배열을 print
한다.
Terminal
에 print
되는 글자의 색은 \033[38;2;{};{};{}m{}\033[38;2;255;255;255m".format(r, g, b chr(output))
의 형태로 입힐 수 있는데,
더 궁금한 경우 ANSI Color Escape Code를 알아보면 된다.
직접 실행해보고 싶다면 다음 저장소의 파일을 실행해 보자. anaclumos/tools-image-to-braille
이 코드의 경우 1600만 색상의 ANSI True Color라는 색상 프로필을 사용하는데, macOS에 내장된 terminal.app에서는 True Color 1600만 색상을 지원하지 않고 256개의 색상만 지원한다. 때문에 True Color을 지원하는 iTerm이나 VS Code 내장 터미널을 사용해서 실행하자.
3. 동영상을 프레임의 연속으로 변환
같은 영상으로 반복해서 코드를 실행할 일이 있을 것이기에 프레임마다 사진이 물리적으로 저장되어야 했다. 이를 위해 Python OpenCV 라이브러리를 활용하기로 결정했다. 간단하게 이야기해서 다음의 과정을 거친다.
- 기본적인 라이브러리와 변수 설정
- 동영상이 존재하지 않을 경우 오류 표시
- 프레임 이미지 파일이 저장될 폴더 생성
- 각 프레임을 저장.