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.
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).
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.
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
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.
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.”
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.
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.
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.
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.
For our CheckAlignment operator we will implement a PointOpImage because we are operating on each pixel independently (see Listing 6.19).
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.
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; } |
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.