Book Home Java Enterprise in a Nutshell Search this book

4.10. Buffered Images

Java 2D introduces a new java.awt.Image subclass, java.awt.image.BufferedImage. BufferedImage represents image data that is present in memory, unlike Image, which typically represents streaming image data being transferred over a network. Java 2D also provides powerful image-processing classes that operate on BufferedImage objects and are much simpler to use than the ImageFilter class of Java 1.0.

As we discussed at the beginning of the chapter, Java knows how to read images in commonly used formats from files and URLs. You can use the getImage() method of either Applet() or Toolkit to retrieve an Image, but the image data may not have been fully read when the method returns. If you want to ensure that the image is fully loaded, you have to use a java.awt.MediaTracker. Note also that both of these methods return read-only Image objects, rather than read/write BufferedImage objects.

If you are writing a Swing application, an easy way to load an image is with the javax.swing.ImageIcon class. This class automatically waits until the image is fully loaded. For example:

Image myimage = new javax.swing.ImageIcon("myimage.gif").getImage();

As useful as ImageIcon is, its getImage() method still returns an Image object, not a BufferedImage object.

4.10.1. Obtaining a BufferedImage

To create an empty BufferedImage object, call the createImage() method of a Component. This method was first introduced in Java 1.0; it returns an Image object. In Java 1.2, however, the returned Image object is always an instance of BufferedImage, so you can safely cast it. After you have created an empty BufferedImage, you can call its createGraphics() method to obtain a Graphics2D object. Then use this Graphics2D object to draw image data from an Image object into your BufferedImage object. For example:

javax.swing.JFrame f;                  // Initialized elsewhere

// Create an image, and wait for it to load
Image i = javax.swing.ImageIcon("myimage.gif").getImage();

// Create a BufferedImage of the same size as the Image
BufferedImage bi = (BufferedImage)f.createImage(i.getWidth(f),i.getHeight(f));



Graphics2D g = bi.createGraphics();    // Get a Graphics2D object
g.drawImage(i, 0, 0, f);      // Draw the Image data into the BufferedImage

Note that we must pass an ImageObserver object to the getWidth(), getHeight() and drawImage() methods in this code. All AWT components implement ImageObserver, so we use our JFrame for this purpose. Although we could have gotten away with passing null, this is exactly the sort of complexity that the BufferedImage API allows us to avoid.

Sun's implementation of Java 1.2 ships with a package named com.sun.image.codec.jpeg that contains classes for reading JPEG image data directly into BufferedImage objects and for encoding BufferedImage image data using the JPEG image format. Although this package is not part of the core Java 2 platform, most Java implementations will probably contain these classes. You can use this package to read JPEG files with code like this:

import java.io.*;
import com.sun.image.codec.jpeg.*;

FileInputStream in = new FileInputStream("myimage.jpeg");
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in);
BufferedImage image = decoder.decodeAsBufferedImage();
in.close();

4.10.2. Drawing a BufferedImage

A BufferedImage is a kind of Image, so you can do anything with a BufferedImage that you can do with an Image. For instance, the Graphics class defines a number of methods for drawing Image objects. Some of these methods take only an X and a Y coordinate at which to draw the image and simply draw the image at its original size. Other drawImage() methods also take a width and a height and scale the image as appropriate.

Java 1.1 introduced more sophisticated drawImage() methods that take coordinates that specify a destination rectangle on the drawing surface and a source rectangle within the image. These methods map an arbitrary subimage onto an arbitrary rectangle of the drawing surface, scaling and flipping as necessary. Each of these drawImage() methods comes in two versions, one that takes a background color argument and one that does not. The background Color is used if the Image contains transparent pixels.

Since all the drawImage() methods of the Graphics object operate on Image objects instead of BufferedImage objects, they all require a Component or other ImageObserver object to be specified.

In Java 2D, the Graphics2D object defines two more drawImage() methods. One of these methods draws an Image object as modified by an arbitrary AffineTransform object. As we'll see a bit later, an AffineTransform object can specify a position, scaling factor, rotation, and shear.

The other drawImage() method of Graphics2D actually operates on a BufferedImage object. This method processes the specified BufferedImage as specified by a BufferedImageOp object and then draws the processed image at the specified position. We'll talk about image processing with BufferedImageOp objects in more detail shortly. Since this drawImage() method operates on a BufferedImage object instead of an Image object, it does not require an ImageObserver argument.

Finally, the Graphics2D class defines a drawRenderedImage() method. BufferedImage implements the RenderedImage interface, so you can pass a BufferedImage to this method, along with an arbitrary AffineTransform that specifies where and how to draw it.

4.10.3. Drawing into a BufferedImage

As I mentioned earlier, the createGraphics() method of a BufferedImage returns a Graphics2D object that you can use to draw into a BufferedImage. Anything you can draw on the screen, you can draw into a BufferedImage. One common reason to draw into a BufferedImage object is to implement double-buffering. When performing animations or other repetitive drawing tasks, the erase/redraw cycle can cause flickering. To avoid this, do your drawing into an off-screen BufferedImage and then copy the contents of the image to the screen all at once. Although this requires extra memory, it can dramatically improve the appearance of your programs.[4]

[4]Recall that Swing components, and custom components subclassed from Swing components, automatically support double-buffering.

4.10.4. Manipulating Pixels of a BufferedImage

The Image class defines very few methods, so about all you can do with an Image object is query its width and height. The BufferedImage class, by contrast, defines quite a few methods. Most of these are required by interfaces that BufferedImage implements. A few important ones, however, allow pixel-level manipulation of images.

For example, getRGB() returns the image pixel at the specified X and Y coordinates, while setRGB() sets the pixel at the specified coordinates. Both of these methods represent the pixel value as an int that contains 8-bit red, green, and blue color values. Other versions of getRGB() and setRGB() read and write rectangular arrays of pixels into int arrays. getSubimage() is a related method that returns a rectangular region of the image as a BufferedImage.

4.10.5. Inside a BufferedImage

Most applications can use the BufferedImage class without ever caring what is inside a BufferedImage. However, if you are writing a program that performs low-level image-data manipulation, such as reading or writing image data from a file, you need to know more. The complete details of the image architecture are beyond the scope of this book; this section explains the basics in case you want to explore on your own.

The image data of a BufferedImage is stored in a java.awt.image.Raster object, which can be obtained with the getData() method of BufferedImage. The Raster itself contains two parts: a java.awt.image.DataBuffer that holds the raw image data and a java.awt.image.SampleModel object that knows how to extract individual pixel values out of the DataBuffer. DataBuffer supports a wide variety of formats for image data, which is why a Raster object also needs a SampleModel.

The Raster object of a BufferedImage stores the pixel values of an image. These pixel values may or may not correspond directly to the red, green, and blue color values to be displayed on the screen. Therefore, a BufferedImage object also contains a java.awt.image.ColorModel object that knows how to convert pixel values from the Raster into Color objects. A ColorModel object typically contains a java.awt.color.ColorSpace object that specifies the representation of color components.

4.10.6. Processing a BufferedImage

The java.awt.image package defines five powerful implementations of the BufferedImageOp interface that perform various types of image-processing operations on BufferedImage objects. The five implementations are described briefly in Table 4-9.

Table 4-9. Java 1.2 Image-Processing Classes

Class Description
AffineTransformOp

Performs an arbitrary geometric transformation--specified by an AffineTransform--on an image. The transform can include scaling, rotation, translation, and shearing in any combination. This operator interpolates pixel values when needed, using either a fast, nearest-neighbor algorithm or a slower, higher-quality bilinear interpolation algorithm. This class cannot process images in place.

ColorConvertOp

Converts an image to a new java.awt.color.ColorSpace. It can process an image in place.

ConvolveOp

Performs a powerful and flexible type of image processing called convolution, which is used for blurring or sharpening images and performing edge detection, among other things. ConvolveOp uses a java.awt.image.Kernel object to hold the matrix of numbers that specify exactly what convolution operation is performed. Convolution operations cannot be performed in place.

LookupOp

Processes the color channels of an image using a lookup table, which is an array that maps color values in the source image to color values in the new image. The use of lookup tables makes LookupOp a very flexible image-processing class. For example, you can use it to brighten or darken an image, to invert the colors of an image, or to reduce the number of distinct color levels in an image. LookupOp can use either a single lookup table to operate on all color channels in an image or a separate lookup table for each channel. LookupOp can be used to process images in place. You typically use LookupOp in conjunction with java.awt.image.ByteLookupTable.

RescaleOp

Like LookupOp, RescaleOp is used to modify the values of the individual color components of an image. Instead of using a lookup table, however, RescaleOp uses a simple linear equation. The color values of the destination are obtained by multiplying the source values by a constant and then adding another constant. You can specify either a single pair of constants for use on all color channels or individual pairs of constants for each of the channels in the image. RescaleOp can process images in place.

To use a BufferedImageOp, simply call its filter() method. This method processes or filters a source image and stores the results in a destination image. If no destination image is supplied, filter() creates one. In either case, the method returns a BufferedImage that contains the processed image. As noted in Table 4-9, some implementations of BufferedImageOp can process an image "in place." These implementations allow you to specify the same BufferedImage object as both the source and destination arguments to the filter() method.

To convert a color image to grayscale, you can use ColorConvertOp as follows:

import java.awt.image.*;
import java.awt.color.*;

ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(CS_GRAY), null);
BufferedImage grayImage = op.filter(sourceImage, null);

To invert the colors in an image (producing a photographic negative effect), you might use a RescaleOp as follows:

RescaleOp op = new RescaleOp(-1.0f, 255f, null);
BufferedImage negative = op.filter(sourceImage, null);

To brighten an image, you can use a RescaleOp to linearly increase the intensity of each color value. More realistic brightening effects require a nonlinear transform, however. For example, you can use a LookupOp to handle brightening based on the square-root function, which boosts midrange colors more than colors that are dark or bright:

byte[] data = new byte[256];
for(int i = 0; i < 256; i++) 
  data[i] = (byte)(Math.sqrt((float)i/255.0) * 255);
ByteLookupTable table = new ByteLookupTable(0, data);
LookupOp op = new LookupOp(table, null);
BufferedImage brighterImage = op.filter(sourceImage, null);

You can blur an image using a ConvolveOp. When processing an image by convolution, a pixel value in the destination image is computed from the corresponding pixel value in the source image and the pixels that surround that pixel. A matrix of numbers known as the kernel is used to specify the contribution of each source pixel to the destination pixel. To perform a simple blurring operation, you might use a kernel like this to specify that the destination pixel is the average of the source pixel and the eight pixels that surround that source pixel:

0.1111  0.1111  0.1111
0.1111  0.1111  0.1111
0.1111  0.1111  0.1111

Note that the sum of the values in this kernel is 1.0, which means that the destination image has the same brightness as the source image. To perform a simple blur, use code like this:

Kernel k = new Kernel(3, 3, new float[] { .1111f, .1111f, .1111f, 
                                          .1111f, .1111f, .1111f, 
                                          .1111f, .1111f, .1111f });
ConvolveOp op = new ConvolveOp(k);
BufferedImage blurry = op.filter(sourceImage, null);


Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.