Pages

7/19/2009

Image Processing in C#

If you've read our previous articles on GDI+, you know how to display images in C#. But how can you manipulate them? Rick Leinecker shows you how!
Displaying images in your programs opens up enormous possibilities. You can display photographs, enhance your user interface with catchy icons, and render data in a graphical format—only to name a few cool things that you can do. This article shows you how to go beyond simple image display by getting you started with image processing.
Images are easy to use in C#. Click here for an article that gets you started with handling images, called GDI+ Image Handling in C#.
We'll start off with some baby steps and create three filters that can be applied to an image. Staying within the RGB color space will make sure that it's as simple as possible—at a later time we'll move into HSV color space because it gives us a wider range of filters that we can apply.
For more information on colors and color spaces, read Color Theory for Developers here on DevSource.com
Pixels and Colors
To get started we need to talk about pixels and colors. Pixels represent a single unit of a display bitmap. Each pixel is a discreet dot on the screen. Images contain thousands of pixels, actually more like hundreds of thousands of pixels. A screen that's 1024 by 800 is composed of 819,200 pixels.
Images achieve their visual perception based on the colors of all of the pixels. Although it would be difficult since pixels are so small, if you could paint each pixel with a paintbrush, then you could create an image. It's kind of like the pointillistic artists who painted in small dots, which in turn created the gestalt of the entire painting. If those artists could have painted small enough dots, then they could have achieved the resolution of modern computer images.
A pixel is represented by three different color components: red, green, and blue. In today's PC graphics, each of these values ranges from 0 to 255. By mixing the three values, you get the visible color. Since each component has a value from 0 to 255, we can mathematically calculate the number of colors that result from the mixing of these three components as 16,777,216.
Bitmap Objects
Images are usually encapsulated in a Bitmap object. This .NET object makes image handling extremely simple. As an example, to load and display an image to a form window, all you need to do is use code similar to the following snippet.
Bitmap objBitmap = new Bitmap( "SomeFile.jpg" );

Graphics g = Graphics.FromHwnd( Handle );

g.DrawImage( objBitmap, 0, 0 );
While the Bitmap object makes image handling easy, it doesn't make image processing easy because it doesn't give you direct access to the image data. Of course, you can loop through the image using the Bitmap object's GetPixel and SetPixel methods to get the color value for a pixel, alter it, and update the image pixel at that location. The following code snippet does this.
Bitmap objBitmap = new Bitmap( "SomeFile.jpg" );

for( int y=0; y{

 for( int x=0; x {

   Color col = objBitmap.GetPixel( x, y );

   // Perform an operation on the Color value here.

   objBitmap.SetPixel( x, y, col );

 }

}
In spite of how easy the previous code snippet is, it's slow. The GetPixel and SetPixel methods are not very fast. On my 2.4 GHz dual core machine, the GetPixel/SetPixel combination code for an image that was 300x300 took 281 milliseconds.
What we need is a good way to get access to the actual data that represents the image. Then we don't have to rely on the GetPixel and SetPixel methods. If we can simply perform image processing on the actual image data, then we can do it with very little perceptible slowness.
There is a way. We can save the image as a BMP file into memory. All we need to do is create a MemoryStream object, let the Bitmap object save into the MemoryStream object, and then get the bytes from the MemoryStream object. The bytes we get are the raw image data, and we can process in whatever way is appropriate. Then, we reverse the process by creating a MemoryStream object that contains the modified image data, and read from it into a Bitmap object.
I wrote a helper class with methods that perform both of the required operations. A method named BitmapDataFromBitmap returns a byte array from a Bitmap object. A method named BitmapFromBitmapData returns a Bitmap object from a byte array. And when I timed using both methods to get data from a Bitmap object and then put it back, it took less than a single millisecond—much better than the 281 milliseconds from the GetPixel/SetPixel combination. The two methods can be seen below. (Please note that you'll need to add using statements for System.Drawing, System.Drawing.Imaging, and System.IO.)
static public Bitmap BitmapFromBitmapData(byte[] BitmapData)

{

    MemoryStream ms = new MemoryStream(BitmapData);

    return (new Bitmap(ms));

}



static public byte[] BitmapDataFromBitmap(Bitmap objBitmap)

{

    MemoryStream ms = new MemoryStream();

    objBitmap.Save(ms, ImageFormat.Bmp);

    return (ms.GetBuffer());

}
Okay, now you get to use the helper class (which in the demonstration program is named BitmapHelper.) The following code shows how to use the two methods.
Bitmap objBitmap = new Bitmap( "SomeFile.jpg" );

byte[] BitmapData = BitmapHelper.BitmapDataFromBitmap(objBitmap);

// Perform image processing on the raw data here…





objBitmap = BitmapHelper.BitmapFromBitmapData(BitmapData);









The ImageProcessingDemo Program
I wrote a simple program named ImageProcessingDemo to demonstrate these concepts. You can download the code here. When the program first runs, it displays a default image that can be found in the program directory as shown in the figure below. You can click the browse button if you want to load a different image.

There are only four operations that can be performed on an image: darken, lighten, restore, and invert. The button with the '<' character darkens the image a bit. The button with the '>' character lightens the image a bit. The button with the 'R' character restores the image to its original state. And finally the button with the 'I' character inverts the image.
Before jumping into the processing code, we need to talk about BMP headers and the data format. BMP files have two headers: the file header and the information header. The file header is the first 14 bytes of the BMP file and contains the string 'BM', a long specifying the size of the file in bytes, two reserved words that must have a value of 0, and finally the offset from the beginning of the file to where the actual image data is. The following chart shows the BMP file header.
Start Size Name Example Value Purpose
0 2 bfType 19778 or 'BM' Identifies the file type.
2 4 bfSize ?? Specifies the size of the file in bytes.
6 2 bfReserved1 0 Must always be set to zero.
8 2 bfReserved2 0 Must always be set to zero.
10 4 bfOffBits 54 Specifies the offset from the beginning of the file to the bitmap data.
The information header contains information about the bitmap including its size, width, height, and format. The following chart shows the BMP information header.
Start Size Name Example Value Purpose
14 4 biSize 40 Specifies the size of the BITMAPINFOHEADER structure, in bytes.
18 4 biWidth 100 Specifies the width of the image, in pixels.
22 4 biHeight 100 Specifies the height of the image, in pixels.
26 2 biPlanes 1 Specifies the number of planes of the target device, must be set to zero.
28 2 biBitCount 24 Specifies the number of bits per pixel.
30 4 biCompression 0 Specifies the type of compression, usually set to zero (no compression).
34 4 biSizeImage ?? Specifies the size of the image data, in bytes. If there is no compression, it is valid to set this member to zero.
38 4 biXPelsPerMeter 0 Specifies the horizontal pixels per meter on the designated targer device, usually set to zero.
42 4 biYPelsPerMeter 0 Specifies the vertical pixels per meter on the designated targer device, usually set to zero.
46 4 biClrUsed 0 Specifies the number of colors used in the bitmap, if set to zero the number of colors is calculated using the biBitCount member.
50 4 biClrImportant 0 Specifies the number of color that are 'important' for the bitmap, if set to zero, all colors are important.

The BMP header information is helpful in some

situations, but we'll ignore the data in it for now. All we really need

to remember is that we have to skip the first 54 bytes of the image

data before doing any processing. Not only is it a waste of time to

operate on the header data, but when we restore the data back into the

Bitmap object there will be many errors introduced.
The data comes in sets of four bytes for each pixel. The first byte is the blue value, the second the green, the third the red, and the fourth is an alpha value that some programs may or may not use.
I have to point out that this is not the format for all BMP files. This is just how the .NET runtime saves BMP files. There are lots of other ways that the data can be organized, including a palette-indexed system. But since the .NET runtime saves in the way that I've described, then we can simplify things by limiting our discussion to that particular format.
I'll start with the code for darkening and lightening the image. First, there's a helper method that adds a value to the current image data value. The value can be positive or negative. The helper method checks to make sure that the value isn't less than 0 or that it isn't greater than 255 since these are the far extremes of the RGB values. The ChangeData method follows.
byte ChangeData(byte DataValue, int AddValue)

{

    if (AddValue + (int)DataValue > 255)

    {

         return (255);

    }

    else if (AddValue + (int)DataValue < 0)

    {

         return (0);

    }

    return( (byte)((int)DataValue+AddValue) );

}
There are two methods that are almost identical. One darkens (btnLess_Click) and one lightens (btnMore_Click). The only difference in these two methods is that one sends the value of -25 (to darken) to the ChangeData method, while the other sends the value of 25 (to lighten). Both methods can be seen here.
private void btnLess_Click(object sender, EventArgs e)

{

    byte[] BitmapData = BitmapHelper.BitmapDataFromBitmap(m_objBitmap);

    int nPixels = m_objBitmap.Width * m_objBitmap.Height;

    for (int i=0; i    {

         BitmapData[54+i] = ChangeData(BitmapData[54+i], -25);

    }

    m_objBitmap = BitmapHelper.BitmapFromBitmapData(BitmapData);

    Invalidate();

    Update();

}

private void btnMore_Click(object sender, EventArgs e)

{

    byte[] BitmapData = BitmapHelper.BitmapDataFromBitmap(m_objBitmap);

    int nPixels = m_objBitmap.Width * m_objBitmap.Height;

    for (int i=0; i    {

         BitmapData[54+i] = ChangeData(BitmapData[54+i], 25);

    }

    m_objBitmap = BitmapHelper.BitmapFromBitmapData(BitmapData);

    Invalidate();

    Update();

}
Conclusion
Once you get past the hurdle of getting started, image processing in C# is easy. In future articles, I'll use the same BitmapHelper class and then do some more interesting filtering.
REF: http://www.devsource.com/c/a/Languages/Image-Processing-in-C/2/
Post a Comment