Click here to Skip to main content
15,899,679 members
Articles / Programming Languages / C#

Generate ASCII Art – A Simple How To in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (19 votes)
21 Oct 2023CPOL10 min read 20K   31   17
Change a picture into ASCII art using your own C# program
Have you ever wanted to change a picture into ASCII art? Now you can with your very own C# program that can generate ASCII art! Sample code is included!

Image 1

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 introduction. Thanks! Let’s look at some code (which, by the way, is available in full on GitHub):

C#
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.

I just wanted to make a quick note about the using declaration syntax that we have here in case you're not familiar with it. In C# 8, we got this language feature which allows us to simplify our using statements. Previously, explicit using blocks used curly braces to denote the scope. With this new language feature, when the variable goes out of scope, it will be the equivalent to the using scope ending as well. While implicit isn't always as obvious, an added benefit for readability here is that we don't have overly nested code.

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:

C#
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:

C#
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.

C#
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.

C#
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:

C#
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:

C#
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

ImageSharp is a powerful, open-source library in the .NET ecosystem that provides functionality for processing images. It’s versatile, efficient, and supports a wide range of image formats. To generate ASCII art, we need a way to read and manipulate images, and ImageSharp fits the bill perfectly.

Because I started this project initially using System.Drawing, I wanted to illustrate that we could interchangeably use ImageSharp or System.Drawing to generate ASCII art. By doing this, I could pull the core logic into one spot, and abstract access to the more image-specific code. And why do this? Well, what if we decide to switch to another image processing library in the future? To keep things modular and maintainable, we introduce an abstraction layer: the IImageSource interface.

The ImageSharpImageSource class serves as a bridge between ImageSharp and our generator. It implements the IImageSource interface, wrapping around an ImageSharp image.

C#
public Rgb GetPixel(int x, int y)
{
    var pixel = _image[x, y];
    return new( pixel.R, pixel.G, pixel.B);
}

The GetPixel method is particularly significant. It retrieves the RGB values of a specific pixel in the image. This method is crucial for our generator, as it uses these RGB values to determine the grayscale value and, subsequently, the appropriate ASCII character for that pixel. Any image library we want to use would then need some way for us to access a specific pixel, which seems like a reasonable feature to expect.

Generate ASCII Art: Project Enhancements

To generate ASCII art, we came up with a solid foundation, but there’s always room for improvement and innovation. To re-iterate, projects like this are awesome for learning because the direction you head in with your enhancements can let you pick and choose how to focus your energy on learning. Here are some suggestions to take this project to the next level:

1. Refining ASCII Art Generation

Different images can benefit from different sets of ASCII characters. Experiment with various character sets to see which ones produce the best results for different kinds of images. Could this be something that you allow the user to configure?

What about the image size and scaling? Can we fit the output to specific dimensions? What about maintaining the aspect ratio or not? These are extras that could really enhance the usability of the generator!

2. Adding Color

While traditional ASCII art is monochrome, there’s no reason you can’t introduce color. By mapping pixel colors to terminal or HTML color codes, you can produce vibrant, colored ASCII art. Even in our C# console on Windows, we do have some basic colors we can work with, so before completely overhauling what’s here, you could alter the code to generate ASCII art such that it returns the pixel color, and have something that can map that to the closest console color!

3. Performance Optimization

For larger images, the generation process could be a bit slow. Dive into performance profiling and see if there are bottlenecks you can address, perhaps by optimizing loops or leveraging parallel processing. You might even be able to use something more effective than a StringBuilder here… Even if we go back to basics on some of the C# data types, we could perhaps pick something that is more performant!

4. Web Application or GUI Integration

Turn this console-based tool into a more user-friendly application. Imagine a web application where users can upload images and instantly see their ASCII art representation. Or a desktop application with a GUI that allows users to tweak settings in real time. You have so many options to explore just by moving away from a traditional console application!

Conclusion

As it turns out, characters in the range from 0 to 255 can teach us a whole lot when we channel that into a program that can generate ASCII art! While the project might seem straightforward on the surface, it offers a deep dive into various programming concepts, from image processing to algorithmic optimization. With some optional enhancements, you can really spice things up and learn some awesome new things!

For those who’ve followed along, I hope you’ve gained not just the knowledge of how to create ASCII art, but also an appreciation for the learning opportunities that even simple projects can present. The real value lies not just in the end product but in the process of problem-solving, experimenting, and iterating.

I encourage you to take this code, adapt it, and make it your own. Experiment with different features, optimize the code further, or integrate it into larger projects. Remember, every line of code you write, and every challenge you overcome, adds to your growth as a developer. And sometimes, it’s the simplest projects that offer the most profound lessons!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Microsoft
United States United States
I'm a software engineering professional with a decade of hands-on experience creating software and managing engineering teams. I graduated from the University of Waterloo in Honours Computer Engineering in 2012.

I started blogging at http://www.devleader.ca in order to share my experiences about leadership (especially in a startup environment) and development experience. Since then, I have been trying to create content on various platforms to be able to share information about programming and engineering leadership.

My Social:
YouTube: https://youtube.com/@DevLeader
TikTok: https://www.tiktok.com/@devleader
Blog: http://www.devleader.ca/
GitHub: https://github.com/ncosentino/
Twitch: https://www.twitch.tv/ncosentino
Twitter: https://twitter.com/DevLeaderCa
Facebook: https://www.facebook.com/DevLeaderCa
Instagram:
https://www.instagram.com/dev.leader
LinkedIn: https://www.linkedin.com/in/nickcosentino

Comments and Discussions

 
GeneralMy vote of 5 Pin
RobertoPasquali4-Nov-23 0:35
RobertoPasquali4-Nov-23 0:35 
GeneralRe: My vote of 5 Pin
Dev Leader22-Nov-23 9:17
mvaDev Leader22-Nov-23 9:17 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA22-Oct-23 4:07
professionalȘtefan-Mihai MOGA22-Oct-23 4:07 
GeneralRe: My vote of 5 Pin
Dev Leader22-Oct-23 16:58
mvaDev Leader22-Oct-23 16:58 
QuestionNice and useful post Pin
Salam Elias23-Sep-23 0:29
Salam Elias23-Sep-23 0:29 
AnswerRe: Nice and useful post Pin
Dev Leader21-Oct-23 22:04
mvaDev Leader21-Oct-23 22:04 
GeneralRe: Nice and useful post Pin
Salam Elias23-Oct-23 1:36
Salam Elias23-Oct-23 1:36 
GeneralRe: Nice and useful post Pin
Dev Leader27-Oct-23 5:05
mvaDev Leader27-Oct-23 5:05 
QuestionThis can't be right... Pin
Stacy Dudovitz2-Sep-23 20:45
professionalStacy Dudovitz2-Sep-23 20:45 
AnswerRe: This can't be right... Pin
Dev Leader6-Sep-23 6:09
mvaDev Leader6-Sep-23 6:09 
GeneralRe: This can't be right... Pin
Stacy Dudovitz19-Oct-23 21:57
professionalStacy Dudovitz19-Oct-23 21:57 
GeneralRe: This can't be right... Pin
Dev Leader21-Oct-23 21:39
mvaDev Leader21-Oct-23 21:39 
Stacy,

Initially, you wrote a lengthy comment about why my using statements do not work. This was the entire focal point of the comment, so I spent time providing different variations of using statements including the behavior of what I have in the article.

Nothing I said was arrogant - and I even spent the time to write out the differences in code behavior and share a link to what I assumed you were having challenges with. After contributing a free article (I'm sorry that this has been a bad experience for you), I spent my own time to try and help explain the cause for what I assumed your confusion was.

For Your List Of Points

1) The "using declaration" feature I am using here is from C# 8. This is from the end of 2019. So 4 years ago. You can see it as one of the features in this list here: The history of C# - C# Guide - C# | Microsoft Learn[^]. And I get it - I wasn't even aware of this syntax until last year. But jumping all the way to making a claim about assuming all developers track the latest changes? We're talking about something from 4 years ago. I can't reasonably understand how you can be so frustrated by this point.

If you took offense to me asking if you ran it - my apologies, but I think it's an entirely valid question based on the claims and suggestions that you provided. And I'm still led to believe that you've not actually had success running this because it sounds like you had compilation issues. So everything that you had suggested in your original comment about why the using declaration syntax doesn't work I'm led to believe is based on assumptions because of this statement:
Quote:
I did try to run the code. No where in the article does it mention what version of C# or the compiler is required
If you had the wrong language version it wouldn't compile. If you had the correct one, you'd observe the proper functionality of the using declaration. I'm open to there being other alternatives, but I still cannot understand the rationale behind being so upset by my response.

I'm happy to update the article to make a note about the using declaration syntax. In fact, I've submit the change before I even post this comment back, but when Code Project editors approve it is on their watch.

I personally think this could have been avoided entirely if you had just asked for clarity about the using declaration syntax if that was the part not compiling for you. Suggesting that the behavior of the code was broken was misleading for me to be able to provide a helpful response.

2) You're reading a free article on the Internet. I'm not forcing you to use the using declaration syntax that was added, it's just what I opted to use in my code. There are trade-offs for everything though. The (seemingly) implicit scope provided by the new using declaration syntax means that for new eyes it's not obvious where the scope ends. However, I assume Microsoft was okay with this as a language feature because where the scope ends *is* predictable: it's the same spot that the variable goes out of scope. With respect to readability, this syntax can reduce nesting which is very helpful for readability.

If you and the team of developers you work with don't agree with that, that's absolutely fine. Readability can be subjective. I'm not forcing you to do anything.

3) I'm not exactly sure how I'm expected to put this point into practice. If I understand correctly, you would like me to add comments to my code in an article about generating ASCII art in C# about using declaration syntax because it could be a breaking change for folks.

As someone producing free articles, how am I to know all of the breaking changes I might have to document for the 8000+ (so far on this site, excluding my own website) readers of this article? Even based on this Stack Overflow answer, the matrix of support for features between language versions and dotnet versions isn't straightforward. What about everyone before .NET 3.5? Is it really my responsibility to go dig up every possible breaking feature across all language versions and write a comment in the code (in my free article) to explain it?
Quote:
That's just plain technical developer malfeasance.

Really? It would be literally impossible for me to do this and try to document every possible thing that might be broken between the current language version and the oldest. It's completely impractical.

Regardless, as mentioned, I took the spirit of your advice and added the section to the article here to call it out for folks. I'm hopeful it helps the next person with using declaration syntax.

4) I suspect I am grossly misunderstanding what this fourth point means. Immediately before introducing any code, I state
Quote:
Let’s look at some code (which, by the way, is available in full on GitHub):
and that has a link to the full source code. I shared an entire working C# solution that you can clone from git. You can clone it and the project file even has the required framework version.

Other than that, I am not sure how else I can properly represent a multi-file project inside of an article. That's why I opted to embed the pieces of code I wanted to discuss further directly in the article and then link to the full set of code right at the start of the example.

Some Other Points

1)
Quote:
If the point of the article is teach, epic fail.
I'm sorry that you've had a bad experience reading my material. I'm not exactly sure why you feel the need to be so rude about someone trying to help bring clarity, but I'll leave that one with you. My apologies that you've had such an awful time reading and engaging here.

2)
Quote:
Seriously, this is 2023.
I hope that my updated explanation about the hyperlink to the entire full solution that you can clone helps with this statement. If you have further suggestions for how I can make it more obvious in my future articles, I'd be open to that feedback.

3)
Quote:
I would never tolerate this kind of behavior.
I am not sure what "kind of behavior" you're referring to with this statement. I wrote you an explanation about using declarations that you appeared to not understand, and then you've now called me arrogant, said my free article has been an epic fail, said that I'm malfeasant for not documenting a language feature, and then of course... this next part:

4)
Quote:
What I dislike is someone waving their credentials like a shiny bauble to be admired and worshiped
I have absolutely no idea where this is coming from. Are you referring to the bio snippet that Code Project puts at the end of the articles? The part where we're supposed to write about ourselves? I don't see anywhere in the article directly where I have mentioned any of my credentials so I am led to believe it's this.

But admired and worshipped? I'm at a loss here. The bio doesn't even have my actual job title/role/position in full, which is arguably even more "shiny". But I'm failing to understand where any of this is being used to demand some type of worship.

In our Code Project settings, it's called our "Professional Profile". So I have some of my professional details set there as we'd be expected to do.

Quote:
I'm a software engineering professional with a decade of hands-on experience creating software and managing engineering teams. I graduated from the University of Waterloo in Honours Computer Engineering in 2012.
Is this truly the part that is "someone waiving their credentials like a shiny bauble to be admired and worshipped"? That I've been in industry for a while and have a degree?

5)
Quote:
This article needs a major shave and a haircut.
I appreciate your feedback and sharing concerns about readers not being familiar with certain language features. Respectfully though, I think you'll have a more enjoyable experience reading someone else's material.
GeneralMy vote of 5 Pin
William Costa Rodrigues29-Aug-23 9:27
William Costa Rodrigues29-Aug-23 9:27 
GeneralRe: My vote of 5 Pin
Dev Leader29-Aug-23 12:38
mvaDev Leader29-Aug-23 12:38 
PraiseNice job! Pin
Ravi Bhavnani25-Aug-23 19:43
professionalRavi Bhavnani25-Aug-23 19:43 
GeneralRe: Nice job! Pin
Dev Leader27-Aug-23 18:47
mvaDev Leader27-Aug-23 18:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.