Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Manipulating Images with .NET Programs : Page 2

Many image processing techniques are simpler than you might expect and can be implemented easily in C# or Visual Basic.


advertisement
Most of the example programs that are available for download with this article use code similar to the following to let you save an image. The code calls the SaveBitmapUsingExtension method, passing it the image to save and the file name selected in the sfdNewFile SaveFileDialog.

ImageMethods.SaveBitmapUsingExtension(    (Bitmap)picResult.Image,    sfdNewFile.FileName);

Before leaving the topic of file saving, I want to mention one other really useful feature. When you save a JPEG file, you can specify the compression level that it should use. This lets you trade between the file's size and quality.

To make it easier to adjust an image's compression, I added the following SaveJpg file method to the ImageMethods class.


// Save the file with a specific compression level. public static void SaveJpg(    Image image, string file_name, long compression) {    try    {        EncoderParameters encoder_params =            new EncoderParameters(1);        encoder_params.Param[0] = new EncoderParameter(            System.Drawing.Imaging.Encoder.Quality, compression);        ImageCodecInfo image_codec_info =            GetEncoderInfo("image/jpeg");        File.Delete(file_name);        image.Save(file_name, image_codec_info, encoder_params);    }    catch (Exception ex)    {        MessageBox.Show("Error saving file '" + file_name +            "'\nTry a different file name.\n" + ex.Message,            "Save Error", MessageBoxButtons.OK,            MessageBoxIcon.Error);    } }

This code creates an EncoderParameters object that contains room for 1 parameter and sets that parameter to the desired compression level. It then gets an image/jpeg encoder and calls the Image's Save method, passing it information about the encoder and the compression level parameter.

Example program SaveImage shown in Figure 1 uses the SaveJpg method to show how an image would look under different compression levels. When you load an image and select a compression index from the combo box, the program saves the image in a temporary file, displays the file, and gives the file's new size.

Figure 1. What's the Difference: Program SaveImage's right PictureBox displays the difference between an original image (left) and a compressed image (middle).

If you look closely at Figure 1, you will see that compression level 50 makes little noticeable difference to the file's appearance (in the middle PictureBox) while reducing its size from 260 KB to under 20 KB.

The third PictureBox in Figure 1 shows the difference between the original and compressed images. If you look very closely at this difference image, you can see where the compression has made changes to the image. How to find this difference image is explained later in this article.

Working with Pixels

The Graphics class provides methods that let you draw lines, ellipses, polygons, and other shapes. Most image processing techniques, however, manipulate individual pixels.

The Bitmap class provides simple GetPixel and SetPixel methods that let you read and write individual pixel values. These are quite easy to use but unfortunately they are also quite slow.

To make accessing pixel values faster, the Bitmap class also provides a LockBits method that lets you work with image data more directly. LockBits takes a parameter that tells it what format you want to use. Two of the more useful formats are Format24bppRgb and Format32bppArgb. The former stores each pixel's color data in 24 bits, 8 bits for each of the red, green, and blue components.

The Format32bppArgb format stores a pixel's data using 32 bits, 8 bits for the red, green, and blue components, and 8 more bits for the pixel's alpha component. The alpha value gives the pixel's opacity where 0 means a completely transparent pixel and 255 means a completely opaque pixel.

LockBits locks the image data in memory so it won't move while you're working with it and returns a BitmapData object that gives you access to the data. That object's Stride property tells you how wide each row of data is in bytes. This may not be the same as the width of the image times the number of bytes per pixel because the image may add extra padding to align each row of data in memory. For example, if you use the Format24bppRgb format, which uses 3 bytes per pixel, the bitmap may add an extra byte at the end of each row to align it on a 4-byte memory boundary (if it doesn't happen to line up naturally).

The BitmapData object's Scan0 property gives the memory address of the first byte of image data. If you're using C# and you don't mind working in an unsafe context, then you can use that address directly and manipulate the data with pointers. You'll get better performance that way but the code is more complex and the .NET runtime cannot verify that the code is safe so you can mess up the program's memory if you make mistakes.

A slightly slower but easier and safer approach is to use the Marshal class's Copy method to copy the image data pointed to by Scan0 into a normal managed one-dimensional array of bytes. To make using the byte data even easier, you can write methods that translate row and column values into positions within the array.

After you have finished manipulating the image data, you should use Marshal.Copy to move your changes from the byte array back into the address given by the Scan0 property. Then you should call the Bitmap's UnlockBits method to unlock the bitmap's memory.

To make all of this easier, I created a Bitmap32 class. This class lets you manipulate Bitmaps by using the Format32bppArgb format. It provides LockBitmap and UnlockBitmap methods that wrap the necessary calls to LockBits, UnlockBits, and Marshal.Copy.

The following code shows how the LockBitmap method works.

public byte[] ImageBytes; // Lock the bitmap's data. public void LockBitmap() {    // If it's already locked, do nothing.    if (IsLocked) return;    // Lock the bitmap data.    Rectangle bounds = new Rectangle(        0, 0, Bitmap.Width, Bitmap.Height);    m_BitmapData = Bitmap.LockBits(bounds,        ImageLockMode.ReadWrite,        PixelFormat.Format32bppArgb);    RowSizeBytes = m_BitmapData.Stride;    // Allocate room for the data.    int total_size = m_BitmapData.Stride * m_BitmapData.Height;    ImageBytes = new byte[total_size];    // Copy the data into the ImageBytes array.    Marshal.Copy(m_BitmapData.Scan0, ImageBytes, 0, total_size);    // It is now locked.    m_IsLocked = true; }

The code creates a Rectangle covering the image's entire area and passes it to the LockBits call to lock all of the image's pixel data. It calculates the total space needed to hold the data and calls Marshal.Copy to copy the data into the ImageBytes array.

The following code shows how the UnlockBitmap method works.

// Copy the data back into the Bitmap // and release resources. public void UnlockBitmap() {    // If it's already unlocked, do nothing.    if (!IsLocked) return;    // Copy the data back into the bitmap.    int total_size = m_BitmapData.Stride * m_BitmapData.Height;    Marshal.Copy(ImageBytes, 0, m_BitmapData.Scan0, total_size);    // Unlock the bitmap.    Bitmap.UnlockBits(m_BitmapData);    // Release resources.    ImageBytes = null;    m_BitmapData = null;    // It is now unlocked.    m_IsLocked = false; }

This code calls Marshal.Copy to copy the ImageData array of bytes back to the location given by the Scan0 property. It then calls UnlockBits.

After you call LockBitmap, the Bitmap32 object's ImageBytes array contains the image's pixel data but it is spread out in a one-dimensional array with 1 byte for each of the pixels' blue, green, red, and alpha values (in that order). For a pixel with a given x and y position, you need to do some math to calculate which bytes represent the pixel.

To make that easier, the Bitmap32 class includes several methods for reading and writing pixel data. For example, the following code shows the GetPixel method, which lets you read a pixel's red, green, blue, and alpha values.

// Provide easy access to the color values. public void GetPixel(int x, int y,    out byte red, out byte green, out byte blue, out byte alpha) {    Debug.Assert(IsLocked);    int i = y * m_BitmapData.Stride + x * 4;    blue = ImageBytes[i++];    green = ImageBytes[i++];    red = ImageBytes[i++];    alpha = ImageBytes[i]; }

This code calculates the index in the ImageBytes array where the pixel's first byte of data lies. It then reads the values, incrementing the index after reading each byte.

The SetPixel method uses similar code to set a pixel's value. The class also provides GetRed, SetRed, GetGreen, SetGreen, and other similar methods to make manipulating the image data easier. Download and look at the example programs to see the details.



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap