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).
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.
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.
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