ASCII Art in 5 minutes

About

ASCII Art is a technique to represent an image using only printable ASCII symbols.
For sure it's not possible to convert any image to such textual format, but there are ways how to achive such approximation.
In this mini-project we will use Golang to transform an image to ASCII art and will learn how to:

Link to Wikipedia: https://en.wikipedia.org/wiki/ASCII_art

Live coding

Here is a live coding session that shows how it can be done.

Duration: 2:49 (coding - 2:24)

Disclaimer: never use this code in production. It was created for fun.

Breakdown

Let's break down the solution and comment on some complex or interesting things.

0:20 - As starting point we'll use an image with Golang logo. This image is nice as it has some contrast - some light and dark areas with clear borders. Usually such type of images give best results for ASCII art.

0:40 - define loadImage function that will load image from a file system. Note that we are importing image/png package - this way we instruct Golang which decoder should be used for loading the image.
For consistency we are checking that there are no errors and the image is loaded properly.

1:07 - define grayscale function that will convert RGB color to a grayscale component.
There are several ways how to do it and in this case we will use a formula that was used for NTSC analogue television encoding system.
The conversion rule is: grayscale = 0.299 * R + 0.587 * G + 0.114 * B
Note that Golang does not allow to multiply int (or in this case uint32) with float64 constants. Therefore we need to do some type conversion here.

1:20 - obviously one pixel of a real image cannot be mapped one-to-one to an ASCII character as ASCII character on the screen takes much more space. For example image 400 by 400 pixels is a rather small image, however you will not find a terminal with 400 colums or 400 rows.
That's why we need to group or scale a number of pixels and replace them with one single "average" pixel. This is what avgPixel function does.
It will convert rectangle (x, y) - (x+w, y+h) of image img to grayscale and will find average value of all pixels in this rectangle.
Note that we are doing some range checks here to avoid going out of the image boundaries.

1:47 - now we can just iterate through our image and find average pixel values.
By doing some experiments I came to conclusion that the good ratio is around 2:1 to look nice on terminal screen. The bigger scaleX and scaleY values the smaller (and with less quality) the resulting ASCII art will be.
The funny part is - how to convert grayscale value to an ASCII character? There are some examples of greyscale ramps - you can check more details here.
For example we can use $@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'. or .:-=+*#%@, but I decided to go with even more simple ramp:
@#+=.
General idea is - the darker color is the more "dense" ASCII character should be.
Notice that we are dividing the value of "average pixel" by 65536 - this is because Golang's color.Color.RGBA() returns red, green and blue components as 16-bit numbers having 65536 possible values each.
Now we are ready to output every character. And don't forget about line breaks.

2:19 - running the application for the first time to check results in the console.

2:33 - the smaller scale is the better results we get.
You can play arround with different ramps (e.g. the the one above) and with different scales to find out what gives better result.

Resources

Sources: https://github.com/5minute/examples/tree/main/ascii-art