Skip to main content

C# Tutorial - Convert a Color Image to Grayscale [Beginner]


We've done a few tutorials about image editing with C#. We're going to continue the series with a tutorial on how to convert a color image to black and white. This tutorial will show three different ways to convert an image to grayscale - starting with the easiest and slowest.

You're probably wondering why you would want to see multiple ways to accomplish the same thing. It's hard to predict what technique will work best for everyone's application, so I've decided to outline several different possibilities.

Below is the image I used to test each algorithm and to benchmark their performance. The image can be downloaded free from Gaming Textures. The conversion times listed were obtained using my computer so your times might be different.
Original color
image
Image converted to black and
white

 

1. Slow and Simple

Convert Time: 1,135ms
The first method I'm going to show you is by far the easiest to understand and implement. Unfortunately, it's also the slowest.
public static Bitmap MakeGrayscale(Bitmap original)
{
   //make an empty bitmap the same size as original
   Bitmap newBitmap = new Bitmap(original.Width, original.Height);

   for (int i = 0; i < original.Width; i++)
   {
      for (int j = 0; j < original.Height; j++)
      {
         //get the pixel from the original image
         Color originalColor = original.GetPixel(i, j);

         //create the grayscale version of the pixel
         int grayScale = (int)((originalColor.R * .3) + (originalColor.G * .59)
             + (originalColor.B * .11));

         //create the color object
         Color newColor =  Color.FromArgb(grayScale, grayScale, grayScale);

         //set the new image's pixel to the grayscale version
         newBitmap.SetPixel(i, j, newColor);
        }
    }

    return newBitmap;
}
 
This code looks at every pixel in the original image and sets the same pixel in the new bitmap to a grayscale version. You can probably figure out why this is so slow. If the image is 2048x2048, this code will call GetPixel and SetPixel over 4 million times. Those functions aren't the most efficient way to get pixel data from the image.

You might be wondering where the numbers .3, .59, and .11 came from. In reality, you could just take the average color by adding up R, G, B and dividing by three. In fact, you'll get a pretty good black and white image by doing that. However, at some point, someone a lot smarter than me figured out that these numbers better approximate the human eye's sensitivity to each of those colors. The Wikipedia article on grayscale has some pretty good information about that.

This method will work fine for small images or when you don't really care about how long it takes to convert it to black and white. If you need it done quickly, I would recommend one of the next two methods.

 

2. Faster and Complicated

Convert Time: 188ms
The next technique I'm going to show you is based on the previous one, but much faster. It still iterates through every pixel, but we're going to utilize C#'s unsafe keyword to make getting the pixel data much more efficient.
public static Bitmap MakeGrayscale2(Bitmap original)
{
   unsafe
   {
      //create an empty bitmap the same size as original
      Bitmap newBitmap = new Bitmap(original.Width, original.Height);

      //lock the original bitmap in memory
      BitmapData originalData = original.LockBits(
         new Rectangle(0, 0, original.Width, original.Height),
         ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

      //lock the new bitmap in memory
      BitmapData newData = newBitmap.LockBits(
         new Rectangle(0, 0, original.Width, original.Height), 
         ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

      //set the number of bytes per pixel
      int pixelSize = 3;

      for (int y = 0; y < original.Height; y++)
      {
         //get the data from the original image
         byte* oRow = (byte*)originalData.Scan0 + (y * originalData.Stride);

         //get the data from the new image
         byte* nRow = (byte*)newData.Scan0 + (y * newData.Stride);

         for (int x = 0; x < original.Width; x++)
         {
            //create the grayscale version
            byte grayScale = 
               (byte)((oRow[x * pixelSize] * .11) + //B
               (oRow[x * pixelSize + 1] * .59) +  //G
               (oRow[x * pixelSize + 2] * .3)); //R

            //set the new image's pixel to the grayscale version
            nRow[x * pixelSize] = grayScale; //B
            nRow[x * pixelSize + 1] = grayScale; //G
            nRow[x * pixelSize + 2] = grayScale; //R
         }
      }

      //unlock the bitmaps
      newBitmap.UnlockBits(newData);
      original.UnlockBits(originalData);

      return newBitmap;
   }
}
 
There's a lot of code here so let's go through it piece by piece. The first thing we need to do is lock the bits in the Bitmap objects. Locking the bits keeps the .NET runtime from moving them around in memory. This is important because we're going to use a pointer, and if the data is moving around the pointer won't point to the correct thing anymore. You'll need to know the pixel format of the image you're trying to convert. I'm using jpeg's, which are 24 bits per pixel. There is a way to get the pixel format from an image, but that's outside the scope of this tutorial. The integer, pixelSize, is the number of bytes per pixel in your original image. Since my images were 24 bits per pixel, that translates to 3 bytes per pixel.

To get pixel data, I start by getting the address to the first pixel in each row of the image. Scan0 returns the address of the first pixel in the image. So in order to get the address of the first pixel in the row, we have to add the number of bytes in the row, Stride, multiplied by the row number, y. Below is a diagram that might help you understand this a little better.
Pixel Data
Diagram

Now we can get color data straight from memory by accessing it like an array. The byte at x * pixelSize will be the blue, x * pixelSize + 1 is green, and x * pixelSize + 2 is red. This is why pixelSize is very important. If the image you provided is not 3 bytes per pixel, you'll be pulling color data from the wrong location in memory.

Next, make the grayscale version using the same process as the previous method and set the exact same pixel in the new image. All that's left to do is to unlock the bitmaps and return the new image.

 

3. Short and Sweet

Convert Time: 62ms
The last method is probably the best way. It's faster than the previous two methods and uses much less code. This technique uses a ColorMatrix to perform the conversion. A ColorMatrix is a 5x5 matrix that can make just about any modifications to the color of an image. A ColorMatrix is pretty complicated and deserves a whole tutorial to itself. For now, if you want more information about it, you'll have to do the research yourself.
public static Bitmap MakeGrayscale3(Bitmap original)
{
   //create a blank bitmap the same size as original
   Bitmap newBitmap = new Bitmap(original.Width, original.Height);

   //get a graphics object from the new image
   Graphics g = Graphics.FromImage(newBitmap);

   //create the grayscale ColorMatrix
   ColorMatrix colorMatrix = new ColorMatrix(
      new float[][] 
      {
         new float[] {.3f, .3f, .3f, 0, 0},
         new float[] {.59f, .59f, .59f, 0, 0},
         new float[] {.11f, .11f, .11f, 0, 0},
         new float[] {0, 0, 0, 1, 0},
         new float[] {0, 0, 0, 0, 1}
      });

   //create some image attributes
   ImageAttributes attributes = new ImageAttributes();

   //set the color matrix attribute
   attributes.SetColorMatrix(colorMatrix);

   //draw the original image on the new image
   //using the grayscale color matrix
   g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height),
      0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);

   //dispose the Graphics object
   g.Dispose();
   return newBitmap;
}
 
This time we're going to use GDI to draw the new black and white image. The benefit of this technique over the last one is that we don't need to know any information about the image's pixel format. The code here is pretty straight forward. First create the blank image and get a Graphics object from it. Next, create the ColorMatrix that will convert the original image to grayscale. Declare an ImageAttributes object that will use the ColorMatrix and use it to draw the new image using DrawImage.

That's it for converting an image to grayscale using C# and .NET. Hopefully one of the above techniques will work for your application.

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# Snippet - The Many Uses Of The Using Keyword [Beginner]

What is the first thing that pops into your mind when you think of the using keyword for C#? Probably those lines that always appear at the top of C# code files - the lines that import types from other namespaces into your code. But while that is the most common use of the using keyword, it is not the only one. Today we are going to take a look at the different uses of the using keyword and what they are useful for. The Using Directive There are two main categories of use for the using keyword - as a "Using Directive" and as a "Using Statement". The lines at the top of a C# file are directives, but that is not the only place they can go. They can also go inside of a namespace block, but they have to be before any other elements declared in the namespace (i.e., you can't add a using statement after a class declaration). Namespace Importing This is by far the most common use of the keyword - it is rare that you see a C# file that does not h

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as