Buffering an Image


The BufferedImage class is a subclass of Image, so it can be employed instead of Image in methods such as drawImage( ). BufferedImage has two main advantages: the data required for image manipulation are easily accessible through its methods, and BufferedImage objects are automatically converted to managed images by the JVM (when possible). A managed image may allow hardware acceleration to be employed when the image is being rendered.

The code in Example 5-2 is the ShowImage applet, recoded to use a BufferedImage.

Example 5-2. ShowImage applet (Version 2) using BufferedImage
 import javax.swing.*; import java.awt.*; import java.io.*; import java.awt.image.*; import javax.imageio.ImageIO; public class ShowImage extends JApplet {   private BufferedImage im;   public void init( )   { try {       im =ImageIO.read( getClass( ).getResource("ball.gif") );     }     catch(IOException e) {       System.out.println("Load Image error:");     }   } // end of init( )   public void paint(Graphics g)   {  g.drawImage(im, 0, 0, this);  } } 

The simplest, and perhaps fastest, way of loading a BufferedImage object is with read( ) from the ImageIO class. Some tests suggest that it may be 10 percent faster than using ImageIcon, which can be significant when the image is large. InputStream, and ImageInputStream are different versions of read( ) for reading from a URL.

Optimizing the BufferedImage is possible so it has the same internal data format and color model as the underlying graphics device. This requires us to make a copy of the input image using GraphicsConfiguration's createCompatibleImage( ). The various steps are packaged together inside a loadImage( ) method; the complete (modified) class is given in Example 5-3.

Example 5-3. ShowImage applet (Version 3) using an optimized BufferedImage
 import javax.swing.*; import java.awt.*; import java.io.*; import java.awt.image.*; import javax.imageio.ImageIO; public class ShowImage extends JApplet {   private GraphicsConfiguration gc;   private BufferedImage im;   public void init( )   {     // get this device's graphics configuration     GraphicsEnvironment ge =          GraphicsEnvironment.getLocalGraphicsEnvironment( );     gc = ge.getDefaultScreenDevice( ).getDefaultConfiguration( );     im = loadImage("ball.gif");   } // end of init( )    public BufferedImage loadImage(String fnm)    /* Load the image from <fnm>, returning it as a BufferedImage       which is compatible with the graphics device being used.       Uses ImageIO. */    {      try {        BufferedImage im = ImageIO.read(getClass( ).getResource(fnm));        int transparency = im.getColorModel( ).getTransparency( );        BufferedImage copy =  gc.createCompatibleImage(                                  im.getWidth( ),im.getHeight( ),transparency );        // create a graphics context        Graphics2D g2d = copy.createGraphics( );        // copy image        g2d.drawImage(im,0,0,null);        g2d.dispose( );        return copy;      }      catch(IOException e) {        System.out.println("Load Image error for " + fnm + ":\n" + e);        return null;      }   } // end of loadImage( )   public void paint(Graphics g)   {  g.drawImage(im, 0, 0, this);  } } // end of ShowImage class 

The three-argument version of createCompatibleImage( ) is utilized, which requires the BufferedImage's width, height, and transparency value. The possible transparency values are TRansparency.OPAQUE, TRansparency.BITMASK, and TRansparency.TRANSLUCENT. The BITMASK setting is applicable to GIFs that have a transparent area, and TRANSLUCENT can be employed by translucent PNG images.

There's a two-argument version of createCompatibleImage( ), which only requires the image's width and height, but if the source image has a transparent or translucent component, then it (most probably) will be copied incorrectly. For instance, the transparent areas in the source may be drawn as solid black.

Fortunately, it's quite simple to access the transparency information in the source BufferedImage, by querying its ColorModel (explained later):

     int transparency = im.getColorModel( ).getTransparency( );

The BufferedImage object copy is initialized by drawing the source image into its graphics context.

Another reason for the use of createCompatibleImage( ) is that it permits J2SE 1.4.2 to mark the resulting BufferedImage as a managed image, which may later be drawn to the screen using hardware acceleration. In J2SE 5.0, the JVM knows that anything read in by ImageIO's read( ) can become a managed image, so the call to createCompatibleImage( ) is no longer necessary for that reason. The call should still be made though since it optimizes the BufferedImage's internals for the graphics device.

From Image to BufferedImage

Legacy code usually employs Image, and it may not be feasible to rewrite the entire code base to utilize BufferedImage. Instead, is there a way to convert an Image object to a BufferedImage object? makeBIM( ) makes a gallant effort:

     private BufferedImage makeBIM(Image im, int width, int height)     // make a BufferedImage copy of im, assuming an alpha channel     {       BufferedImage copy = new BufferedImage(width, height,                                           BufferedImage.TYPE_INT_ARGB);       // create a graphics context       Graphics2D g2d = copy.createGraphics( );       // copy image       g2d.drawImage(im,0,0,null);       g2d.dispose( );       return copy;     }

This method can be used in ShowImage:

     public void init( )     // load an imageIcon, convert to BufferedImage     {       ImageIcon imIcon = new ImageIcon( getClass( ).getResource("ball.gif") );       im = makeBIM(imIcon.getImage( ), imIcon.getIconWidth( ),                                       imIcon.getIconHeight( ));     }

I load an ImageIcon (to save on MediaTracker coding) and pass its Image, width, and height into makeBIM( ), getting back a suitable BufferedImage object.

A thorny issue with makeBIM( ) is located in the BufferedImage( ) constructor. The constructor must be supplied with a type, and there's a lot to choose from (look at the Java documentation for BufferedImage for a complete list). A partial list appears in Table 5-1.

Table 5-1. Some BufferedImage types

BufferedImage type

Description

TYPE_INT_ARGB

8-bit alpha, red, green, and blue samples packed into a 32-bit integer

TYPE_INT_RGB

8-bit red, green, and blue samples packed into a 32-bit integer

TYPE_BYTE_GRAY

An unsigned byte grayscale image (1 pixel/byte)

TYPE_BYTE_BINARY

A byte-packed binary image (8 pixels/byte)

TYPE_INT_BGR

8-bit blue, green, and red samples packed into a 32-bit integer

TYPE_3BYTE_RGB

8-bit blue, green, and red samples packed into 1 byte each


An image is made up of pixels (of course), and each pixel is composed from (perhaps) several samples. Samples hold the color component data that combine to make the pixel's overall color.

A standard set of color components are red, green, and blue (RGB for short). The pixels in a transparent or translucent color image will include an alpha (A) component to specify the degree of transparency for the pixels. A grayscale image only utilizes a single sample per pixel.

BufferedImage types specify how the samples that make up a pixel's data are packed together. For example, TYPE_INT_ARGB packs its four samples into 8 bits each so that a single pixel can be stored in a single 32-bit integer. This is shown graphically in Figure 5-3.

Figure 5-3. A TYPE_INT_ARGB pixel


This format is used for the BufferedImage object in makeBIM( ) since it's the most general. The RGB and alpha components can have 256 different values (28), with 255 being full-on. For the alpha part, 0 means fully transparent, ranging up to 255 for fully opaque.

Is such flexibility always needed, for instance, when the image is opaque or a grayscale? It may not be possible to accurately map an image stored using a drastically different color model to the range of colors here. An example would be an image using 16-bit color components. Nevertheless, makeBIM( ) deals with the normal range of image formats, e.g., GIF, JPEG, and PNG, and so is satisfactory for our needs.

A more rigorous solution is to use AWT's imaging processing capabilities to analyze the source Image object and construct a BufferedImage accordingly. A PixelGrabber can access the pixel data inside the Image and determine if an alpha component exists and if the image is grayscale or RGB.

A third answer is to go back to basics and ask why the image is being converted to a BufferedImage object at all? A common reason is to make use of BufferedImageOp operations, but they're available without the image being converted. It's possible to wrap a BufferedImageOp object in a BufferedImageFilter to make it behave like an AWT ImageFilter.

The Internals of BufferedImage

The data maintained by a BufferedImage object are represented by Figure 5-4.

Figure 5-4. BufferedImage internals


A BufferedImage instance is made up of a Raster object that stores the pixel data and a ColorModel, which contains methods for converting those data into colors. DataBuffer holds a rectangular array of numbers that make up the data, and SampleModel explains how those numbers are grouped into the samples for each pixel.

One way of viewing the image is as a collection of bands or channels: a band is a collection of the same samples from all the pixels. For instance, an ARGB file contains four bands for alpha, red, green, and blue.

The ColorModel object defines how the samples in a pixel are mapped to color components, and ColorSpace specifies how the components are combined to form a renderable color.

Java 2D supports many color spaces, including the standardized RGB (sRGB) color space, which corresponds to the TYPE_INT_ARGB format in Figure 5-3. The BufferedImage method geTRGB(x,y) utilizes this format: (x, y) is the pixel coordinate, and a single integer is returned which, with the help of bit manipulation, can expose its 8-bit alpha, red, green, and blue components.

setRGB( ) updates an image pixel, and there are get and set methods to manipulate all the pixels as an array of integers. Two of the ImagesTests visual effects in Chapter 6 use these methods.

BufferedImageOp Operations

Java 2D's image processing operations are (for the most part) subclasses of the BufferedImageOp interface, which supports an immediate imaging model. Image processing is a filtering operation that takes a source BufferedImage as input and produces a new BufferedImage as output. The idea is captured by Figure 5-5.

Figure 5-5. The BufferedImageOp imaging model


This doesn't appear to be much different from the ImageFilter idea in Figure 5-1. The differences are in the expressibility of the operations that can, for instance, manipulate groups of pixels and affect the color space. This is due to the data model offered by BufferedImage.

The code fragment below shows the creation of a new BufferedImage, by manipulating a source BufferedImage using RescaleOp; RescaleOp implements the BufferedImageOp interface:

     RescaleOp negOp = new RescaleOp(-1.0f, 255f, null);     BufferedImage destination = negOp.filter(source, null);

The filter( ) method does the work, taking the source image as input and returning the resulting image as destination.

Certain image processing operations can be carried out in place, which means that the destination BufferedImage can be the source; there's no need to create a new BufferedImage object.

Another common way of using a BufferedImageOp is as an argument to drawImage( ); the image will be processed, and the result drawn straight to the screen:

     g2d.drawImage(source, negOp, x, y);

The predefined BufferedImageOp image processing classes are listed in Table 5-2.

Table 5-2. Image processing classes

Class name

Description

Some possible effects

In place?

AffineTransformOp

Apply a geometric transformation to the image's coordinates.

Scaling, rotating, shearing.

No

BandCombineOp

Combine bands in the image's Raster.

Change the mix of colors.

Yes

ColorConvertOp

ColorSpace conversion.

Convert RGB to grayscale.

Yes

ConvolveOp

Combine groups of pixel values to obtain a new pixel value.

Blurring, sharpening, edge detection.

No

LookupOp

Modify pixel values based on a table lookup.

Color inversion, reddening, brightening, darkening.

Yes

RescaleOp

Modify pixel values based on a linear equation.

Mostly the same as LookupOp.

Yes


Various examples of these, together with more detailed explanations of the operations, will be given in Chapter 6 when I discuss ImagesTests.



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net