Last Updated on July 24, 2012
IntroductionIf you've ever spent a significant amount of time browsing around the Internet, you've undoubtedly encountered ASCII art, a type of graphic artwork that represents pictures using monospaced text characters in the ASCII character set. Personally, I've always found ASCII art to be rather enjoyable, both to view and produce. Unfortunately, the various image to ASCII art conversion websites don't always offer a wide range of options for image conversion. Therefore, it's sometimes useful for a programmer to create his or her own ASCII art convertor, allowing for an infinite range of customization.
There are a variety of different algorithms that can be used to convert an image to ASCII art, all with varying types of output. In this article, I will describe an algorithm that converts a bitmap image to a solid grayscale ASCII art graphic. Additionally, I will describe how to modify the algorithm for color output.
Algorithm ExampleBefore the algorithm is described in detail, I'd like to show an example of a conversion completed using the algorithm. Please note that this algorithm allows for a wide range of customization. Therefore, the following images represent an application of the algorithm with a single defined set of preferences. One image rendered in monochrome is shown, and one image rendered in color is shown. Please click on the images to zoom in.
Leonardo da Vinci's Mona Lisa
Choosing a FontBefore we can actually perform any sort of conversion, we need to choose the font with which to render the output. Whether you render the output directly to another bitmap or onto a webpage, significant consideration should be taken when choosing the font. First, be sure that you use a monospaced font. Although using a non-monospaced font might produce results that slightly resemble the original image, my experience has shown that it's best to use monospaced fonts to ensure an optimal conversion. Furthermore, the majority of ASCII art on the web has been rendered in a monospaced font.
A variety of monospaced fonts can be downloaded for free on the Internet. Courier and Courier New are two monospaced fonts commonly found on Windows and Mac machines. As a result, much of the ASCII art found on the Internet has been created using these fonts.
An example of Courier font
Depending upon your personal preferences, you may want to choose a font that lacks anti-aliasing, or at least disable the anti-aliasing on the font when rendering the output. I find that turning off anti-aliasing gives a more traditional ASCII art feeling to the produced output.
Choosing a Character SetThe character set is the set of all ASCII characters which are capable of appearing in the generated ASCII artwork. Prior to the conversion process, each character in the specified character set will be analyzed. Therefore, it is important that the character set be chosen with care.
Note that the character set should only contain printable characters, as ASCII control characters cannot be used as a visible part of ASCII art. You may wish to choose the entire range of printable ASCII characters as your character set, but this is not necessary. For example, if you wish to exclude punctuation from the produced ASCII art, you can limit the character to set to all alphanumeric characters.
Choosing an ImageFor best results, I recommend using a high-resolution image for conversion. Although a low-resolution image can produce output resembling the input, using a high-resolution image will allow you a greater degree of customization during the conversion, generating much better results.
Character ProcessingThe first step of the conversion process requires us to analyze the graphic data corresponding to each character in the character set. To begin, we calculate the average brightness of each character.
First, we take the bitmap for a single character and iterate over each pixel. For every pixel, we use its RGB value to calculate its brightness according to the following formula.
Formula 1: Average brightness of a pixel
We then sum together the brightnesses of all pixels in the character's bitmap and divide by the bitmap's area. This will give us a value representing the average brightness of the character. This sum can be represented by the following formula:
Formula 2: Average brightness of a bitmap
where R(x, y), G(x, y), and B(x, y) represent the red, green, and blue values respectively for a pixel at coordinates (x, y), u(a, b, c) corresponds to the aforementioned brightness formula, and W and H are the width and height of the character's bitmap respectively.
This computation should be repeated for every character found in the character set. After we process a single character, we store the character itself and its average brightness as <brightness, character> pairs in a sort of look-up table. This look-up table will allow us to convert from a given average brightness to its corresponding character. A variety of data structures may be used for this purpose.
Be aware that you may come across the situation where two characters both have the same average brightness. During the conversion, we will want only a single character returned for a given average brightness, so it's up to you to decide how to choose which character to return. If two characters have the same average brightness, you might want to return either randomly, or you may wish to skip the inclusion of any characters whose average brightnesses are already present in the look-up table.
Table OptimizationNow that we have our look-up table prepared, we could go ahead with the image conversion and receive a piece of ASCII art that will somewhat resemble the input image. However, I have found that performing a relatively simple optimization on the table will often yield better results.
At the moment, we have a table of characters with brightnesses ranging from a minimum value min to a maximum value max. When we divide our image for processing (which will be covered in the next section), we will find that the average brightnesses of the subsections will range from 0 and 255. Therefore, if we extend our set of character brightnesses lying between [min, max] to cover the range [0, 255], the output ASCII art will contain characters with more visibly contrasting brightnesses. This will make our output resemble the input image much more closely.
In order to perform this scaling optimization, we will need a function f to convert any input value x in the interval [min, max] to the appropriate output value in the interval [0, 255]. The following formula can be used for this purpose:
Formula 3: Scaling x in [min, max] to [0, 255]
We populate our optimized look-up table by looping over the brightness values in the original look-up table and storing the scaled brightnesses with their original pair characters. For each brightness-character pair <b, c> in the original look-up table, we store <f(b), c> in our new look-up table. After each original pair is processed in this manner, our scaling optimization will be completed.
Image ProcessingNow that we've analyzed each character in the character set, we can move on to processing the image, where the actual conversion will begin. We start with the bitmap of our image, which we divide into smaller groups of pixels, each with a width W and a height H. The actual values chosen for W and H are up to you. I find that it's best to experiment with using different values for each image processed using the algorithm. Different resolutions may call for different values of W and H. Each of these subgroups will correspond to a single ASCII character in the output.
Division of image into pixel subgroups
Next, we process each subgroup of pixels individually. Similar to when we analyzed the bitmap of a character, we iterate over the pixels of each subgroup to calculate the subgroup's average brightness using Formula 1 and Formula 2.
Now, we take the value corresponding to the average brightness of this subgroup and plug it into our look-up table. This will return an ASCII character with a brightness similar to that of this subgroup, and as a result, we will use this ASCII character to represent this subgroup in our output. Please note that the situation may arise where the average brightness of a subgroup may not be present in the look-up table, as no character in the processed character set had the same average brightness. In this case, the look-up table should be scanned in order to locate the brightness in the table with a value closest to that of the subgroup brightness.
If we process each subgroup in this manner, we will produce the set of ASCII characters corresponding to the ASCII art for the given image.
Whether you're writing the output to a console, HTML file, or bitmap file, be sure to include a line break after a single row of pixel subgroups is processed. Otherwise, you'll receive one very long string of ASCII characters that most definitely will not resemble your original image!
Adding ColorWe've now converted our original image to a piece of ASCII art composed of a multitude of monochrome ASCII characters. To give the image more dimension, you may wish to add color. In order to accomplish this, we need to figure out what color each character in the output should be. Therefore, we modify the image processing portion of our algorithm to perform additional computations using the pixel subgroups.
Immediately after calculating the average brightness of a single subgroup, we perform another iteration over the pixels in order to calculate the average color of the subgroup. This average color will be an ordered triple in the RGB color space. It is computed by independently summing the values corresponding to the red, green, and blue components of every pixel in the subgroup and dividing by the subgroup's area. The formula for average color is as follows:
Formula 4: Average color of a pixel subgroup
where R(x, y), G(x, y), and B(x, y) represent the red, green, and blue values respectively for a pixel at coordinates (x, y), and W and H are the width and height of the character's bitmap respectively.
The ASCII character corresponding to the subgroup can then be printed in this average color. When this process is repeated for every subgroup, the output will be a wonderful piece of color ASCII art!
ConclusionBy operating on individual subgroups of pixels and matching average brightnesses to ASCII characters, we can accurately convert a bitmap to a corresponding piece of ASCII artwork. Furthermore, we can improve the quality of our output by computing the average color of each subgroup and rendering our ASCII art in color. I hope you've enjoyed this tutorial, and I wish you luck in your ASCII conversions! If you have any questions or comments, feel free to email me.