Skip to content

Instantly share code, notes, and snippets.

@fish2000
Created January 17, 2012 19:43
Show Gist options
  • Select an option

  • Save fish2000/1628413 to your computer and use it in GitHub Desktop.

Select an option

Save fish2000/1628413 to your computer and use it in GitHub Desktop.
PyGame-based image ascii-art-ifier that I got from someone's blog and then edited a little.
#!/usr/bin/env python
# encoding: utf-8
"""
pyascii.py
Render an image as ASCII art using PyGame.
Originally by tommih:
http://tommih.blogspot.com/2009/01/image-to-ascii-art-with-python.html
Created by FI$H 2000 on 2011-09-03.
Copyright (c) 2011 Objects In Space And Time, LLC. All rights reserved.
"""
import pygame
import copy
import sys
import numpy
import os
from processing import Process, Queue
def grayscale(arr):
""" Makes a color image a grayscale one
image is given in 2d numpy array format """
return arr/65536
def compare_blocks(ba1, ba2):
""" Compare two grayscale 2D pixel buffers and calculate their square sum difference """
# this is why I love numpy :)
bares = (ba1 - ba2)**2
price = numpy.add.reduce(bares.flat)/ba1.shape[0]
return price
class AsciiRenderer:
""" Generate some nice looking ASCII-art (not necessarily ASCII) """
def __init__(self, filename):
# load image
if filename.lower().endswith('bmp'):
pic = pygame.image.load(filename)
else:
# import PIL in an attempt to BMP-ize the image.
try:
from PIL import Image
except ImportError:
import Image
# store the BMP fakery temporarily in a StringIO
try:
from cStringIO import StringIO
except:
from StringIO import StringIO
# convert to BMP with PIL, so as to pass muster with pygame
fakebmp = StringIO()
with open(filename, "r+b") as f:
Image.open(f).save(fakebmp, format='BMP')
pic = pygame.image.load(fakebmp, 'yodogg.bmp')
pic_width = pic.get_width()
pic_height = pic.get_height()
if pic_width>1000 or pic_height>1000:
print "Error: Image too large (width>1000 or height>1000)"
return
# Initialise screen
pygame.init()
# load and init font
fontfile = pygame.font.match_font('lucida console')
font = pygame.font.Font(fontfile, 14)
self.fixed_width = 8
# list of characters that are used in the creation of the ASCII art
self.charlist = []
self.used_encoding = 'utf-8'
# generate character set
self.fgcolor = (255,255,255)
self.bgcolor = (0,0,0)
self.max_width = 0
self.max_height = 0
self.char_pix = {}
self.char_rec = {}
for i in range(33,0x1FF):
try:
unichr(i).encode(self.used_encoding)
except UnicodeDecodeError:
continue
except UnicodeEncodeError:
continue
if i!=127:
# init backbuffer
surf = pygame.Surface((50,50),flags=0)
surf.fill(self.bgcolor, surf.get_rect())
# get character code
char = unichr(i)
self.charlist.append(char)
# render character
text = font.render(char, 1, self.fgcolor)
rec = text.get_rect()
# background is color (bgcolor)
surf.fill(self.bgcolor, rec)
surf.blit(text, rec)
# convert to numpy type array
self.char_pix[char] = pygame.surfarray.array2d(surf.subsurface(rec))
self.char_pix[char] = grayscale(self.char_pix[char])
self.char_rec[char] = rec
#print char+": "+repr(char_pix[char].shape[0])+"="+repr(rec.width)
# get max dimensions for the character set
self.max_height = rec.height
if rec.width > self.max_width:
self.max_width = rec.width
self.text_height = self.max_height
print "Max character width: "+repr(self.max_width)
print "Character height: "+repr(self.max_height)
chr_set = ''.join(self.char_pix.keys())
print "Character set: ",chr_set.encode('latin-1', 'ignore')
# convert picture to numpy arrays
self.num_threads = 4
pic_arr = pygame.surfarray.array2d(pic)
pic_arr = grayscale(pic_arr)
# split data for threading
rows = pic_arr.shape[1] / self.text_height
locs = []
locs.append(0)
for i in range(1,self.num_threads):
locs.append(rows * i / self.num_threads)
locs.append(rows)
self.thr_arr = []
for i in range(self.num_threads):
b_beg = locs[i] *self.text_height
b_end = locs[i+1]*self.text_height
self.thr_arr.append(pic_arr[:,b_beg:b_end])
def render(self):
outstring = ""
th = [] # Processes
qs = [] # Message queues
# Init each process
for i in range(self.num_threads):
qs.append(Queue())
th.append(Renderer(self.char_pix, self.text_height, self.max_width, self.thr_arr[i], self.fixed_width, qs[i]))
th[i].start()
# Wait for processes to finish
for thread in th:
thread.join()
# Read the data from message queues
for q in qs:
outstring += q.get()+'\n'
return outstring
class Renderer(Process):
def __init__(self, char_pix, text_height, max_width, pic_arr, fixed_width, q):
Process.__init__(self)
self.char_pix = char_pix
self.text_height = text_height
self.max_width = max_width
self.pic_arr = pic_arr
self.fixed_width = fixed_width
self.q = q
self.outstring = ""
def run(self):
x = 0
y = 0
pic_arr = self.pic_arr
run_loop = True
# Event loop
while run_loop:
# render row
if x<pic_arr.shape[0] - self.max_width:
prices = []
# go through a set of characters to match for fitness
for (char, text) in self.char_pix.items():
prices.append((compare_blocks(pic_arr[x:x+(text.shape[0]),y:y+(text.shape[1])], text),char))
# i love python <3
(best_fit, best_char) = min(prices)
else:
if y< (pic_arr.shape[1] - self.text_height*2+1):
# new line
x = 0
y += self.text_height
self.outstring += '\n'
continue
#print "PID:", os.getpid(), "> "+repr(y/self.text_height)+"/"+repr(pic_arr.shape[1]/self.text_height)
else:
# block finished, quit
run_loop = False
continue
if self.fixed_width <= 0:
x += self.char_pix[best_char].shape[0]
else:
x += self.fixed_width
self.outstring += best_char
# Send data to message queue
self.q.put(self.outstring)
def convert(filename):
r = AsciiRenderer(filename)
# calculate render time
time_start = pygame.time.get_ticks()
ol = r.render()
print ol.encode('latin-1','replace')
of = open("ascii_art.txt", "w")
of.write(ol.encode('utf-8','replace'))
of.close()
print "rendered in: "+repr((pygame.time.get_ticks()-time_start)/1000)+" seconds."
if __name__ == '__main__':
if len(sys.argv) == 2:
convert(sys.argv[1])
else:
print "Error: Need exactly one argument: the filename of image"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment