Java Programming Notes # 454
- Preface
- General
Background Information - Preview
- Discussion and
Sample Code - Run the Program
- Summary
- What’s Next?
- References
- Complete Program
Listing
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.
|
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:
- Use the
nearest neighbor - Perform
bilinear interpolation - Perform
bicubic interpolation
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
|
|
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(); |
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(); |
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 |
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 |
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 |
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:
- 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.) - Use the AffineTransform object to
create an image-filtering object
of the AffineTransformOp class with a specified interpolation scheme. - 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 |
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 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); |
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 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)
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:
- 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.) - 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()); |
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 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 |
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; |
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); |
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); |
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 |
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 |
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; |
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; |
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); |
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); |
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); |
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 |
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.
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 |
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 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); |
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 |
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 |
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 |
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.