Merikanto

一簫一劍平生意,負盡狂名十五年

Convert an Image to ASCIIs in Python

Today we’re going to convert images to ASCII texts. We’ve all seen pictures like this before:

Example

The transformed ASCII texts can be viewed as a collection of a bunch of characters, and each character represents a pixelated color.


Black-and-White Output

Let us first try to convert images to black-and-white texts. We use a simple grayscale formula for conversion:

1
gray = 0.2126 * r + 0.7152 * g + 0.0722 * b

We are also going to use two Python modules: PIL (Python Image Library) and argparse. The code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from PIL import Image
import argparse

r = argparse.ArgumentParser()

r.add_argument('file')
r.add_argument('-o', '--output')
r.add_argument('--width', type = int, default = 100)
r.add_argument('--height', type = int, default = 100)

a = r.parse_args()

IMG = a.file
W = a.width
H = a.height
OUTPUT = a.output

ascii = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")

def map(r, g, b, al = 256):
if al == 0:
return ' '
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
unit = (256.0 + 1) / len(ascii)
return ascii[int(gray / unit)]

if __name__ == '__main__':
m = Image.open(IMG)
m = m.resize((W, H), Image.NEAREST)
c = ""

for i in range(H):
for j in range(W):
p = m.getpixel((j, i)) # p[0], p[1], p[2], p[3]
c += map(*p)
c += '\n'
print(c)

if OUTPUT:
with open(OUTPUT, 'w') as f:
f.write(c)
else:
with open("ascii.txt", 'w') as f:
f.write(c)

We use the image below to test our code:

Test

And below is the screenshot of the output ascii.txt:

Output1

We noticed that the proportion of the output isn’t quite right. Moreover, it would better if the output is colored, but not just black-and-white. So let us make some improvements.


Colored Output

In the previous case, the output format is in .txt form, and we get a black-and-white output. Now if we want to get a colored output, then the format cannot be .txt anymore, since plain text files do not display colored texts. To achieve this, we need the submodule ImageDraw from the PIL library.

As to the proportion of the output, we’re going to set the width and height of the output, and also adjust the font to get the optimum result. Hence one more submodule from PIL is needed: ImageFont.

Enough for the explanation. The code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import argparse
from PIL import Image, ImageFont, ImageDraw

r = argparse.ArgumentParser()
r.add_argument('file')
r.add_argument('-o', '--output')

ag = r.parse_args()
Pic = ag.file
OUTPUT = ag.output

set = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")

def map(r, g, b, a = 256):
if a == 0:
return ' '
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
unit = (256.0 + 1) / len(set)
return set[int(gray / unit)]


if __name__ == '__main__':
m = Image.open(Pic)

# Don't forget to set the W & H
W = int(m.width / 6)
H = int(m.height / 15)
m_txt = Image.new('RGB', (m.width, m.height), (255, 255, 255))
m = m.resize((W, H))
t = ''
color = []

for i in range(H):
for j in range(W):
p = m.getpixel((j, i))
color.append((p[0], p[1], p[2]))
t += map(*p)
t += '\n'
color.append((255, 255, 255))

# Adjust the fonts
draw = ImageDraw.Draw(m_txt)
font = ImageFont.load_default().font
x = y = 0
fw, fh = font.getsize(t[1])
fh *= 1.37

# Coloring the ASCIIs
for i in range(len(t)):
if t[i] == '\n':
x = x + fh
y = -fw # Note: -fw
draw.text([y, x], t[i], color[i])
y = y + fw
m_txt.save('color.png')

Using the same picture, and this time we got the output like this:

Output2