| ## 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.
| ©Copyright, Macmillan Computer Publishing. All rights reserved.