12.5 ImageFilterImage filters provide another way to modify images. An ImageFilter is used in conjunction with a FilteredImageSource object. The ImageFilter, which implements ImageConsumer (and Cloneable), receives data from an ImageProducer and modifies it; the FilteredImageSource, which implements ImageProducer, sends the modified data to the new consumer. As Figure 12.1 shows, an image filter sits between the original ImageProducer and the ultimate ImageConsumer. The ImageFilter class implements a "null" filter that does nothing to the image. To modify an image, you must use a subclass of ImageFilter, by either writing one yourself or using a subclass provided with AWT, like the CropImageFilter. Another ImageFilter subclass provided with AWT is the RGBImageFilter; it is useful for filtering an image on the basis of a pixel's color. Unlike the CropImageFilter, RGBImageFilter is an abstract class, so you need to create your own subclass to use it. Java 1.1 introduces two more image filters, AreaAveragingScaleFilter and ReplicateScaleFilter. Other filters must be created by subclassing ImageFilter and providing the necessary methods to modify the image as necessary. ImageFilters tend to work on a pixel-by-pixel basis, so large Image objects can take a considerable amount of time to filter, depending on the complexity of the filtering algorithm. In the simplest case, filters generate new pixels based upon the color value and location of the original pixel. Such filters can start delivering data before they have loaded the entire image. More complex filters may use internal buffers to store an intermediate copy of the image so the filter can use adjacent pixel values to smooth or blend pixels together. These filters may need to load the entire image before they can deliver any data to the ultimate consumer. To use an ImageFilter, you pass it to the FilteredImageSource constructor, which serves as an ImageProducer to pass the new pixels to their consumer. The following code runs the image logo.jpg through an image filter, SomeImageFilter, to produce a new image. The constructor for SomeImageFilter is called within the constructor for FilteredImageSource, which in turn is the only argument to createImage().
Image image = getImage (new URL ( "http://www.ora.com/images/logo.jpg")); Image newOne = createImage (new FilteredImageSource (image.getSource(), new SomeImageFilter())); ImageFilter MethodsVariables
When you subclass ImageFilter, there are very few restrictions on what you can do. We will create a few subclasses that show some of the possibilities. This ImageFilter generates a new pixel by averaging the pixels around it. The result is a blurred version of the original. To implement this filter, we have to save all the pixel data into a buffer; we can't start delivering pixels until the entire image is in hand. Therefore, we override setPixels() to build the buffer; we override imageComplete() to produce the new pixels and deliver them. Before looking at the code, here are a few hints about how the filter works; it uses a few tricks that may be helpful in other situations. We need to provide two versions of setPixels(): one for integer arrays, and the other for byte arrays. To avoid duplicating code, both versions call a single method, setThePixels(), which takes an Object as an argument, instead of a pixel array; thus it can be called with either kind of pixel array. Within the method, we check whether the pixels argument is an instance of byte[] or int[]. The body of this method uses another trick: when it reads the byte[] version of the pixel array, it ANDs the value with 0xff. This prevents the byte value, which is signed, from being converted to a negative int when used as an argument to cm.getRGB(). The logic inside of imageComplete() gets a bit hairy. This method does the actual filtering, after all the data has arrived. Its job is basically simple: compute an average value of the pixel and the eight pixels surrounding it (i.e., a 3x3 rectangle with the current pixel in the center). The problem lies in taking care of the edge conditions. We don't always want to average nine pixels; in fact, we may want to average as few as four. The if statements figure out which surrounding pixels should be included in the average. The pixels we care about are placed in sumArray[], which has nine elements. We keep track of the number of elements that have been saved in the variable sumIndex and use a helper method, avgPixels(), to compute the average. The code might be a little cleaner if we used a Vector, which automatically counts the number of elements it contains, but it would probably be much slower. Example 12.7 shows the code for the blurring filter. Example 12.7: Blur Filter Source
import java.awt.*; import java.awt.image.*; public class BlurFilter extends ImageFilter { private int savedWidth, savedHeight, savedPixels[]; private static ColorModel defaultCM = ColorModel.getRGBdefault(); public void setDimensions (int width, int height) { savedWidth=width; savedHeight=height; savedPixels=new int [width*height]; consumer.setDimensions (width, height); } We override setDimensions() to save the original image's height and width, which we use later.
public void setColorModel (ColorModel model) { // Change color model to model you are generating consumer.setColorModel (defaultCM); } public void setHints (int hintflags) { // Set new hints, but preserve SINGLEFRAME setting consumer.setHints (TOPDOWNLEFTRIGHT | COMPLETESCANLINES | SINGLEPASS | (hintflags & SINGLEFRAME)); } This filter always generates pixels in the same order, so it sends the hint flags TOPDOWNLEFTRIGHT, COMPLETESCANLINES, and SINGLEPASS to the consumer, regardless of what the image producer says. It sends the SINGLEFRAME hint only if the producer has sent it.
private void setThePixels (int x, int y, int width, int height, ColorModel cm, Object pixels, int offset, int scansize) { int sourceOffset = offset; int destinationOffset = y * savedWidth + x; boolean bytearray = (pixels instanceof byte[]); for (int yy=0;yy<height;yy++) { for (int xx=0;xx<width;xx++) if (bytearray) savedPixels[destinationOffset++]= cm.getRGB(((byte[])pixels)[sourceOffset++]&0xff); else savedPixels[destinationOffset++]= cm.getRGB(((int[])pixels)[sourceOffset++]); sourceOffset += (scansize - width); destinationOffset += (savedWidth - width); } } setThePixels() saves the pixel data for the image in the array savedPixels[]. Both versions of setPixels() call this method. It doesn't pass the pixels along to the image consumer, since this filter can't process the pixels until the entire image is available.
public void setPixels (int x, int y, int width, int height, ColorModel cm, byte pixels[], int offset, int scansize) { setThePixels (x, y, width, height, cm, pixels, offset, scansize); } public void setPixels (int x, int y, int width, int height, ColorModel cm, int pixels[], int offset, int scansize) { setThePixels (x, y, width, height, cm, pixels, offset, scansize); } public void imageComplete (int status) { if ((status == IMAGEABORTED) || (status == IMAGEERROR)) { consumer.imageComplete (status); return; } else { int pixels[] = new int [savedWidth]; int position, sumArray[], sumIndex; sumArray = new int [9]; // maxsize - vs. Vector for performance for (int yy=0;yy<savedHeight;yy++) { position=0; int start = yy * savedWidth; for (int xx=0;xx<savedWidth;xx++) { sumIndex=0; // xx yy sumArray[sumIndex++] = savedPixels[start+xx]; // center center if (yy != (savedHeight-1)) // center bottom sumArray[sumIndex++] = savedPixels[start+xx+savedWidth]; if (yy != 0) // center top sumArray[sumIndex++] = savedPixels[start+xx-savedWidth]; if (xx != (savedWidth-1)) // right center sumArray[sumIndex++] = savedPixels[start+xx+1]; if (xx != 0) // left center sumArray[sumIndex++] = savedPixels[start+xx-1]; if ((yy != 0) && (xx != 0)) // left top sumArray[sumIndex++] = savedPixels[start+xx-savedWidth-1]; if ((yy != (savedHeight-1)) && (xx != (savedWidth-1))) // right bottom sumArray[sumIndex++] = savedPixels[start+xx+savedWidth+1]; if ((yy != 0) && (xx != (savedWidth-1))) //right top sumArray[sumIndex++] = savedPixels[start+xx-savedWidth+1]; if ((yy != (savedHeight-1)) && (xx != 0)) //left bottom sumArray[sumIndex++] = savedPixels[start+xx+savedWidth-1]; pixels[position++] = avgPixels(sumArray, sumIndex); } consumer.setPixels (0, yy, savedWidth, 1, defaultCM, pixels, 0, savedWidth); } consumer.imageComplete (status); } } imageComplete() does the actual filtering after the pixels have been delivered and saved. If the producer reports that an error occurred, this method passes the error flags to the consumer and returns. If not, it builds a new array, pixels[], which contains the filtered pixels, and delivers these to the consumer. Previously, we gave an overview of how the filtering process works. Here are some details. (xx, yy) represents the current point's x and y coordinates. The point (xx, yy) must always fall within the image; otherwise, our loops are constructed incorrectly. Therefore, we can copy (xx, yy) into the sumArray[] for averaging without any tests. For the point's eight neighbors, we check whether the neighbor falls in the image; if so, we add it to sumArray[]. For example, the point just below (xx, yy) is at the bottom center of the 3x3 rectangle of points we are averaging. We know that xx falls within the image; yy falls within the image if it doesn't equal savedHeight-1. We do similar tests for the other points. Even though we're working with a rectangular image, our arrays are all one-dimensional so we have to convert a coordinate pair (xx, yy) into a single array index. To help us do the bookkeeping, we use the local variable start to keep track of the start of the current scan line. Then start + xx is the current point; start + xx + savedWidth is the point immediately below; start + xx + savedWidth-1 is the point below and to the left; and so on. avgPixels() is our helper method for computing the average value that we assign to the new pixel. For each pixel in the pixels[] array, it extracts the red, blue, green, and alpha components; averages them separately, and returns a new ARGB value.
private int avgPixels (int pixels[], int size) { float redSum=0, greenSum=0, blueSum=0, alphaSum=0; for (int i=0;i<size;i++) try { int pixel = pixels[i]; redSum += defaultCM.getRed (pixel); greenSum += defaultCM.getGreen (pixel); blueSum += defaultCM.getBlue (pixel); alphaSum += defaultCM.getAlpha (pixel); } catch (ArrayIndexOutOfBoundsException e) { System.out.println ("Ooops"); } int redAvg = (int)(redSum / size); int greenAvg = (int)(greenSum / size); int blueAvg = (int)(blueSum / size); int alphaAvg = (int)(alphaSum / size); return ((0xff << 24) | (redAvg << 16) | (greenAvg << 8) | (blueAvg << 0)); } } The ImageFilter framework is flexible enough to allow you to return a sequence of images based on an original. You can send back one frame at a time, calling the following when you are finished with each frame:
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE); After you have generated all the frames, you can tell the consumer that the sequence is finished with the STATICIMAGEDONE constant. In fact, this is exactly what the new animation capabilities of MemoryImageSource use. In Example 12.8, the DynamicFilter lets the consumer display an image. After the image has been displayed, the filter gradually overwrites the image with a specified color by sending additional image frames. The end result is a solid colored rectangle. Not too exciting, but it's easy to imagine interesting extensions: you could use this technique to implement a fade from one image into another. The key points to understand are:
Example 12.8: DynamicFilter Source
import java.awt.*; import java.awt.image.*; public class DynamicFilter extends ImageFilter { Color overlapColor; int delay; int imageWidth; int imageHeight; int iterations; DynamicFilter (int delay, int iterations, Color color) { this.delay = delay; this.iterations = iterations; overlapColor = color; } public void setDimensions (int width, int height) { imageWidth = width; imageHeight = height; consumer.setDimensions (width, height); } public void setHints (int hints) { consumer.setHints (ImageConsumer.RANDOMPIXELORDER); } public void resendTopDownLeftRight (ImageProducer ip) { } public void imageComplete (int status) { if ((status == IMAGEERROR) || (status == IMAGEABORTED)) { consumer.imageComplete (status); return; } else { int xWidth = imageWidth / iterations; if (xWidth <= 0) xWidth = 1; int newPixels[] = new int [xWidth*imageHeight]; int iColor = overlapColor.getRGB(); for (int x=0;x<(xWidth*imageHeight);x++) newPixels[x] = iColor; int t=0; for (;t<(imageWidth-xWidth);t+=xWidth) { consumer.setPixels(t, 0, xWidth, imageHeight, ColorModel.getRGBdefault(), newPixels, 0, xWidth); consumer.imageComplete (ImageConsumer.SINGLEFRAMEDONE); try { Thread.sleep (delay); } catch (InterruptedException e) { e.printStackTrace(); } } int left = imageWidth-t; if (left > 0) { consumer.setPixels(imageWidth-left, 0, left, imageHeight, ColorModel.getRGBdefault(), newPixels, 0, xWidth); consumer.imageComplete (ImageConsumer.SINGLEFRAMEDONE); } consumer.imageComplete (STATICIMAGEDONE); } } } The DynamicFilter relies on the default setPixels() method to send the original image to the consumer. When the original image has been transferred, the image producer calls this filter's imageComplete() method, which does the real work. Instead of relaying the completion status to the consumer, imageComplete() starts generating its own data: solid rectangles that are all in the overlapColor specified in the constructor. It sends these rectangles to the consumer by calling consumer.setPixels(). After each rectangle, it calls consumer.imageComplete() with the SINGLEFRAMEDONE flag, meaning that it has just finished one frame of a multi-frame sequence. When the rectangles have completely covered the image, the method imageComplete() finally notifies the consumer that the entire image sequence has been transferred by sending the STATICIMAGEDONE flag. The following code is a simple applet that uses this image filter to produce a new image:
import java.applet.*; import java.awt.*; import java.awt.image.*; public class DynamicImages extends Applet { Image i, j; public void init () { i = getImage (getDocumentBase(), "rosey.jpg"); j = createImage (new FilteredImageSource (i.getSource(), new DynamicFilter(250, 10, Color.red))); } public void paint (Graphics g) { g.drawImage (j, 10, 10, this); } } One final curiosity: the DynamicFilter doesn't make any assumptions about the color model used for the original image. It sends its overlays with the default RGB color model. Therefore, this is one case in which an ImageConsumer may see calls to setPixels() that use different color models. RGBImageFilterRGBImageFilter is an abstract subclass of ImageFilter that provides a shortcut for building the most common kind of image filters: filters that independently modify the pixels of an existing image, based only on the pixel's position and color. Because RGBImageFilter is an abstract class, you must subclass it before you can do anything. The only method your subclass must provide is filterRGB(), which produces a new pixel value based on the original pixel and its location. A handful of additional methods are in this class; most of them provide the behind-the-scenes framework for funneling each pixel through the filterRGB() method. If the filtering algorithm you are using does not rely on pixel position (i.e., the new pixel is based only on the old pixel's color), AWT can apply an optimization for images that use an IndexColorModel: rather than filtering individual pixels, it can filter the image's color map. In order to tell AWT that this optimization is okay, add a constructor to the class definition that sets the canFilterIndexColorModel variable to true. If canFilterIndexColorModel is false (the default) and an IndexColorModel image is sent through the filter, nothing happens to the image. Variables
The only method you care about here is filterRGB(). All subclasses of RGBImageFilter must override this method. It is very difficult to imagine situations in which you would override (or even call) the other methods in this group. They are helper methods that funnel pixels through filterRGB().
Creating your own RGBImageFilter is fairly easy. One of the more common applications for an RGBImageFilter is to make images transparent by setting the alpha component of each pixel. To do so, we extend the abstract RGBImageFilter class. The filter in Example 12.9 makes the entire image translucent, based on a percentage passed to the class constructor. Filtering is independent of position, so the constructor can set the canFilterIndexColorModel variable. A constructor with no arguments uses a default alpha value of 0.75. Example 12.9: TransparentImageFilter Source
import java.awt.image.*; class TransparentImageFilter extends RGBImageFilter { float alphaPercent; public TransparentImageFilter () { this (0.75f); } public TransparentImageFilter (float aPercent) throws IllegalArgumentException { if ((aPercent < 0.0) || (aPercent > 1.0)) throw new IllegalArgumentException(); alphaPercent = aPercent; canFilterIndexColorModel = true; } public int filterRGB (int x, int y, int rgb) { int a = (rgb >> 24) & 0xff; a *= alphaPercent; return ((rgb & 0x00ffffff) | (a << 24)); } } CropImageFilterThe CropImageFilter is an ImageFilter that crops an image to a rectangular region. When used with FilteredImageSource, it produces a new image that consists of a portion of the original image. The cropped region must be completely within the original image. It is never necessary to subclass this class. Also, using the 10 or 11 argument version of Graphics.drawImage() introduced in Java 1.1 precludes the need to use this filter, unless you need to save the resulting cropped image. If you crop an image and then send the result through a second ImageFilter, the pixel array received by the filter will be the size of the original Image, with the offset and scansize set accordingly. The width and height are set to the cropped values; the result is a smaller Image with the same amount of data. CropImageFilter keeps the full pixel array around, partially empty. Constructors
Example 12.10 uses a CropImageFilter to extract the center third of a larger image. No subclassing is needed; the CropImageFilter is complete in itself. The output is displayed in Figure 12.7. Example 12.10: Crop Applet Source
import java.applet.*; import java.awt.*; import java.awt.image.*; public class Crop extends Applet { Image i, j; public void init () { MediaTracker mt = new MediaTracker (this); i = getImage (getDocumentBase(), "rosey.jpg"); mt.addImage (i, 0); try { mt.waitForAll(); int width = i.getWidth(this); int height = i. getHeight(this); j = createImage (new FilteredImageSource (i.getSource(), new CropImageFilter (width/3, height/3, width/3, height/3))); } catch (InterruptedException e) { e.printStackTrace(); } } public void paint (Graphics g) { g.drawImage (i, 10, 10, this); // regular if (j != null) { g.drawImage (j, 10, 90, this); // cropped } } }
You can use CropImageFilter to help improve your animation performance or just the general download time of images. Without CropImageFilter, you can use Graphics.clipRect() to clip each image of an image strip when drawing. Instead of clipping each Image (each time), you can use CropImageFilter to create a new Image for each cell of the strip. Or for times when an image strip is inappropriate, you can put all your images within one image file (in any order whatsoever), and use CropImageFilter to get each out as an Image .
ReplicateScaleFilterBack in Chapter 2, Simple Graphics we introduced you to the getScaledInstance() method. This method uses a new image filter that is provided with Java 1.1. The ReplicateScaleFilter and its subclass, AreaAveragingScaleFilter, allow you to scale images before calling drawImage(). This can greatly speed your programs because you don't have to wait for the call to drawImage() before performing scaling. The ReplicateScaleFilter is an ImageFilter that scales by duplicating or removing rows and columns. When used with FilteredImageSource, it produces a new image that is a scaled version of the original. As you can guess, ReplicateScaleFilter is very fast, but the results aren't particularly pleasing aesthetically. It is great if you want to magnify a checkerboard but not that useful if you want to scale an image of your Aunt Polly. Its subclass, AreaAveragingScaleFilter, implements a more time-consuming algorithm that is more suitable when image quality is a concern. Constructor
AreaAveragingScaleFilterThe AreaAveragingScaleFilter subclasses ReplicateScaleFilter to provide a better scaling algorithm. Instead of just dropping or adding rows and columns, AreaAveragingScaleFilter tries to blend pixel values when creating new rows and columns. The filter works by replicating rows and columns to generate an image that is a multiple of the original size. Then the image is resized back down by an algorithm that blends the pixels around each destination pixel. AreaAveragingScaleFilter methods Because this filter subclasses ReplicateScaleFilter, the only methods it includes are those that override methods of ReplicateScaleFilter. Constructors
Cascading FiltersIt is often a good idea to perform complex filtering operations by using several filters in a chain. This technique requires the system to perform several passes through the image array, so it may be slower than using a single complex filter; however, cascading filters yield code that is easier to understand and quicker to write--particularly if you already have a collection of image filters from other projects. For example, assume you want to make a color image transparent and then render the image in black and white. The easy way to do this task is to apply a filter that converts color to a gray value and then apply the TransparentImageFilter we developed in Example 12.9. Using this strategy, we have to develop only one very simple filter. Example 12.11 shows the source for the GrayImageFilter; Example 12.12 shows the applet that applies the two filters in a daisy chain. Example 12.11: GrayImageFilter Source
import java.awt.image.*; public class GrayImageFilter extends RGBImageFilter { public GrayImageFilter () { canFilterIndexColorModel = true; } public int filterRGB (int x, int y, int rgb) { int gray = (((rgb & 0xff0000) >> 16) + ((rgb & 0x00ff00) >> 8) + (rgb & 0x0000ff)) / 3; return (0xff000000 | (gray << 16) | (gray << 8) | gray); } } Example 12.12: DrawingImages Source
import java.applet.*; import java.awt.*; import java.awt.image.*; public class DrawingImages extends Applet { Image i, j, k, l; public void init () { i = getImage (getDocumentBase(), "rosey.jpg"); GrayImageFilter gif = new GrayImageFilter (); j = createImage (new FilteredImageSource (i.getSource(), gif)); TransparentImageFilter tf = new TransparentImageFilter (.5f); k = createImage (new FilteredImageSource (j.getSource(), tf)); l = createImage (new FilteredImageSource (i.getSource(), tf)); } public void paint (Graphics g) { g.drawImage (i, 10, 10, this); // regular g.drawImage (j, 270, 10, this); // gray g.drawImage (k, 10, 110, Color.red, this); // gray - transparent g.drawImage (l, 270, 110, Color.red, this); // transparent } } Granted, neither the GrayImageFilter or the TransparentImageFilter are very complex, but consider the savings you would get if you wanted to blur an image, crop it, and then render the result in grayscale. Writing a filter that does all three is not a task for the faint of heart; remember, you can't subclass RGBImageFilter or CropImageFilter because the result does not depend purely on each pixel's color and position. However, you can solve the problem easily by cascading the filters developed in this chapter. |
|