Converting an image to an ASCII image in Python



Introduction to ASCII art


ASCII art — is a graphic design technique that uses computers for presentation and consists of images composed of 95 printable (128) characters as defined by the 1963 ASCII standard and ASCII-compatible character sets with their own extended characters (beyond 128 characters). standard 7-bit ASCII characters). The term is also loosely used to refer to textual 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 (disproportionate fonts, as on a traditional typewriter), such as Courier, for presentation. Among the oldest known examples of ASCII art — the work of computer art pioneer Kenneth Knowton from about 1966, who was at Bell Labs at the time. Research on 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 graphic capability, and thus symbols were used instead of graphic characters. In addition, to mark separations between different print jobs from different users, mass printers often used ASCII art to print large banners, making it easier to detect separations so that results can be more easily separated by an operator or clerk. ASCII art was also used in early email when images could not be embedded. You can find more about them. [Source: Wiki .

How it works:


Here are the steps the program follows to generate the ASCII
image:

  • Convert the input image to grayscale.
  • Divide the image into tiles M × N.
  • Correct M (number of lines) to match the aspect ratio of the image and the font.
  • Calculate the average luminance for each section of the image, then find the appropriate ASCII character for each.
  • Collect strings of ASCII character strings and print them in flash to form the final image.

Requirements


You make this program in python and we will use Pillow, which is the Python Imaging Library to read images, access the underlying data, create them and changes, as well as the scientific module Numpy for calculating averages.

Code


You will start by defining the grayscale levels used to create ASCII art. You will then see how the image is tiled and how the average brightness for those tiles is calculated. Next, you will work on replacing the tiles with ASCII characters to generate the final result. Finally, you`ll set up command line parsing for the program so that the user can specify the output size, output file name, and so on.

Grayscale and grid level detection

As a first step in creating your program, define two grayscale levels that are used to convert luma values ​​to ASCII characters as global values.

 & gt; & gt; & gt; gscale1 = "$ @ B% 8 & amp; WM # * oahkbdpqwmZO0QLCJUYXzcvunxrjft / | () 1 {} []? -_ + ~ i! LI;:, "^` "." # 70 levels of gray & gt; & gt; & gt; gscale2 = "@% # * + = - :. "# 10 levels of gray 

The value of gscale1 at point u is a linear gray scale at 70 levels, and gscale2 at point v — a simpler linear scale in grayscale. Both of these values ​​are stored as strings with a range of characters that go from darkest to lightest.

Now that you have the grayscale ramps, you can tweak the image. The following code opens an image and splits it into a grid:

 # open image and convert to grayscale & gt; & gt; & gt; image = Image.open (fileName) .convert (`L`) # store dimensions & gt; & gt; & gt; W, H = image.size [0], image.size [1] # compute width of tile & gt; & gt; & gt; w = W / cols # compute tile height based on aspect ratio and scale & gt; & gt; & gt; h = w / scale # compute number of rows & gt; & gt; & gt; rows = int (H / h) 

Calculating Average Brightness
Then you calculate the average brightness of a tile in a grayscale image. The getAverageL () function does its job.

 #Given PIL Image, return average value of grayscale value & gt; & gt; & gt; 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 as a PIL image object. In the second step, convert the image to an array with an empty slice, after which “im” becomes a two-dimensional array of luma for each pixel. In the third step, you save the dimensions (width and height) of the image. In the fourth step, numpy.average () calculates the average luminance of the image using numpy.reshape () to first convert a two-dimensional array of width and height dimensions (w, h) to a? an array whose length is the product of width and height (w * h). The numpy.average () call then sums these array values ​​and calculates the average.

Generating ASCII content from an image

 # ascii image is a list of character strings & gt; & gt; & gt; aimg = [] # generate list of dimensions & gt; & gt; & gt; 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 

B In this section of the program the ASCII image is first saved as a list of strings, which is initialized in the first step. Then you iterate over the calculated number of image tiles in the row, and in the second stereo and next row, you compute the start and end y coordinates of each image tile. Although these are floating point calculations, trim them to integers before passing them to the image cropping method. Further, since dividing an image into tiles creates edge tiles of the same size only if the image width is an integer multiple of the number of columns, correct the y-coordinate of the tiles in the last row by setting the y-coordinate to the actual height of the image. This will ensure that the top edge of the image is not cropped. In the third step, you add an empty string in ASCII as a compact way to represent the current line of the image. You will complete this line in the next. (You treat the string as a list of characters.) In the fourth step and the next line, you calculate the left and right x-coordinates of each tile, and in the fifth step, you adjust the x-coordinate for the last tile for For the same reasons, you corrected the Y coordinate. Use image .crop () in the sixth step to extract a tile of the image and then pass that tile to the getAverageL () function defined above, you will reduce the average luminance value from [0, 255] to [0, 9] (the range of values ​​for the standard scale gray in 10 levels). Then you use gscale2 (the saved ramp string) as the lookup table for ASCII Art 95 of the corresponding ASCII value. The eight step line is similar, except that it is only used if the command line is checked for using a 70 level ramp. Finally, you add the ASCII value you are looking for, gsval, to the text string in the last step, and the code loops until all lines have been processed.

Adding the CLI and writing ASCII text strings to the text file

Use the built-in python argparse module to add a command line interface.
Finally, take the generated list of ASCII character strings and write those strings to a text file.

 # open a new text file & gt; & gt; & gt; f = open (outFile, `w`) # write each string in the list to the new file & gt; & gt; & gt; for row in aimg: ... f.write (row + ``) # clean up & gt; & gt; & gt; f.close () 

Then add these parts to build your program. The complete code is below.

# Python code to convert image to ASCII image.

import sys, random, argparse

import numpy as np

import math

 

from PIL import Image

 
# grayscale values ​​from:
# http://paulbourke.net/dataformats/asciiart/

 
# 70 gray levels

gscale1 = "$ @ B% 8 & amp; WM # * oahkbdpqwmZO0QLCJUYXzcvunxrjft / | () 1 {} []? -_ + ~ & lt; & gt; i! lI;:," ^ ``. "

  
# 10 gray levels

gscale2 = `@% # * + = - :.`

 

def getAverageL (image):

 

"" "

  For a given PIL image, returns the average value in grayscale.

"" "

# get the image as an array

im = np.array (image)

  

  # get the form

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) return m * n list of images

  "" "

  # declare global

global gscale1, gscale2

  

  # open the 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:% dx% d" % (W, H))

 

# calculate tile width

w = W / cols

 

# calculate tile height based on aspect ratio and scale

h = w / scale

 

# calculate the number of rows

rows = int (H / h)

 

print ( "cols:% d, rows:% d" % (cols, rows))

print ( "tile dims:% dx% d" % (w, h))

 

# check image size is too small

if cols & gt; W or rows & gt; H:

print ( "Image too small for specified cols!" )

exit ( 0 )

 

# ascii image - list of strings characters

aimg = []

# create a list of dimensions

for j in range (rows):

y1 = int (j * h)

y2 = int ((j + 1 ) * h)

 

# fix the last tile

if j = = rows - 1 :

y2 = H

 

  # add blank line

aimg.append ("")

 

for i in range (cols):

 

# crop image to tiles

x1 = int (i * w)

x2 = int ((i + 1 ) * w)

 

# fix the last tile

if i = = cols - 1 :

x2 = W

 

# crop image for extraction tiles

img = image.crop ((x1, y1, x2, y2))

 

# get average brightness

avg = int (getAverageL (img))

 

  # see ascii char

if moreLevels:

gsval = gscale1 [ int ((avg * 69 ) / 255 )]

else :

gsval = gscale2 [ int ((avg * 9 ) / 255 )]

 

# add ascii line to string

aimg [j] + = gsval

 

# return text 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 = F alse )

parser.add_argument ( `--morelevels` , dest = `moreLevels` , action = ` store_true ` )

  

  # parse arguments

  args = parser.parse_args ()

 

  imgFile = args.imgFile

 

  # set the output file

outFile = `out.txt`

  if args.outFile:

outFile = args.outFile

 

# set the default scale to 0.43, which is fine

# Courier font

  scale = 0.43

if args.scale:

scale = float (args.scale)

 

# set columns

cols = 80

if args.cols:

cols = int (args.cols)

 

  print ( ` generating ASCII art ... ` )

# convert image to ascii text

aimg = covertImageToAscii (imgFile, cols, scale, args.moreLevels)

 

# open file

f = open (outFile, ` w` )

 

# write to file

for row in aimg:

f.write (row + ` ` )

  

# cleaning

  f.close ()

  print ( " ASCII art written to% s " % outFile)

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
# call home

if __ name__ = = ` __main__` :

main ()

Input:

 $ python "ASCII_IMAGE_GENERATOR.py" --file data / 11.jpg --cols 120 

Resources

1. Wikipedia: ASCII_ART
2. Python Playground: fun projects for the curious programmer Mahesh Venkitachalam. 
3. Grayscale Level Values ​​
4. Github code for this article

This article courtesy of Subhajit Saha . If you are as Python.Engineering and would like to contribute, you can also write an article using contribute.python.engineering or by posting an article contribute @ python.engineering. See my article appearing on the Python.Engineering homepage and help other geeks.

Please post comments if you find anything wrong or if you`d like to share more information on the topic discussed above.