Crea arte ASCII a partir de videos con Lua y FFmpeg
El arte ASCII ha sido durante mucho tiempo una manera creativa de representar imágenes usando caracteres de texto. Con herramientas modernas como los scripts de Lua y el potente framework multimedia FFmpeg, puedes transformar fotogramas de video en arte ASCII dinámico y dar un toque visual único a tus proyectos multimedia.
Introducción al arte ASCII y sus aplicaciones creativas
El arte ASCII usa caracteres de texto estándar para representar imágenes visualmente. Esta técnica es ideal para proyectos de programación creativa, juegos de estilo retro, aplicaciones de terminal y efectos de video originales. Al convertir fotogramas de video en arte ASCII, puedes crear experiencias visuales atractivas que destacan en el arte digital, el diseño web y las instalaciones interactivas.
Configurar el entorno de Lua y FFmpeg
Antes de entrar en los scripts, asegúrate de tener instalados Lua, LuaRocks (el gestor de paquetes de Lua) y FFmpeg. Estas herramientas son la base de nuestro proceso de conversión de video a ASCII.
Para Ubuntu/Debian:
sudo apt-get update
sudo apt-get install lua5.4 luarocks ffmpeg
Para macOS:
brew install lua luarocks ffmpeg
Para Windows:
Instala con los instaladores oficiales o con gestores de paquetes como Chocolatey:
choco install lua ffmpeg
# Luarocks might require manual setup or alternative package managers on windows.
# Refer to the official luarocks documentation for Windows installation instructions.
Verifica tus instalaciones comprobando sus versiones en la terminal:
lua -v
luarocks --version
ffmpeg -version
Extraer fotogramas de video con FFmpeg
Para convertir fotogramas de video en arte ASCII, primero necesitas extraer los fotogramas individuales de tu video. FFmpeg lo hace sencillo. Usar un escalado adecuado garantiza dimensiones de salida consistentes para el proceso de conversión a ASCII.
# Create a directory for the frames if it doesn't exist
mkdir -p frames
# Extract frames at 10 fps, scaled to 640px width (height adjusted automatically)
ffmpeg -i input.mp4 -vf "fps=10,scale=640:-1" frames/frame_%04d.png
Este comando extrae fotogramas de input.mp4 a una tasa de 10 fotogramas por segundo. La opción -vf
aplica un filtro de video: fps=10 establece la tasa de fotogramas y scale=640:-1 redimensiona cada fotograma a 640
píxeles de ancho manteniendo la relación de aspecto original. Los fotogramas se guardan como
archivos PNG numerados de forma secuencial (p. ej., frame_0001.png, frame_0002.png) en el directorio frames.
Convertir fotogramas en arte ASCII con scripts de Lua
Ahora, creemos un script de Lua (ascii.lua) que tome un archivo de imagen, lo procese con FFmpeg para
obtener los datos de píxeles y convierta esos datos en caracteres ASCII. Este enfoque evita las
bibliotecas externas de procesamiento de imágenes de Lua y se apoya únicamente en Lua y FFmpeg para
lograr mayor fiabilidad y menos dependencias. Los ejemplos de subprocesos que siguen están pensados
para un shell POSIX en Linux, macOS o WSL.
-- ascii.lua
-- ASCII art character set from dense to sparse
local chars = {"@", "#", "S", "%", "?", "*", "+", ";", ":", ",", ".", " "}
-- Alternative simpler set: local chars = {"@", "#", "$", "=", "*", "+", ":", "-", ".", " "}
-- Convert pixel brightness (grayscale) to an ASCII character
local function pixelToChar(r, g, b)
-- Calculate luminance using the standard formula for perceived brightness
local brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255
-- Map brightness (0.0 to 1.0) to an index in the chars table
-- math.max ensures index is at least 1, math.min ensures it doesn't exceed table length
local index = math.floor(brightness * (#chars - 1)) + 1
index = math.max(1, math.min(index, #chars)) -- Clamp index to valid range
return chars[index]
end
-- Convert an image file to an ASCII art string using FFmpeg and ffprobe
local function shellQuote(value)
return "'" .. value:gsub("'", "'\\''") .. "'"
end
local function imageToAscii(imagePath, targetWidth)
targetWidth = targetWidth or 80 -- Default ASCII width if not provided
-- 1. Get image dimensions using ffprobe
-- Use -v error to suppress verbose output, get width,height in csv format
local ffprobeCmd = string.format("ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 -i %s", shellQuote(imagePath))
local dims
-- Use pcall for safe execution of external command
local success, err = pcall(function()
-- Use "r" mode for reading text output
local f = io.popen(ffprobeCmd, "r")
if not f then error("Failed to execute ffprobe command.") end
dims = f:read("*l") -- Read the first line (e.g., "1920x1080")
f:close()
if not dims or dims == "" then error("ffprobe returned empty dimensions.") end
end)
if not success then
return nil, "Error getting image dimensions: " .. (err or "Unknown ffprobe error")
end
-- Parse dimensions
local originalWidth, originalHeight = dims:match("(%d+)x(%d+)")
if not originalWidth then
return nil, "Could not parse dimensions from ffprobe output: '" .. dims .. "'"
end
originalWidth, originalHeight = tonumber(originalWidth), tonumber(originalHeight)
-- 2. Calculate target height maintaining aspect ratio
-- Adjust height by ~0.5 factor because terminal characters are often taller than wide
local targetHeight = math.floor(originalHeight * (targetWidth / originalWidth * 0.5))
-- Ensure targetHeight is at least 1
targetHeight = math.max(1, targetHeight)
-- 3. Use FFmpeg to get raw RGB pixel data, scaled to target dimensions
-- -f rawvideo outputs raw pixel data, -pix_fmt rgb24 specifies 24-bit RGB
local ffmpegCmd = string.format("ffmpeg -nostdin -v error -i %s -vf scale=%d:%d -frames:v 1 -f rawvideo -pix_fmt rgb24 -",
shellQuote(imagePath), targetWidth, targetHeight)
local rawPixelData
success, err = pcall(function()
-- POSIX pipes opened with "r" preserve binary bytes.
local f = io.popen(ffmpegCmd, "r")
if not f then error("Failed to execute ffmpeg command.") end
rawPixelData = f:read("*a") -- Read all binary data
f:close()
if not rawPixelData or #rawPixelData == 0 then error("ffmpeg returned empty pixel data.") end
end)
if not success then
return nil, "Error processing image with ffmpeg: " .. (err or "Unknown ffmpeg error")
end
-- 4. Convert raw pixel data (RGB triplets) to ASCII string
local asciiArt = ""
local expectedDataSize = targetWidth * targetHeight * 3 -- 3 bytes per pixel (R, G, B)
if #rawPixelData < expectedDataSize then
-- Warn about incomplete data but proceed if possible
print(string.format("Warning: Incomplete pixel data received. Expected %d bytes, got %d.", expectedDataSize, #rawPixelData))
end
local pos = 1
for y = 1, targetHeight do
for x = 1, targetWidth do
-- Check if enough data remains for one pixel (3 bytes)
if pos + 2 <= #rawPixelData then
local r = string.byte(rawPixelData, pos)
local g = string.byte(rawPixelData, pos + 1)
local b = string.byte(rawPixelData, pos + 2)
asciiArt = asciiArt .. pixelToChar(r, g, b)
pos = pos + 3
else
-- Not enough data for a full pixel, add padding or break
asciiArt = asciiArt .. " " -- Add a space as padding
if x < targetWidth then -- Fill rest of the row with spaces if data ended mid-row
asciiArt = asciiArt .. string.rep(" ", targetWidth - x)
end
goto next_row -- Jump out of inner loop if data ends prematurely
end
end
::next_row:: -- Label for goto statement
asciiArt = asciiArt .. "\n" -- Newline after each row, including truncated rows
end
return asciiArt, nil -- Return result and nil for error
end
-- Main execution block: parses command-line arguments and calls imageToAscii
local function main()
if not arg[1] then
print("Usage: lua ascii.lua <image_path> [output_file]")
os.exit(1)
end
local imagePath = arg[1]
local outputFile = arg[2]
-- Call the conversion function
local result, err = imageToAscii(imagePath)
-- Handle potential errors during conversion
if err then
io.stderr:write("Error: " .. err .. "\n")
os.exit(1)
end
-- Print the resulting ASCII art to the console
print(result)
-- Optionally save the result to a specified file
if outputFile then
local file, openErr = io.open(outputFile, "w")
if file then
file:write(result)
file:close()
print("\nASCII art saved to " .. outputFile)
else
io.stderr:write("\nError: Could not write to file '" .. outputFile .. "': " .. (openErr or "Unknown error") .. "\n")
-- Don't exit here, maybe user only wanted console output
end
end
end
-- Run the main function
main()
Ejecuta el script sobre un único fotograma extraído para probarlo:
lua ascii.lua frames/frame_0001.png
Para guardar la salida de arte ASCII directamente en un archivo de texto:
lua ascii.lua frames/frame_0001.png ascii_frames/frame_0001.txt
Crear un video de arte ASCII
Para convertir toda la secuencia de fotogramas en un video de arte ASCII, necesitas procesar cada fotograma con el script de Lua y luego ensamblar los resultados. Un método habitual es convertir cada archivo de texto ASCII de vuelta en una imagen y después usar FFmpeg para crear un video a partir de esas imágenes. Esto requiere una herramienta adicional como ImageMagick para el paso de conversión de texto a imagen.
Instalar ImageMagick
ImageMagick proporciona la herramienta convert necesaria para renderizar archivos de texto como imágenes.
# Ubuntu/Debian
sudo apt-get install imagemagick
# macOS
brew install imagemagick
# Windows (using chocolatey)
choco install imagemagick.app
Script de procesamiento (bash)
Este script de shell automatiza el proceso: ejecuta el script de Lua sobre todos los fotogramas,
convierte los archivos de texto resultantes en imágenes con ImageMagick y, por último, compila esas
imágenes en un video con FFmpeg. Elige un archivo de fuente monoespaciada legible. La opción
predeterminada de abajo viene del paquete fonts-dejavu-core de Debian/Ubuntu. En macOS, ejecuta el script con
FONT=/System/Library/Fonts/Menlo.ttc ./create_ascii_video.sh.
#!/bin/bash
# Set variables
FRAME_DIR="frames"
ASCII_TXT_DIR="ascii_frames"
ASCII_IMG_DIR="ascii_images"
OUTPUT_VIDEO="ascii_video.mp4"
FRAME_RATE=10 # Should match the extraction frame rate
FONT="${FONT:-/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf}"
POINT_SIZE=10
IMG_BACKGROUND="black"
IMG_FOREGROUND="white"
# --- safety checks ---
if [ ! -r "$FONT" ]; then
echo "Error: set FONT to a readable monospaced font file."
exit 1
fi
# Check if Lua script exists
if [ ! -f "ascii.lua" ]; then
echo "Error: ascii.lua script not found in the current directory."
exit 1
fi
# Check if frame directory exists and is not empty
if [ ! -d "$FRAME_DIR" ] || [ -z "$(ls -A $FRAME_DIR/*.png 2>/dev/null)" ]; then
echo "Error: '$FRAME_DIR' directory not found or contains no PNG frames."
echo "Please run the FFmpeg frame extraction command first."
exit 1
fi
# Check if ImageMagick's convert command is available
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick 'convert' command not found. Please install ImageMagick."
exit 1
fi
# Check if FFmpeg command is available
if ! command -v ffmpeg &> /dev/null; then
echo "Error: 'ffmpeg' command not found. Please install FFmpeg."
exit 1
fi
# --- processing Steps ---
echo "Starting ASCII art generation process..."
# Create output directories
mkdir -p "$ASCII_TXT_DIR"
mkdir -p "$ASCII_IMG_DIR"
# Count total frames for progress reporting
FRAME_FILES=("$FRAME_DIR"/frame_*.png)
FRAME_COUNT=${#FRAME_FILES[@]}
CURRENT_FRAME=0
echo "Step 1: Processing $FRAME_COUNT frames into ASCII text..."
# Process all frames into text files
for frame_png in "${FRAME_FILES[@]}"; do
CURRENT_FRAME=$((CURRENT_FRAME + 1))
base_name=$(basename "$frame_png" .png)
output_txt="$ASCII_TXT_DIR/${base_name}.txt"
# Lua writes the output file; suppress its duplicate console preview.
if ! lua ascii.lua "$frame_png" "$output_txt" >/dev/null; then
echo "Error processing frame $frame_png with Lua script. Aborting."
exit 1
fi
# Simple progress indicator
printf "Processing frame %d/%d (%.0f%%)\r" "$CURRENT_FRAME" "$FRAME_COUNT" $(echo "scale=2; 100 * $CURRENT_FRAME / $FRAME_COUNT" | bc)
done
echo -e "\nASCII text generation complete."
echo "Step 2: Converting ASCII text files to images..."
# Convert ASCII text frames to PNG images using ImageMagick's convert tool
# Render the preformatted grid without word wrapping, on a fixed even-sized canvas.
TXT_FILES=("$ASCII_TXT_DIR"/*.txt)
TXT_COUNT=${#TXT_FILES[@]}
CURRENT_TXT=0
for txt_file in "${TXT_FILES[@]}"; do
CURRENT_TXT=$((CURRENT_TXT + 1))
base_name=$(basename "$txt_file" .txt)
output_png="$ASCII_IMG_DIR/${base_name}.png"
# Fit the full grid, including portrait frames, before padding for the video encoder.
if ! convert -background "$IMG_BACKGROUND" -fill "$IMG_FOREGROUND" -font "$FONT" -pointsize "$POINT_SIZE" \
label:@"$txt_file" -resize 640x320 -gravity center -extent 640x320 "$output_png"; then
echo "Error converting $txt_file to image using ImageMagick. Aborting."
exit 1
fi
printf "Converting text file %d/%d\r" "$CURRENT_TXT" "$TXT_COUNT"
done
echo -e "\nImage generation complete."
echo "Step 3: Creating final ASCII video from images..."
# Create video from the generated ASCII images using FFmpeg
# -framerate: input frame rate
# -i: input pattern for images
# -c:v libx264: video codec (h.264 is widely compatible)
# -pix_fmt yuv420p: pixel format for compatibility
# -crf 23: constant rate factor (quality level, lower is better quality, 18-28 is typical)
# -y: overwrite output file without asking
if ! ffmpeg -framerate "$FRAME_RATE" -i "$ASCII_IMG_DIR/frame_%04d.png" \
-c:v libx264 -pix_fmt yuv420p -crf 23 \
-y "$OUTPUT_VIDEO"; then
echo "Error creating video with FFmpeg. Aborting."
exit 1
fi
echo "-------------------------------------"
echo "ASCII video creation successful!"
echo "Output file: $OUTPUT_VIDEO"
echo "-------------------------------------"
exit 0
Guarda este script (p. ej., create_ascii_video.sh), hazlo ejecutable
(chmod +x create_ascii_video.sh) y ejecútalo desde tu terminal: ./create_ascii_video.sh. Este
script incluye comprobaciones de las herramientas y los directorios necesarios, muestra el progreso
y compila el ascii_video.mp4 final.
Ejemplos prácticos y casos de uso
Los videos de arte ASCII generados con Lua y FFmpeg se pueden emplear en diversas aplicaciones creativas:
- Animaciones en la terminal: muestra arte dinámico directamente en la consola.
- Efectos de video retro: añade un aspecto nostálgico o estilizado a tus proyectos de procesamiento de video.
- Instalaciones de arte web: crea experiencias interactivas únicas con texto renderizado mediante HTML/CSS o Canvas.
- Intros y outros de video originales: haz que tu contenido destaque con un estilo visual distintivo.
- Herramientas educativas: demuestra visualmente conceptos de procesamiento de imágenes y video.
- Filtros artísticos: aplica a los videos un filtro único basado en texto para redes sociales, videos musicales o proyectos de arte digital.
Consejos para optimizar la calidad del arte ASCII
Selección del conjunto de caracteres
La elección de los caracteres en la tabla chars dentro de ascii.lua influye mucho en el aspecto
final. Experimenta con distintos conjuntos:
-- Denser character set for potentially more detail and smoother gradients
local chars_detailed = {"$", "@", "B", "%", "8", "&", "W", "M", "#", "*", "o", "a", "h", "k", "b", "d", "p", "q", "w", "m", "Z", "O", "0", "Q", "L", "C", "J", "U", "Y", "X", "z", "c", "v", "u", "n", "x", "r", "j", "f", "t", "/", "\\", "|", "(", ")", "1", "{", "}", "[", "]", "?", "-", "_", "+", "~", "<", ">", "i", "!", "l", "I", ";", ":", ",", "\"", "^", "`", "'", ".", " "}
-- Simpler character set for a cleaner, potentially blockier or higher contrast look
local chars_simple = {"@", "#", "$", "=", "*", "+", ":", "-", ".", " "}
-- Choose one set in your ascii.lua script by assigning it to the 'chars' variable
local chars = chars_simple -- Or chars_detailed
Asegúrate de que los caracteres estén ordenados aproximadamente de más densos visualmente (los más oscuros) a más ligeros (los más claros).
Preprocesamiento de fotogramas
Mejora los fotogramas de entrada antes de la conversión a ASCII usando filtros de FFmpeg durante el paso inicial de extracción para obtener potencialmente mejores resultados:
# Example: increase contrast and apply a subtle sharpening filter before extracting
ffmpeg -i input.mp4 -vf "fps=10,scale=640:-1,eq=contrast=1.3,unsharp=5:5:0.5:5:5:0.0" frames/frame_%04d.png
Ajustar el contraste, el brillo (eq=brightness=0.1), la saturación (eq=saturation=1.5) o aplicar un
enfoque (unsharp) puede mejorar a veces la definición de los rasgos que se traducen en
caracteres ASCII. La clave está en experimentar.
Consideraciones sobre la resolución
- Ancho del ASCII: la variable
targetWidthdel script de Lua (con valor predeterminado 80) determina la cantidad de caracteres por línea. Un valor de 80-120 suele ser adecuado para mostrarlo en la terminal o en visores de texto estándar. Los valores más altos capturan más detalle, pero generan archivos de texto e imágenes más grandes. - Relación de aspecto: el script de Lua incluye un ajuste básico (
* 0.5) detargetHeightpara tener en cuenta las proporciones típicas de alto/ancho de los caracteres en las fuentes monoespaciadas. Puede que necesites afinar este factor (p. ej.,* 0.45,* 0.55) según la fuente concreta que uses para mostrarlo o en el pasoconvertde ImageMagick, para evitar una salida estirada o aplastada. - Elección de la fuente: la fuente usada en el comando
convert(especificada mediante-font) y la forma en que se visualiza el video final afectan mucho al resultado. Las fuentes monoespaciadas (como Courier, Monaco, Consolas, Fira Code, DejaVu Sans Mono) son esenciales para una alineación correcta, ya que cada carácter ocupa el mismo ancho.
Consideraciones de rendimiento
Procesar video, especialmente fotograma a fotograma con llamadas a scripts externos, puede consumir muchos recursos:
- Tasa de fotogramas: reducir la tasa de fotogramas (p. ej., 5-10 FPS mediante
fps=5en el comando inicial de FFmpeg y-framerate 5en el final) reduce notablemente la cantidad de fotogramas que hay que procesar. Suele ser suficiente para el efecto de arte ASCII y recorta drásticamente el tiempo de procesamiento. - Resolución: mantén un valor razonable de
scaleen la extracción inicial de FFmpeg y detargetWidthen el script de Lua (p. ej., 640px de ancho para la extracción, 80-120 caracteres para el ancho del ASCII). Las resoluciones más altas aumentan exponencialmente el tiempo de procesamiento y el tamaño de los datos en cada paso. - Procesamiento en paralelo: en sistemas multinúcleo, podrías modificar el script de shell para
procesar fotogramas en paralelo (p. ej., con
xargs -Poparallel), lo que podría acelerar notablemente la ejecución del script de Lua y los pasos de conversión de ImageMagick. - Uso de memoria: aunque este enfoque basado en scripts consume menos memoria que cargar videos enteros en memoria, manejar muchos archivos de imagen intermedios de gran tamaño puede ocupar bastante espacio en disco. Asegúrate de tener almacenamiento suficiente.
Alternativas y mejoras modernas
Aunque este enfoque con Lua y FFmpeg es eficaz y didáctico, considera estas alternativas para otras necesidades o para obtener potencialmente mejor rendimiento:
- Bibliotecas y herramientas dedicadas: muchos lenguajes de programación tienen bibliotecas
específicas para generar arte ASCII (p. ej.,
pyfigletde Python para banners de texto, la herramienta de línea de comandosjp2apara JPEG, o varias bibliotecas de JavaScript para uso web). Algunas pueden ofrecer algoritmos más sofisticados o capacidades de procesamiento de video directo. - Aceleración por GPU: si tienes una GPU NVIDIA compatible y una versión de FFmpeg compilada con
soporte para CUDA, puedes aprovechar la aceleración por hardware para extraer y escalar fotogramas
más rápido e incluso, potencialmente, para el paso final de encoding del video
(
-c:v h264_nvenc). Esto requiere opciones y una configuración específicas de FFmpeg. - WebAssembly (Wasm): para crear efectos de arte ASCII interactivos en el navegador a partir de
streams de video o de subidas sin procesamiento en el servidor, compilar a WebAssembly código de
C/C++/Rust que realice el análisis de imágenes y el mapeo de caracteres es una opción potente.
Bibliotecas como
ffmpeg.wasmincluso permiten ejecutar FFmpeg directamente en el navegador. - Filtros directos de FFmpeg: el propio FFmpeg tiene algunos filtros poco conocidos o experimentales que pueden intentar un renderizado basado en texto, aunque por lo general son menos flexibles que un script a medida.
Potenciar el procesamiento de video con scripts de Lua
La fuerza de Lua está en su simplicidad, velocidad y facilidad de integración como lenguaje de scripting. Combinar scripts de Lua con una herramienta potente como FFmpeg permite automatizar de forma flexible y eficiente tareas multimedia complejas que van mucho más allá del arte ASCII. Puedes extender este concepto para:
- Procesar por lotes directorios enteros de videos con lógica personalizada.
- Crear cadenas de filtros complejas de FFmpeg controladas dinámicamente por la lógica de Lua según las propiedades del video o datos externos.
- Generar superposiciones o efectos de video dinámicos a partir del análisis de audio realizado con FFmpeg/ffprobe.
- Construir herramientas de línea de comandos interactivas para flujos de trabajo especializados de manipulación de video.
Este método ofrece una forma práctica de explorar el procesamiento de video y la programación creativa con herramientas accesibles. Para flujos de trabajo de video robustos, escalables y de nivel profesional, incluidos encoding complejo, transformaciones y análisis de contenido a gran escala, los servicios en la nube dedicados que se apoyan en tecnologías como FFmpeg ofrecen ventajas importantes en rendimiento, fiabilidad y funcionalidades. El servicio de Encoding de video de Transloadit, por ejemplo, utiliza FFmpeg intensamente a nivel interno para impulsar Robots versátiles como 🤖 /video/encode para una transcodificación eficiente entre distintos formatos y códecs, y 🤖 /video/thumbs para la generación fiable de miniaturas, gestionando la complejidad de los pipelines de procesamiento multimedia.
