# Algorithms to “anti-alias” (or somehow improve) binary 1-bit drawings and fonts

These days anti-aliasing uses gray scale pixel values on displays with high pixel counts.

I'd like to take a step back in time and learn what is available for anti-aliasting or at least improving the quality of curved lines and fonts drawn on bindary (on/off) 1-bit displays, such as these low cost OLED displays shown below.

I don't know if the procedure would be called anti-aliasing, or something else. Ideally an explanation or a link/reference to a mathematical procedure or algorithm would be most helpful, or if something exists in Python, that would be great as well.

PIL, the Python Image Library is wonderful but I have a hunch it is mostly useful for gray scale or RGB continuous tone images rather than binary 1-bit images.

See for example the question Drawing multilingual text using PIL and saving as 1-bit and 8-bit bitmaps, where PIL turned this gray scale image:

into this 1-bit binary image:

and similar things would happen if I drew circles in PIL and then converted to 1-bit. The antialiasing is designed for continuous tone display, and so the subsequent thresholding just makes a mess.

Example of a 1-bit binary OLED display (128x64 pixels) cropped from this image from the AdaFruit Page Monochrome 0.96" 128x64 OLED graphic display, Product ID: 326.

• I don't know all the details of your specific usage case, but if you have a decent refresh rate, you could get anti aliasing by faking greyscale through rapidly turning on and off individual pixels. There's more to it than just that though, so you might find these two write ups interesting: blog.demofox.org/2017/10/31/… blog.demofox.org/2017/11/03/… – Alan Wolfe Nov 30 '17 at 17:11
• @AlanWolfe that's a great idea! For this particular class of units the data rate is slow and there are no extra data buffers to toggle between, but I will keep this in mind, and if I have a unit where this would be possible I'll give it a go. Thanks! – uhoh Dec 1 '17 at 0:45

PIL's Image.convert function performs dithering by default when you convert the image to 1-bit—not simply thresholding. That's what creates the noise along the edges of the shapes; the antialiasing isn't the problem. Unfortunately, at such a low resolution, the dithering does more harm than good.

I grabbed your original image from your other question and did a straight threshold at 128, and it comes out looking a lot better:

That's probably about as good as you'll be able to do on a low-resolution 1-bit display, absent a bitmap font that's specifically designed for those circumstances.

In PIL this operation can be implemented using Image.point, as follows:

image1Bit = image8Bit.point(lambda x: 0 if x < 128 else 1, mode='1')

• altough to be honest if you touchup a few pixels you can get a good compromize between the two. – joojaa Nov 30 '17 at 21:17
• Excellent! This is exactly what I needed, thank you. – uhoh Dec 1 '17 at 0:33

Have a look at Improved Alpha-Tested Magnification for Vector Textures and Special Effects [Gre07] (pdf). The gist of their method is to store distance field information in the font texture instead of bitmap glyphs. This information is then used to build much higher quality up-scaled versions of the glyphs. They even describe a rendering path without programmable shading that gives ok results.

• Thanks for the link. Since I'm new to this having something like this to read is very helpful. – uhoh Dec 1 '17 at 0:37

This is just a supplementary answer based on the accepted answer, using the original 64x128 pixel images instead of the zoomed-in interpolated screen shot.

imtype = "RGB": (true RGB)

imtype = "L": (grayscale 8-bit)

imtype = "1": (binary 1-bit, threshold = 128)

imtype = "1": (binary 1-bit, threshold = 80)

Here are screenshots zoomed-in for those without their glasses:

(screenshot of 1-bit, threshold = 128)

(screenshot of 1-bit, threshold = 80)

from PIL import Image, ImageDraw, ImageFont
w_disp   = 128
h_disp   =  64
fontsize =  32
text     =  u"你好!"

imageRGB  = Image.new("RGB", (w_disp, h_disp))
draw      = ImageDraw.Draw(imageRGB)
font      = ImageFont.truetype("/Library/Fonts/Arial Unicode.ttf", fontsize)
w, h      = draw.textsize(text, font=font)

draw.text(((w_disp - w)/2, (h_disp - h)/2), text, font=font)

imageRGB.save("NiHao! " + "RGB" + ".bmp")

image8bit = imageRGB.convert("L")

image8bit.save("NiHao! " + "L" + ".bmp")

threshold = 80
image1bit = image8bit.point(lambda x: 0 if x < threshold else 1, mode='1')
image1bit.save("NiHao! " + "1" + " threshold = " + str(threshold) + ".bmp")

threshold = 128
image1bit = image8bit.point(lambda x: 0 if x < threshold else 1, mode='1')
image1bit.save("NiHao! " + "1" + " threshold = " + str(threshold) + ".bmp")


In the olden days, for things like fonts, they didn't do antialiasing. They generally hand made bitmap fonts because it generated the clearest, easiest-to-read results.

That said, you could look into error diffusion, halftoning or other dithering techniques for changing continuous-tone images into 1-bit images. These are techniques for reducing the bit depth of images.

• Ah! for ellipses (and presumably arbitrary angle lines) PIL does draw fairly "OK-looking" 1-bit curved lines without standard antialiasing, i.stack.imgur.com/HGDox.png and in fact, this answer shows how to do more arbitrary curves. So combining those with a search for historical bitmapped fonts, I should be good to go! If you'd like to include this info into your answer I'll delete this comment. Thanks! – uhoh Nov 30 '17 at 7:08
• The problem is, all those techniques trade off spatial resolution, and it's not like these displays have pixels to spare. – Dan Hulme Nov 30 '17 at 10:34