8

I have to make a program that reads in a file from the command line and covert it to ASCII art. I am using PPM format and here is a link to the project.

Here is what I have so far:

import sys

def main(filename):
    image = open(filename)
    #reads through the first three lines
    color = image.readline().splitlines()
    size_width, size_height = image.readline().split()
    max_color = image.readline().splitlines()

    #reads the body of the file
    pixels = image.read().split()
    red = 0
    green = 0
    blue = 0
    r_g_b_value = []
    #pulls out the values of each tuple and coverts it to its grayscale value 
    for i in pixels:
        if i !=  "\n" or " ":
            if len(i) == 3:
                red = int(i[0]) * .3
                green = int(i[1]) * .59
                blue = int(i[2]) * .11
            elif len(i) == 2:
                red == int(i[0]) * .3
                green == int(i[1]) *.59
                blue == 0
            elif len(i) == 1:
                red == int(i[0]) * .3
                green == 0
                blue == 0

            r_g_b_value = [red + green + blue]

            character = []
        for j in len(r_g_b_value):
            if int(j) <= 16:
                character = " "
            elif int(j) > 16 and int(j) <= 32:
                character = "."
            elif int(j) > 32 and int(j) <= 48:
                character = ","
            elif int(j) > 48 and int(j) <= 64:
                charcter = ":"
            elif int(j) > 64 and int(j) <= 80:
                character = ";"
            elif int(j) > 80 and int(j) <= 96:
                character = "+"
            elif int(j) > 96 and int(j) <= 112:
                character = "="
            elif int(j) > 112 and int(j) <= 128:
                character = "o"
            elif int(j) > 128 and int(j) <= 144:
                character = "a"
            elif int(j) > 144 and int(j) <= 160:
                character = "e"
            elif int(j) > 160 and int(j) <= 176:
                character = "0"
            elif int(j) > 176 and int(j) <= 192:
                character = "$"
            elif int(j) > 192 and int(j) <= 208:
                character = "@"
            elif int(j) > 208 and int(j) <= 224:
                character = "A"
            elif int(j) > 224 and int(j) <= 240:
                character = "#"
            else:
                character = "M"

            grayscale = character
            print(grayscale)

main(sys.argv[1])

I an getting an error that says 'int' object is not iterable, is there is an easy way to fix this and how would someone recommend printing this out while preserving the image.

And the last thing I am unsure about is how to preserve the width and height of the picture.

Any help would be greatly appreciated.

joaquin
  • 82,968
  • 29
  • 138
  • 152
asmith
  • 69
  • 1
  • 1
  • 5
  • I am lost here and it seems that when I print the r_g_b_value it is printing it one value per line. – asmith Sep 12 '11 at 02:27
  • I am thinking if I do for j in range(r_g_b__values) and then do a series of if/else statements of int(j), setting the values to those in the list of available characters, would this work and what would be the best way to save the characters. Would it be a new list? – asmith Sep 12 '11 at 02:35
  • If you're still getting the error, the full traceback would be helpful. – David Eyk Sep 12 '11 at 03:24
  • I got rid of the error, but do you have any idea on printing out this character in while keeping the image – asmith Sep 12 '11 at 03:57

3 Answers3

8

You can use image-to-ansi.py for the conversion.

First, download image-to-ansi.py:

wget https://gist.githubusercontent.com/klange/1687427/raw/image-to-ansi.py

Save this script as ppmimage.py:

# Parses a PPM file and loads it into image-to-ansi.py
import re, itertools

sep = re.compile("[ \t\r\n]+")

def chunks(iterable,size):
    """ http://stackoverflow.com/a/434314/309483 """
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

""" Emulates the Image class from PIL and some member functions (`getpixel`, `size`). """
class Image:
    """ This class emulates the PIL Image class, and it can parse "plain" PPM's.
        You can use PIL instead. """
    @staticmethod
    def fromstdin():
        return Image()
    def __init__(self): # http://netpbm.sourceforge.net/doc/ppm.html
        self.entities = sep.split("\n".join(list(filter(lambda x: not x.startswith("#"), sys.stdin.read().strip().split("\n")))))
        self.size = tuple(list(map(int,self.entities[1:3])))
        self.high = int(self.entities[3]) # z.b. 255
        self.pixels = list(map(lambda x: tuple(map(lambda y: int(int(y) / self.high * 255), x)), list(chunks(self.entities[4:], 3))))
    def getpixel(self, tupl):
        x = tupl[0]
        y = tupl[1]
        pix = self.pixels[y*self.size[0]+x]
        return pix

image_to_ansi = __import__("image-to-ansi") # __import__ because of minuses in filename. From https://gist.github.com/1687427

if __name__ == '__main__':
    import sys
    #import Image
    im = Image.fromstdin() # use Image.open from PIL if using PIL
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            p = im.getpixel((x,y))
            h = "%2x%2x%2x" % (p[0],p[1],p[2])
            short, rgb = image_to_ansi.rgb2short(h)
            sys.stdout.write("\033[48;5;%sm " % short)
        sys.stdout.write("\033[0m\n")
    sys.stdout.write("\n")

You can test the script like this (this assumes you have netpbm and imagemagick installed):

convert -resize $(($COLUMNS*2))x$(($LINES*2)) logo: pnm:- | pnmtoplainpnm | python3 ppmimage.py

On my machine, it looks like this:

ImageMagick logo shown in Xterm

Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
4

Here you have your code modified and working.
It is not optimal, it does not take into account the presence of more or less comments in the header and there is not exception handling but it is a start.

import sys
import numpy as np

RGBS = range(16, 255, 16)
CHARS = [' ', '.', ',', ':', ';', '+', '=', 'o',
         'a', 'e', '0', '$', '@', 'A', '#']
FACTORS = [.3, .59, .11]

def main(filename):
    image = open(filename)
    #reads header lines
    color = image.readline()
    _ = image.readline()
    size_width, size_height = image.readline().split()
    max_color = image.readline()

    size_width = int(size_width)
    max_color = int(max_color)

    #reads the body of the file
    data = [int(p) for p in image.read().split()]
    #converts to array and reshape
    data = np.array(data)
    pixels = data.reshape((len(data)/3, 3))
    #calculate rgb value per pixel
    rgbs = pixels * FACTORS
    sum_rgbs = rgbs.sum(axis=1)
    rgb_values = [item * 255 / max_color for item in sum_rgbs]

    grayscales = []
    #pulls out the value of each pixel and coverts it to its grayscale value 
    for indx, rgb_val in enumerate(rgb_values):
        #if max width, new line
        if (indx % size_width) == 0 : grayscales.append('\n')    

        for achar, rgb in zip(CHARS, RGBS):
            if rgb_val <= rgb:
                character = achar
                break
            else:
                character = 'M'

        grayscales.append(character)

    print ''.join(grayscales)

main('test.ppm')

These are the ppm figure and the ASCII Art result

enter image description here

And the micro ppm file I used for the example:

P3
# test.ppm
4 4
15
 0  0  0    0  0  0    0  0  0   15  0 15
 0  0  0    0 15  7    0  0  0    0  0  0
 0  0  0    0  0  0    0 15  7    0  0  0
15  0 15    0  0  0    0  0  0    0  0  0
joaquin
  • 82,968
  • 29
  • 138
  • 152
  • This solution provided in this answer provides no color support. See my answer for how to sacrifice support for the craptastic Windows console and gain color support. – Janus Troelsen Mar 25 '13 at 11:45
  • @JanusTroelsen Thanks but note the OP is asking about ASCII art, not ANSI art – joaquin Mar 25 '13 at 17:16
0

I wrote one of these in C# a while ago and I calculated the character to use with this formula:

index_into_array = (int)(r_g_b_value * (chars_array_length / (255.0)));

As for the width and height, you could average every two lines of vertical pixels to halve the height.

Edit 1: in response to comment: The basic idea is that it scales your RGB value from 0 to 255 to 0 to the length of the array and uses that as the index.

Edit 2: Updated to correct that I was ignoring your grayscale normalization.

0x5f3759df
  • 2,349
  • 1
  • 20
  • 25
  • I am still not very good at coding, so I am not sure I fully understand, I set the list of available to chars_array_length and then use the function that you posted and then do I print that value to get the character or do I need to do something else. Thank you again for your help – asmith Sep 12 '11 at 01:35
  • @asmith Updated answer to explain further. – 0x5f3759df Sep 12 '11 at 01:40
  • Once again I apologize for my lack of skills but I get this error File "other.py", line 40, in main(sys.argv[1]) File "other.py", line 36, in main current_character = character_choices[int((r_g_b_value * (len(character_choices) /(255.0*3))))] TypeError: can't multiply sequence by non-int of type 'float' – asmith Sep 12 '11 at 01:50
  • Yeah I don't know python, you'll have to convert the algorithm I posted to python code yourself or update your/ask another question. – 0x5f3759df Sep 12 '11 at 01:54