Last active
December 30, 2023 02:32
-
-
Save jdiaz5513/9218791 to your computer and use it in GitHub Desktop.
Console ASCII Art Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python2 | |
# Requires: PIL, colormath | |
# | |
# Improved algorithm now automatically crops the image and uses much | |
# better color matching | |
from PIL import Image, ImageChops | |
from colormath.color_conversions import convert_color | |
from colormath.color_objects import LabColor | |
from colormath.color_objects import sRGBColor as RGBColor | |
from colormath.color_diff import delta_e_cmc as cmc | |
import argparse | |
import sys | |
ANSI_CODES = ( | |
'\033[00;30m', # black | |
'\033[00;31m', # red | |
'\033[00;32m', # green | |
'\033[00;33m', # yellow | |
'\033[00;34m', # blue | |
'\033[00;35m', # magenta | |
'\033[00;36m', # cyan | |
'\033[00;37m', # gray | |
'\033[01;30m', # dark gray | |
'\033[01;31m', # bright red | |
'\033[01;32m', # bright green | |
'\033[01;33m', # bright yellow | |
'\033[01;34m', # bright blue | |
'\033[01;35m', # bright magenta | |
'\033[01;36m', # bright cyan | |
'\033[01;37m', # white | |
) | |
ANSI_COLORS = ( | |
RGBColor(0, 0, 0), # black | |
RGBColor(205, 0, 0), # red | |
RGBColor(0, 205, 0), # green | |
RGBColor(205, 205, 0), # yellow | |
RGBColor(0, 0, 238), # blue | |
RGBColor(205, 0, 205), # magenta | |
RGBColor(0, 205, 205), # cyan | |
RGBColor(229, 229, 229), # gray | |
RGBColor(127, 127, 127), # dark gray | |
RGBColor(255, 0, 0), # bright red | |
RGBColor(0, 255, 0), # bright green | |
RGBColor(255, 255, 0), # bright yellow | |
RGBColor(92, 92, 255), # bright blue | |
RGBColor(255, 0, 255), # bright magenta | |
RGBColor(0, 255, 255), # bright cyan | |
RGBColor(255, 255, 255), # white | |
) | |
ANSI_RESET = '\033[0m' | |
INFINITY = float('inf') | |
def closest_ansi_color(color): | |
# Look up the closest ANSI color | |
color = RGBColor(*color[:3]) | |
closest_dist = INFINITY | |
closest_color_index = 0 | |
for i, c in enumerate(ANSI_COLORS): | |
d = color_distance(c, color) | |
if d < closest_dist: | |
closest_dist = d | |
closest_color_index = i | |
return ANSI_CODES[closest_color_index] | |
def color_distance(c1, c2): | |
# return a value representing a relative distance between two RGB | |
# color values, weighted for human eye sensitivity | |
cl1 = convert_color(c1, LabColor) | |
cl2 = convert_color(c2, LabColor) | |
return cmc(cl1, cl2, pl=1, pc=1) | |
# return (math.pow((c2[0] - c1[0]) * 0.30, 2) + | |
# math.pow((c2[1] - c1[1]) * 0.49, 2) + | |
# math.pow((c2[2] - c1[2]) * 0.21, 2)) | |
def convert_image(filename, output_file, fill_char='##'): | |
# render an image as ASCII by converting it to RGBA then using the | |
# color lookup table to find the closest colors, then filling with | |
# fill_char | |
# TODO: use a set of fill characters and choose among them based on | |
# color value | |
im = Image.open(filename) | |
if im.mode != 'RGBA': | |
im = im.convert('RGBA') | |
# crop the image | |
bg = Image.new(im.mode, im.size, im.getpixel((0,0))) | |
diff = ImageChops.difference(im, bg) | |
diff = ImageChops.add(diff, diff, 2.0, -100) | |
bbox = diff.getbbox() | |
if bbox: | |
im = im.crop(bbox) | |
w = im.size[0] | |
o = '' | |
last_color = None | |
for i, p in enumerate(im.getdata()): | |
if i % w == 0: | |
o += '\n' | |
if im.mode == 'RGBA' and p[3] == 0: | |
o += ' ' * len(fill_char) | |
else: | |
c = closest_ansi_color(p) | |
if last_color != c: | |
o += c | |
last_color = c | |
o += fill_char | |
o += ANSI_RESET + '\n\n' | |
if output_file is not sys.stdout: | |
output_file = open(output_file, 'w') | |
output_file.write(o) | |
output_file.close() | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('filename', help='File to convert to ASCII art') | |
parser.add_argument('-o', '--output_file', nargs='?', default=sys.stdout, | |
help='Path to the output file, defaults to stdout') | |
parser.add_argument('-f', '--fill_char', nargs='?', default='##', | |
help='Character to use for solid pixels in the image') | |
args = parser.parse_args() | |
convert_image(args.filename, args.output_file, fill_char=args.fill_char) |
@jimmykane Good point, Python indeed ships by default with many distributions!
This is really cool. Just a thought: It is quite slow right now, but you could dramatically speed things up by caching the results of color_distance() lookups. Performing those calculations for every pixel is really expensive.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ncancell I ended up here as well from the same post. I prefer the name python. :-p
For me distinguishes much better because I managed to intergrate this approach with multiprocessing and cluster this script at the same time with provisioning plus adding on the repo a small png file. So yhea, why should a vanilla server install go just for some graphics?