ASCII Art using Python Part 1

Nikhil Meena
6 min readMay 10, 2020

--

Note: All the inspiration and credit goes to the two pages: 1 by Robert Heaton and 2 by Shanshan Wang. This is just a documentation of my attempt to follow their instructions and explain what I learnt and understood. Feel free to go through what they have done and then come back to munch on this. Cheers!

ASCII art is the process of forming images using the ASCII character set. This article will demonstrate how to convert an existing image into an equivalent one using only ASCII characters.

Original image and results obtained after Part 1 and Part 2!

Part 1 covers the very basics of reading an image, getting the information from its pixels and mapping this information to characters. The image is then displayed on the command line. Part 2 will go into more detail on the relation between a pixel and font size, adding contrast, color gradients and saving data as an image. Read on!

0. Imports and your image

For this exercise, we will need the Image module from PIL package and good old numpy. We use Image.open(‘iron-man.jpg’) to create an image object from our file.

from PIL import Image
import numpy as np
im = Image.open("iron-man.jpg")

At this point, you can play around with the various functions in the image module to understand more about how data of an image is represented.

For example, this code:

print(im.format, im.size, im.mode)

outputs:

JPEG (640, 480) RGB     #width=640, height=480

1. Converting your image into a 2D pixel array

A common way to represent data in an image is to map each pixel to an RGB value. This is a tuple of size three which represents how much of Red, Green and Blue colour is present in that pixel. Each individual color ranges from a value of 0 (no amount of that colour) to 255 (maximum value possible). Thus, (255,215,0) represents the colour of gold while (32,178,170) represents light sea green. Can you guess the representations for pure black and pure white? This is important for our use case.

Here, we use numpy to generate our 2D array from our Image object:

iar = np.asarray(im)
print(iar)
print("Minimum pixel value: ", iar.min())
print("Maximum pixel value: ", iar.max())

which outputs:

[[[0 0 0]
[0 0 0]
[0 0 0]
...
[0 0 0]
[0 0 0]
[0 0 0]]
...[[1 1 1]
[1 1 1]
[1 1 1]
...
[0 0 0]
[0 0 0]
[0 0 0]]] #left and right corners of the image are darker
Minimum pixel value: 0
Maximum pixel value: 255

2. Converting RGB to brightness values

We can now create a brightness matrix from the data that we have. Why do we need that? Because, we are trying to represent our image in the form of characters. Since this is going to be a black and white image, the only sense of colour that we will have is how bright or dark an image is. On our command line, i.e., a white on black display, a brighter pixel will be represented by a character that appears to fill up that pixel more, for example, ‘X’, while a darker pixel may be represented by a simple ‘:’. We will come to our choice of characters in the next step.

There are multiple ways to calculate this brightness value:

  1. Average: Simply (R+G+B)/3
  2. Average the maximum and minimum values out of R, G and B: (max(R,G,B) + min(R,G,B))/2
  3. A mathematically derived 0.21R + 0.72G + 0.07B, that roughly simulates our eyes’ sensitivity to colours

More details can be found here. For our demonstration, we will use the basic average method.

Code: So I am trying to learn Python (which was one of the reasons I picked up this project) and my solution to calculate the brightness values came out like this:

height = len(iar)width = len(iar[0])brightness = np.zeros((height, width))    #create 2D arrayfor x in range(height):    for y in range(width):        r = int(iar[x][y][0])        g = int(iar[x][y][1])        b = int(iar[x][y][2])        brightness[x][y] = (r+g+b)/3

Going through the 2nd link I have shared above, I found that there is a much shorter way to do the same calculation using numpy:

iar = np.sum(iar, axis=2)   #Each tuple is replaced by its sum

I will leave the average calculation part to you. To understand more about numpy arrays and the axis meanings, look here and here. There are shorter ways to do it without numpy too. Experiment!

3. Map brightness values to characters

Which characters fall in the lower brightness range? Which is the most ‘dense’ character? Our first link gives us a good place to start:

"`^\",:;Il!i~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"

Characters go from the ‘thinnest’ to the ‘thickest’. Hence, a brightness of 0 maps to the backtick(`) character, whereas 255 maps to the dollar ($).

[Try to visualize the image that will be produced with a long list like the one above. How will a shorter list change the image? Which one would look better?]

I will leave it to you to map a brightness value (0–255) to a character in the string (just use simple math). Keep in mind the following things:

  1. Mapping a decimal value to a string index
  2. Making sure the index is within the correct range

Check your process to prevent errors.

4. Printing your image!

Now, all you have to do is print your 2D matrix on the command line. Go ahead and see your results!

5. Wait… what?

Surprised? Are you getting something like this:

Can you guess why?

Go through the code again. We are mapping each pixel to a character on the command line. Thus, if our image is 640x480, there are going to be 640 characters on each line in the output. Now, you should know that a character has a certain width of its own as well, so the line length far exceeds 640 pixels width.

What can we do to fix this? One solution is to reduce the image resolution after importing it. For example, converting a 640x480 image to a ~100x100 image. The Image module has resize functionality as well. Decide on a minimum (height, width) tuple and call it so:

size = (100, 100)
im = im.resize(size) #also checkout the thumbnail function

You can reduce the font size by editing the command line properties or zoom out if possible (Ctrl -). Play around till you can see the image!

You should notice another flaw in our code now. Your image will appear squashed, probably horizontally. This is again due to nature of the font itself. Usually, a character is longer in height and smaller in width. So for a square pixel in our image, we are now using a character that is taller than it is wider. This distorts our image. One fix is to print each character thrice.

for x in range(height):    for y in range(width):        print(ch+ch+ch, end='')    print("\n")       #try to do all this in one line for practice

You can modify the logic to see how it impacts the image produced. Use a shorter ASCII string (Shanshan uses ' .,:irs?@9B&#' ). This significantly changed the image for me. Think why the change occurs!

Try using one of the other methods to calculate the brightness values. Which one works best? Is it true for all types of images?

In Part 2, I will try to refine this process. I was really impressed with the logic Shanshan has used and how it added so much to our result. Till then, have fun!

--

--