Skip to content

How to Create ASCII Art Animations in Python Using OpenCV

Learn how to convert videos into ASCII animations in Python with OpenCV, mapping grayscale frames to terminal characters for real-time playback.

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 pyprind

Create 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 pyprind

The 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_frame

The 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

  1. Run conversion and play:
    python3 CLIPlayVideo.py BadApple.mp4
  2. Convert, save, and play:
    python3 CLIPlayVideo.py BadApple.mp4 -e
  3. Play saved file:
    python3 CLIPlayVideo.py charvideo.txt

Conclusion

This project demonstrates the versatility of Python for multimedia projects.

  1. OpenCV efficiently handles frame extraction and resizing.
  2. ANSI Escape Codes allow for dynamic terminal UI updates, enabling animation playback.
  3. Daemon Threads provide a non-blocking way to listen for user input during execution.
Navigation