TOC
BACK
FORWARD
HOME

Java 1.1 Unleashed

- 51 -
Advanced Image Processing

by K.C. Hopson and Stephen Ingram

IN THIS CHAPTER

  • The Image Model
  • Image Producers
  • Filtering an Image
  • The Mandelbrot Set Project

This chapter teaches you the more advanced concepts involved in Java images. It leads off by introducing Java's fundamental image model. Image filters are explored, and two advanced filters are explained, including a special-effects filter. This chapter ends by using a custom filter to present Mandelbrot sets (visually striking images from chaos theory).

To really appreciate the power behind Java images, you have to understand the consumer/ producer model in detail. Powerful graphics applications use the advantages of this model to perform their visual wizardry. In particular, you can write effective image filters only if you understand the underlying model.

The Image Model

True to Java's object-oriented design, images are not presented by a single class. Rather, a richer and more specialized class system is used. The designers of Java wanted an extensible image system that would allow image manipulation as well as support for a wide variety of image formats. The best way to meet these requirements was to devolve image presentation into its requisite parts. An image producer understands a particular image format but is unaware of the details behind image display. Instead, the producer renders its contents through an image consumer interface. By separating production from consumption, Java allows multiple image filters to be inserted between the base image and its eventual display.

Image Producers

The ImageProducer interface has the following methods:

  • public void addConsumer(ImageConsumer ic);

  • public boolean isConsumer(ImageConsumer ic);

  • public void removeConsumer(ImageConsumer ic);

  • public void startProduction(ImageConsumer ic);

  • public void requestTopDownLeftRightResend(ImageConsumer ic);

All the methods require an ImageConsumer object. There are no back doors; an ImageProducer can output only through an associated ImageConsumer. A given producer can have multiple objects as client consumers, although this is not usually the case. Typically, as soon as a consumer registers itself with a producer using addConsumer(), the image data is immediately delivered through the consumer's interface.

Image Consumers

The ImageProducer interface is clean and straightforward, but the ImageConsumer is quite a bit more complex. It has the following methods:

  • public void setDimensions(int width, int height);

  • public void setProperties(Hashtable props);

  • public void setColorModel(ColorModel model);

  • public void setHints(int hintflags);

  • public void setPixels(int x, int y, int w, int h, ColorModel model, byte pixels[], int off, int scansize);

  • public void setPixels(int x, int y, int w, int h, ColorModel model, int pixels[], int off, int scansize);

  • public void imageComplete(int status);

Figure 51.1 shows the normal progression of calls to the ImageConsumer interface. Several methods are optional: setProperties(), setHints(), and setColorModel(). The core method is setDimensions(), followed by one or more calls to setPixels(). Finally, when there are no more setPixels() calls, imageComplete() is invoked.

Figure 51.1.

Normal flow of calls to an ImageConsumer interface.

Each image has fixed rectangular dimensions, which are passed in setDimensions(). The consumer saves this data for future reference. The setProperties() method has no discernible use right now, and most consumers don't do anything with it. The hint flags, however, are a different story. Hints are supposed to give clues about the format of the producer's data. Table 51.1 lists the values for hint flags.

Table 51.1. The values for the hintflags parameter for setHints().

Name Meaning
RANDOMPIXELORDER=1 Make no assumptions about the delivery of pixels.
TOPDOWNLEFTRIGHT=2 Pixel delivery will paint in top to bottom, left to right.
COMPLETESCANLINES=4 Pixels will be delivered in multiples of complete rows.
SINGLEPASS=8 Pixels will be delivered in a single pass. No pixel will appear in more than one setPixel() call.
SINGLEFRAME=16 The image consists of a single static frame.


When all the pixel information has been transmitted, the producer calls imageComplete(status). The status parameter has one of three values: IMAGEERROR=1, SINGLEFRAMEDONE=2, or STATICFRAMEDONE=3.

SINGLEFRAMEDONE indicates that additional frames will follow; for example, a video camera would use this technique. Special-effects filters can also use SINGLEFRAMEDONE. STATICFRAMEDONE is used to indicate that no more pixels will be transmitted for the image. The consumer should remove itself from the producer after receiving STATICFRAMEDONE.

Two setPixels() calls provide the image data. Keep in mind that the image size was set in advance by setDimensions(). The array within setPixels() calls does not necessarily contain all the pixels within an image. In fact, the calls usually contain only a rectangular subset of the total image. Figure 51.2 shows a rectangle of setPixels() within an entire image.

Figure 51.2.

The relationship of setPixels() calls to an entire image.

The row size of the array is the scansize. The width and height (w and h) parameters indicate the usable pixels within the array, and the offset (off) contains the starting index. It is up to the consumer to map the passed array onto the entire image. The subimage's location within the total image is contained in the x and y parameters.

The ColorModel contains all the necessary color information for the image. The call to setColorModel() is purely informational because each setPixels() call passes a specific ColorModel parameter. No assumptions should be made about the ColorModel from the setColorModel() calls.

Filtering an Image

Image filters sit between an ImageProducer and an ImageConsumer and must implement both these interfaces. Java supplies two separate classes for using filters: FilteredImageSource and ImageFilter.

The FilteredImageSource Class

The FilteredImageSource class implements the ImageProducer interface, which allows the class to masquerade as a real producer. When a consumer attaches to the FilteredImageSource, it's stored in an instance of the current filter. The filter class object is then given to the actual ImageProducer. When the image is rendered through the filter's interface, the data is altered before being forwarded to the actual ImageConsumer. Figure 51.3 shows the filtering operation.

The following is the constructor for FilteredImageSource:

FilteredImageSource(ImageProducer orig, ImageFilter imgf);

Figure 51.3

Image filtering classes.

Producer and filter are stored until a consumer attaches itself to the FilteredImageSource. The following lines show how an application sets up a filter chain:

// Create the filter
ImageFilter filter = new SomeFilter();
// Use the filter to get a producer
ImageProducer p = new FilteredImageSource(myImage.getSource(), filter);
// Use the producer to create the image
Image img = createImage(p);

Writing a Filter

Filters always extend the ImageFilter class, which implements all the methods for an ImageConsumer. In fact, the ImageFilter class is itself a pass-through filter. It passes the data without alteration but otherwise acts as a normal image filter. The FilteredImageSource class works only with ImageFilter and its subclasses. Using ImageFilter as a base frees you from having to implement a method you have no use for, such as setProperties(). ImageFilter also implements one additional method:

public void resendTopDownLeftRight(ImageProducer ip);

When a FilteredImageSource gets a request to resend through its ImageProducer interface, it calls the ImageFilter instead of the actual producer. ImageFilter's default resend function calls the producer and requests a repaint. There are times when the filter does not want to have the image regenerated, so it can override this call and simply do nothing. One example of this type of filter is described in the section "Dynamic Image Filter: FXFilter," later in this chapter. A special-effects filter can simply remove or obscure certain parts of an underlying image. To perform the effect, the filter merely has to know the image dimensions, not the specific pixels it will be overwriting. setPixel() calls are safely ignored, but the producer must be prevented from repainting. If your filter does not implement setPixels() calls, a subsequent resend request destroys the filter's changes by writing directly to the consumer.


NOTE: If setPixels() is not overridden in your filter, you will probably want to override resendTopDownLeftRight() to prevent the image from being regenerated after your filter has altered the image.

Static Image Filter: Rotation

Rotation is a common image manipulation. Unfortunately, no standard Java filter performs this operation. This apparent oversight provides us with an excellent opportunity to develop your first filter.

Static filters perform their manipulations and then issue a STATICFRAMEDONE. Because all the alterations are applied at one time, the filter is said to be static. If a filter applies its changes in stages, it is considered to be a dynamic filter.

Pixel Rotation

To perform image rotation, you have to use some math. You can perform the rotation of points with the following formulas:

new_x = x * cos(angle) - y * sin(angle)
new_y = y * cos(angle) + x * sin(angle)

Rotation is around the z axis. Positive angles cause counterclockwise rotation; negative angles cause clockwise rotation. These formulas are defined for Cartesian coordinates. The Java screen is actually inverted, so the positive y-axis runs down the screen, not up. To compensate for this, invert the sign of the sine coefficients:

new_x = x * cos(angle) + y * sin(angle)
new_y = y * cos(angle) - x * sin(angle)

In addition, the sine and cosine functions compute the angle in radians. The following formula converts degrees to radians:

radians = degrees * PI/180;

This works because there are 2*PI radians in a circle. That's all the math you need; now you can set up the ImageConsumer routines.

Handling setDimensions()

The setDimensions() call tells you the total size of the image. Record the size and allocate an array to hold all the pixels. Because this filter rotates the image, the size may change. In an extreme case, the size can grow much larger than the original image because images are rectangular. If you rotate a rectangle 45 degrees, a new rectangle must be computed that contains all the pixels from the rotated image, as shown in Figure 51.4.

Figure 51.4.

The new bounding rectangle after rotation.

To calculate the new bounding rectangle, each vertex of the original image must be rotated. After rotation, the new coordinate is checked for minimum and maximum x and y values. After all four points are rotated, you'll know what the new bounding rectangle is. Record this information as rotation space and inform the consumer of the size after rotation.

Handling setPixels()

The setPixels() calls are very straightforward. Simply translate the pixel color into an RGB value and store it in the original image array allocated in setDimensions().

Handling imageComplete()

The imageComplete() method performs all the work. After the image is final, populate a new rotation space array and return it to the consumer through the consumer's setPixels() routine. Finally, invoke the consumer's imageComplete() method. Listing 51.1 contains the entire filter; the code is also located on the CD-ROM that accompanies this book.

Listing 51.1. The SpinFilter class.


import java.awt.*;
import java.awt.image.*;
public class SpinFilter extends ImageFilter
{
    private double angle;
    private double cos, sin;
    private Rectangle rotatedSpace;
    private Rectangle originalSpace;
    private ColorModel defaultRGBModel;
    private int inPixels[], outPixels[];
    SpinFilter(double angle)
    {
        this.angle = angle * (Math.PI / 180);
        cos = Math.cos(this.angle);
        sin = Math.sin(this.angle);
        defaultRGBModel = ColorModel.getRGBdefault();
    }
    private void transform(int x, int y, double out[])
    {
        out[0] = (x * cos) + (y * sin);
        out[1] = (y * cos) - (x * sin);
    }
    private void transformBack(int x, int y, double out[])
    {
        out[0] = (x * cos) - (y * sin);
        out[1] = (y * cos) + (x * sin);
    }
    public void transformSpace(Rectangle rect)
    {
        double out[] = new double[2];
        double minx = Double.MAX_VALUE;
        double miny = Double.MAX_VALUE;
        double maxx = Double.MIN_VALUE;
        double maxy = Double.MIN_VALUE;
        int w = rect.width;
        int h = rect.height;
        int x = rect.x;
        int y = rect.y;
       for ( int i = 0; i < 4; i++ )
        {
            switch (i)
            {
            case 0: transform(x + 0, y + 0, out); break;
            case 1: transform(x + w, y + 0, out); break;
            case 2: transform(x + 0, y + h, out); break;
            case 3: transform(x + w, y + h, out); break;
            }
            minx = Math.min(minx, out[0]);
            miny = Math.min(miny, out[1]);
            maxx = Math.max(maxx, out[0]);
            maxy = Math.max(maxy, out[1]);
        }
        rect.x = (int) Math.floor(minx);
        rect.y = (int) Math.floor(miny);
        rect.width = (int) Math.ceil(maxx) - rect.x;
        rect.height = (int) Math.ceil(maxy) - rect.y;
    }
    /**
     * Tell the consumer the new dimensions based on our
     * rotation of coordinate space.
     * @see ImageConsumer#setDimensions
     */
    public void setDimensions(int width, int height)
    {
        originalSpace = new Rectangle(0, 0, width, height);
        rotatedSpace = new Rectangle(0, 0, width, height);
        transformSpace(rotatedSpace);
        inPixels = new int[originalSpace.width * originalSpace.height];
        consumer.setDimensions(rotatedSpace.width, rotatedSpace.height);
    }
    /**
     * Tell the consumer that we use the defaultRGBModel color model
     * NOTE: This overrides whatever color model is used underneath us.
     * @param model contains the color model of the image or filter
     *              beneath us (preceding us)
     * @see ImageConsumer#setColorModel
     */
    public void setColorModel(ColorModel model)
    {
        consumer.setColorModel(defaultRGBModel);
    }
    /**
     * Set the pixels in our image array from the passed
     * array of bytes.  Xlate the pixels into our default
     * color model (RGB).
     * @see ImageConsumer#setPixels
     */
    public void setPixels(int x, int y, int w, int h,
                   ColorModel model, byte pixels[],
                   int off, int scansize)
    {
        int index = y * originalSpace.width + x;
        int srcindex = off;
        int srcinc = scansize - w;
        int indexinc = originalSpace.width - w;
        for ( int dy = 0; dy < h; dy++ )
        {
            for ( int dx = 0; dx < w; dx++ )
            {
                inPixels[index++] = model.getRGB(pixels[srcindex++] & 0xff);
            }
            srcindex += srcinc;
            index += indexinc;
        }
    }
    /**
     * Set the pixels in our image array from the passed
     * array of integers.  Xlate the pixels into our default
     * color model (RGB).
     * @see ImageConsumer#setPixels
     */
    public void setPixels(int x, int y, int w, int h,
                   ColorModel model, int pixels[],
                   int off, int scansize)
    {
        int index = y * originalSpace.width + x;
        int srcindex = off;
        int srcinc = scansize - w;
        int indexinc = originalSpace.width - w;
        for ( int dy = 0; dy < h; dy++ )
        {
            for ( int dx = 0; dx < w; dx++ )
            {
                inPixels[index++] = model.getRGB(pixels[srcindex++]);
            }
            srcindex += srcinc;
            index += indexinc;
        }
    }
    /**
     * Notification that the image is complete and there will
     * be no further setPixel calls.
     * @see ImageConsumer#imageComplete
     */
    public void imageComplete(int status)
    {
        if (status == IMAGEERROR || status == IMAGEABORTED)
        {
            consumer.imageComplete(status);
            return;
        }
        double point[] = new double[2];
        int srcwidth = originalSpace.width;
        int srcheight = originalSpace.height;
        int outwidth = rotatedSpace.width;
        int outheight = rotatedSpace.height;
        int outx, outy, srcx, srcy;
        outPixels = new int[outwidth * outheight];
        outx = rotatedSpace.x;
        outy = rotatedSpace.y;
        double end[] = new double[2];
        int index = 0;
        for ( int y = 0; y < outheight; y++ )
        {
            for ( int x = 0; x < outwidth; x++)
            {
                // find the originalSpace point
                transformBack(outx + x, outy + y, point);
                srcx = (int)Math.round(point[0]);
                srcy = (int)Math.round(point[1]);
                // if this point is within the original image
                // retreive its pixel value and store in output
                // else write a zero into the space. (0 alpha = transparent)
                if ( srcx < 0 || srcx >= srcwidth ||
                     srcy < 0 || srcy >= srcheight )
                {
                    outPixels[index++] = 0;
                }
                else
                {
                    outPixels[index++] = inPixels[(srcy * srcwidth) + srcx];
                }
            }
        }
        // write the entire new image to the consumer
        consumer.setPixels(0, 0, outwidth, outheight, defaultRGBModel,
                           outPixels, 0, outwidth);
        // tell consumer we are done
        consumer.imageComplete(status);
    }

}

The rotation is complex. First, as Figure 51.4 shows, the rotated object is not completely within the screen's boundary. All the rotated pixels must be translated back in relation to the origin. You can do this easily by assuming that the coordinates of rotated space are really 0,0--the trick is how the array is populated. An iteration is made along each row in rotated space. For each pixel in the row, the rotation is inverted. This yields the position of this pixel within the original space. If the pixel lies within the original image, grab its color and store it in rotated space; if it doesn't, store a transparent color.

A Dynamic Image Filter: FXFilter

The SpinFilter is static; the FXFilter is dynamic. A static filter alters an image and sends STATICIMAGEDONE when the alteration is done; a dynamic filter makes the effect take place over multiple frames, much like an animation. The FXFilter has three effects: wipe left, wipe right, and wipe from center out. Each effect operates by erasing the image in stages. The filter calls imageComplete() many times, but instead of passing STATICIMAGEDONE, it specifies SINGLEFRAMEDONE.

Each of the wipes operates by moving a column of erased pixels over the length of the image. The width of the column is calculated to yield the number of configured iterations.

In setHints(), the consumer is told that the filter will send random pixels. This causes the consumer to call resendTopDownLeftRight() when the image is complete. The filter must intercept the call to avoid having the just-erased image repainted by the producer in pristine form.

The filter has two constructors. If you don't specify a color, the image dissolves into transparency, allowing you to phase one image into a second image. You can also specify an optional color, which causes the image to gradually change into the passed color. You can dissolve an image into the background by passing the background color in the filter constructor. The number of iterations is completely configurable. There is no hard-and-fast formula for performing these effects, so feel free to alter the values to get the result you want. Listing 51.2 contains the source code for the filter; the code is also located on the CD-ROM that accompanies this book.

Listing 51.2. The special-effects filter.


public class FXFilter extends ImageFilter
{
    private int outwidth, outheight;
    private ColorModel defaultRGBModel;
    private int dissolveColor;
    private int iterations = 10;
    private Thread runThread;
    private int inPixels[];
    public static final int WIPE_LR =  1;
    public static final int WIPE_RL =  2;
    public static final int WIPE_C =   3;
    private int type = WIPE_C;
    /**
     * Dissolve to transparent constructor
     */
    FXFilter()
    {
        defaultRGBModel = ColorModel.getRGBdefault();
        dissolveColor = 0;
    }
    /**
     * Dissolve to the passed color constructor
     * @param dcolor contains the color to dissolve to
     */
    FXFilter(Color dcolor)
    {
        this();
        dissolveColor = dcolor.getRGB();
    }
    /**
     * Set the type of effect to perform.
     */
    public void setType(int t)
    {
        switch (t)
        {
        case WIPE_LR:  type = t; break;
        case WIPE_RL:  type = t; break;
        case WIPE_C:   type = t; break;
        }
    }
    /**
     * Set the dissolve iterations. (Optional, will default to 10)
     * @param num contains the number of times to loop.
     */
    public void setIterations(int num)
    {
        iterations = num;
    }
    /**
     * @see ImageConsumer#setDimensions
     */
    public void setDimensions(int width, int height)
    {
        outwidth = width;
        outheight = height;
        inPixels = new int[width * height];
        consumer.setDimensions(width, height);
    }
    public void setPixels(int x, int y, int w, int h,
                   ColorModel model, byte pixels[],
                   int off, int scansize)
    {
        int index = y * outwidth + x;
        int srcindex = off;
        int srcinc = scansize - w;
        int indexinc = outwidth - w;
        for ( int dy = 0; dy < h; dy++ )
        {
            for ( int dx = 0; dx < w; dx++ )
            {
                inPixels[index++] = model.getRGB(pixels[srcindex++] & 0xff);
            }
            srcindex += srcinc;
            index += indexinc;
        }
    }
    public void setPixels(int x, int y, int w, int h,
                   ColorModel model, int pixels[],
                   int off, int scansize)
    {
        int index = y * outwidth + x;
        int srcindex = off;
        int srcinc = scansize - w;
        int indexinc = outwidth - w;
        for ( int dy = 0; dy < h; dy++ )
        {
            for ( int dx = 0; dx < w; dx++ )
            {
                inPixels[index++] = model.getRGB(pixels[srcindex++]);
            }
            srcindex += srcinc;
            index += indexinc;
        }
    }
    /**
     * Override this method to keep the producer
     * from refreshing our dissolved image
     */
    public void resendTopDownLeftRight(ImageProducer ip)
    {
    }
    /**
     * Notification that the image is complete and there will
     * be no further setPixel calls.
     * @see ImageConsumer#imageComplete
     */
    public void imageComplete(int status)
    {
        if (status == IMAGEERROR || status == IMAGEABORTED)
        {
            consumer.imageComplete(status);
            return;
        }
        if ( status == SINGLEFRAMEDONE )
        {
            runThread = new RunFilter(this);
            runThread.start();
        }
        else
            filter();
    }
    public void filter()
    {
        switch ( type )
        {
        case WIPE_LR:  wipeLR();     break;
        case WIPE_RL:  wipeRL();     break;
        case WIPE_C:   wipeC();      break;
        default:       wipeC();   break;
        }
        consumer.imageComplete(STATICIMAGEDONE);
    }
    /**
     * Wipe the image from left to right
     */
    public void wipeLR()
    {
        int xw = outwidth / iterations;
        if ( xw <= 0 ) xw = 1;
        int total = xw * outheight;
        int dissolvePixels[] = new int[total];
        for ( int x = 0; x < total; x++ )
            dissolvePixels[x] = dissolveColor;
        for ( int t = 0; t < (outwidth - xw); t += xw )
        {
            setPixels(t, 0, xw, outheight,
                               defaultRGBModel, dissolvePixels,
                               0, xw);
            // tell consumer we are done with this frame
            consumer.setPixels(0, 0, outwidth, outheight,
                defaultRGBModel, inPixels, 0, outwidth);
            consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
            Thread.yield();
        }
    }
    /**
     * Wipe the image from right to left
     */
    public void wipeRL()
    {
        int xw = outwidth / iterations;
        if ( xw <= 0 ) xw = 1;
        int total = xw * outheight;
        int dissolvePixels[] = new int[total];
        for ( int x = 0; x < total; x++ )
            dissolvePixels[x] = dissolveColor;
        for ( int t = outwidth - xw - 1; t >= 0; t -= xw )
        {
            setPixels(t, 0, xw, outheight,
                               defaultRGBModel, dissolvePixels,
                               0, xw);
            // tell consumer we are done with this frame
            consumer.setPixels(0, 0, outwidth, outheight,
                      defaultRGBModel, inPixels, 0, outwidth);
            consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
            Thread.yield();
        }
    }
    /**
     * Wipe the image from the center out
     */
    public void wipeC()
    {
        int times = outwidth / 2;
        int xw = times / iterations;
        if ( xw <= 0 ) xw = 1;
        int total = xw * outheight;
        int dissolvePixels[] = new int[total];
        for ( int x = 0; x < total; x++ )
            dissolvePixels[x] = dissolveColor;
        int x1 = outwidth /2;
        int x2 = outwidth /2;
        while ( x2 < (outwidth - xw) )
        {
            setPixels(x1, 0, xw, outheight,
                               defaultRGBModel, dissolvePixels,
                               0, xw);
            setPixels(x2, 0, xw, outheight,
                               defaultRGBModel, dissolvePixels,
                               0, xw);
            // tell consumer we are done with this frame
            consumer.setPixels(0, 0, outwidth, outheight,
                      defaultRGBModel, inPixels, 0, outwidth);
            consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
            Thread.yield();
            x1 -= xw;
            x2 += xw;
        }
    }
}
class RunFilter extends Thread
{
    FXFilter fx = null;
    RunFilter(FXFilter f)
    {
        fx = f;
    }
    public void run()
    {
        fx.filter();
    }

}

You need RunFilter for image producers created from a memory image source. GIF and JPEG images both spawn a thread for their producers. Because the filter has to loop within the imageComplete() method, a separate thread is necessary for production. Memory images do not spawn a separate thread for their producers, so the filter has to spawn its own.

The only way to differentiate the producers is to key on their status. GIF and JPEG image producers send STATICIMAGEDONE; memory images send SINGLEFRAMEDONE.


NOTE: If you spawn an additional thread for GIF and JPEG images, you won't be able to display the image at all. Producers that are already a separate thread must operate within their existing threads.

The Mandelbrot Set Project

A new project, one that views the Mandelbrot set, gives you examples of some of the more advanced concepts to which you have been introduced. The Mandelbrot set is the most spectacular example of fractals, one of the hot scientific topics of recent years. With the applets in this chapter, you can view or generate an original Mandelbrot image.

Because the Mandelbrot set can take a while to generate (it requires millions of calculations), you have a chance to combine threads and image filters so that you can view the set as it's being generated. You may also want to save the Mandelbrot images. The BmpClass file, found on the CD-ROM that accompanies this book, converts a BMP-formatted file into a Java image.

The applet in the Mandelbrot example, MandelApp, generates the full Mandelbrot set. Depending on your computer, this process can take a little while; for example, on a 486DX2-50 PC, the process takes a few minutes. When the image is complete (as indicated by a message on the browser's status bar), you can save the image to a BMP-formatted file by clicking anywhere on the applet's display area. The file is called mandel.bmp. Remember to run this applet from a program, such as the applet viewer, that lets applets write to disk.

How It Works

Because it can take quite a while to generate the Mandelbrot set, the program was designed by combining a calculation thread with an image filter so that you can see the results as they are generated. However, understanding how the classes interrelate is a little tricky. Figure 51.5 shows the workflow involved in producing a Mandelbrot image. Understanding this flow is the key to understanding this project.

The process begins when an applet displaying Mandelbrot sets constructs a Mandelbrot object. The Mandelbrot object, in turn, creates an instance of the CalculatorImage class. The Mandelbrot set passes itself as a part of the CalculatorImage constructor. It is referenced as a CalculatorProducer object, an interface that the Mandelbrot class implements. This interface implementation is used to communicate with the image filter.

Figure 51.5.

The workflow involved in producing a Mandelbrot image.

In the next step, the applet requests a Mandelbrot image. This request is initiated by calling the getImage() method of the Mandelbrot object, which in turn leads to a call to a like-named method of the CalculatorImage object. At this point, the CalculatorImage object creates a color palette by using an instance of the ImageColorModel class and then creates a MemoryImageSource object. This object, which implements ImageProducer, produces an image initialized to all zeros (black); it's combined with an instance of the CalculatorFilter class to produce a FilteredImageSource.

When the MemoryImageSource object produces its empty image, it is passed to the CalculatorFilter, which takes the opportunity to produce the calculated image. It does this by kicking off the thread of the image to be calculated. The CalculatorFilter doesn't know that it is the Mandelbrot set that's being calculated; it just knows that some calculation must occur in the CalculatorProducer object in which it has a reference.

Once the Mandelbrot thread is started, it begins the long calculations required to produce a Mandelbrot set. Whenever the thread finishes a section of the set, it notifies the filter with new data through the CalculateFilterNotify interface. The filter, in turn, lets the viewing applet know that it has new data to display by updating the corresponding ImageConsumer, which causes the applet's imageUpdate() method to be called. This call causes a repaint, and the new image data is displayed. This process repeats until the full image is created.

As you have probably observed, this is a complicated process. The Calculator classes are meant to provide a generic approach toward manipulating images that require long calculations. You can replace the Mandelbrot class with some other calculation thread that implements CalculatorProducer, and everything should work. A good exercise is to replace Mandelbrot with another fractal calculation or some other scientific imaging calculation. (I found that replacing Mandelbrot with a Julia fractal class calculation was very easy.)

Some of the classes in this project are now presented with their full source code to explain the advanced image processing. The other classes can be found on the CD-ROM that accompanies this book.

The Mandelbrot Class

The Mandelbrot class, shown in Listing 51.3, calculates the Mandelbrot set. It implements the Runnable interface (so that it can run as a thread) and also implements the CalculatorProducer interface (so that it can update an image filter with the progress made in its calculations). This code is also located on the CD-ROM that accompanies this book.

Listing 51.3. The Mandelbrot class.


import java.awt.image.*;
import java.awt.Image;
import java.lang.*;
// Class for producing a Mandelbrot set image...
public class Mandelbrot implements Runnable, CalculatorProducer {
   int width;  // The dimensions of the image...
   int height;
   CalculateFilterNotify filter; // Keeps track of image production...
   int pix[]; // Pixels used to construct image...
   CalculatorImage img;
   // General Mandelbrot parameters...
   int numColors = 256;
   int maxIterations = 512;
   int maxSize = 4;
   double RealMax,ImagineMax,RealMin,ImagineMin;  // Define sizes to build...
   private Boolean stopCalc = new Boolean(false);  // Stop calculations...
   // Create standard Mandelbrot set
   public Mandelbrot(int width,int height) {
      this.width = width;
      this.height = height;
      RealMax = 1.20;  // Default starting sizes...
      RealMin = -2.0;
      ImagineMax = 1.20;
      ImagineMin = -1.20;
   }
   // Create zoom of Mandelbrot set
   public Mandelbrot(int width,int height,double RealMax,double RealMin,
    double ImagineMax,double ImagineMin) {
      this.width = width;
      this.height = height;
      this.RealMax = RealMax;  // Default starting sizes...
      this.RealMin = RealMin;
      this.ImagineMax = ImagineMax;
      this.ImagineMin = ImagineMin;
   }
   // Start producing the Mandelbrot set...
   public Image getImage() {
      img = new CalculatorImage(width,height,this);
      return img.getImage();
   }
   // Start thread to produce data...
   public void start(int pix[],CalculateFilterNotify filter) {
      this.pix = pix;
      this.filter = filter;
      new Thread(this).start();
   }
   // See if user wants to stop before completion...
   public void stop() {
      synchronized (stopCalc) {
         stopCalc = Boolean.TRUE;
      }
      System.out.println("GOT STOP!");
   }
   // Create data here...
   public void run() {
      // Establish Mandelbrot parameters...
      double Q[] = new double[height];
      // Pixdata is for image filter updates...
      int pixdata[] = new int[height];
      double P,diffP,diffQ, x, y, x2, y2;
      int color, row, column,index;
      System.out.println("RealMax = " + RealMax + " RealMin = " + RealMin +
          " ImagineMax = " + ImagineMax + " ImagineMin = " + ImagineMin);
      // Setup calculation parameters...
      diffP = (RealMax - RealMin)/(width);
      diffQ = (ImagineMax - ImagineMin)/(height);
      Q[0] = ImagineMax;
      color = 0;
      // Setup delta parameters...
      for (row = 1; row < height; row++)
         Q[row] = Q[row-1] - diffQ;
      P = RealMin;
      // Start calculating!
      for (column = 0; column < width; column++) {
         for (row = 0; row < height; row++) {
            x = y = x2 = y2 = 0.0;
            color = 1;
            while ((color < maxIterations) &&
               ((x2 + y2) < maxSize)) {
                  x2 = x * x;
                  y2 = y * y;
                  y = (2*x*y) + Q[row];
                  x = x2 - y2 + P;
                  ++color;
            }
            // plot...
            index = (row * width) + column;
            pix[index] = (int)(color % numColors);
            pixdata[row] = pix[index];
         } // end row
         // Update column after each iteration...
         filter.dataUpdateColumn(column,pixdata);
         P += diffP;
         // See if we were told to stop...
         synchronized (stopCalc) {
            if (stopCalc == Boolean.TRUE) {
               column = width;
               System.out.println("RUN: Got stop calc!");
            }
         }  // end sync
      } // end col
      // Tell filter that we're done producing data...
      System.out.println("FILTER: Data Complete!");
      filter.setComplete();
   }
   // Save the Mandelbrot set as a BMP file...
   public void saveBMP(String filename) {
      img.saveBMP(filename,pix);
   }

}

There are two constructors for the Mandelbrot class. The default constructor produces the full Mandelbrot set and takes the dimensions of the image to calculate. The Real and Imagine variables in the constructors and the run() method are used to map the x,y axis to the real and imaginary portions of c in the following formula:

zn+1=zn2 + c

A few other variables are worth noting. The variable maxIterations represents when to stop calculating a number. If this number (set to 512 in the code) is reached, the starting value of c takes a long time to head toward infinity. The variable maxSize is a simpler indicator of how quickly the current value grows. How the current calculation is related to these variables is mapped to a specific color; the higher the number, the slower the growth. If you have a fast computer, you can adjust these variables to get a richer or duller expression of the Mandelbrot set.

Once the thread is started (by the CalculatorFilter object through the start() method), the run() method calculates the Mandelbrot values and stores a color corresponding to the growth rate of the current complex number into a pixel array. When a column is complete, run() uses the CalculateFilterNotify interface to let the related filter know that new data has been produced. The method also checks to see whether you want to abort the calculation. Note how it synchronizes the stopCalc boolean object in the run() and stop() methods.

The calculation can take a while to complete (it takes a couple minutes on a 486-based PC). Nevertheless, this performance is quite a testament to Java! With other interpreted, portable languages, you may be tempted to use your computer's reset button because the calculations take so long. With Java, you get fast visual feedback on how the set unfolds.

A good exercise is to save any partially developed Mandelbrot set; you can use the saveBMP() method to accomplish that task. You also need some kind of data file to indicate where the calculation was stopped.

The code for the following interfaces and classes (the CalculateFilterNotify interface, the CalculatorProducer interface, the CalculatorFilter class, and the CalculatorImage class) is located on the CD-ROM that accompanies this book.

The CalculateFilterNotify Interface

The CalculateFilterNotify interface defines the methods necessary to update an image filter that works with a calculation thread. As shown in Listing 51.4, the data methods are used to convey a new batch of data to the filter. The setComplete() method indicates that the calculations are complete.

Listing 51.4. The CalculateFilterNotify interface.


/* Interface for defining methods for updating a
   Calulator Filter... */
public interface CalculateFilterNotify {
   public void dataUpdate();   // Update everything...
   public void dataUpdateRow(int row); // Update one row...
   public void dataUpdateColumn(int col,int pixdata[]);  // Update one column...
   public void setComplete();

}

The CalculatorProducer Interface

The CalculatorProducer interface, shown in Listing 51.5, defines the method called when a calculation filter is ready to kick off a thread that produces the data used to generate an image. The CalculateFilterNotify object passed to the start() method is called by the producer whenever new data is yielded.

Listing 51.5. The CalculatorProducer interface.


// Interface for a large calculation to produce image...
interface CalculatorProducer {
   public void start(int pix[],CalculateFilterNotify cf);

}

The CalculatorFilter Class

The CalculatorFilter class, shown in Listing 51.6, is a subclass of ImageFilter. Its purpose is to receive image data produced by some long calculation (such as the one used on the Mandelbrot set) and to update any consumer with the new data's image. The CalculatorProducer, indicated by the variable cp, is what produces the data.

Listing 51.6. The CalculatorFilter class.


import java.awt.image.*;
import java.awt.Image;
import java.awt.Toolkit;
import java.lang.*;
public class CalculatorFilter extends ImageFilter
 implements CalculateFilterNotify {
   private ColorModel defaultRGBModel;
   private int width, height;
   private int pix[];
   private boolean complete = false;
   private CalculatorProducer cp;
   private boolean cpStart = false;
   public CalculatorFilter(ColorModel cm,CalculatorProducer cp) {
      defaultRGBModel = cm;
      this.cp = cp;
   }
   public void setDimensions(int width, int height) {
      this.width = width;
      this.height = height;
      pix = new int[width * height];
      consumer.setDimensions(width,height);
   }
   public void setColorModel(ColorModel model) {
      consumer.setColorModel(defaultRGBModel);
   }
   public void setHints(int hints) {
      consumer.setHints(ImageConsumer.RANDOMPIXELORDER);
   }
   public void resendTopDownLeftRight(ImageProducer p) {
    }
   public void setPixels(int x, int y, int w, int h,
      ColorModel model, int pixels[],int off,int scansize) {
   }
   public void imageComplete(int status) {
     if (!cpStart) {
        cpStart = true;
        dataUpdate();  // Show empty pixels...
        cp.start(pix,this);
     } // end if
     if (complete)
         consumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
   }
   // Called externally to notify that more data has been created
   // Notify consumer so they can repaint...
   public void dataUpdate() {
     consumer.setPixels(0,0,width,height,
               defaultRGBModel,pix,0,width);
     consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
   }
   // External call to update a specific pixel row...
   public void dataUpdateRow(int row) {
     // The key thing here is the second to last parameter (offset)
     // which states where to start getting data from the pix array...
     consumer.setPixels(0,row,width,1,
               defaultRGBModel,pix,(width * row),width);
     consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
   }
   // External call to update a specific pixel column...
   public void dataUpdateColumn(int col,int pixdata[]) {
     // The key thing here is the second to last parameter (offset)
     // which states where to start getting data from the pix array...
     consumer.setPixels(col,0,1,height,
               defaultRGBModel,pixdata,0,1);
     consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
   }
   // Called from external calculating program when data has
   // finished being calculated...
   public void setComplete() {
      complete = true;
      consumer.setPixels(0,0,width,height,
         defaultRGBModel,pix,0,width);
      consumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
   }

}

Because the ImageFilter class was explained earlier in this chapter, issues related to this class are not repeated here. However, a couple of things should be pointed out. When the image is first requested, the filter gets the dimensions the consumer wants by calling the setDimensions() method. At this point, the CalculatorFilter allocates a large array holding the color values for each pixel.

When the original ImageProducer is finished creating the original image, the filter's imageComplete() method is called, but the filter must override this method. In this case, the CalculatorFilter starts the CalculatorProducer thread, passing it the pixel array to put in its updates. Whenever the CalculatorProducer has new data, it calls one of the four methods specified by the CalculateFilterNotify interface: dataUpdate(), dataUpdateRow(), dataUpdateColumn(), or setComplete(). The dataUpdateColumn() method is called by the Mandelbrot calculation because it operates on a column basis. In each of these cases, the filter updates the appropriate consumer pixels by using the setPixels() method, then calls the consumer's imageComplete() method to indicate the nature of the change. For the three data methods, the updates are only partial, so a SINGLEFRAMEDONE flag is sent. The setComplete() method, on the other hand, indicates that everything is complete, so it sets a STATICIMAGEDONE flag.

The CalculatorImage Class

The CalculatorImage class, shown in Listing 51.7, is the glue between the CalculatorProducer class that produces the image data and the CalculatorFilter that manages it.

Listing 51.7. The CalculatorImage class.


// This class takes a CalculatorProducer and sets up the
// environment for creating a calculated image.  Ties the
// producer to the CalculatorFilter so incremental updates can
// be made...
public class CalculatorImage {
   int width;  // The dimensions of the image...
   int height;
   CalculatorProducer cp;  // What produces the image data...
   IndexColorModel palette;  // The colors of the image...
   // Create Palette only once per session...
   static IndexColorModel prvPalette = null;
   int numColors = 256;  // Number of colors in palette...
   // Use defines how big of an image they want...
   public CalculatorImage(int width,int height,CalculatorProducer cp) {
      this.width = width;
      this.height = height;
      this.cp = cp;
   }
   // Start producing the Calculator image...
   public synchronized Image getImage() {
      // Hook into the filter...
      createPalette();
      ImageProducer p = new FilteredImageSource(
       new MemoryImageSource(width,height,palette,
            (new int[width * height]),0,width),
            new CalculatorFilter(palette,cp));
      // Return the image...
      return Toolkit.getDefaultToolkit().createImage(p);
   }
   // Create a 256 color palette...
   // Use Default color model...
   void createPalette() {
     // Create palette only once per session...
     if (prvPalette != null) {
         palette = prvPalette;
         return;
     }
     // Create a palette out of random RGB combinations...
     byte blues[], reds[], greens[];
     reds = new byte[numColors];
     blues = new byte[numColors];
     greens = new byte[numColors];
     // First and last entries are black and white...
     blues[0] = reds[0] = greens[0] = (byte)0;
     blues[255] = reds[255] = greens[255] = (byte)255;
     // Fill in other entries...
     for ( int x = 1; x < 254; x++ ){
      reds[x] = (byte)(255 * Math.random());
      blues[x] = (byte)(255 * Math.random());
      greens[x] = (byte)(255 * Math.random());
     }
     // Create Index Color Model...
     palette = new IndexColorModel(8,256,reds,greens,blues);
     prvPalette = palette;
   }
   // Save the image set as a BMP file...
   public void saveBMP(String filename,int pix[]) {
      try {
         BmpImage.saveBitmap(filename,palette,
            pix,width,height);
      }
      catch (IOException ioe) {System.out.println("Error saving file!"); }
   }

}

When an image is requested with the getImage() method, the CalculatorImage class creates a color palette through an instance of the ImageColorModel class and then creates a MemoryImageSource object. This ImageProducer object produces an image initialized to all zeros (black). It is combined with an instance of the CalculatorFilter class to produce a FilteredImageSource. When the createImage() method of the Abstract Windowing Toolkit (AWT) class is called, production of the calculated image begins.

The color palette is a randomly generated series of pixel values. Depending on your luck, these color combinations can be attractive or uninspiring. The createPalette() method is a good place to create a custom set of colors for this applet--if you want to have some control over its appearance. You should replace the random colors with hard-coded RGB values; you may also want to download a URL file that specifies a special color map.

The Mandelbrot Applet

Listing 51.8 shows the complete code for the Mandelbrot applet, MandelApp; the code is also located on the CD-ROM that accompanies this book. The applet does only a few things. When the applet starts, it gets the Mandelbrot image, forcing it to begin being created. The imageUpdate() method is called to repaint the applet whenever there is some new data to be displayed. The only other thing the applet does is to wait for a mouse click to indicate that you want to save the image. The mouse click is handled by a listener class, MandelMouseListener. Recall that this is an example of the Java 1.1 delegation model for events.

Listing 51.8. The MandelApp class.


// Listener for mouse clicks...
class MandelMouseListener implements MouseListener
{
    MandelApp app;
    public MandelMouseListener(MandelApp app) {
        this.app = app;
    }
    public void mouseClicked(MouseEvent e)
    {
        app.saveImage();
    }
    public void mousePressed(MouseEvent e)
    {
    }
    public void mouseReleased(MouseEvent e)
    {
    }
    public void mouseEntered(MouseEvent e)
    {
    }
    public void mouseExited(MouseEvent e)
    {
    }
}
// This applet displays the Mandlebrot set through
// use of the Mandelbrot class...
public class MandelApp extends Applet  {
   Image im;   // Image that displays Mandelbrot set...
   Mandelbrot m; // Creates the Mandelbrot image...
   int NUMCOLS = 640;   // Dimensions image display...
   int NUMROWS = 350;
   boolean complete = false;
   // Set up the Mandelbrot set...
   public void init() {
      m = new Mandelbrot(NUMCOLS,NUMROWS);
      im = m.getImage();
      // Hook the listener to the app...
      addMouseListener(new MandelMouseListener(this));
   }
   // Will get updates as set is being created.
   // Repaint when they occur...
   public boolean imageUpdate(Image im,int flags,
      int x, int y, int w, int h) {
      if ((flags & FRAMEBITS) != 0) {
         showStatus("Calculating...");
         repaint();
         return true;
      }
      if ((flags & ALLBITS) != 0) {
         showStatus("Image Complete!");
         repaint();
         complete = true;
         return false;
      }
      return true;
   }
   // Paint on update...
   public void update(Graphics g) {
      paint(g);
   }
   public synchronized void paint(Graphics g) {
       g.drawImage(im,0,0,this);
   }
   // Save Bitmap when image complete...
   public void saveImage() {
     if (complete) {
         showStatus("Save Bitmap...");
         m.saveBMP("mandel.bmp");
         showStatus("Bitmap saved!");
     } // end if
   }

}

Summary

This chapter covered advanced image concepts and demonstrated the writing and use of image filters, rotation concepts, and special effects. Finally, a Mandelbrot applet was developed to explain the principles presented in this chapter.

Images give Java tremendous flexibility. Once you master image concepts, the endless possibilities of the Java graphics system are yours to explore.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.