Converting videos into ASCII art animations is a fascinating way to explore image processing and terminal manipulation in Python. The core concept involves converting each video frame into grayscale, mapping pixel brightness to specific characters, and then efficiently rendering these frames in the terminal to create the illusion of movement.
This guide walks you through building a CLI tool that converts videos to ASCII animations using OpenCV and plays them directly in your terminal.
Prerequisites and Setup
We need to install opencv-python for video processing and pyprind for displaying progress bars during the conversion process.
sudo pip3 install opencv-python pyprindCreate a file named CLIPlayVideo.py and import the necessary libraries.
import sys
import os
import time
import threading
import termios
import tty
import cv2
import pyprindThe CharFrame Class: Basic Conversion Logic
The foundation of our project is converting a single image (or frame) into ASCII characters. We map pixel brightness (0-255) to a string of characters ranging from dense (like $) to sparse (like ).
class CharFrame:
ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
def pixelToChar(self, luminance):
"""Map a grayscale value to a character."""
return self.ascii_char[int(luminance / 256 * len(self.ascii_char))]
def convert(self, img, limitSize=-1, fill=False, wrap=False):
"""Convert a standard image frame to an ASCII string."""
if limitSize != -1 and (img.shape[0] > limitSize[1] or img.shape[1] > limitSize[0]):
img = cv2.resize(img, limitSize, interpolation=cv2.INTER_AREA)
ascii_frame = ''
blank = ''
if fill:
blank += ' ' * (limitSize[0] - img.shape[1])
if wrap:
blank += '\n'
for i in range(img.shape[0]):
for j in range(img.shape[1]):
ascii_frame += self.pixelToChar(img[i, j])
ascii_frame += blank
return ascii_frameThe V2Char Class: Video Processing and Playback
This class handles reading video files frame-by-frame, converting them, and managing playback in the terminal.
Initializing and Generating Frames
We use cv2.VideoCapture to read the video.
class V2Char(CharFrame):
charVideo = []
timeInterval = 0.033
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
def genCharVideo(self, filepath):
self.charVideo = []
cap = cv2.VideoCapture(filepath)
self.timeInterval = round(1/cap.get(5), 3) # FPS
nf = int(cap.get(7)) # Frame count
print('Generating ASCII video, please wait...')
for i in pyprind.prog_bar(range(nf)):
ret, rawFrame = cap.read()
if not ret: break
grayFrame = cv2.cvtColor(rawFrame, cv2.COLOR_BGR2GRAY)
frame = self.convert(grayFrame, os.get_terminal_size(), fill=True)
self.charVideo.append(frame)
cap.release()Playback with Cursor Control
To play the animation smoothly without flooding the terminal history, we use ANSI escape codes to reset the cursor position after printing each frame.
def play(self, stream=1):
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
breakflag = False
fd = sys.stdin.fileno()
old_settings = None
def getChar():
nonlocal breakflag, old_settings
old_settings = termios.tcgetattr(fd)
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
if ch: breakflag = True
t = threading.Thread(target=getChar)
t.daemon = True
t.start()
rows = len(self.charVideo[0]) // os.get_terminal_size()[0]
for frame in self.charVideo:
if breakflag: break
self.streamOut(frame)
self.streamFlush()
time.sleep(self.timeInterval)
self.streamOut('\033[{}A\r'.format(rows-1))
if old_settings:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
self.streamOut('\033[{}B\033[K'.format(rows-1))
for i in range(rows-1):
self.streamOut('\033[1A')
self.streamOut('\r\033[K')
print("Finished!")Exporting and Loading
To save processing time on subsequent runs, we can save the ASCII frames to a text file.
def export(self, filepath):
if not self.charVideo: return
with open(filepath, 'w') as f:
for frame in self.charVideo:
f.write(frame + '\n')
def load(self, filepath):
self.charVideo = []
for line in open(filepath):
self.charVideo.append(line[:-1])Running the Animation
Wrap the logic in a main block that uses argparse to handle command-line arguments.
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file', help='Video file or exported charvideo file')
parser.add_argument('-e', '--export', nargs='?', const='charvideo.txt', help='Export path')
args = parser.parse_args()
v2char = V2Char(args.file)
if args.export:
v2char.export(args.export)
v2char.play()Usage
- Run conversion and play:
python3 CLIPlayVideo.py BadApple.mp4 - Convert, save, and play:
python3 CLIPlayVideo.py BadApple.mp4 -e - Play saved file:
python3 CLIPlayVideo.py charvideo.txt
Conclusion
This project demonstrates the versatility of Python for multimedia projects.
- OpenCV efficiently handles frame extraction and resizing.
- ANSI Escape Codes allow for dynamic terminal UI updates, enabling animation playback.
- Daemon Threads provide a non-blocking way to listen for user input during execution.