** The Windows application port is provided by David Luu, and is not maintained by me.
Original image
ASCII Art image (colored and monochrome) [click for enlarged view]
ASCII Art image (pure color-HTML and unformatted text-only) [click for enlarged view]
Introduction
Have you ever seen a C# application that converts a given image to a text-based ASCII Art image like the ones shown above?
Well, a few months ago, I stumbled across an article4 on Code Project by Daniel Fisher which talks about creating an application that does just this. According to Daniel, he was not able to find any C# application on the web that does image-to-ASCII conversions, so he decided to write his own, and hence his article. This is yet another article on the same topic, but with a slightly more enhanced ASCII Art generation. What I did was I searched through the web, found some web sites with image-to-ASCII conversion applications in PHP, combined all their ideas (including Daniel's), and implemented another (more enhanced) version of the ASCII Art generator in .NET.
For the list of web sites from where I got all my ideas/information, or if you just want to know more about ASCII Art in general, please check out the References section given below.
Using the code (Installation)
In the source files included above, you will find the following two Visual Studio projects:
- ASCII - An ASP.NET web page that demonstrates the ASCII Art generator functionality.
- Library - A library (DLL) for generating ASCII Art.
To install the project:
- First, unzip the files to an empty directory. Let us call this directory ASCIIArt.
- Next, make a virtual directory in IIS that links to the ASCII sub-folder mentioned above.
- After that, give read permissions to the following (local) user accounts for the ASCII sub-folder:
- IUSR_<MachineName>
- ASPNET (See Note below)
- Also, give read/write permissions to the following (local) user accounts for the Images sub-folder (residing under ASCII sub-folder):
Note: On some machines (especially servers), IIS doesn't actually use the ASPNET account to run ASP.NET pages. To find out exactly what account your IIS is using, just copy and paste the following code into Notepad and save it as who.aspx, then put the file into a web-folder under IIS and view it in Internet Explorer. The page will display the correct user account that ASP.NET is running under.
<%@ Page Language="C#" %>
<%@ import Namespace="System.Security.Principal" %>
<html>
<body>
ASP.NET User Account: <b><%= WindowsIdentity.GetCurrent().Name %></b>
</body>
</html>
The library logic
The logic of my Image-To-ASCII conversion library is actually not that complicated. (No, really!) Derived from IMG2ASCII1, Boosty's ASCII Artist2, Daniel Fisher's ASCII Art with C# 4, ASCGEN 6, and Playing with ColorMatrix8, the basic idea is as follows:
- From the set of allowed ASCII characters given by the user, find the brightness/luminance of each character from a pre-defined XML data file. (Data mainly based on the ASCII.sql from IMG2ASCII 1)
- Construct an array of the above characters sorted by their brightness/luminance.
- Resize the given image to the proper scale/dimension. There are two ways of doing this. The simple way is to use
System.Drawing.Bitmap
's built-in image resize feature:
using System;
using System.Drawing;
public GetResizedImage (Image originalImage,
int newWidth, int newHeight)
{
return (new Bitmap (originalImage, newWidth,
newHeight));
}
The above method is fast, but may not produce a high-quality resized image, and you don't have the option to select only a portion/section of the image to work with. The other alternative is to use System.Drawing.Graphics
, as follows:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
public GetResizedImage (Image originalImage,
Rectangle section, int newWidth, int newHeight)
{
Bitmap resizedImage = new Bitmap (newWidth,
newHeight);
Rectangle imageArea = new Rectangle (0, 0,
newWidth, newHeight);
Graphics g = Graphics.FromImage (resizedImage);
g.InterpolationMode =
InterpolationMode.HighQualityBicubic;
g.DrawImage (originalImage, imageArea, section.X,
section.Y, section.Width, section.Height,
GraphicsUnit.Pixel);
return (resizedImage);
}
- Now, if you need to modify the brightness, contrast, saturation, and gamma of the image, do the following:
using System;
using System.Drawing;
using System.Drawing.Imaging;
public GetTransformedImage (Image resizedImage,
float brightness, float contrast,
float saturation, float gamma)
{
Bitmap transformedImage =
new Bitmap (resizedImage.Width, resizedImage.Height);
Rectangle imageArea =
new Rectangle (0, 0, resizedImage.Width,
resizedImage.Height);
ImageAttributes transformData = new ImageAttributes();
transformData.SetColorMatrix (
CreateColorMatrix (brightness, contrast, saturation));
transformData.SetGamma (gamma);
Graphics g = Graphics.FromImage (transformedImage);
g.DrawImage (resizeImage, imageArea, imageArea.X,
imageArea.Y, imageArea.Width,
imageArea.Height, GraphicsUnit.Pixel,
transformData);
return (transformedImage);
}
Now, at this stage, you may be wondering whether we can merge Step 3 and 4 together (assuming you use the second resizing method above), the short answer is: yes. The merged version will look like the following:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
public GetResizedAndTransformedImage (
Image originalImage, Rectangle section,
int newWidth, int newHeight,
float brightness, float contrast,
float saturation, float gamma)
{
Bitmap newImage = new Bitmap (newWidth,
newHeight);
Rectangle imageArea = new Rectangle (0, 0,
newWidth, newHeight);
ImageAttributes transformData =
new ImageAttributes();
transformData.SetColorMatrix (CreateColorMatrix(
brightness, contrast, saturation));
transformData.SetGamma (gamma);
Graphics g = Graphics.FromImage (newImage);
g.InterpolationMode =
InterpolationMode.HighQualityBicubic;
g.DrawImage (originalImage, imageArea,
section.X, section.Y, section.Width,
section.Height, GraphicsUnit.Pixel,
transformData);
return (newImage);
}
However, there is a catch in using the above merged method. To illustrate this, consider the case where you want to resize a HUGE image down to a small size and then convert it to ASCII Art. If you use the merged method, you will need to transform the entire image before the resizing takes place. This takes a lot of time. Therefore, it is better to resize it first before transforming it, hence the two separate methods given above.
Of course, you may argue that if somebody wants to resize a small image to a larger one and then transform it, the above situation will be reversed. But the fact is that this doesn't happen often in real life, so using the two separate methods will still be a better option.
But then, you could always implement both ways and choose the correct way depending on the original vs. resized size ratio. I leave this up to you.
- In Step 4 above, we use a custom method called
CreateColorMatrix()
. This method simply calculates a proper 5x5 color matrix for the specified brightness, contrast, and saturation. This color matrix is used for transforming the image by means of matrix multiplication. In fact, the construction of this matrix is also based on matrix multiplication. In this article, I will not talk about how matrix multiplication works. But if you want to know more about it, you can check out the MSDN Site10 for an excellent explanation on this topic in relation to the usage of the ColorMatrix
object.
Warning: Implementing the CreateColorMatrix()
method involves a lot of mathematical details. If you find this boring, simply skip the details given below and go straight here for the source code of this method.
The following shows the actual matrix values of the brightness, contrast, and saturation matrices:
Brightness Matrix Contrast Matrix Saturation Matrix
R G B A W R G B A W R G B A W
R [1 0 0 0 0] R [c 0 0 0 0] R [sr+s sr sr 0 0]
G [0 1 0 0 0] G [0 c 0 0 0] G [ sg sg+s sg 0 0]
B [0 0 1 0 0] B [0 0 c 0 0] B [ sb sb sb+s 0 0]
A [0 0 0 1 0] A [0 0 0 1 0] A [ 0 0 0 1 0]
W [b b b 0 1] W [t t t 0 1] W [ 0 0 0 0 1]
b = brightness c = contrast s = saturation
t = (1.0 - c) / 2.0 sr = (1 - s) * lumR
Legend sg = (1 - s) * lumG
R = red sb = (1 - s) * lumB
G = green
B = blue lumR = 0.3086 or 0.2125
A = alpha (transparency) lumG = 0.6094 or 0.7154
W = white (always = 1) lumB = 0.0820 or 0.0721
- The brightness matrix is a simple translation
matrix on the RGB elements.
- The contrast matrix is a scaling matrix on the RGB elements.
The extra translation parameters in the contrast matrix is used
for shifting the base color (when c = 0)from black to gray.
- The saturation matrix re-adjust the RGB color distribution so
that at s = 0, R = G = B = luminance (brightness in grayscale).
Notice that the saturation matrix has three special constants: lumR
, lumG
, and lumB
. These represent the proportion of each RGB value that contributes to the luminance (brightness) value. In short, luminance for a pixel is calculated as follows:
double luminance =
0.2125 * red + 0.7154 * green + 0.0721 * blue;
double luminance =
0.3086 * red + 0.6094 * green + 0.0820 * blue;
0.299 * red + 0.587 * green + 0.114 * blue;
From the above information, we can calculate the proper color matrix to transform a given image. To use all three matrices, we need to multiply them together into one single transformation matrix (using matrix multiplication). The result of multiplication is as follows:
R G B A W R G B A W R G B A W
R [1 0 0 0 0] R [c 0 0 0 0] R [sr+s sr sr 0 0]
G [0 1 0 0 0] G [0 c 0 0 0] G [ sg sg+s sg 0 0]
B [0 0 1 0 0] X B [0 0 c 0 0] X B [ sb sb sb+s 0 0]
A [0 0 0 1 0] A [0 0 0 1 0] A [ 0 0 0 1 0]
W [b b b 0 1] W [t t t 0 1] W [ 0 0 0 0 1]
Brightness Matrix Contrast Matrix Saturation Matrix
R G B A W
R [c(sr+s) c(sr) c(sr) 0 0 ]
G [ c(sg) c(sg+s) c(sg) 0 0 ]
===> B [ c(sb) c(sb) c(sb+s) 0 0 ]
A [ 0 0 0 1 0 ]
W [ t+b t+b t+b 0 1 ]
Transformation Matrix
So, based on the above derived transformation matrix, we can proceed to implement the CreateColorMatrix()
method:
using System;
using System.Drawing.Imaging;
private const float LumR = 0.3086f;
private const float LumG = 0.6094f;
private const float LumB = 0.0820f;
private ColorMatrix CreateColorMatrix (float brightness,
float contrast, float saturation)
{
if (brightness < -1f) brightness = -1f;
if (brightness > 1f) brightness = 1f;
if (contrast < 0f) contrast = 0f;
if (saturation < 0f) saturation = 0f;
float Wf = (1f - contrast) / 2f + brightness;
float Rf = (1f - saturation) * LumR * contrast;
float Gf = (1f - saturation) * LumG * contrast;
float Bf = (1f - saturation) * LumB * contrast;
float Rf2 = Rf + saturation * contrast;
float Gf2 = Gf + saturation * contrast;
float Bf2 = Bf + saturation * contrast;
return (new ColorMatrix (new float[][]
{
new float[] {Rf2, Rf, Rf, 0f, 0f},
new float[] {Gf, Gf2, Gf, 0f, 0f},
new float[] {Bf, Bf, Bf2, 0f, 0f},
new float[] {0f, 0f, 0f, 1f, 0f},
new float[] {Wf, Wf, Wf, 0f, 1f}
}));
}
- First, you need to create a transformed image that is purely grayscale (saturation = 0). The brightness and contrast can be user-given. Then for each pixel in the grayscale image, get the luminance/brightness of that pixel, find the matching ASCII character with a similar proportion of brightness/luminance from the constructed character array in Step 2 above, and then output that character.
using System;
using System.Drawing;
using System.Text;
public string GetAsciiArt (Image originalImage,
Rectangle section, int outputWidth,
int outputHeight, float brightness,
float contrast, float saturation, float gamma)
{
StringBuilder asciiArt = new StringBuilder();
char[] AsciiCharSet = ... ;
Bitmap resizedImage = GetResizedImage (originalImage,
section, outputWidth, outputHeight)
Bitmap grayImage = GetTransformedImage (resizedImage,
brightness, contrast, 0, gamma);
for (int y = 0; y < outputHeight; y++)
{
for (int x = 0; x < outputWidth; x++)
{
byte lum = grayImage.GetPixel (x, y).R;
int index =
((int) lum) * AsciiCharSet.Length / 256;
asciiArt.Append (AsciiCharSet[index]);
}
asciiArt.Append ("\n");
}
return (asciiArt.ToString());
}
- Sometimes, you need to display the transformed image in colors, or display the ASCII Art output in colors. In this case, you need to create one more transformed image based on the user-given saturation. Then for each generated ASCII character, color it based on the corresponding pixel color in the transformed colored image. For example, if you are displaying the ASCII Art in a web page, you can use the HTML
<font>
tags to change the character color.
Bitmap colorImage = GetTransformedImage (resizedImage,
brightness, contrast, saturation, gamma);
int color =
(colorImage.GetPixel (x, y).ToArgb() & 0xFFFFFF);
string hexColor = "00000" + color.ToString ("X");
asciiArt.Append ("<font color='#");
asciiArt.Append (hexColor.Substring (hexColor.Length - 6));
asciiArt.Append ("'>");
asciiArt.Append ("</font>");
- The above code is actually not optimal as it always encloses each character with a separate
<font>
tag, even if adjacent characters have the same color. Therefore, you can optimize the code given above so that adjacent characters with the same font color will share the same <font>
tags.
For example, the HTML ASCII Art output:
<font color="#FFCC00">W</font><font color="#FFCC00">N</font>
should be changed to:
<font color="#FFCC00">WN</font>
- Finally, if you are displaying the ASCII Art onto a web page, you should wrap the entire ASCII image in a properly-formatted block so that spaces between characters can be reduced. You can use style-sheet to do this:
FontSize = (user-given font-size in px)
LineHeight = Math.Round(FontSize * 3.0 / 5.0);
<pre style=
"font-size: FontSizepx; line-height: LineHeightpx">
...
</pre>
Besides the above logic, the library also does a few other bits and pieces to provide more functionality. However, those are not so important as far as the 'technology' of ASCII Art is concerned.
The web page
The web page uses the following information to generate the ASCII image.
- Image URL or upload a new image.
- Make use of all alphabets in ASCII image.
- Make use of all numbers in ASCII image.
- Make use of all basic symbols in ASCII image (Non-Unicode symbols, Font-independent brightness).
- Make use of all extended symbols in ASCII image (Non-Unicode symbols, Font-dependent brightness).
- Make use of all block symbols in ASCII image (Unicode symbols: Blocks, pipes, etc.).
- Only use a custom user-defined set of characters.
- User-defined font-size.
- User-defined background color.
- Use only a single (user-defined) font color.
- Use multiple font colors (in full color).
- Use multiple font colors (in gray-scale).
- Output image down-scaling (reduce size of image), from 1x to 16x.
- Custom output image size (width & height), either in pixels or percentages.
- Text-only output (no style-sheet formatting, useful for text images displayed in Notepad or other pure text editors).
- Download a generated ASCII image file rather than viewing online.
Known issues and suggested solutions
- The character weighting (brightness/luminance) information is mainly from IMG2ASCII 1, which unfortunately is not very accurate.
- Some block symbols do not have the same fixed-width as other characters. Therefore, distortion can occur if you use block symbols.
- I suggest using custom character sets to compensate the above two problems. The default chosen custom character set is "
.:,;+ijtfLGDKW#
" (without the quotes). (This character set is derived from the ASCII data used in IMG2TXT 3.)
- If you just want color HTML, then just use the letter '
W
' for the custom character set.
- If you include custom characters that are not found in XML data file, then those characters will automatically be removed from the custom character set. This behavior is due to the design.
That's all!
If you want to see the full implementation of the above logic, just download the source files given above and have a look at the source codes. Note that the C# code presented in this article is a simplified version of the actual code; I have left out all that is irrelevant to the purpose of this article, such as error-handling and freeing resources.
Also, in my source files, I have put open curly braces at the end of a line, not at the beginning of a new line. Now, I'm not going to argue with you guys about which style is the best one that we should use. Personally, I use the end-of-line curly braces because I find it easier to read long lines of code. (Each start-of-line curly brace use up one line of code, so long lines of code will get even longer.) And no, I don't have a problem locating the start and end of a block since indentation is used.
Other than that, have fun! :)
Please don't hesitate to write me suggestions/comments if you have any.
The following web sites were referred to while building this web application:
- IMG2ASCII - An open source PHP image-to-ASCII converter.
- Boosty's ASCII Artist - A online image-to-ASCII converter in PHP (with source).
- IMG2TXT - An online image-to-ASCII converter.
- Daniel Fisher's ASCII Art with C#.
- Glass-giant's ASCII Artist - An online image-to-ASCII converter.
- ASCGEN - A powerful .NET-based ASCII Art Generator (with source) - Thanks to Sire404 for this link!.
- Multiple Color Matrices - Transforming images in .NET.
- Playing with ColorMatrix - Transforming images brightness, contrast, saturation, hue, and gamma.
- Image transformation source code for Linux KDE Print Library.
- MSDN - Recoloring images.
The following web site contains lots of information on ASCII Art in general:
To-do list
- Modify the web page code to allow user to change the brightness, contrast, and saturation of the image.
- Modify the web page code to allow user to select only a portion of the image for ASCII Art output.
- Modify the web page architecture so that uploading a file to a permanent physical location of the server is not necessary to generate its ASCII Art.
- Implement hue adjustment of the image in ASCII Art library.
History
- 12th July, 2005
- Article: Added a few more sample ASCII Art images.
- Article: Added a References section.
- Article: Added a History section.
- Article: Minor cosmetic changes in wording.
- 13th July, 2005
- Article: Added two more sample ASCII Art images.
- 16th July, 2005
- CODE: Improved the graphics manipulation logic in ASCII Art library.
- Article: Added a To-do list section.
- Article: Modified lot of stuff in The Library Logic section in response to the new changes in code.
- 23 May 2007
- Added a Windows desktop GUI port submitted by David Luu.