devxlogo

Manipulating Images with .NET Programs

Manipulating Images with .NET Programs

With a cheap digital camera you can take a picture, transfer it to your computer, and inflict it on your friends and relatives in a matter of minutes. Sometimes, however, the results aren’t quite perfect. Images may be too bright or dark, washed out, or may suffer from red-eye. The first part of this two-part series explains ways you adjust pictures to fix these problems and generally make them look better.

These days you can buy a fairly nice digital camera for under $100. Depending on what you want to do with the picture, however, you may need to do more than just say “cheese” and press a button.

For portraits you may want to perform red-eye reduction or insert an image of your favorite movie star into the picture. I’ve seen high-end group portraits where the photographer has copied one person’s open eyes from one picture into another picture where that person blinked. You may also want to adjust the image’s brightness, contrast, or color levels, and add a filter to make the image look softer.

When you’re building user interfaces, you might want to add drop shadows, glow, or embossed effects. With scientific images you may want to highlight sharp edges or compare two pictures of the same scene (that’s a good way to discover comets, asteroids, supernovas, and other astronomical phenomena that appear, disappear, or move quickly).

All of these are image processing techniques. Many of these techniques are much simpler than you might expect and you can implement them easily in C# or Visual Basic.

In this two-part article, I’ll explain how you can implement some of these techniques. The first part of this article explains point processes: techniques that modify images one pixel at a time. Some of these techniques include brightness and contrast adjustment, image subtraction, and converting images to gray scale. The second part of this article will explain area processes such as blurring, sharpening, beveling, and embossing techniques.

Before I explain point processing techniques, however, you should learn a few things about loading, saving, and manipulating images in .NET programs.

Loading Images

One easy way to load images in a .NET program is to create a new Bitmap object, passing its constructor the name of the file you want to load. For example, the LoadImageLocked example program, uses the following code to display an image when you use the File menu’s Open command. (All of the examples in this article are available for download in C# and Visual Basic versions.) This code calls the Bitmap class’s constructor, passing it the name of the file you selected in the ofdFile OpenFileDialog control.
// Load and display the file.picResult.Image = new Bitmap(ofdFile.FileName);
There are two important things to note about this code. First, the picResult PictureBox uses this Bitmap whenever it needs to redraw itself. That means you cannot call the Bitmap’s Dispose method to free its resources. If you do, the program crashes the next time it needs to refresh the PictureBox.

Not calling the Bitmap’s Dispose method is easy enough but it leads to the second important thing to note: until you dispose of the Bitmap, it keeps the image file locked. If you try to delete or rename the file while the Bitmap still exists, you’ll get the error “The action can’t be completed because the file is open in another program.”

One way around this problem is to load the Bitmap, copy it into a new Bitmap in memory, and then dispose of the original Bitmap. The new Bitmap holds everything it needs to know in memory so it doesn’t need to keep the image file locked. After you call the first Bitmap’s Dispose method, the file is unlocked so the program won’t interfere with other programs that need to use the file, for example if you try to delete the file in Windows Explorer.

To make this operation and some others easier, I created an ImageMethods class in the file ImageStuff.cs. This class holds static methods that make working with images easier. The LoadBitmap method shown in the following code uses this technique of loading a file into a Bitmap, creating a copy of the Bitmap, and then disposing of the original (with a using statement).
// Load the image without leaving the file locked.public static Bitmap LoadBitmap(string file_name){    using (Bitmap bm = new Bitmap(file_name))    {        return new Bitmap(bm);    }}

If you open the LoadImageLocked program's File menu and select Open Unlocked, the program uses the following code to invoke the LoadBitmap method. This loads the image but doesn't lock the file.

// Load and display the file.picResult.Image = ImageMethods.LoadBitmap(ofdFile.FileName);

Saving Images

The Bitmap class provides a useful Save method for saving image files. An optional second parameter tells Save what format to give the file. This value can be Bmp, Emf, Exit, Gif, Icon, Jpeg, MemoryBmp, Png, Tiff, and Wmf. For example, the following code saves the Bitmap named bm as a PNG file. (Lately I've come to like PNG files a lot because they provide pretty good compression without losing any of the image's data the way the GIF and JPEG formats do.)
bm.Save(filename, ImageFormat.Png);

The Save method doesn't really care what extension the file name has so you should be sure it matches the format that you use. For example, you could make a file named Happy.bmp that is saved in the JPEG format. The Save method doesn't care but that could lead to confusion.

To make saving images in the correct format easier, I added the following SaveBitmapUsingExtension method to the ImageMethods class.
// Save the file with the appropriate format.// Throw a NotSupportedException if the file// has an unknown extension.public static void SaveBitmapUsingExtension(    Bitmap bm, string filename){    string extension = Path.GetExtension(filename);    switch (extension.ToLower())    {        case ".bmp":            bm.Save(filename, ImageFormat.Bmp);            break;        case ".exif":            bm.Save(filename, ImageFormat.Exif);            break;        case ".gif":            bm.Save(filename, ImageFormat.Gif);            break;        case ".jpg":        case ".jpeg":            bm.Save(filename, ImageFormat.Jpeg);            break;        case ".png":            bm.Save(filename, ImageFormat.Png);            break;        case ".tif":        case ".tiff":            bm.Save(filename, ImageFormat.Tiff);            break;        default:            throw new NotSupportedException(                "Unknown file extension " + extension);    }}

This method uses a file name's extension to figure out which format is appropriate and then saves the image appropriately. That makes it easier to accommodate users who select different file formats.

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 +            "'
Try a different file name.
" + 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.

Bright Ideas

Now that you have some tools for loading, saving, and manipulating images, you're ready to learn about some point processing techniques. Those techniques modify an image one pixel at a time. When you modify a pixel's value, you don't need to know anything about the values of the surrounding pixels. Most of these techniques are reasonably straightforward once you know what they need to do.

For example, the AdjustBrightness example program shown in Figure 2 lets you adjust an image's brightness. It adjusts each pixel's brightness based on its current value without knowing anything about the other pixels' values.

Figure 2. A Bright Idea: Program AdjustBrightness adjusts the brightness of an image's pixels.

The Bitmap32 class includes several methods that manipulate the image it represents. The AdjustBrightness method shown in the following code adjusts the image's brightness. The brightness parameter should be a value between -1 and 1 to darken or lighten the image.

// Adjust the image's brightness by -100% to 100%.// The brightness value should be between -1 and 1.public void AdjustBrightness(float brightness){    // Remember if we are locked and lock the bitmap.    bool was_locked = IsLocked;    LockBitmap();    if (brightness 

The code uses a slightly different approach depending on whether the brightness value is greater than or less than zero. In either case, the code adjusts a pixel's red, green, and blue color components the indicated distance toward 0 or 255. For example, if the brightness parameter is 0.5, then the code adjusts the components halfway between their current values and 255, brightening the image.

The following code shows how the AdjustBrightness program uses this method.

// Adjust the image's brightness.private void AdjustBrightness(){    if (OriginalBitmap == null) return;    // Get the selected brightness.    float brightness = hscrBrightness.Value / 100f;    lblBrightness.Text = brightness.ToString("P0");    // Make a Bitmap24 object.    Bitmap bm = new Bitmap(OriginalBitmap);    Bitmap32 bm32 = new Bitmap32(bm);    // Average the colors.    bm32.AdjustBrightness(brightness);    // Display the result.    picResult.Image = bm;}

This code gets a value between -100 and 100 from the horizontal scrollbar named hscrBrightness. It divides that value by 100 to convert it from a percentage into a fraction.Next the code creates a new Bitmap object that contains a copy of the original version named OriginalBitmap. It makes a corresponding Bitmap32 object, calls its AdjustBrightness method, and displays the result.The AdjustColors example program shown in Figure 3 uses similar techniques to adjust the pixels' red, green, and blue components separately. See the code for the details.

Figure 3. Colorful Pictures: Program AdjustColors lets you adjust the image's red, green, and blue amounts.

Get the Red Out

Adjusting colors globally is interesting but it doesn't have all that many practical applications. For example, you could make your vacation photos bluer to make the sky and ocean look pretty but it would probably also make the people look slightly seasick.

You can refine this method by restricting your adjustments to specific areas in an image. For example, you could increase the blueness only in areas that are already blue.

The RemoveRedEye example program shown in Figure 4 uses this approach to remove red-eye from pictures. Click and drag to select an area. When you release the mouse, the program searches the selected area for pixels that are mostly red and converts them to gray scale. In Figure 4, the eye on the left has been converted and the eye on the right is being selected.

Figure 4. Seeing Red: Program RemoveRedEye converts pixels that are mostly red to gray scale.

The following code shows how the Bitmap32 class removes red-eye from an area.

// Remove red eye from the indicated area.public void RemoveRedEye(int x1, int y1, int x2, int y2){    // Lock the bitmap.    bool this_locked = this.IsLocked;    this.LockBitmap();    // Process the indicated area.    for (int x = x1; x  green) && (red > blue))            {                // Convert to grayscale.                byte clr = (byte)((red + green + blue) / 3);                SetPixel(x, y, clr, clr, clr);            }        }    }    // Unlock the bitmap.    if (!this_locked) UnlockBitmap();}

This code loops thorough the pixels in the selected area. If a pixel's red component is larger than its green and blue components, the code converts it into gray scale.

A Sharp Contrast

In image processing, contrast is the difference in colors between various pieces of an image. If the colors are very similar, then the image has low contrast and it can be hard to distinguish features. If the colors vary widely, then the image has sharp contrast and different parts of the image tend to stand out.

For example, in Figure 5 the AdjustContrast example program is displaying a normal image on the left and a high-contrast version on the right. Compared to the high-contrast version, the original image looks sort of washed out.

Figure 5. A Study in Contrasts: Program AdjustContrast lets you adjust an image's contrast.

The Bitmap32 class uses the following code to adjust an image's contrast. You could easily modify it to intensify colors, for example, to make blues bluer and greens greener.

// Change contrast by spreading values linearly// to or from an origin value.// Use://      To decrease contrasst   0  1// The origin can be 128 to spread to or from a middle value,// or use the median brightness value.public void AdjustContrast(float amount, int origin){    // Lock the bitmap.    bool was_locked = IsLocked;    LockBitmap();    for (int x = 0; x  255) red = 255;            int green = (int)(origin +                (GetGreen(x, y) - origin) * amount);            if (green  255) green = 255;            int blue = (int)(origin +                (GetBlue(x, y) - origin) * amount);            if (blue  255) blue = 255;            SetPixel(x, y, (byte)red, (byte)green, (byte)blue);        }    }    // Unlock the bitmap.    if (!was_locked) UnlockBitmap();}

This code moves each pixel's color values away from an origin value. For example, suppose the average value of all of the image's pixels' red, green, and blue color components is 128. Then the image is roughly halfway between completely dark (black) and completely bright (white), at least in brightness. If you pass 128 to the AdjustContrast method as the origin, then the code makes bright pixels brighter and dark pixels darker. That tends to spread the color values farther apart and increases contrast.

There are lots of other ways you can adjust contrast. For example, the AdjustContrast example program uses a hard-coded origin of 128 but you could use some other value such as the image's average or median brightness.If you look closely at Figure 5, you'll also see that some areas are so bright or dark that their detail is lost. For example, the folds in the green jacket on the right are hidden in the high-contrast version. You can reduce that affect by spreading the colors out in a different way. For example, instead of simply scaling color values by a linear factor, you could use a function that adjusts colors less when they are close to 0 or 255. Values that are closer to the origin get spread out more and those far away get spread out less to preserve more detail.

Area Processes

This article explains several useful techniques for working with images and modifying them one pixel at a time. It explains how to load and save images in different formats and with different compression levels. It also explains some useful point processes such as adjusting an image's contrast, brightness, and color levels.

The second part of this article will explain area processes that use the values of many pixels to set a pixel's new value. Those techniques let you perform such operations as sharpening or blurring an image, highlighting edges, or creating an embossed effect.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist