Skip to main content

Java Tech: Process Images with Imagician

February 7, 2006

{cs.r.title}









Contents
Introducing Imagician
Architecture Highlights
   An Image Operator for Embossing Images
   Status Bar Integration
The Trouble with ImageIcon
Conclusion
Resources

In the mid-1980s, while attending university, I took a course on image processing. That course introduced me to many techniques for enhancing and extracting information from digitized images. A few months after I took that course, the Voyager 2 spacecraft sailed past Uranus, returning many images taken during that planetary encounter. Because I had learned about image-processing techniques that were probably used to convert Voyager's raw noisy data into presentable images of the Uranian system, I became fascinated with image processing.

Processing images sent from a distant spacecraft is something that most of us will never need to handle. But you might find yourself needing to sharpen a blurred image that you took with your digital camera, for example. Although you can use off-the-shelf software to accomplish this and other image-processing tasks, that software might not support your special requirements. Instead of building your own Java-based image-processing software to meet these requirements, you might want to investigate Imagician.

Imagician (rhymes with "politician") is my own Java application for opening, processing (using several built-in image-processing operations), and saving processed images. After introducing you to Imagician, this article highlights two important pieces of this application's architecture, a custom Java 2D image operator for embossing images, and the status bar component. While developing Imagician, I discovered a strange problem with javax.swing.ImageIcon. This article concludes by presenting that problem and its solution.

Introducing Imagician

Imagician is an easy-to-use image-processing application for blurring, brightening, darkening, embossing, inverting, sharpening, and thresholding images. You can also use Imagician to detect edges within images, remove the red, green, or blue color component from images, and convert colored images to shades of gray. For users' convenience, Imagician presents a GUI rather than a command-line interface. This Swing-based GUI largely consists of a menu bar, a scrollable window that displays the current image, and a status bar; file choosers and a few confirmation and error message dialog boxes round out the application. Figure 1 reveals Imagician's GUI.

Select Emboss or another image-processing operation from Imagician's Process menu
Figure 1. Select Emboss or another image-processing operation from Imagician's Process menu

The menu bar presents File and Process menus. Menu items in the File menu allow you to open a BMP, GIF, JPEG, or PNG image; save a processed image to the file from which it was opened (BMP, JPEG, and PNG files only--an error message dialog box appears if you try to save an image opened from a GIF file back to the GIF file); save a processed image to a different BMP, JPEG, or PNG file; and to exit the application. The Process menu provides items to invoke image-processing operations on the current image. These operations are cumulative. For example, you can blur an image and then invert the blurred image. If you make a mistake, select the Undo menu item.

When you select either "Open..." or "Save as..." from the File menu, an appropriate file chooser appears. Each file chooser displays a small icon next to BMP, GIF, JPEG, or PNG files (Open file chooser only); and a small icon next to BMP, JPEG, or PNG files (Save file chooser only). Furthermore, each file chooser presents a small preview window that conveniently displays the image of the highlighted GIF, JPEG, or PNG file. No image is displayed for a highlighted BMP file because ImageIcon is used to load preview images, and ImageIcon does not support BMP files. Figure 2 reveals the Open file chooser.

The open file chooser lets you select BMP, GIF, JPEG, and PNG files
Figure 2. The Open file chooser lets you select BMP, GIF, JPEG, and PNG files. However, only GIF, JPEG, and PNG images can be previewed

Eventually, you'll want to run and play with Imagician. Before you can do that, you must compile Imagician.java. That file (and other necessary files) are located in this article's code file (see the Resources for this article's sample code).

After interacting with this application, you might want to package Imagician into a JAR file. Accomplish that task by executing the following command: jar cfm imagician.jar manifest.mf *.class images. You will find manifest.mf and images in the sample code file. Once you've created the JAR file, execute java -jar imagician.jar to run Imagician from the command line. Or, run Imagician from the desktop by placing imagician.jar on the desktop and double-clicking its icon.

Architecture Highlights

Although Imagician is a simple program, it contains interesting features. This section introduces you to two of these features. You first learn how Imagician implements a more efficient version of the previous article's image-embossing algorithm as a java.awt.image.BufferedImageOp (for convenience to developers). Moving on, this article shows how Imagician implements its status bar. This GUI feature is more than an embellishment: status bars offer the perfect place to present users with simplified help on various menu items.

An Image Operator for Embossing Images

My previous article presented you with an algorithm for embossing images, and then translated that algorithm into Java as an ImageIcon subclass. An even more convenient way to work with embossing is to implement that algorithm as a BufferedImageOp, so that embossing nicely fits into Java 2D's image-processing model. I've done just that for Imagician; the following Imagician.java excerpt reveals the simplicity of this approach:

BufferedImageOp embossOp = new EmbossOp ();

bi = embossOp.filter (bi, null);
pp.setBufferedImage (bi);

After creating an instance of the embossing image operator, the code fragment employs that operator to filter a BufferedImage, called bi, resulting in a new BufferedImage containing an embossed version of its former image contents. The embossed image is subsequently presented to the user via Imagician's picture panel component.

A custom image operator is a class that implements the BufferedImageOp interface's five methods. The implementation of that interface's BufferedImage filter(BufferedImage src, BufferedImage dst) method, which takes care of the image-processing task, should be as efficient as possible. The following Imagician.java excerpt presents the EmbossOp class, whose filter() method makes my former embossing implementation somewhat more efficient from a performance perspective, at the expense of additional memory:

class EmbossOp implements BufferedImageOp
{
   // Filter the src buffered image according to the
   // embossing algorithm presented in the previous
   // article. The embossed image is stored in the
   // dst buffered image. If dst is null, an
   // appropriate buffered image (compatible with
   // the src image) is created to hold the result.

   public final BufferedImage filter
     (BufferedImage src, BufferedImage dst)
   {
      if (dst == null)
          dst = createCompatibleDestImage (src,
                                           null);

      int width = src.getWidth ();
      int height = src.getHeight ();

      int [] srcPixels = src.getRGB (0, 0, width,
                                     height, null,
                                     0, width);

      for (int i = 0, offset = 0; i < height; i++)
           for (int j = 0; j < width; j++, offset++)
           {
                int current = srcPixels [offset];

                int upperLeft = 0;
                if (i > 0 && j > 0)
                    upperLeft = srcPixels [offset -
                                           width -
                                           1];

                int rDiff = ((current >> 16) & 255)
                            - ((upperLeft >> 16) &
                            255);
                int gDiff = ((current >> 8) & 255)
                            - ((upperLeft >> 8) &
                            255);
                int bDiff = (current & 255) -
                            (upperLeft & 255);

                int diff = rDiff;
                if (Math.abs (gDiff) >
                    Math.abs (diff)) diff = gDiff;
                if (Math.abs (bDiff) >
                    Math.abs (diff)) diff = bDiff;

                int grayLevel = Math.max
                                (Math.min (128 +
                                           diff,
                                           255), 0);
                dst.setRGB (j, i,
                            (grayLevel << 16) +
                            (grayLevel << 8) +
                            grayLevel);
           }

      srcPixels = null;

      return dst;
   }

   // Create a buffered image compatible with the
   // given src image and its color model.

   public BufferedImage createCompatibleDestImage
     (BufferedImage src, ColorModel dstCM)
   {
      if (dstCM == null)
          dstCM = src.getColorModel ();

      int width = src.getWidth ();
      int height = src.getHeight ();

      BufferedImage image;
      image = new BufferedImage (dstCM,
      dstCM.createCompatibleWritableRaster (width,
                                            height),
      dstCM.isAlphaPremultiplied (), null);
      return image;
   }

   // An image operator can create a destination
   // image larger or smaller than the src image.
   // The destination image's bounds are returned by
   // this method. Because embossing does not change
   // the image's size, the src image's bounds are
   // returned.

   public final Rectangle2D getBounds2D
     (BufferedImage src)
   {
      return src.getRaster ().getBounds ();
   }

   // Return the location of each source point in
   // the destination image. The destination point
   // location is the same as the source point
   // location because the embossing algorithm
   // doesn't geometrically transform the image.

   public final Point2D getPoint2D (Point2D srcPt,
                                    Point2D dstPt)
   {
      if (dstPt == null)
          dstPt = new Point2D.Float ();

      dstPt.setLocation (srcPt.getX (),
                         srcPt.getY ());
      return dstPt;
   }

   // No rendering hints are associated with the
   // embossing image operator.

   public final RenderingHints getRenderingHints ()
   {
      return null;
   }
}

I won't bother to describe the source code because you can easily read the comments. Instead, I want to point out that the performance-efficiency improvement is due to replacing two getRGB() method calls from the old version with indexing operations into a previously obtained array of image pixels. I could have tried to be even more efficient by working directly with the data buffer. At some point, however, I would have needed to pass the data buffer's pixel samples to the image's color model, to obtain each pixel's RGB color. I'm not sure how much of an efficiency increase (if any) that would have provided, and am interested in learning how you would make the embossing implementation more efficient.

The embossing image operator is one technique for embossing images. Another technique uses Java 2D's convolution and (possibly) color conversion image operators. I found some code on the internet that uses only the convolution operator to perform embossing. That code's kernel has the following values:

-2 0 0
0 1 0
0 0 2

In an experiment, I took this kernel, used it as the basis for a convolution operator, performed the filtering, created a color conversion operator for mapping the resulting image's colors to shades of gray (with the hope of achieving a more metallic-looking image), and grayscaled the embossed image. The code fragment (in an Imagician.java context) to accomplish this task appears below:

Kernel kernel = new Kernel (3, 3,
                            new float [] { -2, 0,
                                            0, 0,
                                            1, 0,
                                            0, 0,
                                            2 });

BufferedImageOp op = new ConvolveOp (kernel);
BufferedImage tmp = op.filter (bi, null);

ColorConvertOp grayOp = new ColorConvertOp
  (ColorSpace.getInstance (ColorSpace.CS_GRAY),
  null);

bi = grayOp.filter (tmp, null);
pp.setBufferedImage (bi);

Figure 3 presents the result of executing the aforementioned code fragment to emboss an image. I'm not satisfied with this result.

An embossed image based on the previous code fragment
Figure 3. An embossed image based on the previous code fragment

In contrast to Figure 3, Figure 4 shows the image embossed with my own embossing image operator. Which figure looks more metallic to you?

An embossed image based on my embossing image operator
Figure 4. An embossed image based on my embossing image operator

Status Bar Integration

Status bars are an important part of GUIs. They can provide help to users, and even though Swing doesn't provide a dedicated status bar component, you can still implement one of your own. A status bar is not hard to create and maintain. All you really need is a javax.swing.JLabel to represent the status bar. The following Imagician.java excerpt creates a JLabel with default status bar text, establishes an etched border (to make the status bar stand out) that surrounds the JLabel status bar, and adds the JLabel to the bottom of the frame window:

// Create a status bar for displaying help text
// associated with the menus and their items.

status = new JLabel (defaultStatusText);
status.setBorder
  (BorderFactory.createEtchedBorder ());

// Add the status bar to the bottom of the
// application's contentpane.

getContentPane ().add (status,
  BorderLayout.SOUTH);

To properly present menu-related text on the status bar, Imagician creates two different listeners, registers the first listener with the File and Process javax.swing.JMenus, and registers the second listener with each JMenu's javax.swing.JMenuItems. The first listener is an instance of javax.swing.event.MenuListener. After being added to each JMenu, this listener displays, in the status bar, text that is specific to each menu (and not menu item) when the menu is selected, and default text when the menu is deselected. The Imagician.java excerpt below creates a MenuListener that accomplishes these tasks:

// Create a menu listener shared by all menus on
// the menu bar. This menu listener either displays
// default text or menu-specific text on the status
// bar.

MenuListener menul;
menul = new MenuListener ()
{
   public void menuCanceled (MenuEvent e)
   {           
   }

   public void menuDeselected (MenuEvent e)
   {           
      status.setText (defaultStatusText);
   }

   public void menuSelected (MenuEvent e)
   {
      JMenu m = (JMenu) e.getSource ();
      status.setText (m.getActionCommand ());
   }
};

The second listener is an instance of java.awt.event.MouseListener. It is added to each JMenuItem via mi.addMouseListener (statusl);. When the user moves the mouse pointer over a menu item, the listener's public void mouseEntered(MouseEvent e) method for the appropriate JMenuItem is invoked. This method obtains the menu item's status bar text and then assigns this text to the status bar JLabel. When the user moves the mouse pointer off of the menu item, the listener's public void mouseExited(MouseEvent e) method is called. This method assigns the default text to the status bar (which is handy when the user moves the mouse pointer over a menu separator line, which would have no status bar text). The following Imagician.java excerpt creates a MouseListener and carries out these tasks:

// Create a mouse listener shared by all menu items
// on all menus. This mouse listener displays
// menu-item specific text on the status bar
// whenever the mouse pointer enters the menu item.
// It displays default text when the mouse pointer
// exits a menu item.

MouseListener statusl = new MouseAdapter ()
{
   public void mouseEntered (MouseEvent e)
   {
      JMenuItem mi = (JMenuItem) e.getSource ();
      status.setText (mi.getActionCommand ());
   }

   public void mouseExited (MouseEvent e)
   {
      status.setText (defaultStatusText);
   }
};

Each of the previous two listeners depends on JMenu's or JMenuItem's inherited public void setActionCommand(String command) method having previously been called, in order to assign status bar text to each menu or menu item. This text is retrieved from inside of the listener by invoking the equivalent public String getActionCommand() method. Although these methods are easy to work with, you might prefer an alternative: actions. The javax.swing.Action interface has a LONG_DESCRIPTION constant that is passed to void putValue(String key, Object value) as the value of key, and signifies descriptive text that can appear on the status bar. You should think about using actions if you plan on adding a tool bar to Imagician. I did not plan to add a tool bar, which is why I did not take advantage of actions.

The Trouble with ImageIcon

While developing Imagician, I encountered a strange problem: sometimes the Open file chooser's preview window would not display the correct image when I selected an image file. For example, I opened the image named flower.jpg and that image appeared in Imagician's window. I then embossed the image and saved the result to a file named emboss.jpg. I next activated the Open file chooser and selected the emboss.jpg file. Prior to clicking the Open button, I expected to see the embossed image appear in the preview window. Instead, nothing was displayed.

It took some time to solve this problem. My quest to find a solution began by noting that when you activate the Save file chooser and either select an image file with the mouse or type in the name of a file, the Save file chooser invokes the public void propertyChange(PropertyChangeEvent e) method in its ImagePreview property change listener. The property in question is represented by the JFileChooser.SELECTED_FILE_CHANGE_PROPERTY constant.

Suppose you type in the name of an image file that does not exist. The propertyChange() method with the former property is invoked. After determining that the file is not a directory, the method invokes icon = new ImageIcon (file.getPath ()); to read the image associated with the nonexistent file. The ImageIcon constructor that is invoked to load the preview image internally calls one of java.awt.Toolkit's getImage() methods. Those methods cache images (and presumably image-file paths and names) in memory.

Because there is no image, the internal getImage() method creates a java.awt.Image with -1 as the width and height, and then caches the "image" with the file's name and path information. Nothing appears in the preview window, but this is OK, because you click the Save button and the file chooser disappears. The next time you activate the Open file chooser and select the file in which you previously saved the current image, the propertyChange() method, followed by icon = new ImageIcon (file.getPath ());, executes. Even though an image is present in this file, the internal getImage() method returns the cached "image" whose width and height are -1. As a result, nothing is displayed.

To solve the problem, the "image" must be flushed from the cache and the former ImageIcon code fragment must re-execute. The Imagician.java excerpt below causes that to happen:

if (icon.getIconWidth () == -1)
{
    icon.getImage ().flush ();
    icon = new ImageIcon (file.getPath ());
}

Conclusion

I regard Imagician as a starting point for creating a full-blown image-processing application. You can improve Imagician by introducing new image-processing operations (such as rotations), and dialog boxes for configuring various operations (such as a dialog box that lets the user determine the amount of blurring). At some point, you might event want to support the Fourier Transform (for transforming time-domain data into frequency-domain data) and other sophisticated processing.

This is the final installment of Java Tech. I hope you have enjoyed this column, and I hope you've learned a few things about Java. If you've worked with the JTwain libraries (presented earlier in this column), you should know that I'm currently integrating them into a unified library. I am also adding several new features to make this library more capable. In a few weeks, you will be able to download the new JTwain library from javajeff.mb.ca. This adventure has come to an end.

Resources

width="1" height="1" border="0" alt=" " />
Jeff Friesen is a freelance software developer and educator specializing in Java technology. Check out his site at javajeff.mb.ca.
Related Topics >> GUI   |   Programming   |