How to Convert Images to ASCII Art with Python
ASCII art converts images into visual representations made of text characters. Python with Pillow lets you build complete image-to-ASCII converters: black and white, ANSI color, high-resolution Unicode characters, and even animations from GIFs.
Core Concept
The process works in three steps:
- Resize the image to the desired column count
- Convert to grayscale (each pixel → value 0-255)
- Map each luminosity value to a character of increasing density
The aspect ratio must be adjusted because text characters are approximately twice as tall as they are wide.
Installation
pip install Pillow
Basic ASCII Converter
from PIL import Image
# Character scale ordered from darkest (dense) to lightest
DENSE_SCALE = '@#S%?*+;:,. '
SIMPLE_SCALE = '@Oo+-. '
def image_to_ascii(image_path, width=120, scale=None, invert=False):
"""
Convert an image to ASCII art.
width: number of character columns
invert: useful for light backgrounds (dark terminal)
"""
scale = scale or DENSE_SCALE
if invert:
scale = scale[::-1]
img = Image.open(image_path).convert('L')
w, h = img.size
# Adjust height for character aspect ratio (≈2:1 h:w)
height = int(h * width / w * 0.45)
img = img.resize((width, height), Image.LANCZOS)
pixels = img.getdata()
n = len(scale) - 1
lines = []
for i in range(height):
row = ''
for j in range(width):
value = pixels[i * width + j]
idx = int(value / 255 * n)
row += scale[idx]
lines.append(row)
return '\n'.join(lines)
# Generate and print in terminal
ascii_art = image_to_ascii("photo.jpg", width=100, invert=True)
print(ascii_art)
# Save to text file
with open("photo_ascii.txt", "w", encoding="utf-8") as f:
f.write(ascii_art)
print("ASCII art saved to photo_ascii.txt")
ANSI Color Output (for Terminals)
from PIL import Image
def image_to_color_ascii(image_path, width=80):
"""
Generate colored ASCII art using ANSI escape codes.
Requires a terminal with 24-bit color support.
"""
img = Image.open(image_path).convert('RGB')
w, h = img.size
height = int(h * width / w * 0.45)
img = img.resize((width, height), Image.LANCZOS)
SCALE = '@#S%?*+;:,. '
output = []
for y in range(height):
line = ''
for x in range(width):
r, g, b = img.getpixel((x, y))
lum = int(0.299 * r + 0.587 * g + 0.114 * b)
idx = int(lum / 255 * (len(SCALE) - 1))
ch = SCALE[idx]
# ANSI 24-bit RGB color code
line += f'\033[38;2;{r};{g};{b}m{ch}'
output.append(line + '\033[0m') # reset color at end of line
return '\n'.join(output)
# Print in terminal with colors
ascii_color = image_to_color_ascii("landscape.jpg", width=120)
print(ascii_color)
Export to Colored HTML
from PIL import Image
import html as html_lib
def image_to_html_ascii(image_path, width=120, output_path="ascii_art.html"):
"""Generate an HTML file with color ASCII art."""
img = Image.open(image_path).convert('RGB')
w, h = img.size
height = int(h * width / w * 0.45)
img = img.resize((width, height), Image.LANCZOS)
SCALE = '@#S%?*+;:,. '
html_lines = [
'<!DOCTYPE html>',
'<html><head><meta charset="utf-8">',
'<style>',
' body { background: #000; margin: 0; padding: 10px; }',
' pre { font-family: "Courier New", monospace; font-size: 8px; line-height: 1.1; }',
'</style>',
'</head><body><pre>',
]
for y in range(height):
line = ''
for x in range(width):
r, g, b = img.getpixel((x, y))
lum = int(0.299 * r + 0.587 * g + 0.114 * b)
idx = int(lum / 255 * (len(SCALE) - 1))
char = html_lib.escape(SCALE[idx])
line += f'<span style="color:rgb({r},{g},{b})">{char}</span>'
html_lines.append(line)
html_lines.extend(['</pre>', '</body></html>'])
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(html_lines))
print(f"HTML generated: {output_path}")
image_to_html_ascii("portrait.jpg", width=150, output_path="portrait_ascii.html")
Unicode Block Characters (Higher Resolution)
from PIL import Image
BLOCKS = ' ░▒▓█'
def pixel_to_braille(pixels_2x4):
"""Convert a 2×4 pixel matrix to a Unicode Braille character."""
braille_map = [0x01, 0x02, 0x04, 0x40, 0x08, 0x10, 0x20, 0x80]
code = 0
for i, pixel in enumerate(pixels_2x4):
if pixel < 128: # dark = active
code |= braille_map[i]
return chr(0x2800 + code)
def image_to_braille(image_path, width=60):
"""Convert an image to Braille Unicode ASCII art (2×4 pixels per char)."""
img = Image.open(image_path).convert('L')
iw, ih = img.size
width_px = width * 2
height_px = int(ih * width_px / iw)
height_px = (height_px // 4) * 4 # round to multiple of 4
img = img.resize((width_px, height_px), Image.LANCZOS)
arr = list(img.getdata())
lines = []
for y in range(0, height_px, 4):
line = ''
for x in range(0, width_px, 2):
block = [
arr[(y + dy) * width_px + (x + dx)]
for dy in range(4)
for dx in range(2)
]
line += pixel_to_braille(block)
lines.append(line)
return '\n'.join(lines)
braille = image_to_braille("face.png", width=80)
print(braille)
Animated ASCII from GIF
from PIL import Image
import os
import time
def gif_to_ascii_animation(gif_path, width=80, fps=None, loops=2):
"""Convert an animated GIF to terminal ASCII animation."""
gif = Image.open(gif_path)
SCALE = '@#S%?*+;:,. '
frames = []
try:
while True:
frame = gif.copy().convert('L')
fw, fh = frame.size
height = int(fh * width / fw * 0.45)
frame = frame.resize((width, height), Image.LANCZOS)
pixels = list(frame.getdata())
lines = []
for i in range(height):
row = ''
for j in range(width):
idx = int(pixels[i * width + j] / 255 * (len(SCALE) - 1))
row += SCALE[idx]
lines.append(row)
duration = gif.info.get('duration', 100) / 1000.0
frames.append(('\n'.join(lines), duration))
gif.seek(gif.tell() + 1)
except EOFError:
pass
print(f"GIF: {len(frames)} frames loaded")
for _ in range(loops):
for frame_text, duration in frames:
os.system('cls' if os.name == 'nt' else 'clear')
print(frame_text)
time.sleep(fps and 1.0/fps or duration)
gif_to_ascii_animation("animation.gif", width=100, loops=3)
Edge-Detection ASCII
from PIL import Image, ImageFilter
import numpy as np
def image_to_edge_ascii(image_path, width=100, threshold=50):
"""Combine edge detection with luminosity for more detailed ASCII art."""
img = Image.open(image_path).convert('L')
w, h = img.size
height = int(h * width / w * 0.45)
img = img.resize((width, height), Image.LANCZOS)
edges = img.filter(ImageFilter.FIND_EDGES)
orig_pixels = np.array(img)
edge_pixels = np.array(edges)
SCALE = '@#S%?*+;:,. '
lines = []
for y in range(height):
row = ''
for x in range(width):
lum = orig_pixels[y, x]
edge = edge_pixels[y, x]
if edge > threshold:
row += '+'
else:
idx = int(lum / 255 * (len(SCALE) - 1))
row += SCALE[idx]
lines.append(row)
return '\n'.join(lines)
result = image_to_edge_ascii("architecture.jpg", width=120)
print(result)
Character Scale Options
# For dark image on light background (printing on paper)
INVERTED_SCALE = ' .,:;+*?%S#@'
# Unicode block scale
BLOCK_SCALE = ' ·:;i|=+xX$&#@'
# Letters only (more artistic)
LETTERS_SCALE = 'wmqpdbkhaoeuiYXZO0QCJUYXZoahkbdpqwm'
DENSE_SCALE = '@#S%?*+;:,. '
def configurable_convert(path, width=80, scale=None, invert=False):
scale = scale or DENSE_SCALE
if invert:
scale = scale[::-1]
img = Image.open(path).convert('L')
w, h = img.size
height = int(h * width / w * 0.45)
img = img.resize((width, height), Image.LANCZOS)
n = len(scale) - 1
pixels = list(img.getdata())
lines = []
for i in range(height):
row = ''.join(scale[int(pixels[i*width+j]/255*n)] for j in range(width))
lines.append(row)
return '\n'.join(lines)
print(configurable_convert("logo.png", width=60, scale=LETTERS_SCALE))
Additional Resource
For converting images between JPG, PNG, WebP, GIF and other formats without any coding, use KaijuConverter — free and no registration required.
Related conversions
Most teams that read this guide convert images in one of these directions: