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.
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.
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
Post a Comment