Introduction to ASCII art
ASCII art is a graphic design technique that uses computers for presentation and consists of pictures pieced together from the 95 printable (from a total of 128) characters defined by the ASCII Standard from 1963 and ASCII compliant character sets with proprietary extended characters (beyond the 128 characters of standard 7-bit ASCII). The term is also loosely used to refer to text-based visual art in general. ASCII art can be created with any text editor and is often used with free-form languages. Most examples of ASCII art require a fixed-width font (non-proportional fonts, as on a traditional typewriter) such as Courier for presentation. Among the oldest known examples of ASCII art are the creations by computer-art pioneer Kenneth Knowlton from around 1966, who was working for Bell Labs at the time. “Studies in Perception I” by Ken Knowlton and Leon Harmon from 1966 shows some examples of their early ASCII art. ASCII art was invented, in large part, because early printers often lacked graphics ability and thus characters were used in place of graphic marks. Also, to mark divisions between different print jobs from different users, bulk printers often used ASCII art to print large banners, making the division easier to spot so that the results could be more easily separated by a computer operator or clerk. ASCII art was also used in an early e-mail when images could not be embedded. You can find out more about them. [Source : Wiki.
How it works:
here are the steps the program takes to generate the ASCII
image:
- Convert the input image to grayscale.
- Split the image into M×N tiles.
- Correct M (the number of rows) to match the image and font aspect ratio.
- Compute the average brightness for each image tile and then look up a suitable ASCII character for each.
- Assemble rows of ASCII character strings and print them to a file to form the final image.
Requirements
You will do this program in python and we will use Pillow which is Python Imaging Library for reading the images, accessing their underlying data, creating and modifying them, and also the scientific module Numpy to compute averages.
The Code
You’ll begin by defining the grayscale levels used to generate the ASCII art. Then you’ll look at how the image is split into tiles and how the average brightness is computed for those tiles. Next, you’ll work on replacing the tiles with ASCII characters to generate the final output. Finally, you’ll set up command line parsing for the program to allow a user to specify the output size, output filename, and so on.
Defining the Grayscale Levels and Grid
As the first step in creating your program, define the two grayscale levels used to convert brightness values to ASCII characters as global values.
>>>gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~i!lI;:,\"^`". " #70 levels of gray >>>gscale2 = "@%#*+=-:. " #10 levels of gray
The value gscale1 at u is the 70-level grayscale ramp, and gscale2 at v is the simpler 10-level grayscale ramp. Both of these values are stored as strings, with a range of characters that progress from darkest to lightest.
Now that you have your grayscale ramps, you can set up the image. The following code opens the image and splits it into a grid:
# open image and convert to grayscale >>> image = Image.open(fileName).convert('L') # store dimensions >>> W, H = image.size[0], image.size[1] # compute width of tile >>> w = W/cols # compute tile height based on aspect ratio and scale >>> h = w/scale # compute number of rows >>> rows = int(H/h)
Computing the average brightness
Next, you compute the average brightness for a tile in the grayscale image. The function getAverageL() does the job.
#Given PIL Image, return average value of grayscale value >>>def getAverageL(image): # get image as numpy array ... im = np.array(image) # get shape ... w,h = im.shape # get average ... return np.average(im.reshape(w*h))
First, the image tile is passed in as a PIL Image object. Convert the image into a numpy array at the second step, at which point ‘im’ becomes a two-dimensional array of brightness for each pixel. In the third step, you store the dimensions (width and height) of the image. At the fourth step, numpy.average() computes the average of the brightness values in the image by using numpy.reshape() to first convert the two-dimensional array of the dimensions width and height (w,h) into a ?at one-dimensional array whose length is a product of the width times the height (w*h). The numpy.average() call then sums these array values and computes the average.
Generating the ASCII Content from the Image
# ascii image is a list of character strings >>> aimg = [] # generate list of dimensions >>> for j in range(rows): ... y1 = int(j*h) ... y2 = int((j+1)*h) # correct last tile ... if j == rows-1: ... y2 = H # append an empty string ... aimg.append("") ... for i in range(cols): # crop image to tile ... x1 = int(i*w) ... x2 = int((i+1)*w) # correct last tile ... if i == cols-1: ... x2 = W # crop image to extract tile ... img = image.crop((x1, y1, x2, y2)) # get average luminance ... avg = int(getAverageL(img)) # look up ascii char ... if moreLevels: ... gsval = gscale1[int((avg*69)/255)] ... else: ... gsval = gscale2[int((avg*9)/255)] # append ascii char to string ... aimg[j] += gsval
In this section of the program, the ASCII image is first stored as a list of strings, which is initialized at the first step. Next, you iterate through the calculated number of row image tiles, and at the second step and the following line, you calculate the starting and ending y-coordinates of each image tile. Although these are floating-point calculations, truncate them to integers before passing them to an image-cropping method. Next, because dividing the image into tiles creates edge tiles of the same size only when the image width is an integer multiple of the number of columns, correct for the y-coordinate of the tiles in the last row by setting the y-coordinate to the image’s actual height. By doing so, you ensure that the top edge of the image isn’t truncated. At the third step, you add an empty string into the ASCII as a compact way to represent the current image row. You’ll fill in this string next. (You treat the string as a list of characters.) At the fourth step and the next line, you compute the left and right x-coordinates of each tile, and a the fifth step, you correct the x-coordinate for the last tile for the same reasons you corrected the y-coordinate. Use image.crop() at the sixth step to extract the image tile and then pass that tile to the getAverageL() function defined above, you scale down the average brightness value from [0, 255] to [0, 9] (the range of values for the default 10-level grayscale ramp). You then use gscale2 (the stored ramp string) as a lookup table for ASCII Art 95 the relevant ASCII value. The line at eight step is similar, except that it’s used only when the command line ?ag is set to use the ramp with 70 levels. Finally, you append the looked-up ASCII value, gsval, to the text row at last step, and the code loops until all rows are processed.
Adding Command Line interface and Writing the ASCII Art-Strings to a text file
To add a command line interface use the python built-in module argparse.
And now finally, take the generated list of ASCII character strings and write those strings to a text file.
# open a new text file >>> f = open(outFile, 'w') # write each string in the list to the new file >>> for row in aimg: ... f.write(row + '\n') # clean up >>> f.close()
Then add these parts to create your program. The complete code is given below.
Python
# Python code to convert an image to ASCII image. import sys, random, argparse import numpy as np import math from PIL import Image # gray scale level values from: # 70 levels of gray gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " # 10 levels of gray gscale2 = '@%#*+=-:. ' def getAverageL(image): """ Given PIL Image, return average value of grayscale value """ # get image as numpy array im = np.array(image) # get shape w,h = im.shape # get average return np.average(im.reshape(w * h)) def covertImageToAscii(fileName, cols, scale, moreLevels): """ Given Image and dims (rows, cols) returns an m*n list of Images """ # declare globals global gscale1, gscale2 # open image and convert to grayscale image = Image. open (fileName).convert( 'L' ) # store dimensions W, H = image.size[ 0 ], image.size[ 1 ] print ( "input image dims: %d x %d" % (W, H)) # compute width of tile w = W / cols # compute tile height based on aspect ratio and scale h = w / scale # compute number of rows rows = int (H / h) print ( "cols: %d, rows: %d" % (cols, rows)) print ( "tile dims: %d x %d" % (w, h)) # check if image size is too small if cols > W or rows > H: print ( "Image too small for specified cols!" ) exit( 0 ) # ascii image is a list of character strings aimg = [] # generate list of dimensions for j in range (rows): y1 = int (j * h) y2 = int ((j + 1 ) * h) # correct last tile if j = = rows - 1 : y2 = H # append an empty string aimg.append("") for i in range (cols): # crop image to tile x1 = int (i * w) x2 = int ((i + 1 ) * w) # correct last tile if i = = cols - 1 : x2 = W # crop image to extract tile img = image.crop((x1, y1, x2, y2)) # get average luminance avg = int (getAverageL(img)) # look up ascii char if moreLevels: gsval = gscale1[ int ((avg * 69 ) / 255 )] else : gsval = gscale2[ int ((avg * 9 ) / 255 )] # append ascii char to string aimg[j] + = gsval # return txt image return aimg # main() function def main(): # create parser descStr = "This program converts an image into ASCII art." parser = argparse.ArgumentParser(description = descStr) # add expected arguments parser.add_argument( '--file' , dest = 'imgFile' , required = True ) parser.add_argument( '--scale' , dest = 'scale' , required = False ) parser.add_argument( '--out' , dest = 'outFile' , required = False ) parser.add_argument( '--cols' , dest = 'cols' , required = False ) parser.add_argument( '--morelevels' ,dest = 'moreLevels' ,action = 'store_true' ) # parse args args = parser.parse_args() imgFile = args.imgFile # set output file outFile = 'out.txt' if args.outFile: outFile = args.outFile # set scale default as 0.43 which suits # a Courier font scale = 0.43 if args.scale: scale = float (args.scale) # set cols cols = 80 if args.cols: cols = int (args.cols) print ( 'generating ASCII art...' ) # convert image to ascii txt aimg = covertImageToAscii(imgFile, cols, scale, args.moreLevels) # open file f = open (outFile, 'w' ) # write to file for row in aimg: f.write(row + '\n' ) # cleanup f.close() print ( "ASCII art written to %s" % outFile) # call main if __name__ = = '__main__' : main() |
Input:
$python "ASCII_IMAGE_GENERATOR.py" --file data/11.jpg --cols 120
Resources
1. Wikipedia: ASCII_ART
2. Python Playground: Geeky Projects for the Curious Programmer by Mahesh Venkitachalam.
3. Gray Scale Level Values
4. Github Code for this article
This article is contributed by Subhajit Saha. If you like Lazyroar and would like to contribute, you can also write an article using write.geeksforgeeks.org or mail your article to review-team@geeksforgeeks.org. See your article appearing on the Lazyroar main page and help other Geeks.
Please write comments if you find anything incorrect, or if you want to share more information about the topic discussed above.