Convert Photos to ASCII Arts with Python
Printed ASCII art is a fabulous gift for a geeky friend. The great thing about coded artwork is that you can easily give it your own personal touch! It’s a stylish decoration that looks great on mugs, t-shirts, and even curtains. Making ASCII art is much easier than it looks, and I’m going to show you how to do it in Python!
We’ll use a few Python libraries– Pillow, Colour and Numpy. Pillow handles all the image processing - reading, resizing, writing. Colour gives us the beautiful gradient color. Numpy makes the grayscale conversion easy. Once you understand how sample codes work, you can change fonts/symbols and try out different colors, to turn your favorite picture into your unique signature artwork.



Prerequisits: Install Python 3 and libraries Pillow, Colour and Numpy
Step 1: load a picture
The Image class in PIL (Pillow) provides a lazy function Image.open
to open an image file. At this step, we only identify the image but not actually load it into memory. Image
supports a variety of image types including bmp, jpeg, gif, eps.
from PIL import Image
#open the input file
img = Image.open("LincolnPortrait.jpeg")
Step 2: resize the picture
ASCII art is composed of symbols. Symbols naturally have different degrees of darkness because of their shapes. For example, .
is whiter than :
which is whiter than !
. It’s easy to illustrate a range of grayscale with symbol patches.

One challenge as you can see from above is that symbols are not necessarily square. If we simply replace image pixels with symbols one-to-one, we will get a picture with distorted width and height. To avoid deformation, we need to carefully calculate how many symbols to use in each row and column in the new image. As a simple example, to draw a small square using symbols of shape ratio 3:4 (width:height), we need to use 4 columns and 3 rows.

Another thing to notice is that a symbol is much larger than a pixel(px). So it’s not a bad idea to reduce the picture resolution before the conversion. Our program handles the pixel reduction using a scaling factor SC
between 0(exclusive) and 1(inclusive). The bigger the number is, the more details you would see in the output and the bigger the output image would be.
# Load the fonts and
# then get the the height and width of a typical symbol
# You can use different fonts here
font = ImageFont.load_default()
letter_width = font.getsize("x")[0]
letter_height = font.getsize("x")[1]
WCF = letter_height/letter_width
# Based on the desired output image size,
# calculate how many ASCII letters are needed on the width and height
widthByLetter=round(img.size[0]*SC*WCF)
heightByLetter = round(img.size[1]*SC)
S = (widthByLetter, heightByLetter)
# Resize the image based on the symbol width and height
img = img.resize(S)
Step 3: convert colors to grayscale
Since img.resize(...)
returns an Image object, it contains RGB values of each pixel on the image. Assume the images is m px width and n px height, np.asarray(img)
returns a m* n *3 matrix.
There are a few methods to convert RGB color to grayscale (John D. Cook describes them well in his post). Here we simply take the average of RGB values and normalize them to get monochrome values. The result is a m *n matrix of float numbers between (0,1). The whitest point of the picture is 0, and the darkest point is 1. Optionally, we can further tune the image brightness using parameter GCF
. If GCF
is set to be >1, the picture would look brighter. If 0<GCF
<1, the picture would look darker.
# Get the RGB color values of each pixel point
# and convert them to graycolor using
# the average method from numpy
img = np.sum(np.asarray(img), axis=2)
# Normalize the results
img -= img.min()
img = (1.0 - img/img.max())
# (optional) adjust the image brightness
img = img**GCF
Step 4: create an ASCII art image
The next step is to map grayscale to symbols of different darkness. The first line of the code below defines an array of symbols from the whitest to the darkest. Next, grayscale values are mapped to symbols in the array. There is a lot of flexibility how you can define the array: letters, punctuations, symbols, special characters, etc. It’s totally up to you to make it more fun.
Image.new
and Image.Draw
provide easy ways to create a blank image, where we will print the mapped symbols line by line. The for
loop is where the printing actually happen. You can certainly try using the same color for all the symbols, or use functions such as range_to
in Colour
to create color patterns.
# The array of ascii symbols from white to black
chars = np.asarray(list(' .,:irs?@9B&#'))
# Map grayscale values to the symbol's index in the array
img = img*(chars.size-1).astype(int)
# Generate the ascii art symbols
lines = ("\n".join(
("".join(r) for r in chars[img]) )).split("\n")
# Create an image object, set its width and height,
# background color
bgcolor='white'
newImg_width= letter_width *widthByLetter
newImg_height = letter_height * heightByLetter
newImg = Image.new("RGBA", (
newImg_width, newImg_height), bgcolor)
draw = ImageDraw.Draw(newImg)
# pick color for each line by a gradient spectrum
nbins = len(lines)
colorRange =list(
Color('black').range_to(
Color('blue'), nbins))
# Print symbols to image
leftpadding=0
y = 0
lineIdx=0
for line in lines:
color = colorRange[lineIdx]
lineIdx +=1
draw.text((leftpadding, y),
line, color.hex, font=font)
y += letter_height
Step 5: save the image
# Save the image file
newImg.save('result.jpg')
Click here to download the sample code. Change the input file name in the bottom section and try it with your own image!
Leave a Comment