HomeDigital EditionSearch Dotnet Cd
ASP.NET C# Certification Exams The CLI Data Access Editorials Extending .NET Fundamentals Interoperability Interviews Migrate Mobile .NET Mono .NET Interface Object-Oriented Programming Open Source Optimization Product/Book Reviews Security Source Code UML Visual Studio .NET

A good picture may say a thousand words, but in the world of digital imaging, a thousand pixels does not a good picture make. When you happen to have a good digital picture, it is important to understand some underlying truths about digital image processing before you manipulate and alter the image. As we'll see, even the simple act of rotating a picture can muck up the quality of the underlying image file.

This article introduces some basic concepts and techniques used in image processing, and introduces some of the image-processing operations provided by the .NET Framework. I will discuss images and bitmaps, the notion of an image codec, and take a closer look at the JPEG image codec supported by the .NET Framework.

Images in .NET
As you may know, Microsoft provides the graphical device interface, or GDI+, to support graphics programming within Windows operating systems. The System.Drawing namespace provides access to GDI+ functionality, along with the System.Drawing.Drawing2D, System.Drawing.Imaging, and System.Drawing.Text namespaces. Our focus here will be the main drawing namespace and the imaging namespace.

The System.Drawing.Image class is the basis for most imaging in .NET, including the Bitmap class, which encapsulates pixel and other data for an image and its attributes. Digital pictures and documents are typically displayed and manipulated within an application using a Bitmap object. Since bitmap images tend to be rather large, they are usually compressed when written to disk. Some of the more common storage formats for images, called image file formats, include the GIF format, typically used for graphical images; the JPEG format, typically used for photographs; and the TIFF and PDF formats, often used for scanned documents.

The Windows Forms interface, used for the creation of Windows-based applications, can render GDI+ graphics within Windows Forms and controls. Web-based interfaces cannot render graphics directly, although the Web-based Image control can be used to display graphical images within Web applications. I will use Windows Forms illustrations throughout this article, although the code presented here could certainly be used in a Web context. For the purposes of our discussion, the term Image class will always refer to the drawing-based Image class rather than the Web-based version.

Bitmap Manipulation
Let's begin our discussion with the Bitmap class. This class is used to represent, manipulate, and display images defined by pixel data. A bitmap presents a series of pixels, which you can think of as small dots, arranged in neat rows and columns to form a rectangle. When a bitmap is said to be 600 by 400, this means 600 pixels wide by 400 pixels high. Each pixel stores the color to display at its particular location within the rectangle. The pixel format of an image defines how color data is represented for each pixel in the image. For example, a simple black-and-white image could represent each pixel as a single bit, with 0 for white and 1 for black. Grayscale images often represent images as 16 bits per pixel, with each pixel representing a graduated scale of gray from 0 for pure white to 65535 for pure black.

Images from most digital cameras are stored as 24-bit images with 8 bits (one byte) for each RGB color. RGB color represents the amount of red, green, and blue to display for each pixel, with 0 for no color and 256 for full color. Another common format is 32-bit pixels, which adds an "A" (alpha) component to an RGB image to represent transparency. The alpha component of an ARGB image adds another byte, from 0 to 256, to each pixel that represents how much of the color behind the displayed image should be visible. The value 255 represents none (or opaque) and the value 256 is a totally transparent, or invisible, pixel.

You can read more about pixel formats in the .NET documentation. Look up the PixelFormat enumeration for a list of formats, and search on this term for additional references. The format of an image is set automatically when the image is loaded, so for our purposes we will not need to discuss this setting any further.

Regardless of the pixel format, the Bitmap constructor will read an image file into memory for manipulation and display. For Windows Forms applications, the OpenFileDialog class can be used to permit a user to select a file. A selected file can be displayed in a PictureBox or similar control, as shown in Figure 1. For a discussion of how to select a file and display it in a control, see my previous article "Customizing the Picture Box Control" (.NETDJ Vol. 1, issue 1).

Figure 1

As seen in Figure 1, buttons are provided to rotate the displayed image 90 degrees clockwise or counterclockwise. The Image.RotateFlip() method permits an image to be rotated and/or flipped. Listing 1 shows one way to implement event handlers for the rotate buttons in Figure 1. The variable pbox here refers to the control that displays the image, which we assume to have an Image property that contains the displayed image object. This is true, for example, of the PictureBox control. Also in Listing 1, notice the use of the private field _totalRotation to track the overall rotation applied by the user. This will be important later.

Once the user has rotated an image, it can be saved to disk using the Image.Save() method. A number of overrides for this method exist, most of which accept a stream or file name in which to store the image. One particularly useful version accepts an ImageFormat enumeration value to specify the format for the exported file. Listing 2 provides an event handler for the Save button shown in Figure 1. This method uses a SaveFileDialog object to receive a file location from the user, and then stores the file in JPEG format. Note that this method relies on a private field to hold the original filename selected by the user. As an exercise, look up the ImageFormat enumeration in the .NET documentation, and modify this event handler to permit the user to save the file in any of the formats supported by this enumeration.

The JPEG image format uses lossy compression, meaning that information is lost as a bitmap image is compressed from the original image into its JPEG representation. This loss of data means that each time an image is compressed, the quality of the image is reduced. JPEG compression algorithms typically work on blocks of 4, 8, or 16 pixels at a time. Most digital cameras use a multiple of 16 for both the width and height of their images. When an image is rotated, the width and height are reversed. This can play havoc with image quality if the dimensions are not multiples of the block size, since this means that different blocks of pixels are compressed together each time the image is saved. An extreme example of this quality degradation is shown in Figure 2, in which the standard Sample.jpg file provided by Microsoft is rotated and saved to disk a few hundred times. This file has an image size of 283 by 212, making it a prime candidate for degradation after multiple decompress-rotate-compress iterations.

Figure 2

More advanced processing such as color correction and cropping can also result in reduced quality. Since JPEG compression works on localized regions, or blocks of pixels, a modification to one area of the image does not drastically affect the compression quality of other areas. Cropping an image typically alters the pixel blocks used for compression, which normally results in additional loss of data. Imperfections in a displayed image as a result of compressing the image data are referred to as artifacts in the image.

The typical technique to minimize such artifacts is to operate on the original image as much as possible. The best situation occurs when the user can manipulate the original bitmap data before it is compressed and saved to disk. This results in the least amount of data loss, since the image is compressed a single time. Alternately, try to base all storage operations on the original JPEG file whenever possible, even if the user has saved the image to disk multiple times. This is the technique we will use toward the end of the article.

Image Codecs and Parameters Algorithms that implement image formats such as JPEG are referred to as image codecs. The term codec is an acronym for compression/decompression. Audio codecs, as you might guess, operate on audio files, while image codecs operate on image files. The .NET Framework uses the image codecs provided with GDI+ to implement image file compression and decompression. The ImageCodecInfo class encapsulates such codecs. The static GetImageEncoders() and GetImageDecoders() methods in this class return the set of encoding (or compressing) and decoding (or decompressing) ImageCodecInfo objects, respectively, supported by the framework. At this time, custom ImageCodecInfo objects cannot be added to the .NET Framework.

The Bitmap class includes a Save() method that accepts an ImageCodecInfo instance to use when storing the image. We can modify Listing 2 to use an ImageCodecInfo instance rather than an ImageFormat enumeration value. One interesting aspect of this change lies in locating the appropriate codec object. We will use the MIME type string to find a codec. Listing 3 shows a method that returns the matching image codec for a given MIME type string. We will make use of this method in a moment.

There is no reason to prefer an ImageCodecInfo object over the ImageFormat enumeration unless we can somehow modify the behavior of the internal algorithm. This is done with EncoderParameter objects. The use of parameters can be a bit confusing for this purpose. The .NET documentation on this and other members of the System.Drawing.Imaging namespace is a bit light, so the best way to learn this is to look at some code. Some good code to look at (aside from this article, of course) is provided with the GDI+ documentation. The examples can be translated to .NET with a small amount of work, and will provide some good illustrations of how to use the GDI+ imaging interfaces.

Each encoder image codec in .NET supports its own set of parameter objects. The Bitmap class provides a GetEncoderParameterList() method to retrieve the set of parameters for a given encoder identifier. I'm not sure why this method is nonstatic, as you need to have a valid Bitmap object to invoke this call. Be wary, however, as a NotYetImplemented exception is thrown by codecs that do not support any encoder parameters.

The only .NET codecs to support parameters right now are the JPEG and TIFF codecs. The TIFF codec supports the ability to have multiple pages in a single Image object, as well as the compression and color depth for an image.

The JPEG Image Codec
The JPEG image codec provided by .NET is used to work with JPEG data. JPEG stands for Joint Photographic Experts Group, and strictly speaking, represents a standard compression scheme and not a file format. The JPEG File Interchange Format (JFIF) is the file format commonly used to store JPEG data. Another common format is EXIF, used by a number of digital camera manufacturers and with products such as Kodak Picture CD. The EXIF format extends the JFIF format to store additional information in the file such as the type of camera used and various color analysis data.

The JPEG format is designed to take advantage of gradual changes in color common in most photographs, by transforming regular blocks of pixels into a single color setting in the compressed data. JPEG images are very good at preserving the continuous tones that occur in most photographic images, but not as good at preserving distinct changes in color such as those found in line and other graphics drawings. This is why the daily Dilbert cartoon at the United Media site (www.unitedmedia.com) is stored as a GIF file rather than a JPEG file.

There are two factors that play a major role in the quality of a displayed or printed image from a JPEG file. The first is the size of the original image. Small bitmap sizes such as 600 by 400 pixels do not have enough data to accurately represent the image on a large monitor or in a photographic print, even though they easily surpass the 1000-pixel size mentioned the first sentence of the article. Consumer film development stores that provide images online or on a CD tend to use images sizes of 1024 by 1536 or higher. This provides enough data to produce a good-quality 4 by 6 print, but not so much that the data transfer and storage issues become overly difficult.

The second factor that affects the final print or display quality of JPEG images is the compression setting. Higher compression reduces the storage requirements for the image by throwing away more of the original data. While a number of programs and printers claim to interpolate data to expand an image, you cannot accurately recreate data that is gone, so the amount of quality improvement from such fancy algorithms is open for debate.

There are a number of mechanisms used to represent compression quality. A simple quality setting from 0 to 100 is quite common, and is the one used by .NET. The value of 100 represents the least compression, while the value of 0 represents the greatest compression. Typically, a value of 90 or so is used. Values below 70 or 80 tend to throw away too much of the image data for practical use.

The JPEG quality setting in .NET is accessible via the Encoder class in the System.Drawing.Imaging namespace. The Encoder class contains static fields that define globally unique identifiers for various image codec parameters. The two we will discuss here are the Quality field and the Transformation field. Figure 3 illustrates a Windows Forms application that we will refer to in discussing these settings. Consult the .NET documentation for a list of the other settings available in the Encoder class.

Figure 3

The Quality encoder value defines the compression level to use when compressing an image in JPEG format. This parameter expects a long value from 0 to 100. For the purposes of file compression, an encoder parameter is created as a member of an EncoderParameters object. For example, the definition of an EncoderParameters instance for a quality level of 95 might look something like the following:

EncoderParameters params
= new EncoderParameters(1);
EncoderParameter qParam
= new EncoderParameter(
Encoder.Quality,
(long)95);
params.Param[0] = qParam;

This code excerpt defines a single EncoderParameter object called qParam that is the only member of an EncoderParameters collection. EncoderParameter objects contain an array of values for a particular Encoder instance. In this case, the encoder parameter is created for the quality parameter category with a single long value of 95. This parameter is assigned as the only member of the encoders array.

The classes used here are all members of the Imaging namespace. The documentation for the Encoder.Quality field contains some sample code showing how to compress a JPEG image at various quality levels. We will look at our own example in Listing 4 in a moment. Before we do, let's take a look at the transformation encoder field.

The Encoder.Transformation field identifies a transformation to use when compressing an image. There are five supported transformations, for flipping an image horizontally or vertically or for rotating an image 90, 180, or 270 degrees. They may be used in isolation or in combination. Assigning a Transformation value works much like assigning a value for the Quality field, except that transformation values are set using the EncoderValue enumeration. This enumeration contains the constant values used by all encoders. The possible values for transformation encoders are as follows:

TransformFlipHorizontal
TransformFlipVertical
TransformRotate180
TransformRotate270
TransformRotate90

You may wonder why you would want to use a transformation when the Image.RotateFlip() method used in Listing 1 is readily available. The reason is the way in which these transformations are applied to JPEG images. As I've mentioned, the act of decompressing, manipulating, and recompressing a JPEG image can result in the loss of image quality. For the defined transformation values, the transformation encoder can perform this operation on the compressed data, such that there is no loss of image quality. For this to occur, the following conditions must be true:
1.   The file used to construct the Image object is a JPEG file.
2.   The width and height of the image are both multiples of 16.

When both conditions are met, the encoder will generate the resulting image by rotating the compressed pixel data, rather than using the uncompressed data. If additional modifications are made to the image, or the compression quality setting is altered, then all bets are off and the transformation occurs on the uncompressed data.

Listing 4 shows an alternate event handler for our Save method that makes use of encoder parameters. This code minimizes the data loss from the selected image by performing an image transformation whenever possible, and always storing the image using the original Bitmap object. This method begins much like Listing 2, and will save the image using the ImageFormat enumeration whenever no rotation is ultimately applied and the quality setting is not set.

A subtle aspect of this listing is that the _origBitmap field representing the original Bitmap selected by the user cannot be the same Image object assigned to the Windows Forms control. This is because our rotate button handlers in Listing 1 modify the image assigned to this control, such that it can no longer be used to store the image in Listing 4. A simple way to ensure we have two Image objects for this purpose is to clone the original image before assigning it to the control. For example, the following code will do this:

pbox.Image = _origBitmap.Clone() as Bitmap;

Referring back to Listing 4, whenever the user rotates an image or applies a quality setting, the Save button handler uses the JPEG ImageCodecInfo object to store the image. The code uses one or two EncoderParameter objects, depending on the image orientation and quality setting. The image is stored using the version of the Image.Save() method that accepts an ImageCodecInfo object and corresponding EncoderParameters object.

Topics for Further Reading
As previously mentioned, the documentation on GDI+ provides some good information on the purpose and use of the classes discussed in this article. Related topics include the cropping and transformation of images using members of the Graphics class, and color correction of images in unsafe code. There is a good article on the latter topic by Eric Gunnerson entitled "Unsafe Image Processing," which is available at http://msdn.microsoft.com/library/en-us/ dncscol/html/csharp11152001.asp.

About The Author
Erik Brown is a principal at E.E. Brown Consulting, providing intellectual property and development assistance to innovative companies. He is the author of Windows Forms Programming with C# from Manning Publications. erikebrown@eebrown.com

	



Listing 1: Rotating a Bitmap Image

private int _totalRotation = 0;


private void btnRotateCW_Click
  (object sender, System.EventArgs e)
{
  // Rotate image clockwise (90)
  if (pbox.Image != null)
  {
    _totalRotation += 90;
    pbox.Image.RotateFlip(
      RotateFlipType.Rotate90FlipNone);
    pbox.Invalidate();
  }
}


private void btnRotateCC_Click
  (object sender, System.EventArgs e)
{
  // Rotate image counter-clockwise (270)
  if (pbox.Image != null)
  {
    _totalRotation -= 90;
    pbox.Image.RotateFlip(
      RotateFlipType.Rotate270FlipNone);
    pbox.Invalidate();
  }
}



Listing 2: Saving a Bitmap Image

private string _origFileName = null;


private void btnSave_Click
    (object sender, EventArgs e)
{
  if (_origFileName == null)
    return;


  SaveFileDialog dlg = new SaveFileDialog();


  dlg.Filter = "JPG Files (*.jpg)|*.jpg";
  dlg.FileName = _origFileName;


  if (dlg.ShowDialog() == DialogResult.OK)
  {
    pbox.Image.Save(dlg.FileName,
                    ImageFormat.Jpeg);
  }
}



Listing 3: Locating an ImageCodecInfo object

private ImageCodecInfo GetImageCodec(string mimeType)
{
  ImageCodecInfo[] codecs
    = ImageCodecInfo.GetImageEncoders();


  foreach (ImageCodecInfo codec in codecs)
  {
    if (String.Compare(codec.MimeType,
                       mimeType, true) == 0)
    {
      return codec;
    }
  }


  return null;
}






Listing 4: Saving an image using encoder parameters


private string _origFileName = null;
private Bitmap _origBitmap = null;
private ImageCodecInfo jpgCodec = null;


private void btnSave_Click
    (object sender, EventArgs e)
{
  if (_origFileName == null)
    return;


  SaveFileDialog dlg = new SaveFileDialog();


  dlg.Filter = "JPG Files (*.jpg)|*.jpg";
  dlg.FileName = _origFileName;


  if (dlg.ShowDialog() != DialogResult.OK)
    return;


  // Ready to save the image
  while (_totalRotation < 0)
    _totalRotation += 360;
  _totalRotation %= 360;
  if (_totalRotation == 0 && !cboxQuality.Checked)
  {
    // No parameters, so just save image.
    _origBitmap.Save(dlg.FileName,
                     ImageFormat.Jpeg);
  }
  else
  {
    if (jpgCodec == null)
      jpgCodec = GetImageCodec("image/jpeg");


    // Determine required number of parameters
    int paramSize = udQuality.Enabled ? 1 : 0;
    if (_totalRotation != 0)
      paramSize ++;
    EncoderParameters encoderParams
        = new EncoderParameters(paramSize);


    if (_totalRotation != 0)
    {
      // Create transformation parameter
      EncoderValue rValue
          = EncoderValue.TransformRotate90;
      if (_totalRotation == 180)
        rValue = EncoderValue.TransformRotate180;
      else if (_totalRotation == 270)
        rValue = EncoderValue.TransformRotate270;


      EncoderParameter tParam
          = new EncoderParameter(
                    Encoder.Transformation,
                    (long)rotateValue);
      encoderParams.Param[paramSize-1] = tParam;
    }


    if (udQuality.Enabled)
    {
      // Create quality parameter
      EncoderParameter qParam
        = new EncoderParameter(
                  Encoder.Quality,
                  (long)udQuality.Value);
      encoderParams.Param[0] = qParam;
    }


    // Save the image using encoder settings
    _origBitmap.Save(dlg.FileName,
        jpgCodec, encoderParams);
  }
}

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.

  E-mail: info@sys-con.com