Convert a Color Image to Grayscale

Introduction:

In photography and computing, a grayscale or greyscale digital image is an image in which the value of each pixel is a single sample, that is, it carries only intensity information. Images of this sort, also known as black-and-white, are composed exclusively of shades of gray, varying from black at the weakest intensity to white at the strongest.

Grayscale images are often the result of measuring the intensity of light at each pixel in a single band of the electromagnetic spectrum (e.g. infrared, visible light, ultraviolet, etc.), and in such cases they are monochromatic proper when only a given frequency is captured. But also they can be synthesized from a full color image.

Below is the image I used to test my  algorithm and to benchmark its performance:

METHOD 1:

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;
}

EXPLANATION:

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 2048×2048, 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.

faster and complicated

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),

//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;
}
}

EXPLANATION:

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.

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.

That is it..!