JavaUsing the Java 2D AffineTransformOp Filter Class to Process Images

Using the Java 2D AffineTransformOp Filter Class to Process Images

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Java Programming Notes # 454


Preface

Part of a series

In an earlier lesson entitled
A Framework for Experimenting with Java 2D
Image-Processing Filters
, I taught you how to write a framework program that
makes it easy to use the image-filtering classes of the Java 2D API to process the pixels in an
image and to display the processed image.

In the previous lesson entitled
Using the Java 2D LookupOp Filter Class
to Process Images
, I taught you how to write programs that use the LookupOp
image-filtering class of the Java 2D API for a variety of image-processing
purposes.

At the close of that lesson, I told you that future lessons would teach you
how to use the following image-filtering classes from the Java 2D
API:

  • AffineTransformOp
  • BandCombineOp
  • ConvolveOp
  • RescaleOp
  • ColorConvertOp

In this lesson, I will keep that promise and teach you how to use the
AffineTransformOp
image-filtering class to perform a variety of
transformations on images.  I will teach you how to use the remaining classes
from the above list in future lessons.

Viewing tip

You may find it useful to open another copy of this lesson in a
separate browser window.  That will make it easier for you to
scroll back
and forth among the different listings and figures while you are
reading
about them.

Supplementary material

I recommend that you also study the other lessons in my extensive
collection of online Java tutorials.  You will find those lessons
published
at Gamelan.com
However, as of the date of this writing, Gamelan doesn’t maintain a
consolidated index of my Java tutorial lessons, and sometimes they are
difficult to locate there.  You will find a consolidated index at www.DickBaldwin.com.

I also recommend that you pay particular attention to the lessons listed in
the References section of this document.

General Background Information

Constructing images

Before getting into the programming details, it may be useful for you to
review the concept of how images are constructed, stored, transported, and
rendered in Java (and in most modern computer environments for that matter).

I provided a great deal of information on those topics in the earlier lesson
entitled

Processing Image Pixels using Java, Getting Started
.  Therefore, I won’t
repeat that information here.  Rather, I will simply refer you back to the

earlier lesson
.

The framework program named ImgMod05

It will also be useful for you to understand the behavior of the framework
program named ImgMod05.  Therefore, I strongly recommend that you
study the earlier lesson entitled
A Framework for Experimenting with Java 2D
Image-Processing Filters
.

However, if you don’t have the time to do that, I can summarize that
framework program as follows:

Purpose of ImgMod05

The purpose of ImgMod05 is to make it easy for you to experiment with
the modification of images using the image-filtering classes of the Java 2D API
and to display the modified version of the image along with the original image.
(See an example of the graphic output format in Figure 5.)

The Replot button

The ImgMod05 program GUI contains a Replot button (as shown
in Figure 5)
.  At the beginning of the run, and each time thereafter that
the Replot button is clicked:

  • The image-processing method belonging to an object of specified
    image-processing class is invoked.
  • The original image is passed to the image-processing method, which
    returns a reference to a processed image.
  • The resulting processed image is displayed along with the original image.
  • The processed image is written into an output JPEG file named junk.jpg.

Display of the images

When the ImgMod05 program is started, the original image and the
processed version of the image are displayed in a frame with the original image
above the processed image (as shown in Figure 5).  The program attempts
to set the size of the display so as to accommodate both images.  If both images
are not totally visible, the user can manually resize the display frame.

Input and output file format

The ImgMod05 program will read gif and jpg input files and possibly
some other input file types as well.  The output file is always a JPEG file.

Typical usage

Enter the following at the command-line to run the ImgMod05 program:

java ImgMod05 ProcessingProgramName ImageFileName

Preview

In this lesson, I will present and explain an image-processing program named
ImgMod40 that is compatible with the framework program named ImgMod05
This program provides a GUI that allows the user to perform the following
transforms on an input image:

  • Scaling
  • Translation
  • Rotation
  • Mirror Image

With the exception of the Mirror Image transform, all of the transforms in the above list
allow the user to input important parameters via the GUI.

The program GUI is a JTabbedPane with four pages.  Let’s begin by
taking a look at each of those pages.

Scaling

The Scaling page of the GUI is shown
in Figure 1.


Figure 1

As you can see from Figure 1, the program allows the user to specify
independent horizontal and vertical scale factors.  In addition, the user
is allowed to choose one of three optional interpolation schemes.  (I
will have more to say about interpolation shortly.)

This program allows the user to translate the image to the right and down by
specifying positive translation distances in pixels.  Translation to the left
and up can also be performed by specifying negative translation distances. 
However, translation to the left has the effect of chopping off part of the
image on the left side, while translation up has the effect of chopping off part of
the image at the top.

Rotating the image.
Note that prior to performing the rotation, the image in
Figure 8 was
translated to the right and down a distance sufficient to make it possible to
rotate it without chopping off its corners on the left and the top.

This program allows the user to rotate the image about its center by a
positive or negative angle specified in degrees, where positive rotation is
clockwise rotation.

An example of rotating an image is shown in Figure 8.

Mirror Image

The Mirror Image page in the GUI is shown in Figure 4.


Figure 4

The mirror image is created by translating the image to the right by a
distance equal to its width, and then flipping it about its left edge.  There are no user input parameters for this process.  An
example of creating the mirror image of an image is shown in Figure 9.

A very significant capability

In my opinion, (with the possible exception of the ColorConvertOp
class)
, the AffineTransformOp class is the most significant of all
the image-filtering classes in the Java 2D API.

Many sequential transforms can be performed

Although this demonstration program doesn’t allow the user to perform a
series of different transforms in sequence on the same image, be aware that once
you start writing your own code, you can create many different image-processing
effects by performing multiple transforms in sequence.

(Actually you could do that with this program by running the program
several times in succession using the output file from one run as the input
file to the next run, but that wouldn’t be very convenient.)

Interpolation is a major programming issue

I will explain more about interpolation shortly.  For now, suffice it to
say that because of the interpolation issue, a great deal of programming effort
would be required for you to write your own class that replicates the behavior
of the AffineTransformOp class.  That is the main reason that I
consider the capability provided by this class to be so significant.  Therefore, if this class will serve your
needs, this is clearly a case where you should use the existing class
instead of inventing a new class.

(It would also be very difficult to write your own class to replicate
the behavior of the ColorConvertOp class.  However, I may be
wrong on this, but I consider the AffineTransformOp class to be more
useful, more of the time than the ColorConvertOp class.)

Creating new color values through interpolation

Whenever you change the size, the location, or the orientation of an image,
you usually need to
recreate the color values for all of the pixels
in the transformed image on the
basis of the color values contained in the original image.  This is not a
trivial computational task.  (I discuss this
requirement
in additional detail in a later section that explains the
requirements for scaling and then rendering images.)

The AffineTransformOp class provides three
interpolation choices

When you use the AffineTransformOp class to transform one image into
another image, you have three choices regarding how the color values for the new
pixels will be created:

The Scaling page of the program GUI shown in Figure 1 makes it
possible for the user to select which of the three interpolation schemes will be
used. 

Higher quality equates to higher computational cost

Generally speaking, the quality of the resulting image will improve and the
computer time required to generate the new image will increase as you go down
the above list from top to bottom.  In other words, bicubic
interpolation usually requires more computational effort and provides better
output image quality than the nearest neighbor scheme.  Bilinear
interpolation falls somewhere in between the other two.

Technical material on interpolation

If you compare the bottom image in Figure 5
above with the bottom image in Figure 6
below, you should be able to see that the image quality in
Figure 6 is superior to that in
Figure 5.


Figure 6

Note the vertical stripes.
For example, note the vertical stripes in the light gray stem in the
upper-left of the bottom image in Figure 5
Although there is some striping in this area of
Figure 6, it isn’t nearly as pronounced.

Neither image has outstanding quality

Although neither image shows outstanding quality, (which is a common
result of enlarging images of this type)
, the image produced using the
nearest neighbor
scheme in Figure 5 is
more grainy than the image produced using bicubic interpolation in
Figure 6.

Translation

Rotation

Mirror image

Now it’s time to examine the code.

Discussion
and Sample Code


The program named ImgMod40

As explained
earlier, in this lesson, I will present and explain an image-processing program
named ImgMod40 that is compatible with the framework program named
ImgMod05
.  This program provides a GUI that allows the user to perform the
following transforms on an input image:

  • Scaling
  • Translation
  • Rotation
  • Mirror Image

With the exception of the Mirror Image transform, all of the transforms in the above list
allow the user to input important parameters via the GUI.

Purpose

The purpose of this program is to show you how to write image-processing
programs of this type, and also to illustrate a variety of different uses for
the AffineTransformOp image-filtering class of the Java 2D API.

General comments

The program named ImgMod038, which I explained in the earlier lesson
entitled Using the Java 2D LookupOp Filter Class to Process Images,
contained a number of general comments that apply to this program also. 
You are encouraged to take a look at those comments.

Compatible with ImgMod50

This class is compatible with the use of the framework program named ImgMod05,
which I explained in the earlier lesson entitled
A Framework for
Experimenting with Java 2D Image-Processing Filters
.  The framework program named
ImgMod05 displays the original and the processed images in the format
shown in Figure 5.  It also writes the processed
image into an output file in
JPEG format.  The name of the output file is junk.jpg and it is written into the current directory.

The program GUI

Image-processing programs such as this one may provide a program GUI for
data input making it possible for the user to modify the behavior of the
image-processing method each time the Replot button (shown in
Figure
5
)
is clicked.

This
program creates a GUI consisting of a tabbed pane containing four pages.  The tabs
on the pages are labeled:

  • Scaling
  • Translation
  • Rotation
  • Mirror Image

Each page
contains a set of controls that make it possible to process the image in a way
that illustrates the processing concept indicated by the label on the
tab.

The four pages of the tabbed pane are shown in Figure 1 through Figure 4.

Processing details for each page are provided in the comments in the code
used to construct and process the individual pages.

Instructions for running the program

Enter the following at the command line to run
this program:

java ImgMod05 ImgMod40 ImageFileName

If the program is unable to load
the image file within ten seconds, it will abort with an error message.

The program was tested using J2SE 5.0 under
WinXP.

Will discuss in fragments

I will discuss and explain this program in fragments.  You can view a
complete listing of the program in Listing 29 near the end of the lesson.

The class named ImgMod40

This program consists of a single class named ImgMod40, which begins
in Listing 1.

class ImgMod40 extends Frame implements ImgIntfc05{

  JTabbedPane tabbedPane = new JTabbedPane();

Listing 1

The class extends Frame, making an object of the class eligible to
serve as its own program GUI.

The class also implements the interface named ImgIntfc05 making it
eligible to be run under control of the framework program named ImgMod05.

(Recall that image-processing programs that are compatible with the
framework program named ImgMod05 must implement the interface named
ImgIntfc05.)

Listing 1 instantiates an object of the JTabbedPane class, which
serves as the primary container for the GUI shown in Figure 1.

Instantiate control components for the GUI pages

Listing 2 instantiates a number of components required to construct the four
pages of the GUI shown in Figure 1 through Figure 4.  Those components that
require local access only are instantiated closer to where they are used. 
Those that require broader access are instantiated in Listing 2 as instance
variables.

  //Components used to construct the Scaling page.
  Panel page00 = new Panel();
  TextField page00TextFieldHorizontal = 
                                    new TextField("0.5",6);
  TextField page00TextFieldVertical = 
                                    new TextField("0.5",6);
  //Components for radio buttons
  CheckboxGroup Page00Group = new CheckboxGroup();
  Checkbox page00NearestNeighbor = new Checkbox(
       "Nearest Neighbor Interpolation",Page00Group,false);
  Checkbox page00Bilinear = new Checkbox(
               "Bilinear Interpolation",Page00Group,false);
  Checkbox page00Bicubic = new Checkbox(
                 "Bicubic Interpolation",Page00Group,true);


  //Components used to construct the Translation page.
  Panel page01 = new Panel();
  TextField page01TextFieldHorizontal = 
                                      new TextField("5",6);
  TextField page01TextFieldVertical = 
                                     new TextField("10",6);


  //Components used to construct the Rotation page.
  Panel page02 = new Panel();
  TextField page02TextField = new TextField("45.0",6);


  //Components used to construct the Mirror Image page.
  Panel page03 = new Panel();

Listing 2

The constructor

The constructor for the class is shown in its entirety in Listing 3
This is the primary constructor.  It calls other methods to separate the
construction of the GUI into easily understandable units.  Each method that
it calls constructs one page in the tabbed pane.

  ImgMod40(){//constructor
  
    constructPage00();
    tabbedPane.add(page00);//Add page to the tabbedPane.
    
    constructPage01();
    tabbedPane.add(page01);//Add page to the tabbedPane.
    
    constructPage02();
    tabbedPane.add(page02);//Add page to the tabbedPane.
    
    constructPage03();
    tabbedPane.add(page03);//Add page to the tabbedPane.
    
    add(tabbedPane);//Add tabbedPane to the Frame.

    setTitle("Copyright 2006, R.G.Baldwin");
    setBounds(555,0,470,300);
    setVisible(true);

    //Define a WindowListener to terminate the program.
    addWindowListener(
      new WindowAdapter(){
        public void windowClosing(WindowEvent e){
          System.exit(1);
        }//end windowClosing
      }//end windowAdapter
    );//end addWindowListener
  }//end constructor

Listing 3

The processImg method

The method named processImg is declared in the interface named
ImgIntfc05
, and must be defined by any program that is compatible with being
executed under control of the framework program named ImgMod05.

This is the primary image-processing method of the program.  It is
called by the program named ImgMod05 at startup, and each time thereafter
that the user clicks the Replot button shown in Figure 5.

The processImg method is shown in its entirety in Listing 4.

  public BufferedImage processImg(BufferedImage theImage){
    
    BufferedImage outputImage = null;
    
    //Process the page in the tabbed pane that has been
    // selected by the user.
    switch(tabbedPane.getSelectedIndex()){
      case 0:outputImage = processPage00(theImage);
             break;
      case 1:outputImage = processPage01(theImage);
             break;
      case 2:outputImage = processPage02(theImage);
             break;
      case 3:outputImage = processPage03(theImage);
             break;
    }//end switch

    return outputImage;
  }//end processImg

Listing 4

Behavior of the processImg method

Each time the processImg method is invoked, it queries the
JTabbedPane
object in the GUI to determine which page has been selected by
the user.  Then, depending on which page has been selected, it invokes one of the following four methods to process the
image in accordance with the concept embodied by the selected page:

  • processPage00 – Scaling
  • processPage01 – Translation
  • processPage02 – Rotation
  • processPage03 – Mirror Image

Scaling

Construct the Scaling page

The primary constructor shown in Listing 3 invokes the method named
constructPage00
to construct the Scaling page shown in Figure 1.  The
method named constructPage00 is shown in its entirety in
Listing 5.

  void constructPage00(){
    page00.setName("Scaling");//Label on the tab.
    page00.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE SCALINGn"
      + "This page illustrates the scaling of an image "
      + "using three different types of interpolation."
      + "nn"
      + "Enter a positive scale factor between 0.001 and "
      + "10.0 in each of the text fields, select an "
      + "interpolation type, and click the Replot button. "
      + "The best quality interpolation will probably be "
      + "achieved with the use of Bicubic Interpolation. "
      + "Nearest Neighbor Interpolation will probably "
      + "produce the poorest quality.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,7,1,
                                 TextArea.SCROLLBARS_NONE);
    page00.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    Panel page00ControlPanel = new Panel();
    page00ControlPanel.setLayout(new GridLayout(5,1));
    
    //Construct and populate the panels that contain the
    // radio buttons, the labels, and the text fields.
    // Add each such panel an a new cell in the grid
    // layout going from the top of the grid to the bottom
    // of the grid.
    
    //Begin with the radio buttons.  The purpose of putting
    // the radio buttons on panels is to cause them to be
    // left justified in their cells.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel00.add(page00NearestNeighbor);
    page00ControlPanel.add(subControlPanel00);
    
    Panel subControlPanel01 = new Panel();
    subControlPanel01.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel01.add(page00Bilinear);
    page00ControlPanel.add(subControlPanel01);
    
    Panel subControlPanel02 = new Panel();
    subControlPanel02.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel02.add(page00Bicubic);
    page00ControlPanel.add(subControlPanel02);
    
    //Now create and populate panels that contain labels
    // and associated TextField objects
    Panel subControlPanel03 = new Panel();
    subControlPanel03.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel03.add(new Label(
                               "Horizontal Scale Factor"));
    subControlPanel03.add(page00TextFieldHorizontal);
    page00ControlPanel.add(subControlPanel03);
    
    Panel subControlPanel04 = new Panel();
    subControlPanel04.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel04.add(new Label(
                                 "Vertical Scale Factor"));
    subControlPanel04.add(page00TextFieldVertical);
    page00ControlPanel.add(subControlPanel04);
    
    page00.add(page00ControlPanel,BorderLayout.CENTER);
  }//end constructPage00

Listing 5

Straightforward code

Although the code in Listing 5 is rather long and tedious, there is nothing
complicated about it.  If you use Figure 1 as a guide, you should be able
to follow the code in Listing 5 with no difficulty.

The processPage00 method

When the user selects the Scaling tab in Figure 1 and clicks the
Replot
button shown in Figure 5, the switch statement in Listing 4 invokes
the method named processPage00 to process the image.

General processing methodology

In general, the methodology used to process an image using the
AffineTransformOp
image-filtering class of the Java 2D API consists of the
following three steps:

  1. Get an object of the AffineTransform class that reflects the type of transformation that
    is required.  (See the earlier lesson entitled
    Java 2D
    Graphics, Simple Affine Transforms
    for more information on the AffineTransform class.)
  2. Use the AffineTransform object to
    create an image-filtering object
    of the AffineTransformOp class with a specified interpolation scheme.
  3. Invoke the filter method on the image-filtering object to apply
    the filter to the image.

As you will see, some of the above steps require additional code in
preparation for accomplishing the step.

Processing the image

The processPage00 method processes the image according to the controls
located on the Scaling page shown in Figure 1.  This method uses the
AffineTransformOp filter class to process the image.

This method illustrates image scaling using independent horizontal and
vertical scale factors, giving the user a choice of three different
interpolation schemes.  (See the earlier lesson entitled
Java 2D
Graphics, Simple Affine Transforms
for more information on the use of a
scaling affine transform.)

The processPage00 method begins in Listing 6.

  BufferedImage processPage00(BufferedImage theImage){
    
    //Set a non-zero default value for the horizontal scale
    // factor.
    double horizontalScale = 0.001;
    try{//Get horizontalScale from the text field.
      horizontalScale = Double.parseDouble(
                      page00TextFieldHorizontal.getText());
    }catch(java.lang.NumberFormatException e){
      page00TextFieldHorizontal.setText("Bad Input");
      horizontalScale = 0.001; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for horizontal scale
    if((horizontalScale < 0.001) || 
                                 (horizontalScale > 10.0)){
      page00TextFieldHorizontal.setText("Bad Input");
      horizontalScale = 0.001;//Override bad user input.
    }//end if
  
    //Set a non-zero default value for the vertical scale
    // factor.
    double verticalScale = 0.001;
    try{//Get verticalScale from the text field.
      verticalScale = Double.parseDouble(
                        page00TextFieldVertical.getText());
    }catch(java.lang.NumberFormatException e){
      page00TextFieldHorizontal.setText("Bad Input");
      verticalScale = 0.001; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for verticalScale
    if((verticalScale < 0.001) || (verticalScale > 10.0)){
      page00TextFieldHorizontal.setText("Bad Input");
      verticalScale = 0.001;//Override bad user input.
    }//end if

Listing 6

Getting the scale factors

The first task that the method must accomplish is to get the horizontal and
vertical scale factors that were entered by the user into the text fields in
Figure 1.  This is accomplished by the code in Listing 6.  The code in
Listing 6 is straightforward, and should not require further explanation beyond
the comments embedded in the code.

Identify the desired interpolation scheme

The next task that the method must perform is to determine which
interpolation scheme has been selected by the user and to become prepared to use that
interpolation scheme in the creation of the
image-filtering object
This is accomplished in Listing 7.

    int interpolationScheme;

    if(page00Bicubic.getState() == true){
      interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;
    }else if(page00Bilinear.getState() == true){
      interpolationScheme = 
                           AffineTransformOp.TYPE_BILINEAR;
    }else{//page00NearestNeighbor must be selected
      interpolationScheme = 
                   AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
    }//end else

Listing 7

Listing 7 gets the selected interpolation scheme from the radio buttons and
reflects that selection in an int variable named interpolationScheme
This variable will be used later in the creation of the
image-filtering object.

Create the required AffineTransform object

As indicated in the above list
of three required steps
, an AffineTransform object is required later
to create the image-filtering object.  The creation of that object is accomplished by
Listing 8.

    AffineTransform transformObj = 
                         AffineTransform.getScaleInstance(
                            horizontalScale,verticalScale);

Listing 8

An examination of the documentation for the AffineTransform class
shows that there are several different ways to create such an object.  The
statement in Listing 8 is probably the simplest of those ways.

(The getScaleInstance method of the AffineTransform
class is a convenience method that is designed to make it easy to create
scaling transforms.)

Create the image-filtering object

Listing 9 accomplishes the second
step
in the above list of three required steps.

    AffineTransformOp filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);

Listing 9

Listing 9 instantiates a new object of the AffineTransformOp class
passing a reference to the AffineTransform object, along with the
variable that identifies the interpolation scheme to the constructor for the
AffineTransformOp
class.  The resulting object will be used to filter
the image.

Now for something different

For all of the image-filtering classes in the Java 2D API (except for the
AffineTransformOp
class)
, all that is necessary to filter the image and
to return a reference to a BufferedImage object (that encapsulates the
filtered image)
is to execute a statement similar to the following:

return filterObj.filter(theImage,null);

For example, this is very similar to the code that I used to filter the
images in the earlier lesson entitled
Using the Java 2D LookupOp Filter
Class to Process Images
.

However, for reasons that I am unable to explain, when I use that approach
for the AffineTransformOp class, the ColorModel of the
BufferedImage
object that is returned to the framework program named
ImgMod05
is not compatible with the method used by that program to write the
output JPEG file.  This results in an output file in which the image data
appears to be scrambled.  Therefore, it was necessary for me to find and
use an alternative approach that provides better control over the color model.

An alternative approach

Figure 10 contains partial documentation for the filter method of the
AffineTransformOp class.

public final BufferedImage filter(BufferedImage src,
                                  BufferedImage dst)

Transforms the source BufferedImage and stores the
results in the destination BufferedImage. If the color
models for the two images do not match, a color conversion into the
destination color model is performed. If the destination image is
null, a BufferedImage is created with the source
ColorModel
.
 

Parameters:

src – The BufferedImage to
transform.

dst – The BufferedImage in which
to store the results of the transformation.

Returns:

The filtered BufferedImage.

Figure 10

Two ways to access the filtered image

According to the information in Figure 10, there are two ways to
gain access to the filtered image:

  1. Capture the return value from the
    filter method, which is
    a reference to a BufferedImage object that encapsulates the filtered
    image.  (With this approach, null can be passed as the second parameter
    to the filter method.)
  2. Provide a reference to a
    BufferedImage
    object as the second parameter to the filter method. 
    (The
    method will deposit the filtered image in this BufferedImage object.)

The simple return statement that I have used
with the other image-filtering classes of the Java 2D API is based on the
first approach in the above list.

The somewhat more complicated alternative approach that I was forced to
use in this program is based on the second
approach
in the above list.

The
createCompatibleDestImage method

In summary, this alternative approach invokes the
createCompatibleDestImage
method of the AffineTransformOp class to
create "a zeroed destination image with the correct size and number of
bands."
  A reference to this object is passed as the second parameter
to the filter method.

The createCompatibleDestImage method requires two parameters:

  • src – The BufferedImage to be transformed.
  • destCM – The ColorModel of the destination image.

Explicit control over the color model

I’m not certain that it was the ColorModel that caused the problem in
the first place. 
However, as you will see in Listing 10, I forced the ColorModel of the
destination object to match the ColorModel of the image being filtered
(by invoking the getColorModel method on the image being filtered to
create the ColorModel for the second parameter to the
createCompatibleDestImage
method)
and that resolved the problem.

Create the destination object

Listing 10 creates a destination BufferedImage object that will be
passed to the filter method to receive the filtered image.

    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());

Listing 10

Filter the image and return the filtered image

Listing 11 invokes the filter method to
apply the filter to the image and save the filtered image in
the destination object.  Then the code in Listing 11 returns a reference to the destination
object.  This reference is eventually returned to the framework program named ImgMod05, where the filtered image
is displayed as shown in Figure 5 and also written into an output JPEG file
named junk.jpg.

    filterObj.filter(theImage, dest);
    
    //Return the destination object's reference.
    return dest;

  }//end processPage00

Listing 11

Listing 11 also signals the end of the method named processPage00, and
the end of the explanation of the scaling transform under the control of
the Scaling page shown in Figure 1.

Translation

Construct the Translation page

The primary constructor shown in Listing 3 invokes the method named
constructPage01
to construct the Translation page shown in Figure 2.  The method
named constructPage01 is shown in its entirety in Listing 12.

  void constructPage01(){
    page01.setName("Translation");//Label on the tab.
    page01.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE TRANSLATIONn"
      + "This page illustrates the translation of an "
      + "image to a new location relative to the "
      + "upper-left corner of the container using Bicubic "
      + "Interpolation.nn"
      + "Enter the horizontal and vertical translation "
      + "distances in pixels into the text fields and "
      + "click the Replot button.nn"
      + "Note that the translation distances must be "
      + "between -1000 and +1000 pixels.  Also note that "
      + "negative translations may shift the image "
      + "completely out of the Frame on the top or the "
      + "left side of the image.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,9,1,
                                 TextArea.SCROLLBARS_NONE);
    page01.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    Panel page01ControlPanel = new Panel();
    page01ControlPanel.setLayout(new GridLayout(3,1));
    
    //Place each label and its corresponding text field
    // on a panel.  Place the panels in the cells in the
    // grid layout from top to bottom.  Note that there
    // is an empty cell at the bottom for cosmetic
    // purposes.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(
                          new FlowLayout(FlowLayout.LEFT));
    subControlPanel00.add(new Label(
             "Horizontal Translation Distance in Pixels"));
    subControlPanel00.add(page01TextFieldHorizontal);
    page01ControlPanel.add(subControlPanel00);
    
    Panel subControlPanel01 = new Panel();
    subControlPanel01.setLayout(
                          new FlowLayout(FlowLayout.LEFT));
    subControlPanel01.add(new Label(
               "Vertical Translation Distance in Pixels"));
    subControlPanel01.add(page01TextFieldVertical);
    page01ControlPanel.add(subControlPanel01);
    
    page01.add(page01ControlPanel,BorderLayout.CENTER);
  }//end constructPage01

Listing 12

If you use Figure 2 as a guide, you should have no problems following the
code in Listing 12.

Processing the image

The processPage01 method is called from within the switch
statement in
Listing 4 to process the image according to the controls
located on the Translation page shown in Figure 2.  As before, this
method uses the AffineTransformOp filter class to process the image.

This method illustrates image translation using independent horizontal and
vertical distance values.  The method uses bicubic Interpolation.

Get translation distances and set the interpolation
scheme

The processPage01 method begins in Listing 13.

  BufferedImage processPage01(BufferedImage theImage){
    
    double horizontalDistance = 0.0;
    try{//Get horizontalDistance from the text field.
      horizontalDistance = Double.parseDouble(
                      page01TextFieldHorizontal.getText());
    }catch(java.lang.NumberFormatException e){
      page01TextFieldHorizontal.setText("Bad Input");
      horizontalDistance = 0.0; //Override bad user input.
    }//end catch
   
    //Guarantee reasonable values for horizontalDistance
    if((horizontalDistance < -1000.0) || 
                            (horizontalDistance > 1000.0)){
      page01TextFieldHorizontal.setText("Bad Input");
      horizontalDistance = 0.0;//Override bad user input.
    }//end if

    double verticalDistance = 0.0;
    try{//Get verticalDistance from the text field.
      verticalDistance = Double.parseDouble(
                        page01TextFieldVertical.getText());
    }catch(java.lang.NumberFormatException e){
      page01TextFieldHorizontal.setText("Bad Input");
      verticalDistance = 0.0; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for verticalDistance
    if((verticalDistance < -1000.0) || 
                              (verticalDistance > 1000.0)){
      page01TextFieldHorizontal.setText("Bad Input");
      verticalDistance = 0.0;//Override bad user input.
    }//end if
  
    //Set the interpolation scheme to the best available.
    // Note that this page doesn't allow the user to
    // select the interpolation scheme.
    int interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;

Listing 13

The first two tasks that must be accomplished by the method are to get the translation distances that were
entered by the user into the text fields in Figure 2, and to set the
interpolation scheme.  This is accomplished by the code in Listing 13,
which is straightforward, and shouldn’t require further explanation.

Create the required AffineTransform object

As explained earlier, the methodology used to process an image using the
AffineTransformOp
image-filtering class of the Java 2D API consists of
three steps.

The first step is to get
an object of the AffineTransform class that reflects the type of
transformation that is required.  (See the earlier lesson entitled
Java 2D
Graphics, Simple Affine Transforms
for more information on the
AffineTransform
class.)
  This step is accomplished by the code in
Listing 14.

    AffineTransform transformObj = 
                    AffineTransform.getTranslateInstance(
                      horizontalDistance,verticalDistance);

Listing 14

The getTranslateInstance method

Listing 14 creates an AffineTransform object for translation that
matches the user input from the control panel shown in Figure 2
The code in Listing 14 invokes the static
getTranslateInstance
convenience method of the AffineTransform class
to create the object.

Note that even though the actual translation is ultimately performed in terms
of integer pixels, the getTranslateInstance method of the
AffineTransform
class requires the horizontal and vertical translation
distances to be provided as type double.

What does it mean to translate an image?

By way of explanation, my earlier article entitled
Java 2D
Graphics, Simple Affine Transforms
has this to say about translation:

"The purpose of translation is to move the origin of the coordinate system
in device space. 

For example, the default position of the origin is the upper left-hand
corner of the component on which the graphic is being displayed.  Assume that
the component is a Frame object that is four inches on each side.  You might
like for the origin to be in the center of the Frame instead of at the top
left-hand corner.  You could accomplish this by translating the origin by two
inches in both the horizontal and vertical directions.

Or, you might like for the origin to be just barely inside the borders of
the Frame object instead of outside the borders as is the default.  This can be
accomplished by getting the widths of the top border and left border by invoking
getInsets on the Frame, and then using those values to translate the
origin to a location that is just barely inside the borders."

Moving the origin

Thus, when you translate an image, you are actually moving the origin. 
The pixels, in turn, move along with the origin.  A positive horizontal translation
distance will cause the image to appear to move to the right in its container
and a negative horizontal distance will cause the image to appear to move to the
left.  Similarly, a positive vertical distance will cause the image to
appear to move down and a negative vertical distance will cause the image to
appear to move up.

Understanding this concept of moving the origin will become somewhat more
important later when I explain the code that produces the mirror image.

Create a filtering object

The second required step that I described
earlier is to use the
AffineTransform
object to create an image-filtering object of the class
AffineTransformOp
with a specified interpolation scheme.  This is
accomplished in Listing 15, which is essentially the same as the code in
Listing
9
.

    AffineTransformOp filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);

Listing 15

Filter the image and return the filtered image

The third required step that I described
earlier
is to invoke the filter method on the image-filtering object
to apply the filter to the image.  This is accomplished in
Listing 16.

    //Create a destination object.
    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(theImage, dest);
    
    //Return a reference to the destination object.
    return dest;
  }//end processPage01

Listing 16

The code in Listing 16 is essentially the same as the code that I explained
earlier in Listing 10 and Listing 11

Listing 16 also signals the end of the processPage01 method, and the
end of the explanation of the Translation page sown in Figure 2.

Rotation

Construct the Rotation page

The primary constructor shown in Listing 3 invokes the method named
constructPage02
to construct the Rotation page shown in Figure 3
The method named constructPage02 is shown in its entirety in
Listing 17.

  void constructPage02(){
    page02.setName("Rotation");//Label on the tab.
    page02.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE Rotationn"
      + "This page illustrates translation of an image "
      + "followed by Rotation of the same image using "
      + "Bicubic Interpolation.nn"
      + "The image is rotated around its center after "
      + "being translated to the right and down by a "
      + "distance that is sufficient to give it room to "
      + "rotate.nn"
      + "Enter the desired rotation angle in degrees and "
      + "click the Replot button.  Positive rotation "
      + "angles represent clockwise rotation and negative "
      + "rotation angles represent counter-clockwise "
      + "rotation.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,9,1,
                                 TextArea.SCROLLBARS_NONE);
    page02.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    // Use a control panel with a GridLayout for 
    // cosmetic purposes.  Note that there are two empty
    // cells at the bottom of the grid.
    Panel page02ControlPanel = new Panel();
    page02ControlPanel.setLayout(new GridLayout(3,1));
    
    //Place the label and the text field on a panel and
    // place that panel in the top cell in the grid.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    //Note, a positive value in degrees represents
    // clockwise rotation.
    subControlPanel00.add(new Label(
                                   "Rotation in Degrees"));
    subControlPanel00.add(page02TextField);
    page02ControlPanel.add(subControlPanel00);
       
    page02.add(page02ControlPanel,BorderLayout.CENTER);
  }//end constructPage02

Listing 17

The method named ConstructPage02 constructs the Rotation page
shown in Figure 3.  If you use Figure 3 as a guide, you should have no
problems following the code in Listing 17.

Processing the image

The processPage02 method is called from within the switch
statement in
Listing 4 to process the image according to the controls
located on the Rotation page shown in Figure 3.  As before, this method
uses the AffineTransformOp filter class to process the image.

This method illustrates image rotation where the angle of rotation is
specified by the user in degrees.  A positive angle in degrees results in
clockwise rotation.  A negative angle in degrees results in
counter-clockwise rotation.

The processPage02 method uses bicubic Interpolation.

Get the rotation angle in degrees

The processPage02 method begins in Listing 18.  The code in
Listing 18 gets the user input in degrees rotation from the text field in Figure
3.  Then it converts the angle from degrees to radians.

  BufferedImage processPage02(BufferedImage theImage){
    
    //Get the rotation angle in degrees.  A positive angle
    // in degrees corresponds to clockwise rotation.
    double rotationAngleInDegrees = 0.0;
    try{//Get rotationAngleInDegrees from the text field.
      rotationAngleInDegrees = Double.parseDouble(
                                page02TextField.getText());
    }catch(java.lang.NumberFormatException e){
      page02TextField.setText("Bad Input");
      rotationAngleInDegrees = 0.0;//Override bad input.
    }//end catch
    
    //Compute the rotation angle in radians.
    double rotationAngleInRadians = 
                      rotationAngleInDegrees*Math.PI/180.0;

Listing 18

If you use Figure 3 as a guide, you should have no problems following the
code in Listing 18.

Set the interpolation scheme

Listing 19 sets the interpolation scheme to bicubic.

    int interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;

Listing 19

Translate the image down and to the right

If the image in the top of Figure 8 were to be simply rotated about its
center, the resulting image would be too wide and too tall to fit in the same
amount of space.  Therefore, before rotating the image, this program
attempts to translate the image down and to the right a sufficient distance to
allow it to be rotated about its center without chopping of the corners on the
left side and the top.  This is accomplished by estimating the length of
the image diagonal as the hypotenuse of a right triangle and then using that
distance to perform the translation.

This translation operation is shown in Listing 20.

    //Translate the image down and to the right far enough
    // that the corners won't be chopped off by the top and
    // left edges of the container when the image is
    // rotated by 45 degrees.
    
    //Get the length of half the diagonal dimension of the
    // image using the formula for the hypotenuse of a
    // right triangle.
    int halfDiagonal = (int)(Math.sqrt(
         theImage.getWidth()*theImage.getWidth() + 
           theImage.getHeight()*theImage.getHeight())/2.0);
                                 
    //Set the horizontal and vertical translation
    // distances.
    int horizontalDistance = 
                      halfDiagonal - theImage.getWidth()/2;
    int verticalDistance = 
                     halfDiagonal - theImage.getHeight()/2;
    
    //Create an Affine Transform object that can be used
    // to translate the image by the distances computed
    // above.
    AffineTransform transformObj = 
                    AffineTransform.getTranslateInstance(
                      horizontalDistance,verticalDistance);
    
    //Get a translation filter object based on the
    // AffineTransform object.
    AffineTransformOp filterObj = new AffineTransformOp(
              transformObj,AffineTransformOp.TYPE_BICUBIC);
    
    //Perform the translation and save the modified image
    // as type BufferedImage.  This image will be the input
    // to the rotation transform.
    BufferedImage translatedImage = 
                          filterObj.filter(theImage, null);

Listing 20

Since you already understand image translation, further explanation of the
code in Listing 20 shouldn’t be necessary.

Get the AffineTransform object

Listing 21 invokes the static getRotateInstance method
of the AffineTransform class to get an
AffineTransform
object suitable for causing the image to be rotated around a
point that represents the center of the image before it was translated to the
right and down.  (This satisfies the
first step in the earlier list of
three required steps.)

    transformObj = AffineTransform.getRotateInstance(
                rotationAngleInRadians,
                horizontalDistance + theImage.getWidth()/2,
                verticalDistance + theImage.getHeight()/2);

Listing 21

The angle in radians

The first parameter to the getRotateInstance method in Listing 21 specifies the
rotation angle in radians (note the conversion from degrees to
radians in Listing 18 above)
.

The anchor point

The second and third parameters specify the coordinates of an anchor point around
which the image is to be rotated.  According to Sun, this overloaded
version of the getRotateInstance method:

"Returns a transform that rotates coordinates around an anchor point.
This operation is equivalent to translating the coordinates so that the
anchor point is at the origin (S1), then rotating them about the new origin
(S2), and finally translating so that the intermediate origin is restored to
the coordinates of the original anchor point (S3)."

Coordinate considerations

When the original image was translated by the filtering operation in
Listing
20
, and the translated image was returned in an object of type
BufferedImage
, the coordinate origin was effectively reset back to the top left corner of the
new larger image.  Because the actual
picture that constitutes the original image was shifted to the right and down, the
coordinates of the center of that picture were then greater than was the case
before the translation took place.  The expressions passed as the second
and third parameters in Listing 21 specify the new coordinates of the center of
the picture.

Get a rotation image-filtering object

Listing 22 gets a rotation filter object based on the transform object and the specified interpolation scheme. 
(This satisfies the second step in the
earlier list of three required steps.)

    filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);

Listing 22

Note that Listing 22 re-uses the variable named filterObj of type
AffineTransformOp
that was declared in Listing 20
and originally used to translate the image down and to the right.

Apply the filter

Listing 23 satisfies the
third step in
the earlier list of three required steps by applying the rotation filter to the
image.  Listing 23 also returns a reference to the
processed image such that
it will eventually be displayed by the program named ImgMod05, as shown
in Figure 8.

    BufferedImage dest = 
               filterObj.createCompatibleDestImage(
                 translatedImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(translatedImage, dest);
    
    //Return a reference to the destination object.
    return dest;  

  }//end processPage02

Listing 23

The code in Listing 23 is essentially the same as the code that I explained
in Listing 10 and Listing 11.

Listing 23 also signals the end of the method named processPage02, and
the end of the explanation of the processing of the Rotation page shown
in Figure 3.

Mirror Image

The Mirror Image operation consists of the concatenation of a
translation operation followed by a scaling operation.

What does it really mean to scale an image?

To understand the process of creating a mirror image, we first need to
understand what it really means to scale an image.

The representation of an image

An image consists of a set of red, green, and blue sample values specified at
a corresponding set of coordinates.  You can think of these sample values
as representing elevation samples taken from a set of three 3D surfaces. 
One surface represents red, one represents green, and the other represents blue.

Stretching or shrinking the surfaces

When we scale the image, we effectively multiply each of the coordinate
values by a scale factor without modifying the sample values.  This has the
effect of causing the original sample values to occur at a different set of
coordinate values, either stretching or shrinking the surfaces that they
represent.

If the scale factor is greater than 1.0, this stretches the surface causing
the distance between the sample values to increase.  If the scale factor is
less than 1.0, this causes the surface to shrink and the distance between the
sample values decreases.

Reconstructing the three surfaces

For rendering purposes, once we have scaled the image, we need to estimate the elevation values of each
surface at each pixel location in a uniform grid of pixels.  Therefore, the
new surfaces described by the elevation sample values at the new locations must
be
reflected onto the grid that represents the pixels.

Interpolation

This is where interpolation comes into play. 
If the new locations of the sample values don’t fall exactly on the locations of
pixels in the grid, those sample values must be used to estimate the elevation
of the surfaces at the pixel locations.  Even if the new locations of the
sample values do fall on the locations of pixels in the grid, if the surface was
stretched, then interpolation must be used to estimate the elevation of the
surface at pixel locations in between the new locations of the sample values.

What if the scale factors are negative?

An interesting thing happens if, for example, the horizontal scale factor is
negative.  This causes the locations of samples that were originally to the
right of the origin to be moved to the negative plane on the left of the origin. 
This is essentially the process that is used to create the mirror image shown in
Figure 9 under control of the page shown in Figure 4.  In this case, the
origin is translated to the right by an amount equal to the width of the
original image.  Then the horizontal coordinate value for each pixel is
multiplied by -1 causing the locations of all the samples to be moved to the
left of the new origin.

Construct the Mirror Image page

The primary constructor shown in Listing 3 invokes the method named
constructPage03
to construct the Mirror Image page shown in
Figure
4
.  The method named constructPage03 is shown in its entirety in
Listing 24.

  void constructPage03(){
    page03.setName("Mirror Image");//Label on the tab.
    page03.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    //This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="Mirror Imagenn"
      + "This page translates the image to the right by "
      + "an amount equal to its width, and then flips it "
      + "around its left edge to produce a mirror image "
      + "of the original image.nn"
      + "Click the Replot button to create the mirror "
      + "image.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,6,1,
                                 TextArea.SCROLLBARS_NONE);
    page03.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

  }//end constructPage03

Listing 24

The processPage03 method

When the user selects the Mirror Image tab in Figure 4 and clicks the
Replot button in Figure 9, the switch statement in Listing 4
invokes the method named processPage03 to process the image.  The
processPage03
method begins in Listing 25.

Note that unlike Scaling, Translation, and Rotation, there is no affine
transform for Mirror Image.  Rather, as mentioned earlier, this method
illustrates the use of a horizontal translation followed by scaling with a
negative scale factor to produce a mirror image of the original
image.

  BufferedImage processPage03(BufferedImage theImage){

    AffineTransform transformObj = 
                      AffineTransform.getTranslateInstance(
                                    theImage.getWidth(),0);

Listing 25

Listing 25 gets an AffineTransform object that can be used to shift the image to the right by an amount equal to its width.

A transformation matrix

Although I don’t plan to get into the details here, as I explained in the
earlier lesson entitled
Java 2D
Graphics, Simple Affine Transforms
, an AffineTransform is a linear
transform, so the transformation can be expressed in the matrix notation of
linear algebra.  An arbitrary AffineTransform can be mathematically
expressed by six numbers in a matrix.

Modification of the transform matrix

Those six numbers can be manipulated and modified at will in numerous ways
prior to the actual application of the transform process.  One of the most
interesting aspects of such manipulation is that the matrices for different
kinds of transforms can be combined through concatenation into a single
matrix.  Then, when the transformation is actually performed using the
resulting transformation matrix, the result will be as if each of the individual
transformations had been performed separately and in sequence.

The scale method of the AffineTransform class

Although it is possible to perform such matrix modifications by working
directly with the six values in the matrix, for some cases, there is an easier
way.  For example, one of the methods of the AffineTransform class
is a convenience method named scale.

The scale method concatenates an existing transform matrix with a
scaling transform matrix, producing a matrix that performs both transforms at
the same time.

Listing 26 invokes the scale method to concatenate
the translation matrix created in Listing 25 with a scaling matrix.  In
this case, the horizontal scale factor is -1.0 and the vertical scale factor is
1.0.  When this transform matrix is applied to an image, it will be
translated and scaled in a single operation.

    transformObj.scale(-1.0, 1.0);

Listing 26

Display the transform matrix

For illustration purposes only, Listing 27 displays the contents of the
resulting transform matrix.

    //Display the six values in the transformation matrix.
    double[] theMatrix = new double[6];
    transformObj.getMatrix(theMatrix);
    
    //Display first row of values by displaying every
    // other element in the array starting with element
    // zero.
    for(int cnt = 0; cnt < 6; cnt+=2){
      System.out.print(theMatrix[cnt] + "t");
    }//end for loop
    
    //Display second row of values displaying every
    // other element in the array starting with element
    // number one.
    System.out.println();//new line
    for(int cnt = 1; cnt < 6; cnt+=2){
      System.out.print(theMatrix[cnt] + "t");
    }//end for loop
    System.out.println();//end of line
    System.out.println();//blank line

Listing 27

The code in listing 27 is not required for the Mirror Image transform to be
performed.  Rather, it is provided to allow you to examine the values in
the transformation matrix.  If you don’t care about the mathematics involved, you can
completely ignore the code in Listing 27.

Perform the three required steps

Listing 28 performs the
three required steps that were listed earlier, using code that has been
previously explained.

    //Get a translation filter object based on the
    // AffineTransform object.
    AffineTransformOp filterObj = new AffineTransformOp(
              transformObj,AffineTransformOp.TYPE_BICUBIC);

    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(theImage, dest);
    
    //Return a reference to the destination object.
    return dest;

  }//end processPage03

Listing 28

The end of the program

Listing 28 also signals the end of the method named processPage03, and
the end of the explanation of the Mirror Image transform that produces
results similar to those shown in Figure 9 under control of the page shown in
Figure 4.

Finally, Listing 28 also signals the end of the explanation of the program
named ImgMod40.

Run the Program

I encourage you to copy the code from Listing 29 into your text editor,
compile it, and execute it.  Experiment with it, making changes, and observing
the results of your changes.

Remember, you will also need to compile the code for the framework program
named ImgMod05 and the interface named ImgIntfc05.  You will
find that source code in the earlier lesson entitled
A Framework for
Experimenting with Java 2D Image-Processing Filters
.

You will also need one or more JPEG image files to experiment with.  You
should have no difficulty finding such files at a variety of locations on the
web.  I recommend that you stick with relatively small images so that both the
original image and the processed image will fit in the vertical space on your
screen in the format shown in Figure 8.

Summary

In this lesson, I provided and explained an image-processing program named
ImgMod40
that is compatible with the framework program named ImgMod05.

The purpose of this program is to show you how to write such programs, and also
to illustrate a variety of different uses for the AffineTransformOp class
of the Java 2D API.

Four specific uses of the AffineTransformOp class
were illustrated, and you should be able to devise many more.

What’s Next?

Future lessons in this series will teach you how to use the following
image-filtering classes from the Java 2D API:

  • BandCombineOp
  • ConvolveOp
  • RescaleOp
  • ColorConvertOp

References

  • 400
    Processing Image Pixels using Java, Getting Started
  • 402
    Processing Image Pixels using Java, Creating a Spotlight
  • 404
    Processing Image Pixels Using Java: Controlling Contrast and Brightness
  • 406
    Processing Image Pixels, Color Intensity, Color Filtering, and Color
    Inversion
  • 408
    Processing Image Pixels, Performing Convolution on Images
  • 410
    Processing Image Pixels, Understanding Image Convolution in Java
  • 412
    Processing Image Pixels, Applying Image Convolution in Java, Part 1
  • 414
    Processing Image Pixels, Applying Image Convolution in Java, Part 2
  • 416 Processing Image Pixels, An Improved Image-Processing Framework in
    Java
  • 450 A Framework for Experimenting with Java 2D Image-Processing
    Filters
  • 452 Using the Java 2D LookupOp Filter Class to Process Images

Complete Program Listing


A complete listing of the program discussed in this lesson is shown in Listing
29 below.
 

/*File ImgMod40.java
Copyright 2006, R.G.Baldwin

The purpose of this class is to illustrate a variety of 
different uses for the AffineTransformOp filter class of 
the Java 2D API.

See general comments in the class named ImgMod038.

This class is compatible with the use of the driver program
named ImgMod05.

The driver program named ImgMod05 displays the original and
the modified images.  It also writes the modified image 
into an output file in JPEG format.  The name of the output
file is junk.jpg and it is written into the current 
directory.

Image-processing programs such as this one may provide a 
GUI for data input making it possible for the user to 
modify the behavior of the image-processing method each 
time the Replot button is clicked.  Such a GUI is provided
for this program.

Enter the following at the command line to run this 
program:

java ImgMod05 ImgMod40 ImageFileName

If the program is unable to load the image file within ten
seconds, it will abort with an error message.

This program creates a GUI consisting of a tabbed pane 
containing four pages.  The tabs on the pages are labeled:

Scaling
Translation
Rotation
Mirror Image

Each page contains a set of controls that makes it possible
to process the image in a way that illustrates the 
processing concept indicated by the label on the tab.
Processing details for each page are provided in the 
comments in the code used to construct and process the 
individual pages.

Tested using J2SE 5.0 under WinXP.
**********************************************************/

import java.awt.image.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.geom.AffineTransform;

class ImgMod40 extends Frame implements ImgIntfc05{
  //Primary container used to construct the GUI.
  JTabbedPane tabbedPane = new JTabbedPane();
  
  //Components used to construct the Scaling page.
  // Components that require local access only are defined
  // locally.  Others are defined here as instance
  // variables.
  Panel page00 = new Panel();
  TextField page00TextFieldHorizontal = 
                                    new TextField("0.5",6);
  TextField page00TextFieldVertical = 
                                    new TextField("0.5",6);
  //Components for radio buttons
  CheckboxGroup Page00Group = new CheckboxGroup();
  Checkbox page00NearestNeighbor = new Checkbox(
       "Nearest Neighbor Interpolation",Page00Group,false);
  Checkbox page00Bilinear = new Checkbox(
               "Bilinear Interpolation",Page00Group,false);
  Checkbox page00Bicubic = new Checkbox(
                 "Bicubic Interpolation",Page00Group,true);
  
  //Components used to construct the Translation page.
  // Components that require local access only are defined
  // locally.  Others are defined here as instance
  // variables.
  Panel page01 = new Panel();
  TextField page01TextFieldHorizontal = 
                                      new TextField("5",6);
  TextField page01TextFieldVertical = 
                                     new TextField("10",6);

  //Components used to construct the Rotation page.
  // Components that require local access only are defined
  // locally.  Others are defined here as instance
  // variables.
  Panel page02 = new Panel();
  TextField page02TextField = new TextField("45.0",6);
  
  //Components used to construct the Mirror Image page.
  // Components that require local access only are defined
  // locally.  Others are defined here as instance
  // variables.
  Panel page03 = new Panel();
  
  //-----------------------------------------------------//
  
  //This is the primary constructor.  It calls other
  // methods to separate the construction of the GUI into
  // easily understandable units.  Each method that it
  // calls constructs one page in the tabbed pane.
  ImgMod40(){//constructor
  
    constructPage00();
    tabbedPane.add(page00);//Add page to the tabbedPane.
    
    constructPage01();
    tabbedPane.add(page01);//Add page to the tabbedPane.
    
    constructPage02();
    tabbedPane.add(page02);//Add page to the tabbedPane.
    
    constructPage03();
    tabbedPane.add(page03);//Add page to the tabbedPane.
    
    add(tabbedPane);//Add tabbedPane to the Frame.

    setTitle("Copyright 2006, R.G.Baldwin");
    setBounds(555,0,470,300);
    setVisible(true);

    //Define a WindowListener to terminate the program.
    addWindowListener(
      new WindowAdapter(){
        public void windowClosing(WindowEvent e){
          System.exit(1);
        }//end windowClosing
      }//end windowAdapter
    );//end addWindowListener
  }//end constructor
  //-----------------------------------------------------//
  
  //This method constructs the Scaling page.
  // This method is called from the primary constructor.
  void constructPage00(){
    page00.setName("Scaling");//Label on the tab.
    page00.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE SCALINGn"
      + "This page illustrates the scaling of an image "
      + "using three different types of interpolation."
      + "nn"
      + "Enter a positive scale factor between 0.001 and "
      + "10.0 in each of the text fields, select an "
      + "interpolation type, and click the Replot button. "
      + "The best quality interpolation will probably be "
      + "achieved with the use of Bicubic Interpolation. "
      + "Nearest Neighbor Interpolation will probably "
      + "produce the poorest quality.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,7,1,
                                 TextArea.SCROLLBARS_NONE);
    page00.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    Panel page00ControlPanel = new Panel();
    page00ControlPanel.setLayout(new GridLayout(5,1));
    
    //Construct and populate the panels that contain the
    // radio buttons, the labels, and the text fields.
    // Add each such panel an a new cell in the grid
    // layout going from the top of the grid to the bottom
    // of the grid.
    
    //Begin with the radio buttons.  The purpose of putting
    // the radio buttons on panels is to cause them to be
    // left justified in their cells.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel00.add(page00NearestNeighbor);
    page00ControlPanel.add(subControlPanel00);
    
    Panel subControlPanel01 = new Panel();
    subControlPanel01.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel01.add(page00Bilinear);
    page00ControlPanel.add(subControlPanel01);
    
    Panel subControlPanel02 = new Panel();
    subControlPanel02.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel02.add(page00Bicubic);
    page00ControlPanel.add(subControlPanel02);
    
    //Now create and populate panels that contain labels
    // and associated TextField objects
    Panel subControlPanel03 = new Panel();
    subControlPanel03.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel03.add(new Label(
                               "Horizontal Scale Factor"));
    subControlPanel03.add(page00TextFieldHorizontal);
    page00ControlPanel.add(subControlPanel03);
    
    Panel subControlPanel04 = new Panel();
    subControlPanel04.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    subControlPanel04.add(new Label(
                                 "Vertical Scale Factor"));
    subControlPanel04.add(page00TextFieldVertical);
    page00ControlPanel.add(subControlPanel04);
    
    page00.add(page00ControlPanel,BorderLayout.CENTER);
  }//end constructPage00
  //-----------------------------------------------------//

  //This method processes the image according to the
  // controls located on the Scaling page.
  //This method uses the AffineTransformOp filter class to
  // process the image.
  //The method is called from within the switch statement
  // in the method named processImg, which is the primary
  // image-processing method in this program.
  //This method illustrates image scaling, giving the user
  // a choice of three different interpolation schemes.
  //See the earlier lesson at
  // http://www.developer.com/java/other/article.php
  // /626051
  // for additional information on the use of a scaling
  // affine transform.
  BufferedImage processPage00(BufferedImage theImage){
    
    //Set a non-zero default value for the horizontal scale
    // factor.
    double horizontalScale = 0.001;
    try{//Get horizontalScale from the text field.
      horizontalScale = Double.parseDouble(
                      page00TextFieldHorizontal.getText());
    }catch(java.lang.NumberFormatException e){
      page00TextFieldHorizontal.setText("Bad Input");
      horizontalScale = 0.001; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for horizontal scale
    if((horizontalScale < 0.001) || 
                                 (horizontalScale > 10.0)){
      page00TextFieldHorizontal.setText("Bad Input");
      horizontalScale = 0.001;//Override bad user input.
    }//end if
  
    //Set a non-zero default value for the vertical scale
    // factor.
    double verticalScale = 0.001;
    try{//Get verticalScale from the text field.
      verticalScale = Double.parseDouble(
                        page00TextFieldVertical.getText());
    }catch(java.lang.NumberFormatException e){
      page00TextFieldHorizontal.setText("Bad Input");
      verticalScale = 0.001; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for verticalScale
    if((verticalScale < 0.001) || (verticalScale > 10.0)){
      page00TextFieldHorizontal.setText("Bad Input");
      verticalScale = 0.001;//Override bad user input.
    }//end if
    
    //Get the selected interpolation scheme from the radio
    // buttons and reflect that selection in an int value
    // corresponding to the selected button.
    int interpolationScheme;
    if(page00Bicubic.getState() == true){
      interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;
    }else if(page00Bilinear.getState() == true){
      interpolationScheme = 
                           AffineTransformOp.TYPE_BILINEAR;
    }else{//page00NearestNeighbor must be selected
      interpolationScheme = 
                   AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
    }//end else
    
    //An AffineTransform object is required later to create
    // the filter object.  An examination of the
    // documentation for the AffineTransform class will
    // show that there are several different ways to create
    // such an object.  The following statement is probably
    // the simplest of those ways.
    //Create an AffineTransform object for scaling that
    // matches the user input from the control panel.
    AffineTransform transformObj = 
                         AffineTransform.getScaleInstance(
                            horizontalScale,verticalScale);
    
    //At this point, you could use the methods of the
    // AffineTransform class to get, modify, and restore
    // the transform matrix in order to modify the
    // behavior of the transformation process.  See the
    // earlier lesson at
    // http://www.developer.com/java/other/article.php
    // /626051
    // for additional information on this topic.
        
    //Use the AffineTransform object to create a filtering
    // object.
    AffineTransformOp filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);

    /*Note:  Normally, I would perform the filtering 
     *operation and return the filtered result simply by 
     *executing the following statement:
    
     *  return filterObj.filter(theImage, null);
      
     *However, for reasons that I am unable to explain, 
     *when I do that for the AffineTransformOp class, the
     *ColorModel of the BufferedImage object that is 
     *returned to the framework program named ImgMod05 is
     *not compatible with the method used by that program 
     *to write the output JPEG file.  This results in an 
     *output file in which the image data appears to be 
     *scrambled.  Therefore, it was necessary for me to
     *use the following alternative code instead.*/
    
    //Create a destination BufferedImage object to receive
    // the filtered image.  Force the ColorModel of the
    // destination object to match the ColorModel of the
    // incoming object.
    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(theImage, dest);
    
    //Return a reference to the destination object.
    return dest;

  }//end processPage00
  //-----------------------------------------------------//
  
  //This method constructs the Translation page.
  //The method is called from the primary constructor.
  void constructPage01(){
    page01.setName("Translation");//Label on the tab.
    page01.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE TRANSLATIONn"
      + "This page illustrates the translation of an "
      + "image to a new location relative to the "
      + "upper-left corner of the container using Bicubic "
      + "Interpolation.nn"
      + "Enter the horizontal and vertical translation "
      + "distances in pixels into the text fields and "
      + "click the Replot button.nn"
      + "Note that the translation distances must be "
      + "between -1000 and +1000 pixels.  Also note that "
      + "negative translations may shift the image "
      + "completely out of the Frame on the top or the "
      + "left side of the image.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,9,1,
                                 TextArea.SCROLLBARS_NONE);
    page01.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    Panel page01ControlPanel = new Panel();
    page01ControlPanel.setLayout(new GridLayout(3,1));
    
    //Place each label and its corresponding text field
    // on a panel.  Place the panels in the cells in the
    // grid layout from top to bottom.  Note that there
    // is an empty cell at the bottom for cosmetic
    // purposes.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(
                          new FlowLayout(FlowLayout.LEFT));
    subControlPanel00.add(new Label(
             "Horizontal Translation Distance in Pixels"));
    subControlPanel00.add(page01TextFieldHorizontal);
    page01ControlPanel.add(subControlPanel00);
    
    Panel subControlPanel01 = new Panel();
    subControlPanel01.setLayout(
                          new FlowLayout(FlowLayout.LEFT));
    subControlPanel01.add(new Label(
               "Vertical Translation Distance in Pixels"));
    subControlPanel01.add(page01TextFieldVertical);
    page01ControlPanel.add(subControlPanel01);
    
    page01.add(page01ControlPanel,BorderLayout.CENTER);
  }//end constructPage01
  //-----------------------------------------------------//

  //This method processes the image according to the
  // controls located on the Translation page.
  //This method uses the AffineTransformOp filter class
  // to process the image.  The method is called from
  // within the switch statement in the method
  // named processImg, which is the primary image
  // processing method in this program.
  //This method illustrates image translation using Bicubic
  // Interpolation.
  BufferedImage processPage01(BufferedImage theImage){
    
    double horizontalDistance = 0.0;
    try{//Get horizontalDistance from the text field.
      horizontalDistance = Double.parseDouble(
                      page01TextFieldHorizontal.getText());
    }catch(java.lang.NumberFormatException e){
      page01TextFieldHorizontal.setText("Bad Input");
      horizontalDistance = 0.0; //Override bad user input.
    }//end catch
   
    //Guarantee reasonable values for horizontalDistance
    if((horizontalDistance < -1000.0) || 
                            (horizontalDistance > 1000.0)){
      page01TextFieldHorizontal.setText("Bad Input");
      horizontalDistance = 0.0;//Override bad user input.
    }//end if

    double verticalDistance = 0.0;
    try{//Get verticalDistance from the text field.
      verticalDistance = Double.parseDouble(
                        page01TextFieldVertical.getText());
    }catch(java.lang.NumberFormatException e){
      page01TextFieldHorizontal.setText("Bad Input");
      verticalDistance = 0.0; //Override bad user input.
    }//end catch
    
    //Guarantee reasonable values for verticalDistance
    if((verticalDistance < -1000.0) || 
                              (verticalDistance > 1000.0)){
      page01TextFieldHorizontal.setText("Bad Input");
      verticalDistance = 0.0;//Override bad user input.
    }//end if
  
    //Set the interpolation scheme to the best available.
    // Note that this page doesn't allow the user to
    // select the interpolation scheme.
    int interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;

    //Create an AffineTransform object for translation that
    // matches the user input from the control panel.
    //Note that even though the actual translation is
    // performed in terms of integer pixels, the 
    // getTranslateInstance method requires the horizontal
    // and vertical distances to be provided as type
    // double.  A positive horizontal distance will cause
    // the image to appear to move to the right in its
    // container and a negative horizontal distance will
    // cause the image to appear to move to the left.
    // Similarly, a positive vertical distance will cause
    // the image to appear to move down and a negative
    // vertical distance will cause the image to appear to
    // move up.
    AffineTransform transformObj = 
                    AffineTransform.getTranslateInstance(
                      horizontalDistance,verticalDistance);
        
    //Use the AffineTransform object to create a filtering
    // object.
    AffineTransformOp filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);
    
    /*Note:  Normally, I would perform the filtering 
     *operation and return the filtered result simply by 
     *executing the following statement:
    
     *  return filterObj.filter(theImage, null);
      
     *However, for reasons that I am unable to explain, 
     *when I do that for the AffineTransformOp class, the
     *ColorModel of the BufferedImage object that is 
     *returned to the framework program named ImgMod05 is
     *not compatible with the method used by that program 
     *to write the output JPEG file.  This results in an 
     *output file in which the image data appears to be 
     *scrambled.  Therefore, it was necessary for me to
     *use the following alternative code instead.*/
    
    //Create a destination BufferedImage object to receive
    // the filtered image.  Force the ColorModel of the
    // destination object to match the ColorModel of the
    // incoming object.
    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(theImage, dest);
    
    //Return a reference to the destination object.
    return dest;

  }//end processPage01
  //-----------------------------------------------------//
  
  //This method constructs the Rotation page.
  //This method is called from the primary constructor. 
  // It illustrates the translation of an image followed by
  // rotation of the translated image.
  void constructPage02(){
    page02.setName("Rotation");//Label on the tab.
    page02.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    // This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="IMAGE Rotationn"
      + "This page illustrates translation of an image "
      + "followed by Rotation of the same image using "
      + "Bicubic Interpolation.nn"
      + "The image is rotated around its center after "
      + "being translated to the right and down by a "
      + "distance that is sufficient to give it room to "
      + "rotate.nn"
      + "Enter the desired rotation angle in degrees and "
      + "click the Replot button.  Positive rotation "
      + "angles represent clockwise rotation and negative "
      + "rotation angles represent counter-clockwise "
      + "rotation.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,9,1,
                                 TextArea.SCROLLBARS_NONE);
    page02.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

    //Construct the control panel and add it to the page.
    // Use a control panel with a GridLayout for 
    // cosmetic purposes.  Note that there are two empty
    // cells at the bottom of the grid.
    Panel page02ControlPanel = new Panel();
    page02ControlPanel.setLayout(new GridLayout(3,1));
    
    //Place the label and the text field on a panel and
    // place that panel in the top cell in the grid.
    Panel subControlPanel00 = new Panel();
    subControlPanel00.setLayout(new FlowLayout(
                                         FlowLayout.LEFT));
    //Note, a positive value in degrees represents
    // clockwise rotation.
    subControlPanel00.add(new Label(
                                   "Rotation in Degrees"));
    subControlPanel00.add(page02TextField);
    page02ControlPanel.add(subControlPanel00);
       
    page02.add(page02ControlPanel,BorderLayout.CENTER);
  }//end constructPage02
  //-----------------------------------------------------//

  //This method processes the image according to the
  // controls located on the Rotation page.
  //This method uses the AffineTransformOp filter class
  // to process the image.  The method is called from
  // within the switch statement in the method named
  // processImg, which is the primary image-processing
  // method in this program.
  //This method illustrates image Translation followed by 
  //image Rotation using Bicubic Interpolation.
  BufferedImage processPage02(BufferedImage theImage){
    
    //Get the rotation angle in degrees.  A positive angle
    // in degrees corresponds to clockwise rotation.
    double rotationAngleInDegrees = 0.0;
    try{//Get rotationAngleInDegrees from the text field.
      rotationAngleInDegrees = Double.parseDouble(
                                page02TextField.getText());
    }catch(java.lang.NumberFormatException e){
      page02TextField.setText("Bad Input");
      rotationAngleInDegrees = 0.0;//Override bad input.
    }//end catch
    
    //Compute the rotation angle in radians.
    double rotationAngleInRadians = 
                      rotationAngleInDegrees*Math.PI/180.0;
  
    //Set the interpolation scheme.
    int interpolationScheme = 
                            AffineTransformOp.TYPE_BICUBIC;
    
    //Translate the image down and to the right far enough
    // that the corners won't be chopped off by the top and
    // left edges of the container when the image is
    // rotated by 45 degrees.
    
    //Get the length of half the diagonal dimension of the
    // image using the formula for the hypotenuse of a
    // right triangle.
    int halfDiagonal = (int)(Math.sqrt(
         theImage.getWidth()*theImage.getWidth() + 
           theImage.getHeight()*theImage.getHeight())/2.0);
                                 
    //Set the horizontal and vertical translation
    // distances.
    int horizontalDistance = 
                      halfDiagonal - theImage.getWidth()/2;
    int verticalDistance = 
                     halfDiagonal - theImage.getHeight()/2;
    
    //Create an Affine Transform object that can be used
    // to translate the image by the distances computed
    // above.
    AffineTransform transformObj = 
                    AffineTransform.getTranslateInstance(
                      horizontalDistance,verticalDistance);
    
    //Get a translation filter object based on the
    // AffineTransform object.
    AffineTransformOp filterObj = new AffineTransformOp(
              transformObj,AffineTransformOp.TYPE_BICUBIC);
    
    //Perform the translation and save the modified image
    // as type BufferedImage.  This image will be the input
    // to the rotation transform.
    BufferedImage translatedImage = 
                          filterObj.filter(theImage, null);
    
    //Now rotate the image around a point that is at the
    // center of the original image.  Begin by getting a
    // rotation transform object.  The second and third
    // parameters specify the point about which the image
    // will be rotated.
    transformObj = AffineTransform.getRotateInstance(
                rotationAngleInRadians,
                horizontalDistance + theImage.getWidth()/2,
                verticalDistance + theImage.getHeight()/2);
                  
    //Now get a rotation filter object based on the
    // transform object and the specified interpolation
    // scheme.
    filterObj = new AffineTransformOp(
                         transformObj,interpolationScheme);

    /*Note:  Normally, I would perform the filtering 
     *operation and return the filtered result simply by 
     *executing the following statement:
    
     *  return filterObj.filter(translatedImage, null);
      
     *However, for reasons that I am unable to explain, 
     *when I do that for the AffineTransformOp class, the
     *ColorModel of the BufferedImage object that is 
     *returned to the framework program named ImgMod05 is
     *not compatible with the method used by that program 
     *to write the output JPEG file.  This results in an 
     *output file in which the image data appears to be 
     *scrambled.  Therefore, it was necessary for me to
     *use the following alternative code instead.*/
    
    //Create a destination BufferedImage object to receive
    // the filtered image.  Force the ColorModel of the
    // destination object to match the ColorModel of the
    // incoming object.
    BufferedImage dest = 
               filterObj.createCompatibleDestImage(
                 translatedImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(translatedImage, dest);
    
    //Return a reference to the destination object.
    return dest;  

  }//end processPage02
  //-----------------------------------------------------//
  
  //This method constructs the Mirror Image page.
  //This method is called from the primary constructor.
  void constructPage03(){
    page03.setName("Mirror Image");//Label on the tab.
    page03.setLayout(new BorderLayout());
    
    //Create and add the instructional text to the page.
    //This text appears in a disabled text area at the
    // top of the page in the tabbed pane.
    String text ="Mirror Imagenn"
      + "This page translates the image to the right by "
      + "an amount equal to its width, and then flips it "
      + "around its left edge to produce a mirror image "
      + "of the original image.nn"
      + "Click the Replot button to create the mirror "
      + "image.";
      
    //Note:  The number of columns specified for the
    // following TextArea is immaterial because the
    // TextArea object is placed in the NORTH location of
    // a BorderLayout.
    TextArea textArea = new TextArea(text,6,1,
                                 TextArea.SCROLLBARS_NONE);
    page03.add(textArea,BorderLayout.NORTH);
    textArea.setEnabled(false);

  }//end constructPage03
  //-----------------------------------------------------//

  //This method processes the image according to the
  // Mirror Image page.
  //This method uses the AffineTransformOp filter class to
  // process the image.  The method is called from within
  // the switch statement in the method named processImg,
  // which is the primary image processing method in this
  // program.
  //Note that unlike Scaling, Translation, and Rotation, 
  // there is no affine transform for Mirror Image. Rather,
  // this method illustrates the use of horizontal
  // translation followed by scaling with negative scale
  // factors to produce a mirror image of the original
  // image.
  BufferedImage processPage03(BufferedImage theImage){

    //Get an AffineTransform object that can be used to
    // shift the image to the right by an amount equal to
    // its width.
    AffineTransform transformObj = 
                      AffineTransform.getTranslateInstance(
                                    theImage.getWidth(),0);
    
    //Concatenate this transform with a scaling
    // transformation.
    transformObj.scale(-1.0, 1.0);
    
    //Display the six values in the transformation matrix.
    double[] theMatrix = new double[6];
    transformObj.getMatrix(theMatrix);
    
    //Display first row of values by displaying every
    // other element in the array starting with element
    // zero.
    for(int cnt = 0; cnt < 6; cnt+=2){
      System.out.print(theMatrix[cnt] + "t");
    }//end for loop
    
    //Display second row of values displaying every
    // other element in the array starting with element
    // number one.
    System.out.println();//new line
    for(int cnt = 1; cnt < 6; cnt+=2){
      System.out.print(theMatrix[cnt] + "t");
    }//end for loop
    System.out.println();//end of line
    System.out.println();//blank line

    //Get a translation filter object based on the
    // AffineTransform object.
    AffineTransformOp filterObj = new AffineTransformOp(
              transformObj,AffineTransformOp.TYPE_BICUBIC);
    
    /*Note:  Normally, I would perform the filtering 
     *operation and return the filtered result simply by 
     *executing the following statement:
    
     *  return filterObj.filter(theImage, null);
      
     *However, for reasons that I am unable to explain, 
     *when I do that for the AffineTransformOp class, the
     *ColorModel of the BufferedImage object that is 
     *returned to the framework program named ImgMod05 is
     *not compatible with the method used by that program 
     *to write the output JPEG file.  This results in an 
     *output file in which the image data appears to be 
     *scrambled.  Therefore, it was necessary for me to
     *use the following alternative code instead.*/
    
    //Create a destination BufferedImage object to receive
    // the filtered image.  Force the ColorModel of the
    // destination object to match the ColorModel of the
    // incoming object.
    BufferedImage dest = 
                      filterObj.createCompatibleDestImage(
                        theImage,theImage.getColorModel());
    
    //Filter the image and save the filtered image in the
    // destination object.
    filterObj.filter(theImage, dest);
    
    //Return a reference to the destination object.
    return dest;

  }//end processPage03
  //-----------------------------------------------------//

  //The following method must be defined to implement the
  // ImgIntfc05 interface.  It is called by the framework
  // program named ImgMod05.
  public BufferedImage processImg(BufferedImage theImage){
    
    BufferedImage outputImage = null;
    
    //Process the page in the tabbed pane that has been
    // selected by the user.
    switch(tabbedPane.getSelectedIndex()){
      case 0:outputImage = processPage00(theImage);
             break;
      case 1:outputImage = processPage01(theImage);
             break;
      case 2:outputImage = processPage02(theImage);
             break;
      case 3:outputImage = processPage03(theImage);
             break;
    }//end switch

    return outputImage;
  }//end processImg
}//end class ImgMod40


Listing 29


Copyright 2007, Richard G. Baldwin.  Reproduction in whole or in part in any
form or medium without express written permission from Richard Baldwin is
prohibited.

About the author

Richard Baldwin is a
college professor (at Austin Community College in Austin, TX) and private
consultant whose primary focus is a combination of Java, C#, and XML. In
addition to the many platform and/or language independent benefits of Java and
C# applications, he believes that a combination of Java, C#, and XML will become
the primary driving force in the delivery of structured information on the Web.

Richard has participated in numerous consulting projects and he
frequently provides onsite training at the high-tech companies located in and
around Austin, Texas.  He is the author of Baldwin’s Programming
Tutorials, which have gained a
worldwide following among experienced and aspiring programmers. He has also
published articles in JavaPro magazine.

In addition to his programming expertise, Richard has many years of
practical experience in Digital Signal Processing (DSP).  His first job after he
earned his Bachelor’s degree was doing DSP in the Seismic Research Department of
Texas Instruments.  (TI is still a world leader in DSP.)  In the following
years, he applied his programming and DSP expertise to other interesting areas
including sonar and underwater acoustics.

Richard holds an MSEE degree from Southern Methodist University and has
many years of experience in the application of computer technology to real-world
problems.

Baldwin@DickBaldwin.com

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories