Skip to main content

Java Tech: Image Embossing

December 8, 2005

{cs.r.title}




Contents
An Algorithm for Embossment
The EmbossedIcon class
Conclusion
Resources

What do books written in Braille, documents sealed with hot wax to guarantee their authenticity, and coins have in common? These objects illustrate embossment--the act of raising in relief from a surface. Embossment helps blind individuals read via the Braille system of raised dots, imprints seals onto documents for the purpose of authentication, and imprints seals onto coins (in the past, these seals guaranteed the weights and values of precious metals used in the coins).

In addition to its uses in the real world, embossment provides a three-dimensional chiseled look for computer images. This look tends to result in images that have a more professional appearance. For example, suppose you are responsible for creating professional investment software. Given a choice between the dialogs shown in Figure 1, which dialog do you think would be more appropriate?

A comparison of dialogs with non-embossed and embossed images
Figure 1. A comparison of dialogs with non-embossed and embossed images

From time to time, you might want to emboss your own images. Although you can use existing image-enhancement software to accomplish that task, you may find it more convenient to let your Java programs do the work for you.

This article presents an algorithm for embossing images. The article then implements it as EmbossedIcon, a class that displays embossed images. Because EmbossedIcon subclasses javax.swing.ImageIcon, you can easily convert source code that depends on ImageIcon to source code based on EmbossedIcon.

An Algorithm for Embossment

Think of an image as mountainous terrain. Each pixel represents an elevation: brighter pixels represent higher elevations. When an imaginary light source shines down on this terrain, the uphills that face the light source are lit, whereas the downhills that face away from the light source are shaded. An embossment algorithm captures this information.

The algorithm scans an image in the direction that a light ray is moving. For example, if the light source is located to the image's left, its light rays move from left to right, so the scan proceeds from left to right. During the scan, adjacent pixels (in the scan direction) are compared. The difference in intensities is represented by a specific level of gray (from black, fully shaded, to white, fully lit) in the destination image.

There are eight possible scanning directions: left to right, right to left, top to bottom, bottom to top, and four diagonal directions. To simplify the code, the algorithm usually scans left to right and top to bottom, and chooses its neighbors as appropriate. For example, if the light source is above and to the left of the image, the algorithm would compare the pixel above and to the left of the current pixel with the current pixel during the left-to-right and top-to-bottom scan. The comparison is demonstrated in the following embossment algorithm pseudocode:

FOR row = 0 TO height-1
    FOR column = 0 TO width-1
        SET current TO src.rgb [row][column]

        SET upperLeft TO 0
        IF row > 0 AND column > 0
           SET upperLeft TO
               src.rgb [row-1][column-1]

        SET redIntensityDiff TO
            red (current)-red (upperLeft)
        SET greenIntensityDiff TO
            green (current)-green (upperLeft)
        SET blueIntensityDiff TO
            blue (current)-blue (upperLeft)

        SET diff TO redIntensityDiff
        IF ABS (greenIntensitydiff) > ABS (diff)
           SET diff TO greenIntensityDiff
        IF ABS (blueIntensityDiff) > ABS (diff)
           SET diff TO blueIntensityDiff

        SET grayLevel TO
            MAX (MIN (128 + diff, 255), 0)

        SET dst.rgb [row][column] TO grayLevel
    NEXT column
NEXT row

The pseudocode identifies the image to be embossed as src and the image to hold the embossment as dst. These images are assumed to be rectangular buffers of RGB pixels.

Because pixels in the topmost row don't have neighbors above them, and because pixels in the leftmost column don't have neighbors to their left, the pseudocode assigns a mid-gray value to each pixel in the top row and in the left column. Figure 1 reveals these highlights along the top and left edges of the embossed image.

After obtaining the red, green, and blue intensities for the current pixel and its upper-left neighbor, the pseudocode calculates the difference in each intensity. The upper-left neighbor's intensities are subtracted from the current pixel's intensities because the light ray is moving in an upper-left to lower-right direction.

The pseudocode identifies the greatest difference (which might be negative) between the current pixel and its upper-left neighbor's three intensity differences. That's done to obtain the best-possible chiseled look. The difference is then converted to a level of gray between 0 and 255, and that gray level is stored in the destination image at the same location as the current pixel in the source image.

The EmbossedIcon class

To make it convenient for your Java programs to emboss their own images, I have created an EmbossedIcon class that subclasses ImageIcon. The code fragment below (see the Resources section for this article's sample code) shows how easy it is to choose between an ImageIcon and EmbossedIcon:

ImageIcon ii = (embossed)
               ? new EmbossedIcon ("logo.gif")
               : new ImageIcon ("logo.gif");
JLabel lbl = new JLabel (ii);

EmbossedIcon overrides ImageIcon's public ImageIcon(String filename) constructor and public void setImage(Image image) method to load an image into a java.awt.Image object, and then cache an embossed version of the image in a java.awt.image.BufferedImage object. The public void paintIcon(Component c, Graphics g, int x, int y) method is also overridden to paint the embossed image.

EmbossedIcon's constructor and overridden setImage() method depend on the private BufferedImage emboss(BufferedImage src) method to perform the embossing. That method assumes its src image has the same TYPE_INT_RGB type as the destination image. The emboss() method appears below:

private BufferedImage emboss (BufferedImage src)
{
   int width = src.getWidth ();
   int height = src.getHeight ();

   BufferedImage dst =
     new BufferedImage (width, height,
                        BufferedImage.TYPE_INT_RGB);

   for (int i = 0; i < height; i++)
        for (int j = 0; j < width; j++)
        {
             int current = src.getRGB (j, i);

             int upperLeft = 0;
             if (i > 0 && j > 0)
                 upperLeft = src.getRGB (j - 1,
                                         i - 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);
        }

   return dst;
}

Because emboss()'s source code closely approximates the embossment algorithm's pseudocode, and because you have already discovered how that pseudocode performs embossment, you should have no difficulty in figuring out how emboss() works. Instead, you might want to learn how EmbossedIcon copies an Image's pixels into a BufferedImage. The following EmbossedIcon constructor reveals all:

public EmbossedIcon (String filename)
{
   super (filename);

   bi = new BufferedImage (getIconWidth (),
                           getIconHeight (),
                           BufferedImage.TYPE_INT_RGB);
   Graphics2D bg = bi.createGraphics ();
   bg.drawImage (getImage (), null, null);
   bi = emboss (bi);
}

After invoking the ImageIcon superclass constructor, which loads the image from the file identified by filename and saves it into an Image, a BufferedImage is created. Its width and height are determined by invoking getIconWidth() and getIconHeight(). The Image's pixels are then copied into the BufferedImage by first obtaining a Graphics2D from the new BufferedImage and then invoking one of its drawImage() methods to draw the original image into the Graphics2D. Finally, emboss() is called to emboss the contents of the BufferedImage.

Conclusion

Embossing your Java program's images can add a degree of professionalism to the program. An embossment algorithm simulates a light ray moving across an image in a specific direction, and computes intensity differences between neighboring pixels in this direction. The greatest of these differences determines a gray level in the destination image. The EmbossedIcon class implements embossing, via its emboss() method. You can conveniently use EmbossedIcon wherever you would use its ImageIcon superclass.

Now that you have explored the embossment algorithm and the EmbossedIcon class, you might want to enhance EmbossedIcon, by overriding other ImageIcon constructors and public Image getImage() to return the BufferedImage instead of the Image. Finally, you might want to change the embossment algorithm, to create different chiseled looks.

Resources

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   |   Swing   |