Introduction
Yesterday, my neighbour dared me to write an ASCII art generator in Python in less than half an hour. As the honourable knight I am I accepted the challenge. Most generators of this kind work the same way. Looking at every pixel, they map a letter to the corresponding colour and use that. Of course, good ASCII artists use the pixel density of certain characters to emulate dark and light regions of the image, but unfortunately computers don’t own any sense of aesthetics.
So after writing a simple ASCII art generator in less than 10 minutes (this will be left as an exercise to the reader) we started discussing how it could be improved. At that time, the programme used the chr() function which takes an integer argument between 0 and 255 inclusive. Fortunately this corresponds exactly to the amount of color in each RGB channel, so I used it to map map colours to characters from the ASCII alphabet by calculating the average of the RGB channels, modulus the number of letters in the alphabet, and then adding 97 (thus getting the characters from A-Z).
We discovered that this was far from optimal since dark areas would sometimes seem light and light areas dark. Also, capital Roman letters don’t seem to have a very high pixel density (which is nice for printing, but useless for art) making dark areas very bright.
Experiment
But these were all just assumptions to be proved or disproved, so today, while waiting for my group mates at the university, I sat down and wrote another Python script for estimating the pixel density of any printable ASCII character. To full source code of the programme is shown below and should be pretty self-explanatory.
import sys
from operator import itemgetter
import ImageFont, ImageDraw, Image
font_file = sys.argv[1]
font_size = sys.argv[2]
brightness_map = {}
font = ImageFont.truetype(font_file, int(font_size))
# only printable characters are of interese, these are
# located between 32 and 126.
for i in range(32, 126):
c = chr(i)
# get the size of the bounding box of this character
# as if it had been printed.
size = font.getsize(c)
width, height = size
image = Image.new('RGB', size, (0, 0, 0))
draw = ImageDraw.Draw(image)
# draw the text to the canvas
draw.text((0, 0), c, font=font)
del draw
pixels = image.load()
brightness = 0.0
for x in range(width):
for y in range(height):
r, g, b = pixels[x, y]
# find the average of the red, green and blue
# components (corresponds to making the pixel
# grayscale) and normalize the value.
brightness += (r + g + b) / 3.0 / 255.0
# normalize the brightness by dividing it by the area
# of the character and get brightness percentage.
brightness_map[c] = brightness / (width * height) * 100
sorted_map = brightness_map.items()
sorted_map.sort(key = itemgetter(1), reverse=True)
for k, v in sorted_map:
print v, k
Results
Of course certain fonts have different pixel density characteristics, so test using different fonts to get the best end-result. However, the relative percentage is largely the same, so the following table (read from left to right) should cover most monospaced fonts.
- ------------- - ------------- - ------------- - ------------- - ------------- - -------------
@ 35.2881758764 $ 34.7593582888 B 32.1984551396 W 32.0855614973 # 31.5567439097 M 30.3089720737
Q 29.9465240642 8 29.156268568 E 28.7878787879 R 28.2412358883 N 27.3440285205 9 26.5002970885
5 26.4884135472 D 26.357694593 0 26.3101604278 6 26.2982768865 & 25.8704693999 H 25.7575757576
g 25.4842543078 G 25.193107546 O 24.789067142 S 24.4206773619 3 23.9275103981 % 23.8799762329
U 23.8562091503 P 23.7551990493 K 22.8817587641 F 22.7272727273 I 22.7272727273 p 22.3351158645
2 22.2043969103 Z 22.192513369 b 22.073677956 A 21.990493167 d 21.7349970291 q 21.6815210933
4 21.4022578728 w 20.3802733214 C 20.2733214498 h 20.2554961378 a 19.8752228164 [ 19.696969697
] 19.696969697 1 19.5306001188 { 19.275103981 } 19.1087344029 J 19.0493166964 X 18.6868686869
e 18.5977421272 k 18.53238265 V 18.5264408794 l 18.1818181818 i 18.1818181818 o 17.3915626857
f 17.3559120618 u 17.3083778966 n 17.201426025 j 17.1420083185 L 16.6666666667 T 16.6666666667
z 16.5715983363 7 16.5300059418 Y 15.4010695187 s 15.3951277481 t 15.3832442068 = 15.1515151515
> 15.1039809863 < 15.0029708853 ? 14.5098039216 ) 13.7730243613 ( 13.7195484254 | 13.6363636364
+ 13.6363636364 x 13.3749257279 c 13.2501485443 m 12.9513623631 v 12.0142602496 r 9.97029114676
^ 9.79203802733 / 9.37017231135 \ 9.33452168746 _ 9.09090909091 * 9.02554961378 ! 8.16399286988
y 7.7769289534 " 7.45098039216 ; 6.06654783125 , 4.5513963161 - 4.54545454545 ' 3.70766488414
: 3.0303030303 ` 2.63814616756 . 1.51515151515 0.0
- ------------- - ------------- - ------------- - ------------- - ------------- - -------------
Now that we know the optimal characters to represent certain ranges of brightness we are able to construct the ASCII representation of any image. This is done by iterating over every pixel in the image, calculating its brightness (that is, the average of its Red, Green, and Blue components), and using the appropriate character for that brightness.