Filtering Image Data

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

Name

Description

AffineTransformOp

Transforms an image geometrically

ColorConvertOp

Converts from one color space to another

ConvolveOp

Performs a convolution, a mathematical operation that can be used to blur, sharpen, or otherwise process an image

LookupOp

Uses one or more lookup tables to process image values

RescaleOp

Uses a multiplication to process image values

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);
  }
}
The ImageProcessor application

Figure 18-6. The ImageProcessor application

There’s quite a bit packed into the ImageProcessor application. After you’ve played around with it, come back and read about the details.

How ImageProcessor Works

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.

Converting an Image to a BufferedImage

Image processing is performed on BufferedImage s, not Images. 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);

Using the RescaleOp Class

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.

Using the AffineTransformOp Class

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.