Creating ASCII Art GIFs with Python

Introduction

In our previous discussion, we explored creating custom QR codes. Building upon that foundation, this article demonstrates how to transform images into ASCII art representations where characters replace pixel colors, with each character's color matching the original pixel values.

Required Dependencies

For image manipulation, we'll utilize PIL, imageio, and image2gif libraries. OpenCV-Python handles pixel processing operations. NumPy provides support for array operations and mathematical functions. Install these packages:

pip install pillow
pip install imageio
pip install numpy
pip install opencv-python
pip install images2gif

During installation, you might encounter compatibility issues:

  • When importing cv2 fails due to missing DLLs, it's typically caused by version mismatches between Python and OpenCV. Specify the compatible version during installation: pip install opencv-python==3.4.1.15
  • If images2gif fails to import writeGif interface, modify the source code by adding periods after the two 'from' statements in images2gif.py:
from .images2gif import readGif as readGif
from .images2gif import writeGif as writeGif

Understanding Animated GIF Processing

GIF files essentially combine multiple images into a single animated sequence. Creating ASCII art anmiations involves converting each frame into charatcer-based representation and reassembling them.

The process involves five key steps:

  1. Obtain a source GIF file
  2. Decompose the GIF into individual PNG frames
  3. Process each frame into ASCII art format
  4. Recombine processed frames into a new GIF
  5. Save the resulting animated ASCII art

Converting Between GIF and PNG Formats

Extracting Frames from GIF Files

Three approaches can decompose GIF animations:

Method 1 - Using Image Library:

def extract_frames_method1():
    gif_image = Image.open(source_gif_path)
    try:
        frame_index = 0
        while True:
            current_frame = gif_image.tell()
            gif_image.save(output_directory + "/frame_%d.png" % current_frame)
            gif_image.seek(current_frame + 1)
            frame_index += 1
    except EOFError:
        pass

Method 2 - Palette-based Extraction:

def extract_frames_method2():
    try:
        source_image = Image.open(source_gif_path)
    except IOError:
        print("Cannot load", source_gif_path)
        return
        
    frame_count = 0
    palette_data = source_image.getpalette()

    try:
        while True:
            source_image.putpalette(palette_data)
            rgba_frame = source_image.convert('RGBA')
            new_canvas = Image.new("RGBA", source_image.size)
            new_canvas.paste(rgba_frame)
            new_canvas.save(output_directory + "\\frame_%d.png" % frame_count)
            
            frame_count += 1
            source_image.seek(source_image.tell() + 1)

    except EOFError:
        pass

Method 3 - Using imageio:

def extract_frames_method3():
    frame_list = imageio.mimread(source_gif_path)
    for index, frame_data in enumerate(frame_list):
        array_data = np.asarray(frame_data)
        imageio.imwrite(output_directory + "\\frame_%d.png" % index, array_data)

Reconstructing GIF from Individual Frames

Two reconstruction methods:

Method 1:

def create_gif_method1():
    frame_numbers = sorted([int(os.path.splitext(filename)[0]) for filename in os.listdir(frames_directory)])
    frame_images = []
    
    for number in frame_numbers:
        file_path = frames_directory + "/" + str(number) + '.png'
        frame_images.append(imageio.imread(file_path))
    
    imageio.mimsave(output_gif_path, frame_images, 'GIF', duration=0.1)

Method 2:

def create_gif_method2():
    frame_numbers = sorted([int(os.path.splitext(filename)[0]) for filename in os.listdir(frames_directory)])
    image_paths = []
    
    for number in frame_numbers:
        path = frames_directory + "/" + str(number) + '.png'
        image_paths.append(path)
        
    processed_frames = []
    for image_location in image_paths:
        loaded_image = Image.open(image_location)
        rgb_converted = loaded_image.convert("RGB")
        array_format = np.array(rgb_converted)
        processed_frames.append(array_format)
        
    writeGif(output_gif_path, processed_frames, duration=0.1, subRectangles=False)

Direct Memory Processing Approach

To optimize performance, process frames directly in memory rather than saving intermediate files:

def convert_gif_to_ascii(input_gif_path):
    ascii_frames = []
    frame_data = imageio.mimread(input_gif_path)
    
    for frame in frame_data:
        height, width, channels = frame.shape
        output_canvas = frame * 0 + 255
        grayscale_version = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        
        for row in range(0, height, 6):
            for col in range(0, width, 6):
                pixel_value = grayscale_version[row, col]
                blue, green, red, alpha = frame[row, col]
                char_index = int(((len(character_set) - 1) * pixel_value) / 256)
                selected_char = character_set[char_index]
                
                if alpha != 0:
                    cv2.putText(output_canvas, selected_char, (col, row), 
                              cv2.FONT_HERSHEY_COMPLEX, 0.3, 
                              (int(blue), int(green), int(red), int(alpha)))
                
        ascii_frames.append(output_canvas)

    base_filename = os.path.splitext(os.path.basename(input_gif_path))[0]
    result_path = os.getcwd() + '/output/' + base_filename + "_ascii.gif"
    imageio.mimsave(result_path, ascii_frames, 'GIF', duration=0.1)

ASCII Art Animated QR Codes

Building upon standard GIF processing techniques, ASCII art animated QR codes can be generated following similar principles.

Tags: python image-processing OpenCV Pillow gif-manipulation

Posted on Mon, 03 Aug 2026 16:54:25 +0000 by sgoldenb