ASCII art, a technique of creating visuals using characters from the ASCII standard, has been a part of the computing world for decades. It’s a fascinating way to represent images without the need for traditional graphics. For those new to programming, building a program to generate ASCII art can serve as an insightful introduction.
In this guide, we’ll walk through a C# approach to transform standard images and generate ASCII art from them. Not only will you have the full source code to have a functioning C# app that can generate ASCII art, but I’ll also explain why simple programs like this can be critical for helping you learn.
Before I Provide Code to Generate ASCII Art…
I realize many of you coming here are just looking to jump directly into the code. I get it! But that’s why I want to put an important message beforehand, especially for the more junior developers.
Many times beginner programmers are stuck in some of the early phases of learning because they are not sure how to allocate their time. They are trying to read books, articles, and blog posts (just like this one!) to learn theory, or watching videos and trying to find the best BootCamp so they have the best chance of success. I regularly remind my audience that I think building things and actually writing code is one of the absolute best ways to learn.
As we navigate this code together, I want you to keep this in mind! At the end of the article, I propose some variations and enhancements that you may want to consider. I’ve included this list not just because I think it’s pretty cool, but to get your creative juices flowing! Think about the different things you want to focus on as a developer and see if you can incorporate them into your ASCII art generator!
Being able to leverage simple programs like this takes the stress away from “what’s the right thing to build” and allows you to focus on learning and exploring. Watch the video as you follow along!
Example Code to Generate ASCII Art
Alright, you toughed it out through my intro. Thanks! Let’s look at some code (which, by the way, is available in full on GitHub):
string imagePath = "your file path here";
using var inputStream = new FileStream(
imagePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
var generator = new Generator();
using var sourceImage = Image.Load(inputStream);
using var imageRgba32 = sourceImage.CloneAs<Rgba32>();
using var image = new ImageSharpImageSource(imageRgba32);
var asciiArt = generator.GenerateAsciiArtFromImage(image);
Console.WriteLine(asciiArt.Art);
This is the entry point to our C# program. Here, we’re setting up the path to our image and creating a stream to read it. We also instantiate our main Generator
class, which will handle the ASCII conversion, along with the ImageSharpImageSource
that will hold the image data. The magic happens inside of the GenerateAsciiArtFromImage
method, which we will look at shortly.
The ImageSharp library is used to load the image and then clone it into a format (Rgba32
) that allows us to work with individual pixel colors. The ImageSharpImageSource
class acts as a bridge between the ImageSharp library and our ASCII generation logic. When we look at the code for this class, we’ll be able to see the indexer method that allows us to get the pixel data for an X and Y coordinate.
Let’s look at the implementation for the image source next:
internal interface IImageSource : IDisposable
{
int Width { get; }
int Height { get; }
float AspectRatio { get; }
Rgb GetPixel(int x, int y);
}
internal sealed class ImageSharpImageSource : IImageSource
{
private readonly Image<Rgba32> _image;
public ImageSharpImageSource(Image<Rgba32> image)
{
_image = image;
}
public int Width => _image.Width;
public int Height => _image.Height;
public float AspectRatio => _image.Width / (float)_image.Height;
public Rgb GetPixel(int x, int y)
{
var pixel = _image[x, y];
return new(
pixel.R,
pixel.G,
pixel.B);
}
public void Dispose() => _image.Dispose();
}
In the above code, we can see that we are implementing the IImageSource
interface. This was done because you can actually implement this same functionality with the System.Drawing namespace and the Bitmap
class, but it will only work on Windows. The code _image[x, y]
allows us to get the pixel information from the image!
The final class of importance is the actual generator. We’ll examine the code in more detail in the following sections:
internal sealed class Generator
{
public AsciiArt GenerateAsciiArtFromImage(
IImageSource image)
{
var asciiChars = "@%#*+=-:,. ";
var aspect = image.Width / (double)image.Height;
var outputWidth = image.Width / 16;
var widthStep = image.Width / outputWidth;
var outputHeight = (int)(outputWidth / aspect);
var heightStep = image.Height / outputHeight;
StringBuilder asciiBuilder = new(outputWidth * outputHeight);
for (var h = 0; h < image.Height; h += heightStep)
{
for (var w = 0; w < image.Width; w += widthStep)
{
var pixelColor = image.GetPixel(w, h);
var grayValue = (int)(pixelColor.Red * 0.3 + pixelColor.Green * 0.59 + pixelColor.Blue * 0.11);
var asciiChar = asciiChars[grayValue * (asciiChars.Length - 1) / 255];
asciiBuilder.Append(asciiChar);
asciiBuilder.Append(asciiChar);
}
asciiBuilder.AppendLine();
}
AsciiArt art = new(
asciiBuilder.ToString(),
outputWidth,
outputHeight);
return art;
}
}
Breaking Down the Image Processing
When we talk about images in computers, we’re essentially discussing a matrix of pixels. Each pixel has a color, and this color is typically represented by three primary components: Red, Green, and Blue (RGB). You might also see a 4th component in this mix, which is “alpha” (or transparency) represented by an A (RGBA). The combination of these components in varying intensities gives us the vast spectrum of colors we see in digital images.
ASCII art doesn’t deal with colors in the traditional sense. Instead, it represents images using characters that have varying visual weights or densities. This is where the concept of grayscale comes into play. A grayscale image is one where the RGB components of each pixel have the same value, resulting in various shades of gray. The significance of converting an image to grayscale for ASCII art is to simplify the representation. By reducing an image to its luminance, we can then map different shades of gray to specific ASCII characters and generate ASCII art from an image.
In our code, the IImageSource
interface serves as an abstraction for our image source. It provides properties to get the width, height, and aspect ratio of the image and a method to retrieve the color of a specific pixel. The ImageSharpImageSource
class is an implementation of this interface using the ImageSharp library. As we saw, it wraps around an ImageSharp image and provides the necessary data for our program to generate ASCII art.
As we’ll see in a later section, there are still some considerations around image scaling including downsizing the image to fit into the console output and considering aspect ratios. Additionally, the code itself has not been benchmarked to see if there are opportunities to reduce memory usage and/or generate the output more effectively.
The Generator Class: The Key To Generate ASCII Art
The Generator
class is where the magic happens. It’s responsible for transforming our image into a piece of ASCII art. Let’s dive deeper into its primary method: GenerateAsciiArtFromImage
.
var asciiChars = "@%#*+=-:,. ";
This line defines our palette of ASCII characters. These characters are chosen based on their visual density, with @
being the densest and a space ( ) being the least dense. You can customize this list to have different visual appearances and add more or remove some characters to change the granularity of the shading being used.
var aspect = image.Width / (double)image.Height;
var outputWidth = image.Width / 16;
var widthStep = image.Width / outputWidth;
var outputHeight = (int)(outputWidth / aspect);
var heightStep = image.Height / outputHeight;
This code is actually incomplete, but it’s a good opportunity to think about enhancements. The purpose of this block is to work on getting the right output resolution of the image and considering how it needs to be scaled. It would be ideal to have this be configurable so there are no magic numbers!
One important detail that we have for looping through each pixel in the image is that we start from the top left and then work across the row before going to the next row. This is because it’s much more straightforward to print a line to the console than to print column by column. As we loop through the image’s pixels, we need to determine which ASCII character best represents a particular pixel’s color. To do this, we first convert the pixel’s color to a grayscale value:
var pixelColor = image.GetPixel(w, h);
var grayValue = (int)(pixelColor.Red * 0.3 + pixelColor.Green * 0.59 + pixelColor.Blue * 0.11);
This formula can be tweaked to get a grayscale value, but the current magic numbers here emphasize the green component due to the human eye’s sensitivity to it. With the grayscale value in hand, we map it to one of our ASCII characters:
var asciiChar = asciiChars[grayValue * (asciiChars.Length - 1) / 255];
This mapping ensures that darker pixels get represented by denser ASCII characters and lighter pixels by less dense characters. The resulting character is then added to our ASCII art representation.
Going Cross Platform: ImageSharp to Generate ASCII Art
If you enjoyed this article so far, you can check out the full post to see how to get this working on Linux AND how you can extend this ASCII art generator! Thank you so much for your support, and consider subscribing to my weekly email newsletter so you can have content like this delivered to your inbox every weekend!
Oldest comments (0)