Advanced Topics

What follows are some advanced topics that we are unable to cover to the extent that they deserve. We have provided enough information to allow you to understand the basics of each topic. We have also provided code samples so that you'll be able to experiment with these features without having to worry about getting things set up and running.

RenderedOps Versus RenderableOps

In some situations a RenderedOp is created without knowing how its RenderedImage is going to be used. For example, it could be displayed on a low resolution monitor or a high resolution printer. Similarly, it could be displayed as a thumbnail image or it could be scaled to fit a user specified region. The user could specify that it be rendered with an emphasis on speed or an emphasis on quality. For these reasons, it might make sense to wait until all the relevant rendering information is decided before any rendering hints are set. When working with RenderedOps, this isn't possible because the RenderingHints objects are contained in the RenderedOp object and must be available when the RenderedOp is instantiated. For this reason, the RenderableOp layer was developed.

Unlike a RenderedOp, a RenderableOp doesn't contain a RenderingHints because it isn't capable of creating a RenderedImage directly. Instead, after all the rendering hints have been decided, you can pass the RenderingHints to a RenderableOp, which then uses it to create a RenderedOp. The RenderedOp then makes the RenderedImage, which is returned. Although there are RenderableOp methods to create a rendering, this rendering is done in two steps using an intermediate RenderedOp.

One way to think about the relationship between RenderedOps and RenderableOps is that the RenderableOp DAGs are templates to create RenderedOp DAGs. This relationship is similar to that of a class and an object. A single class can be used to form many objects which, depending on the values of their instance variables, can act very differently. Similarly, a RenderableOp can be used to create many RenderedOps which, depending on the values of their RenderingHints, can act very differently.

When a RenderableOp is rendered, it is usually through its

public RenderedImage createScaledRendering(int width,
                                           int height,
                                           RenderContext rc)

method. After this method is called, the RenderableOp checks whether its sources are rendered. If not, it requests that they become rendered, and this request makes its way up the RenderableOp DAG in the same way that a rendering request makes its way up a RenderedOp DAG. Thus, both RenderedOps and RenderableOps operate in the pull imaging mode. The main difference is that the RenderableOp DAG creates a RenderedOp DAG to produce the final RenderedImage and, of course, a RenderedOp DAG creates the RenderedImage itself.

One last operator that needs to be discussed is the Renderable operator. This operator takes a RenderedImage source, such as a RenderedOp, and converts it into a RenderableOp for use in a RenderableOp DAG (see Table 6.12).

Table 6.12. The Renderable Operator
Operator Parameter Block Format/Description
Renderable
addSource(PlanarImage pi);
add(RenderedOp downSampler);
add(int maxLowResDim);
add(float minX);
add(float minY);
add(float height);

The Renderable operator produces a RenderableImage from a RenderedImage source, pi. The default value for downSampler is null, the default value for maxLowResDim is 64, the default values for minX and minY are 0.0, and the default value for height is 1.0.

Listing 6.15 provides an example of the "Renderable" operator. In this example, an image is loaded from a file, inverted and rotated using a RenderableOp DAG. It is then displayed two times, once as a small image with an emphasis on rendering speed and once as a larger image with an emphasis on rendering quality.

Listing 6.15 RenderableImageTester
package ch6;

import java.awt.*;
import javax.swing.*;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.RenderedImage;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
import javax.media.jai.RenderableOp;

/**
   RenderableImageTester.java -- objects of this class
   3.  create a RenderableOp by loading image contained in filename
   4.  create a 2nd RenderableOp representing the rotation
       of the first RenderableOp
   5.  displays the final RenderedOp
   6.  displays the RenderableOp according to desired image width and
       height and rendering hints
*/
public class RenderableImageTester extends JFrame {
    public RenderableImageTester(String filename) {
        int xScale, yScale;

        RenderedOp imageSource = readInputFile(filename);
        RenderableOp renderableInput = getRenderable(imageSource);
        RenderableOp invertedRenderable;
        invertedRenderable = createRenderableInverted(renderableInput);
        RenderableOp rotatedRenderable;
        rotatedRenderable = createRenderableRotated(invertedRenderable);


        RenderingHints rh;
        rh = new RenderingHints(RenderingHints.KEY_RENDERING,
                                RenderingHints.VALUE_RENDER_SPEED);

        xScale = imageSource.getWidth()/2;
        yScale = imageSource.getHeight()/2;

        RenderedImage smallRendered;
        smallRendered = rotatedRenderable.createScaledRendering(xScale,
                                                                yScale,
                                                                rh);

        rh = new RenderingHints(RenderingHints.KEY_RENDERING,
                                RenderingHints.VALUE_RENDER_QUALITY);
        xScale = imageSource.getWidth()*2;
        yScale = imageSource.getHeight()*2;

        RenderedImage largeRendered;
        largeRendered = rotatedRenderable.createScaledRendering(xScale,
                                                                yScale,
                                                                rh);
        getContentPane().setLayout(new GridLayout(1,2));
        getContentPane().add(new ch6Display(smallRendered));
        getContentPane().add(new ch6Display(largeRendered));

        pack();
        show();
    }

    private RenderedOp readInputFile(String filename) {
        ParameterBlock pb = new ParameterBlock();
        pb.add(filename);
        return JAI.create("fileload", pb);
    }

    private RenderableOp getRenderable(RenderedOp ro) {
        ParameterBlock pb = new ParameterBlock();
        pb.addSource(ro);
        pb.add(null);
        pb.add(null);
        pb.add(null);
        pb.add(null);
        pb.add(null);
        return JAI.createRenderable("renderable", pb);
    }


    /**
       Returns a RenderableOp representing a inverted
       version of RenderableOp toBeInverted
    */
    private RenderableOp createRenderableInverted(RenderableOp inputro) {
        ParameterBlock param;
        param =  new ParameterBlock();
        param.addSource(inputro);
        RenderableOp ro = JAI.createRenderable("Invert", param);

        return ro;
    }


    /**
       Returns a RenderableOp representing a rotated
       version of RenderableOp toBeRotatedRO
    */
    private RenderableOp createRenderableRotated(RenderableOp inputro) {
        float angle = (float)((45.0/180.0)*Math.PI); //45 degree rotation

        ParameterBlock param;
        param =  new ParameterBlock();
        param.addSource(inputro);
        param.add(new Float(inputro.getWidth()/2));
        param.add(new Float(inputro.getHeight()/2));
        param.add(new Float(angle));
        RenderableOp ro = JAI.createRenderable("Rotate", param);

        return ro;
    }

    public static void main(String[] args) {
        if (args.length != 1)
            System.err.println("Usage:  filename");
        else
            new RenderableImageTester(args[0]);
    }
}
						

Client/Server Imaging

In jdk1.2, Java introduced support for remote method invocation (RMI), which allows you to run a JVM on two different machines and have objects running in one JVM call methods on objects running on another JVM. This can be useful in situations in which there is a reason for running a method on a different computer; for instance it might have a faster CPU, a database, or special files. In JAI, Java not only continues its support for RMI, but also has greatly simplified working with remote images.

The basic idea behind RMI is that a client appears to make a method call on a remote object, but actually makes a method call on something called a stub object. This stub object serializes all the method parameters and brings them over to the remote machine where they are placed into the remote machines local memory. The remote object then performs the specified method using these local objects and provides a return object to the stub. (This return object could be a thrown exception.) The stub serializes this return object and brings it back to the client machine where it is placed into local memory. If it is an exception, it is rethrown in the space of the original method call. Thus, in summary, both the client and the server are performing local operations, but the stub object provides the appropriate data transfer, data packaging, and data unpackaging so that it appears as if the client is actually calling a remote method.

Tip

The process of serializing and packaging parameters is usually referred to as parameter marshalling.


One obstacle to implementing this type of system is that for it to work, the stub object needs to be instantiated on the remote server and retrieved by the client when it is needed. Thus, the client must know where to get this stub object. This is done by having the server registering its stub object with an rmi registry. The rmi registry is an application provided as part of the standard jdk. The rmi registry runs on the server using a known port (1099 is the default) so that the client knows how to find it.

In basic Java RMI, the programmer must create the client application, the remote classes, and the remote server application containing the code to register the stub object(s) with the rmi registry. In JAI, you only need to write the client application. After starting the rmi registry, you simply need to start the predefined JAI remote class (java.media.jai.JAIRMIImageServer), which registers predefined remote classes with the rmi registry. Two examples of setting up the JAIRMIImageServer are shown below.

For UNIX:

#!/bin/sh

CLASSPATH=
rmiregistry &

JAI=/usr/java/jre/lib/ext

CLASSPATH=$JAI/jai_core.jar:$JAI/jai_codec.jar:$JAI/mlibwrapper_jai.jar

java -Djava.rmi.server.codebase="file:$JAI/jai_core.jar file:$JAI/jai_codec.jar
file:$JAI/mlibwrapper_jai.jar"
  -Djava.rmi.server.useCodebaseOnly=false
  -Djava.security.policy==file:$PWD/policy
  com.sun.media.jai.rmi.JAIRMIImageServer

FOR WINDOWS:

SET CLASSPATH=
start rmiregistry

SET JAI=/usr/java/jre/lib/ext
SET CLASSPATH=%JAI%jai_core.jar;%JAI%jai_codec.jar;%JAI%mlibwrapper_jai.jar

java -Djava.rmi.server.codebase="file:%JAI%jai_core.jar file:%JAI%jai_codec.ja
r" -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=file:%JAI%pol
icy  com.sun.media.jai.rmi.JAIRMIImageServer

Caution

When the rmi registry is started, the current directory is examined to see whether it contains any class files. This is a very common problem with setting up remote servers. Make sure that no classfiles are in the directory where the rmiregistry is started and make sure that no classfiles are on the classpath (that is, clear the classpath).


When writing the remote client application, you specify that you are working with remote objects by using the remote subclasses of PlanarImage and RenderedOp, namely RemoteImage and RemoteRenderedOp, respectively.

In Listing 6.16, a client application is provided. This application simply takes as input the name of the remote server and a file located on that server. It then accesses that file through the JAIRMIImageServer and displays it.

Listing 6.16 RemoteTester
package ch6;

import java.awt.*;
import javax.swing.*;
import java.awt.image.renderable.ParameterBlock;
import javax.media.jai.remote.RemoteJAI;
import javax.media.jai.remote.RemoteRenderedOp;
/**
   RemoteTester.java -- takes as parameters a remote host and a
   filename.  It then reads and displays the image contained in this
   remote file.
*/
public class RemoteTester extends JFrame {

    public RemoteTester(String serverName, String fileName) {
        ParameterBlock pb;
        String protocolName = "jairmi";
        RemoteJAI rc = new RemoteJAI(protocolName, serverName);

        // Create the operations to load the images from files.
        pb = new ParameterBlock();
        pb.add(fileName);
        pb.add(null);
        pb.add(Boolean.FALSE);
        RemoteRenderedOp remoteImage = rc.create("fileload", pb, null);

        getContentPane().add(new ch6Display(remoteImage));

        pack();
        show();
    }

    public static void main(String[] args) {
        if (args.length == 2)
            new RemoteTester(args[0], args[1]);
        else
            System.err.println("Usage:  RemoteTester serverName fileName");
    }
}
						

RMI Security

Remote imaging is potentially very dangerous because you must ensure that no unwanted users acquire any of your image data. You also need to ensure that a remote client doesn't do any damage to the files on the server machine. For this reason, the JAIRMIImageServer requires the user to specify a policy file when it is started. This is Java's way of making sure that the server only has the permissions that you specify.

The following policy file will provide the client application all possible permissions and might be helpful in getting your application up and running. It should be used for development purposes only.

grant {
     // Allow everything for now
        permission java.security.AllPermission;
     };

A better policy file to use for the previous example is as follows:

grant {
  permission java.net.SocketPermission "*:1024-",
          "listen, resolve, accept, connect, listen, resolve";
  permission java.io.FilePermission "/usr/java/jre/lib/ext/-", "read";
  permission java.io.FilePermission "remoteImages/-", "read";
};

where the first two permissions are necessary for the JAIRMIImageServer to function properly and the last permission is to allow the client to read images contained in a directory called ”remoteImages.”

Extending JAI

The most successful image processing packages are those that are built on a strong foundation, supply the most common operators, and provide a means for users to add their own operators. JAI does all three things. In this section, the process of adding new operators in JAI will be discussed. The steps are as follows:

1.
Provide an operator descriptor.

2.
Create a RIF or a CRIF.

3.
Create an OpImage.

4.
Register the new operator.

In order to describe this process, we will develop an operator called the CheckAlignment operator. In many fields such as medical imaging, it is common to combine two images in order to detect abnormalities that couldn't easily be detectable in a single image. Before this can be done, it is important to make sure that the two images are aligned. One way to do this is to create a new image composed of squares, where the pixel values in the squares alternate between the two source images. This operator will take as input two source images and an integer specifying the square dimension.

OperatorDescriptors

The first thing that needs to be done when you are writing a new operator is to provide a description of it using a class that implements the java.media.jai.OperatorDescriptor interface. This interface describes the functionality necessary to provide information about the new operator such as operation name, number of sources, number of parameters, types and ranges of parameters, and so on. In order to make this task easier, the java.media.jai.OperatorDescriptorImpl class which implements this interface and contains default behaviors for many of the methods is available. Thus, the easiest way to create a new OperatorDescriptor is to extend the OperatorDescriptorImpl class and define a constructor as shown in Listing 6.17.

Listing 6.17 CheckAlignmentDescriptor
package ch6.checkalignment;

import javax.media.jai.OperationDescriptorImpl;
import javax.media.jai.registry.RenderedRegistryMode;

public class CheckAlignmentDescriptor extends OperationDescriptorImpl {
    private static final String[] paramNames = {"samplingPeriod"};
    private static final Object[] paramDefaults = {new Integer(1)};
    private static final Class[] paramClasses = {Integer.class};
    private static final int numSources = 2;
    private static final String[] supportedModes = {"rendered"};
    private static final Object[] validParamValues = {
        new javax.media.jai.util.Range(Integer.class,
                                       new Integer(1),
                                       new Integer(Integer.MAX_VALUE))
            };


    private static final String[][] resources = {
        {"GlobalName", "CheckAlignment"},
        {"LocalName", "CheckAlignment"},
        {"Vender", "MyCompanyName"},
        {"Description", "Provides Visual Alignment Check of Two Images"},
        {"DocURL", "none"},
        {"Version", "Beta"},
    };

    public CheckAlignmentDescriptor() {
        super(resources,
              supportedModes,
              numSources,
              paramNames,
              paramClasses,
              paramDefaults,
              validParamValues);
    }
}
							

RIFS and CRIFS

After the descriptor is written, you must create a class implementing either the java.awt.image. renderable.RenderedImageFactory class or the java.awt.image.renderable.ContextualRenderedImageFactory class. The RenderedImageFactory (RIF for short) interface is for use with RenderedImages, and the ContextualRenderedImageFactory (CRIF for short) is for use with RenderableImages. However, a CRIF (which is a subclass of RIF) can also support RenderedImages. Because our operator will only be used with RenderedImages, we will only implement a RIF.

The main method that must be defined in any class implementing the RIF interface is the

public RenderedImage create(ParameterBlock paramBlock,
                            RenderingHints rh)

method, which is used for rendering RenderedOps (see Listing 6.18). The main methods that must be defined in any class implementing the CRIF interface is the previous create method along with the additional

public RenderedImage create(RenderContext renderContext,
                            ParameterBlock paramBlock)

method, which is used for rendering RenderableOps.

Listing 6.18 CheckAlignmentRIF.java
package ch6.checkalignment;

import java.awt.image.RenderedImage;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.renderable.RenderedImageFactory;
import java.awt.RenderingHints;
import javax.media.jai.ImageLayout;

public class CheckAlignmentRIF implements RenderedImageFactory {
    public CheckAlignmentRIF() {}

    public RenderedImage create(ParameterBlock paramBlock,
                                RenderingHints renderingHints) {

        RenderedImage source1 = paramBlock.getRenderedSource(0);
        RenderedImage source2 = paramBlock.getRenderedSource(1);
        int samplingPeriod = paramBlock.getIntParameter(0);
        ImageLayout layout = null;
        return new CheckAlignmentOpImage(source1,
                                         source2,
                                         samplingPeriod,
                                         layout,
                                         renderingHints,
                                         false);
    }
}
							

OpImages

The third thing that needs to be done is to implement your operator using a javax.media.jai.OpImage, which is the base class for all image operators. Image operations can be divided into different categories based on the OpImage subclass they extend. Each of these subclasses has a particular set of characteristics which allow them to easily perform certain image processing tasks. The subclasses are shown in Table 6.13.

Table 6.13. OpImage Subclasses
OpImage subclass Brief Description
AreaOpImage A destination pixel at location x, y is computed using a single source pixel at location x, y and a fixed region around that source pixel.
GeometricOpImage A destination pixel is computed using a geometric transformation of the source pixels.
PointOpImage A destination pixel at location x, y is computed using a single source pixel at location x, y.
SourcelessOpImage Destination pixels are computed without using source pixels.
StatisticsOpImage No destination pixels are computed. Instead, statistical measures are computed on the source image.
UntiledOpImage A computed destination image will consist of a single tile equal in size to the image bounds.

For our CheckAlignment operator we will implement a PointOpImage because we are operating on each pixel independently (see Listing 6.19).

Listing 6.19 CheckAlignmentOpImage
package ch6.checkalignment;

import javax.media.jai.ImageLayout;
import javax.media.jai.PointOpImage;
import java.awt.image.RenderedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

public class CheckAlignmentOpImage extends PointOpImage {

    public CheckAlignmentOpImage(RenderedImage s1,
                                 RenderedImage s2,
                                 int sp,
                                 ImageLayout layout,
                                 java.util.Map configuration,
                                 boolean cobbleSources) {

        super(s1, s2, layout, configuration, cobbleSources);
        source1 = s1;
        source2 = s2;
        samplingPeriod = sp;
    }

    public Raster computeTile(int x, int y) {
        Raster r1 = source1.getTile(x, y);
        Raster r2 = source2.getTile(x, y);

        int xBounds = r1.getWidth();
        if (r2.getWidth() < xBounds)
            xBounds = r2.getWidth();
        int yBounds = r1.getHeight();
        if (r2.getHeight() < yBounds)
            yBounds = r2.getHeight();

        WritableRaster wr;
        wr = r1.createCompatibleWritableRaster(xBounds, yBounds);

        int tmpi;
        int tmpj;
        for (int i=0;i<wr.getWidth();i++)
            for (int j=0;j<wr.getHeight();j++) {
                tmpi = i/samplingPeriod;
                tmpj = j/samplingPeriod;
                if ((tmpi % 2 == 0) && (tmpj %2 == 0))
                    wr.setDataElements(i,j,r2.getDataElements(i,j,null));
                else if ((tmpi % 2 != 0) && (tmpj %2 != 0))
                    wr.setDataElements(i,j,r2.getDataElements(i,j,null));
                else
                    wr.setDataElements(i,j,r1.getDataElements(i,j,null));
            }
        return wr;
    }

    private RenderedImage source1, source2;
    private int samplingPeriod;
}
							

JAI Registry

The last step in creating an operator is registering the new operator with the JAI registry. The easiest way to do this is to register it in the application. The downside to this is that only applications that add this addition code will be able to use the new operator (see Listing 6.20). It is also possible to make your operator a permanent part of the registry—in which case it is available to all applications on that platform.

Listing 6.20 CheckAlignmentTester
package ch6;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import javax.media.jai.OperationRegistry;
import javax.media.jai.registry.RIFRegistry;
import java.awt.image.renderable.RenderedImageFactory;
import java.awt.image.renderable.ParameterBlock;
import ch6.checkalignment.*;

public class CheckAlignmentTester extends JFrame{

    /*
      The following static block registers the "CheckAlignment" operator
      with the JAI registry
    */
    static {
        OperationRegistry or;
        or = JAI.getDefaultInstance().getOperationRegistry();
        or.registerDescriptor(new CheckAlignmentDescriptor());
        RenderedImageFactory rif = new CheckAlignmentRIF();
        RIFRegistry.register(or, "CheckAlignment", "ch6example", rif);
    }

    public CheckAlignmentTester(String fileName1,
                                String fileName2,
                                String samplingPeriod) {
        pb = new ParameterBlock();
        pb.add(fileName1);
        RenderedOp sourceImage1 = JAI.create("fileload", pb);
        pb.set(fileName2,0);
        RenderedOp sourceImage2 = JAI.create("fileload", pb);

        pb = new ParameterBlock();
        pb.addSource(sourceImage1);
        pb.addSource(sourceImage2);
        pb.add(Integer.parseInt(samplingPeriod));
        RenderedOp destinationImage = JAI.create("CheckAlignment", pb);

        getContentPane().add(new ch6Display(destinationImage));

        pack();
        show();
    }

    public static void main(String[] args) {
        if (args.length != 3) {
            System.err.print("Usage:  CheckAlignment ");
            System.err.println("filename1 filename2 samplingPeriod");
        }
        else
            new CheckAlignmentTester(args[0], args[1], args[2]);
    }
    private ParameterBlock pb;
}

Figure 6.10. Illustration of output from the CheckAlignmentTester application shown in Listing 6.20.


In one image, a single blue square lies in a white background and in the second image a single red square lies in a white background. As you can see, these two colored squares are not perfectly aligned because the resulting image is not a perfect checkerboard.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset