Create Ascii pictures to enrich your chatting meme

Jifan Zhang
5 min readJul 22, 2020

Preface

Following the Stay At Home order, I’ve been spending an increasing amount of time online chatting with my friends. One day, my friend send me a meme:

Take a close look, this picture consists of pure ASCII printable characters! 🆒! I Googled the original image and found the original one:

Translate: “秀啊” means you are awesome. “狗子” literally means doggie, but here works as a pronoun. This meme is typically used when your friend or buddy doing something extremely carefully, but got a funny result like⬇️:

In this war of memes, I want to reply with similar Ascii memes. How to generate those images?

Intuition first

Take a careful look at the ASCII meme. The characters like ‘.’ and ‘:’ replaces white pixels, while characters like ‘@’ and ‘#’ replace darker/colored ones.

It gives us an intuition that we can use pointy characters to represent white pixels, and use space-consuming characters to represent colored/dark pixels. 🆒! How do we quantitatively know which character is pointy or space-consuming? Character encoding bitmap gives an answer.

The answer lies in the character bitmap encoding. There’s a ton of stories regarding character encoding history. Essentially we should know that there are three types of fonts that computer stores each printable character, namely Bitmap fonts, Vector fonts, and Stroke fonts. Here we will go with Bitmap fonts.

Bitmap fonts stores each character in a dot matrix format. Take this the ‘a’ picture as an example, it stores ‘a’ in a matrix of size (7, 5). Traditionally computer scientists store the dark pixels with 1 and empty pixels with 0.

One bitmap font representing character ‘A’

In this bitmap font, we can use 14 dark pixels to represent character ‘A’. 🆒 That’s the quantitative number we want!✌️

Computer ready, let’s go!

Packages Importing

import requests
from bs4 import BeautifulSoup
from PIL import Image, ImageDraw
from io import BytesIO
import numpy as np
  • requests: Handeling http requests 📡
  • BeautifulSoup: Powerful HTML parsing package 💡
  • Pillow: Image manipulation 🌠🏞🌉
  • io: Deal with various types of IO 📥📤
  • numpy: Awesome array computation package ✈️

Retrieve the character encoding bitmap

res=requests.get("http://www.piclist.com/techref/datafile/charset/8x12.htm")
soup=BeautifulSoup(res.text)
text=soup.find("pre").text
print(text)
filtered=list(filter(lambda x: len(x)>10,text.split(' ')))

First, politely👔 request the 8x12 bitmap font from the URL, and then parse it. Now for each ASCII character, we have an array representation in the list named filtered.

For example, [0,24,60,60,60,24,24,0,24,24,0,0] represents character ‘!’. Why is that? The following picture explains the reason. The number on the right equals the middle arithmetic formulation.

Aha👏! Now for each row, we can calculate the number of pixels colored in black with the array. Mathematically, the number of black pixels in each row is a function of the respective array value, but not vice versa. Here we implement that function with:

def bit_count(num: int) -> int:
res=0
while(num>0):
res+=num&1
num=num>>1
return res

We can use bitwise operators: bit AND, and bitwise shift operations: right logical shift. The functions swallows an integer number and calculate the number of “black pixels” as the return value.

Character mapping

Let’s map🗺 number of pixels with characters using that defined function!

# Characters we use to make the ASCII meme 
keys=[".",":","!","@","#","$","%","^","-","+"]
symbol_to_count={}for symbol in keys:
count=0
for num in filtered[ord(symbol)].split(','):
count+=bit_count(int(num))
symbol_to_count[count]=symbol
print(symbol_to_count)#{4: '.', 7: '-', 8: ':', 12: '^', 14: '+', 21: '%', 22: '!', # 38: '$', 42: '#', 44: '@'}

As you see here, the most space-consuming character is ‘@’, which occupies 44 pixels, while the most pointy one is ‘.’, which takes only 4 pixels.

Then, we want to scale-up the dictionary keys to the same scale as the color encoding range [0, 255], so that we can directly map pixel values to ASCII characters.✅

Note: RGB pixel channels are generally encoded with 8-bits unsigned integer.

minimum=min(symbol_to_count.keys())
maximum=max(symbol_to_count.keys())
symbol_to_value={(key-minimum)*255/(maximum-minimum):val for key,val in symbol_to_count.items()}keys=sorted(symbol_to_value.keys())
n_key=len(keys)
print(symbol_to_value)#{0.0: '.', 19.125: '-', 25.5: ':', 51.0: '^', 63.75: '+',
# 108.375: '%', 114.75: '!', 216.75: '$', 242.25: '#', 255.0: '@'}

Done!👍

Moreover, we should build🛠 another function that returns a character for each input pixel value. For example, if I pass a value of 20, I should get ‘-’ because 20 is closest to 19.125.

Here, the function find_closest takes an unsigned integer as an argument and returns a replacing character. Here we can apply the binary search algorithm, with worst-case complexity O(log n).

def find_closest(val) -> str:
low=0
high=n_key-1
while(high-low>1):
mid=low+int((high-low)/2)
if(val>keys[mid]):
low=mid
else:
high=mid
else:
key=keys[high] if val>(keys[high]+keys[low])/2 \
else keys[low]
return symbol_to_value[key]
# If we call find_closest(200), we'll get this all-human-love character: '$'.

We’ve done all the preparation work. Now, ASCII-Art 101, let’s draw the picture! 🖼

Create ASCII memes

Now we’ll create ASCII meme for this new meme⬇️

Translation: “愣住” literally means shocked. Here means “I feel stupid-fied and don’t know what to do.”
res=requests.get("https://img4.duote.com/duoteimg/dtnew_techup_img/douyin/20191217153654_77217.jpg")img=Image.open(BytesIO(res.content)).resize((200,100))
arr=np.array(img)
arr=255-np.mean(arr,axis=2)
display(img)

First, politely👔 request the picture from the URL. Then read the image and resize it. Finally, make a pixel-wise black and white shuffle🔁, so that we can directly apply the find_closest function.

Note: The height of a character is typically larger than the width. We’ll resize the original picture to a landscape shaped image, so that after replacing pixels with characters, the output image would look just normal.

Final step: Paint it!

img = Image.new('RGB', (1200, 1000),color='white')
hand = ImageDraw.Draw(img)
for i in range(len(arr)):
s=[]
for num in arr[i]:
s.append(find_closest(num))
hand.text((2, 2+10*i), ''.join(s), fill=(0, 0, 0))
display(img)

Now I’ll reply this ASCII meme to my friend. 😉

Ending words

Feel free to add characters to the candidate list and generate a better-carved meme. Thanks for following me all the way here.☺️

References

  1. “秀啊!狗子” image link http://www.kanqq.com/wx/190548.htm
  2. Character encoding https://en.wikipedia.org/wiki/Character_encoding
  3. 8x12 ASCII character bitmap http://www.piclist.com/techref/datafile/charset/8x12.htm
  4. “愣住” image link https://img4.duote.com/duoteimg/dtnew_techup_img/douyin/20191217153654_77217.jpg

--

--