Cómo convertir imágenes a ASCII art con Python
El ASCII art convierte imágenes en representaciones visuales formadas por caracteres de texto. Python con Pillow permite crear converters de imagen a ASCII completos: en blanco y negro, con color ANSI, con caracteres Unicode de mayor resolución y hasta animaciones desde GIFs.
Concepto básico
El proceso funciona en tres pasos:
- Redimensionar la imagen al número de columnas deseado
- Convertir a escala de grises (cada píxel → valor 0-255)
- Mapear cada valor de luminosidad a un carácter de densidad creciente
La relación de aspecto debe ajustarse porque los caracteres de texto tienen aproximadamente el doble de alto que de ancho.
Instalación
pip install Pillow
Conversor básico a ASCII
from PIL import Image
# Escala de caracteres ordenados de más oscuro (denso) a más claro
ESCALA_DENSA = '@#S%?*+;:,. '
ESCALA_SIMPLE = '@Oo+-. '
def imagen_a_ascii(ruta_imagen, ancho=120, escala=None, invertir=False):
"""
Convierte una imagen a ASCII art.
ancho: número de columnas de caracteres
invertir: útil para fondos blancos (terminal oscura)
"""
escala = escala or ESCALA_DENSA
if invertir:
escala = escala[::-1]
# Abrir y convertir a escala de grises
img = Image.open(ruta_imagen).convert('L')
w, h = img.size
# Calcular alto ajustado por relación de aspecto de caracteres (≈2:1 h:w)
alto = int(h * ancho / w * 0.45)
img = img.resize((ancho, alto), Image.LANCZOS)
# Convertir píxeles a caracteres
pixeles = img.getdata()
n = len(escala) - 1
lineas = []
for i in range(alto):
fila = ''
for j in range(ancho):
valor = pixeles[i * ancho + j]
idx = int(valor / 255 * n)
fila += escala[idx]
lineas.append(fila)
return '\n'.join(lineas)
# Generar y mostrar en terminal
ascii_art = imagen_a_ascii("foto.jpg", ancho=100, invertir=True)
print(ascii_art)
# Guardar en archivo de texto
with open("foto_ascii.txt", "w", encoding="utf-8") as f:
f.write(ascii_art)
print("ASCII art guardado en foto_ascii.txt")
Con color ANSI (para terminales)
from PIL import Image
def imagen_a_ascii_color(ruta_imagen, ancho=80):
"""
Genera ASCII art con colores ANSI (para terminales que soporten color).
Usa códigos de escape ANSI 256 colores.
"""
img = Image.open(ruta_imagen).convert('RGB')
w, h = img.size
alto = int(h * ancho / w * 0.45)
img = img.resize((ancho, alto), Image.LANCZOS)
ESCALA = '@#S%?*+;:,. '
resultado = []
for y in range(alto):
linea = ''
for x in range(ancho):
r, g, b = img.getpixel((x, y))
# Calcular luminosidad para elegir el carácter
lum = int(0.299 * r + 0.587 * g + 0.114 * b)
idx = int(lum / 255 * (len(ESCALA) - 1))
car = ESCALA[idx]
# Código de color ANSI RGB (24-bit)
linea += f'\033[38;2;{r};{g};{b}m{car}'
resultado.append(linea + '\033[0m') # resetear color al final de línea
return '\n'.join(resultado)
# Mostrar en terminal con colores
ascii_color = imagen_a_ascii_color("paisaje.jpg", ancho=120)
print(ascii_color)
Exportar a HTML con color
from PIL import Image
import html
def imagen_a_html_ascii(ruta_imagen, ancho=120, ruta_salida="ascii_art.html"):
"""Genera un archivo HTML con ASCII art coloreado."""
img = Image.open(ruta_imagen).convert('RGB')
w, h = img.size
alto = int(h * ancho / w * 0.45)
img = img.resize((ancho, alto), Image.LANCZOS)
ESCALA = '@#S%?*+;:,. '
html_lineas = [
'<!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; }',
' span { display: inline; }',
'</style>',
'</head><body><pre>',
]
for y in range(alto):
linea_html = ''
for x in range(ancho):
r, g, b = img.getpixel((x, y))
lum = int(0.299 * r + 0.587 * g + 0.114 * b)
idx = int(lum / 255 * (len(ESCALA) - 1))
car = html.escape(ESCALA[idx])
linea_html += f'<span style="color:rgb({r},{g},{b})">{car}</span>'
html_lineas.append(linea_html)
html_lineas.extend(['</pre>', '</body></html>'])
with open(ruta_salida, 'w', encoding='utf-8') as f:
f.write('\n'.join(html_lineas))
print(f"HTML generado: {ruta_salida}")
imagen_a_html_ascii("retrato.jpg", ancho=150, ruta_salida="retrato_ascii.html")
Usando caracteres Unicode de alta resolución
Los caracteres Unicode de bloques permiten mayor resolución que los ASCII básicos:
from PIL import Image
# Bloques Unicode: resolución efectiva 2x más alta
BLOQUES = ' ░▒▓█'
# Braille Unicode: resolución 2×4 por carácter (muy detallado)
def pixel_a_braille(pixeles_2x4):
"""Convierte una matriz 2×4 de píxeles a un carácter Braille."""
# Mapa de bits Braille (posición → bit Braille)
mapa = [0x01, 0x02, 0x04, 0x40, 0x08, 0x10, 0x20, 0x80]
codigo = 0
for i, pixel in enumerate(pixeles_2x4):
if pixel < 128: # oscuro = activo
codigo |= mapa[i]
return chr(0x2800 + codigo)
def imagen_a_braille(ruta_imagen, ancho=60):
"""Convierte una imagen a ASCII art usando caracteres Braille."""
img = Image.open(ruta_imagen).convert('L')
w, h = img.size
# Cada carácter Braille representa 2×4 píxeles
ancho_px = ancho * 2
alto_px = int(h * ancho_px / w)
# Ajustar al múltiplo de 4
alto_px = (alto_px // 4) * 4
img = img.resize((ancho_px, alto_px), Image.LANCZOS)
arr = list(img.getdata())
lineas = []
for y in range(0, alto_px, 4):
linea = ''
for x in range(0, ancho_px, 2):
# Extraer bloque 2×4
bloque = [
arr[(y + dy) * ancho_px + (x + dx)]
for dy in range(4)
for dx in range(2)
]
linea += pixel_a_braille(bloque)
lineas.append(linea)
return '\n'.join(lineas)
braille_art = imagen_a_braille("cara.png", ancho=80)
print(braille_art)
Animación ASCII desde GIF
from PIL import Image
import os
import time
def gif_a_ascii_animado(ruta_gif, ancho=80, fps=None, loops=2):
"""
Convierte un GIF animado en animación ASCII en la terminal.
"""
gif = Image.open(ruta_gif)
ESCALA = '@#S%?*+;:,. '
frames = []
try:
while True:
frame = gif.copy().convert('L')
w, h = frame.size
alto = int(h * ancho / w * 0.45)
frame = frame.resize((ancho, alto), Image.LANCZOS)
datos = list(frame.getdata())
lineas = []
for i in range(alto):
fila = ''
for j in range(ancho):
idx = int(datos[i * ancho + j] / 255 * (len(ESCALA) - 1))
fila += ESCALA[idx]
lineas.append(fila)
frames.append('\n'.join(lineas))
# Duración del frame
try:
duracion = gif.info.get('duration', 100) / 1000.0
except Exception:
duracion = 0.1
frames[-1] = (frames[-1], duracion)
gif.seek(gif.tell() + 1)
except EOFError:
pass
print(f"GIF: {len(frames)} frames cargados")
# Reproducir en bucle
for _ in range(loops):
for frame_text, duracion in frames:
# Limpiar terminal
os.system('cls' if os.name == 'nt' else 'clear')
print(frame_text)
time.sleep(fps and 1.0/fps or duracion)
gif_a_ascii_animado("animacion.gif", ancho=100, loops=3)
Conversor con detección de bordes
from PIL import Image, ImageFilter
import numpy as np
def imagen_a_ascii_bordes(ruta_imagen, ancho=100, umbral=50):
"""
Combina detección de bordes con luminosidad para ASCII más detallado.
"""
img = Image.open(ruta_imagen).convert('L')
w, h = img.size
alto = int(h * ancho / w * 0.45)
img = img.resize((ancho, alto), Image.LANCZOS)
# Detectar bordes
bordes = img.filter(ImageFilter.FIND_EDGES)
pixeles_orig = np.array(img)
pixeles_borde = np.array(bordes)
ESCALA = '@#S%?*+;:,. '
BORDES_H = '─━═'
BORDES_V = '│┃║'
ESQUINAS = '┼╋╬'
lineas = []
for y in range(alto):
fila = ''
for x in range(ancho):
lum = pixeles_orig[y, x]
borde = pixeles_borde[y, x]
if borde > umbral:
# Pixel de borde — usar carácter de línea
fila += '+'
else:
# Pixel normal — mapear por luminosidad
idx = int(lum / 255 * (len(ESCALA) - 1))
fila += ESCALA[idx]
lineas.append(fila)
return '\n'.join(lineas)
resultado = imagen_a_ascii_bordes("arquitectura.jpg", ancho=120)
print(resultado)
Parámetros y personalizaciones
# Escala para imagen oscura sobre fondo claro (imprimir en papel)
ESCALA_INVERT = ' .,:;+*?%S#@'
# Escala de bloques Unicode
ESCALA_BLOQUES = ' ·:;i|=+xX$&#@'
# Solo letras (más artístico)
ESCALA_LETRAS = 'wmqpdbkhaoeuiYXZO0QCJUYXZoahkbdpqwm'
def convertir_con_parametros(ruta, ancho=80, escala=ESCALA_DENSA,
invertir=False, formato='txt'):
"""Conversor configurable."""
img = Image.open(ruta).convert('L')
w, h = img.size
alto = int(h * ancho / w * 0.45)
img = img.resize((ancho, alto), Image.LANCZOS)
if invertir:
escala = escala[::-1]
n = len(escala) - 1
datos = list(img.getdata())
lineas = []
for i in range(alto):
fila = ''.join(escala[int(datos[i*ancho+j]/255*n)] for j in range(ancho))
lineas.append(fila)
return '\n'.join(lineas)
ESCALA_DENSA = '@#S%?*+;:,. '
print(convertir_con_parametros("logo.png", ancho=60, escala=ESCALA_LETRAS))
Recurso adicional
Para convertir imágenes entre JPG, PNG, WebP, GIF y más formatos sin necesidad de programar, usa KaijuConverter — gratis y sin registro.
Conversiones relacionadas
Conversiones frecuentes del catálogo: