Created
January 17, 2012 19:43
-
-
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.
This file contains hidden or 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 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