Published on

ASCII Image and Video Renderer

Authors

I recently made a project where with Python that converts images and videos into ASCII art directly in your terminal. So I decided to write about it; I’ll explain what ASCII art is, how images work, and walk you through my code step by step.

What is ASCII Art?

ASCII art is a design technique that uses characters from the ASCII (American Standard Code for Information Interchange) set to create images. In simpler terms, it transforms pictures into a collection of letters, numbers, and symbols. This art form became popular when computers had limited graphic capabilities, and people used text to create pictures.

For example, an image of a cat might look something like this in ASCII art:

 /\_/\  
( o.o ) 
 > ^ <

How Do Images Work?

Images are made up of tiny dots called pixels. Each pixel has a specific color, and together, these pixels create the overall image you see on the screen. When we convert images to ASCII art, we replace each pixel with a character that represents its brightness level. Darker pixels might be represented by characters like #, while lighter pixels could be represented by characters like . or (space).

Building the ASCII Art Converter

Setting Up the Project

First, lets import the necessary libraries:

import os
import sys
import time
from PIL import Image
import cv2
from blessed import Terminal
from colorama import init

term = Terminal()
init()
  • PIL (Pillow): A library for opening, manipulating, and saving image files.
  • cv2 (OpenCV): A library for working with video files.
  • blessed and colorama: Libraries that help with handling terminal display and colors.

Defining Constants

Next, lets define some constants that would be used throughout the code:

DEFAULT_CHARSET = "@#$%&*()0!=-.,"
DEFAULT_COLOR = False
IMG_SIZE = (0, 0)
imgs = []
  • DEFAULT_CHARSET: This is the set of characters that would be used to represent the different brightness levels in the ASCII art.
  • DEFAULT_COLOR: A flag to determine whether to use color in the ASCII art.
  • IMG_SIZE: This will hold the size of the terminal, which we’ll set later.

Getting the Terminal Size

We need to know the size of the terminal to ensure the ASCII art fits nicely. Here’s the function we’ll use to get the terminal size:

def get_terminal_size():
    return os.get_terminal_size()

Converting Pixels to ASCII Characters

This part of the code converts a pixel’s color value into a character from the DEFAULT_CHARSET based on its brightness:

def pix_to_code(i, img):
    x = i % IMG_SIZE[0]
    y = i // IMG_SIZE[0]
    code = img.getpixel((x, y))
    return f"\033[38;2;{code[0]};{code[1]};{code[2]}m"

The pix_to_code function converts a pixel at index i in an image img to an ANSI escape code that changes the terminal's text color to match the pixel's RGB color. It calculates the pixel's (x, y) position in the image, retrieves its color using getpixel, and formats it into a string that sets the terminal's foreground color using the RGB values.

Printing the Image as ASCII Art

Now to the main function for printing the ASCII art; print_img, where we resize the image and convert it to ASCII:

def print_img(image, is_colored):
    img = image.resize(IMG_SIZE, Image.Resampling.LANCZOS)
    gimg = img.convert('L')
    img = img.quantize(colors=32).convert('RGB')
    pixels = list(gimg.getdata())

    output_string = ""
    last_color = None

    for i, pixel in enumerate(pixels):
        if i % IMG_SIZE[0] == 0 and i != 0:
            output_string += "\n"
        
        ind = int(pixel / 255 * (len(DEFAULT_CHARSET) - 1))
        char = DEFAULT_CHARSET[ind]

        if is_colored:
            color = pix_to_code(i, img)
            if color != last_color:
                output_string += "\033[0;39m" + color + char
                last_color = color
            else:
                output_string += char
        else:
            output_string += char

    with term.hidden_cursor():
        sys.stdout.write(term.home + output_string)
        sys.stdout.flush()

It starts by resizing the input image to a fixed size. Then, it creates a grayscale version of the image, which is used to map pixel brightness to ASCII characters. The original image is also quantized to 32 colors and converted back to RGB format to ensure consistency when generating color output.

For each pixel in the grayscale image, the pixel brightness is scaled to a character from the predefined character set (DEFAULT_CHARSET), where darker pixels are mapped to denser characters, and lighter pixels to lighter ones. The function then checks if the color mode is enabled (is_colored). If true, it retrieves the RGB color of the corresponding pixel from the quantized image using the pix_to_code function, which returns an ANSI escape code for the RGB values. To avoid redundant color codes, the function only applies the color change when the pixel color differs from the previous one. If the color changes, it resets the text color before applying the new one.

As the function processes the image, it constructs an output string by appending characters, row by row, corresponding to each pixel. Line breaks are inserted to maintain the aspect ratio of the image. Finally, the constructed ASCII art is printed directly to the terminal.

Handling Images and Videos

The project can handle both images and videos. Here’s how image files are opened:

def open_image(file):
    try:
        return Image.open(file)
    except Exception as e:
        print(f"Error: Unable to open image file {file}. {e}")
        return None

And for videos, it opens a video file, reads its frames, converts each frame from BGR to RGB, transforms them into Pillow image objects, and stores them in a list.

def open_video(file):
    vid = cv2.VideoCapture(file)
    if not vid.isOpened():
        print(f"Error: Unable to open video file {file}.")
        return

    while True:
        ret, img = vid.read()
        if not ret:
            break
        conv = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        imgs.append(Image.fromarray(conv))

    vid.release()

Main Function

The main function ties everything together. It checks for file input, processes images or videos accordingly, allows for color and character set customization via command-line arguments, and displays the result at a 30fps-like rate for videos.

def main():
    global IMG_SIZE, DEFAULT_COLOR, DEFAULT_CHARSET

    IMG_SIZE = get_terminal_size()
    if len(sys.argv) < 2:
        print("No file provided. Please provide an image or video file as an argument.")
        return

    file_path = sys.argv[1]

    if file_path.split('.')[-1] in ["mp4", "avi", "mov", "gif"]:
        open_video(file_path)
    else:
        img = open_image(file_path)
        if img:
            imgs.append(img)

    for arg in sys.argv[2:]:
        match arg:
            case "-c":
                DEFAULT_COLOR = True
            case "-f":
                DEFAULT_CHARSET = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,^`'."
    
    for img in imgs:
        print_img(img, DEFAULT_COLOR)
        time.sleep(0.033)  # Approx. 30fps

    print("\033[0;39m")

if __name__ == "__main__":
    main()

Example Usage

To run the script, you need to provide the image or video file as an argument in the terminal. Here’s an example command:

python ascii_art_converter.py your_image.png -c

This command will convert your_image.png into ASCII art and display it in color.

Visual Example

Here is an example of the output you can expect from the program:

Original Image:
Actual Image

ASCII Art Representation:
ASCII Art

Conclusion

Creating this project was a fun way to explore programming. It allowed me to practice my Python skills while learning about image and video processing. I hope this blog post inspires you to try and recreate yours, or explore similar projects!

Feel free to check out my code on GitHub and try it out for yourself!it out yourself