An
image filter is an object that performs
transformations on image data. The Java
2D API supports image filtering through the
BufferedImageOp
interface. An image filter takes a
BufferedImage
as input (the source
image
) and performs some processing on the
image data, producing another BufferedImage
(the
destination image).
The
2D API comes with
a handy toolbox of
BufferedImageOp
implementations, as summarized in
Table 18.2.
Table 18-2. Image Operators in the 2D API
Let’s take a look at two of the simpler image operators. First, try the following application. It loads an image (the first command-line argument is the filename) and processes it in different ways as you select items from the combo box. The application is shown in Figure 18.6; the source code follows:
//file: ImageProcessor.java import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.image.*; import javax.swing.*; public class ImageProcessor extends JComponent { private BufferedImage source, destination; private JComboBox options; public ImageProcessor(BufferedImage image) { source = destination = image; setBackground(Color.white); setLayout(new BorderLayout( )); // create a panel to hold the combo box JPanel controls = new JPanel( ); // create the combo box with the names of the area operators options = new JComboBox( new String[] { "[source]", "brighten", "darken", "rotate", "scale" } ); // perform some processing when the selection changes options.addItemListener(new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { // retrieve the selection option from the combo box String option = (String)options.getSelectedItem( ); // process the image according to the selected option BufferedImageOp op = null; if (option.equals("[source]")) destination = source; else if (option.equals("brighten")) op = new RescaleOp(1.5f, 0, null); else if (option.equals("darken")) op = new RescaleOp(.5f, 0, null); else if (option.equals("rotate")) op = new AffineTransformOp( AffineTransform.getRotateInstance(Math.PI / 6), null); else if (option.equals("scale")) op = new AffineTransformOp( AffineTransform.getScaleInstance(.5, .5), null); if (op != null) destination = op.filter(source, null); repaint( ); } }); controls.add(options); add(controls, BorderLayout.SOUTH); } public void paintComponent(Graphics g) { int imageWidth = destination.getWidth( ); int imageHeight = destination.getHeight( ); int width = getSize( ).width; int height = getSize( ).height; g.drawImage(destination, (width - imageWidth) / 2, (height - imageHeight) / 2, null); } public static void main(String[] args) { String filename = args[0]; // load the image Image i = Toolkit.getDefaultToolkit( ).getImage(filename); Component c = new Component( ) {}; MediaTracker tracker = new MediaTracker(c); tracker.addImage(i, 0); try { tracker.waitForID(0); } catch (InterruptedException ie) {} // draw the Image into a BufferedImage int w = i.getWidth(null), h = i.getHeight(null); BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Graphics2D imageGraphics = bi.createGraphics( ); imageGraphics.drawImage(i, 0, 0, null); // create a frame window JFrame f = new JFrame("ImageProcessor"); f.addWindowListener(new WindowAdapter( ) { public void windowClosing(WindowEvent e) { System.exit(0); } }); Container content = f.getContentPane( ); content.setLayout(new BorderLayout( )); content.add(new ImageProcessor(bi)); f.setSize(bi.getWidth(), bi.getHeight( )); f.setLocation(100, 100); f.setVisible(true); } }
There’s quite a bit packed into the
ImageProcessor
application. After you’ve
played around with it, come back and read about the details.
The basic operation of ImageProcessor
is very
straightforward. It loads a source image, specified with a
command-line argument, in its main( )
method. The
image is displayed along with a combo box. When you select different
items from the combo box, ImageProcessor
performs
some image-processing operation on the source image and displays the
result (the destination image). Most of this work occurs in the
ItemListener
event handler that is created in
ImageProcessor
’s constructor. Depending on
what option is selected, a BufferedImageOp
(called
op) is instantiated and used to process the
source image, like this:
destination = op.filter(source, null);
The destination image is returned from the
filter( )
method. If we already had a destination image of the right size, we
could have passed it as the second argument to filter( )
, which would improve the performance of the application a
little bit. If you just pass null
, as we have
here, an appropriate destination image is created and returned to
you. Once the destination image is created, paint( )
’s job is very simple—it just
draws the
destination image, centered on the component.
Image processing is performed on
BufferedImage
s, not Image
s. This
example demonstrates an important technique: how to convert an
Image
to a BufferedImage
. The
main( )
method loads an Image
from a file using Toolkit
’s
getImage( )
method:
Image i = Toolkit.getDefaultToolkit( ).getImage(filename);
Next, main( )
uses a
MediaTracker
to make sure the image data is fully
loaded.
The trick of converting an Image
to a
BufferedImage
is to draw the
Image
into the drawing surface of the
BufferedImage
. Since we know the
Image
is fully loaded, we just need to create a
BufferedImage
, get its graphics context, and draw
the Image
into it:
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Graphics2D imageGraphics = bi.createGraphics( ); imageGraphics.drawImage(i, 0, 0, null);
Rescaling
is an
image operation that multiplies all the pixel values in the image by
some constant. It doesn’t affect the size of the image in any
way (in case you thought rescaling meant
scaling), but it does affect the colors of its
pixels. In an RGB image, for example, each of the red, green, and
blue values for each of the pixels would be multiplied by the
rescaling multiplier. If you want, you can also adjust the results by
adding an offset. In the 2D API, rescaling is performed by the
java.awt.image.RescaleOp
class. To create such an operator, specify the multiplier, offset,
and a set of hints that control the quality of the
conversion. In this case, we’ll use a zero offset and not
bother with the hints (by passing null
):
op = new RescaleOp(1.5f, 0, null);
Here we’ve specified a multiplier of 1.5 and an offset of 0.
All values in the destination image will be 1.5 times the values in
the source image, which has the net result of making the image
brighter. To perform the operation, we call the filter( )
method from the
BufferedImageOp
interface.
The
java.awt.image.AffineTransformOp
image operator
geometrically transforms a
source image to produce the destination image. To create an
AffineTransformOp
, specify the transformation you
want, in the form of an
java.awt.geom.AffineTransform
. The
ImageProcessor
application includes two examples
of this operator, one for rotation and one for scaling. As before,
the AffineTransformOp
constructor accepts a set of
hints—we’ll just pass null
to keep
things simple:
else if (option.equals("rotate")) op = new AffineTransformOp( AffineTransform.getRotateInstance(Math.PI / 6), null); else if (option.equals("scale")) op = new AffineTransformOp( AffineTransform.getScaleInstance(.5, .5), null);
In both of these cases, we obtain an
AffineTransform
by calling one of its static
methods. In the first case, we get a rotational
transformation by supplying an angle. This transformation is wrapped
in an AffineTransformOp
. This operator has the
effect of rotating the source image around its origin to create the
destination image. In the second case, a scaling transformation is wrapped in an
AffineTransformOp
. The two scaling values, .5 and
.5, specify that the image should be reduced to half its original
size in both the x and y axes.
One interesting aspect of AffineTransformOp
is
that you may “lose” part of your image when it’s
transformed. In the rotational and image operator in the
ImageProcessor
application, the destination image
has clipped some of the original image out. This has to do with how
images are processed—both the source and destination need to
have the same origin, so if any part of the image gets transformed
into negative x or y space, it is lost. To work around this problem,
you can structure your transformations such that no information will
be lost. You could, for example, rotate the image around the
bottom-left corner, or add a translational component to the rotation
so that the entire destination image
would be in
positive
coordinate space.
Get Learning Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.