The Source for Java Technology Collaboration
User: Password:



Chet Haase

Chet Haase's Blog

ImageIO: Just another example of better living by doing it yourself

Posted by chet on July 19, 2004 at 03:17 AM | Comments (16)

I've always known that ImageIO was a good thing to use since its inception. It reads and writes more formats than the original Image loading APIs, it has a pluggable interface for new image formats, it's the way of the future, it's more robust than the previous APIs, it's synchronous without the need for MediaTracker or that hacky ImageIcon workaround, blah, blah, blah....

I knew all that without actually working with it because it was apparent from the API. It is the way of the future for image reading and writing, and it is more robust and all the rest of that stuff. It's just a great full-featured package that's written from the ground up to provide what developers have been asking us to provide after using the old Image APIs for years.

But I'd never actually used the darned package...

(Petty justification: The folks working on the libraries for Java don't usually get enough time to play around with using the APIs; we're too busy making the stuff work. That's not to say that the people who wrote ImageIO didn't use it substantially in implementing it; of course they did. But I wasn't involved in that part of the API, and I'm busy enough in my little rendering and performance closet that I didn't take the opportunity to play around with ImageIO since it first came out.)

This all changed in the last month.

See, JavaOne was coming, which meant that I had a few demos to write. And it turns out I needed a couple of image utilities, and I needed them quickly. For both utilities, I began thinking "Now where could I find some simple program to do this ..." and then I realized how easy it would be to accomplish it from scratch in ImageIO. A few lines of code later and, presto!, I had my applications.

Not only did I get exactly the functionality that I was looking for, but I got the satisfaction of actually having written the code myself, which is so darned gratifying to us geeks. As they say, "Just another example of better living by doing it yourself." Or at least that's what I say.

Anyway, on with the code.

ImageScaler

The first utility came from a need to scale some images. We had a demo that was going to show images in a particular size all the time. Rather than scaling them on the fly or caching the scaled versions in the application, we figured it would be better to simply load images that were already of the size needed.

Knowing how these projects tend to go, what I really wanted was the ability to scale our full-size images (always keep the original full-size versions of media around...) down to some arbitrary size. And then, when the designers changed their minds about the size at the last minute, be able to quickly re-scale the images to some other arbitrary size.

We could do this through some image manipulation program, but it would be a major hassle to keep doing it (including having to go through the person that knew the program that ran the macro that ... you get the picture). So what I wanted was a simple standalone application to scale the images.

What I wrote was ImageScaler. This application takes the pathname to a directory (or uses "." as the default) and width/height values. It reads in all image files from that directory, creates a subdirectory ("scaled/"), and writes out scaled versions of those images in the specified width and height.

Here's the code:

package jcg.lhdemo;

import java.awt.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;

/**
 * ImageScaler
 *
 * This class loads all images in a given directory and scales them to the
 * given sizes, saving the results as JPEG files in a new "scaled/" subdirectory
 * of the original directory.
 */
public class ImageScaler {
    
    // Default w/h values; overriden by command-line -width/-height parameters
    static int IMAGE_W = 150;
    static int IMAGE_H = 250;
    
    public static void main(String args[]) {
        // Default directory is current directory, overridden by -dir parameter
        String imagesDir = ".";
        for (int i = 0; i < args.length; ++i) {
            if (args[i].equals("-dir") && ((i + 1) < args.length)) {
                imagesDir = args[++i];
            } else if (args[i].equals("-width") && ((i + 1) < args.length)) {
                IMAGE_W = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-height") && ((i + 1) < args.length)) {
                IMAGE_H = Integer.parseInt(args[++i]);
            }
        }
        // new subdirectory for scaled images
        String scaledImagesDir = imagesDir + File.separator + "scaled";
        // directory that holds original images
        File cwd = new File(imagesDir);
        // directory for scaled images
        File subdir = new File(scaledImagesDir);
        subdir.mkdir();
        File files[] = cwd.listFiles();
        // temporary image for every scaled instance
        BufferedImage scaledImg = new BufferedImage(IMAGE_W, IMAGE_H, 
                                                    BufferedImage.TYPE_INT_RGB);
        Graphics2D gScaledImg = scaledImg.createGraphics();
        // Note the use of BILNEAR filtering to enable smooth scaling
        gScaledImg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        for (int i = 0; i < files.length; ++i) {
            try {
                // For every file in the directory, assume it's an image and
                // load it
                BufferedImage img = ImageIO.read(files[i]);
                // If we get here, we must have read the image file successfully.
                // Create a new File in the scaled subdirectory
                File scaledImgFile = new File(scaledImagesDir + File.separator + 
                                              files[i].getName());
                // Scale the original image into the temporary image
                gScaledImg.drawImage(img, 0, 0, IMAGE_W, IMAGE_H, null);
                // Save the scaled version out to the file
                ImageIO.write(scaledImg, "jpeg", scaledImgFile);
            } catch (Exception e) {
                System.out.println("Problem with " + files[i]);
            }
        }
    }
}

Things to note about ImageScaler:
  • It assumes that the images are in JPEG format. Actually, it does read in the files in any format, but it writes them out in JPEG format with the original filenames, so if the images weren't JPEG to begin with, things will get a bit confusing. We could, of course, use any format that ImageIO supports (or any for which we have plugins); in this case I knew that we were dealing with JPEG images so I made that simplifying assumption in the code. The code could be easily extended to handle other or multiple formats instead.
  • Note the use of the BILINEAR rendering hint to the Graphics2D object; this tells Java 2D to do some smooth filtering on the image to make the scaled version look better than it would by using the default NEAREST_NEIGHBOR method. NEAREST_NEIGHBOR works great for many purposes (and is faster in general), but if you're trying to get higher quality and don't mind waiting just a tad longer for the result, use BILINEAR. And for those developers using jdk 5.0, note that the BICUBIC hint now works; you may get even better results with that value.
  • Aspect ratio: This code assumes that the original images are in the same aspect ratio as that determined by the width/height that we are scaling to. This may not be the case in any given situation, and might cause unattractive artifacts with widely differing aspect ratios. A more robust application could provide the ability to preserve the aspect ratio of the original instead of using the width/height parameters blindly.
  • A batch processing application like ImageScaler which acts on several files in the same format in a loop might be more performant by locating the appropriate reader and writer once, outside the for() loop, and then just calling the appropriate read() or write() functions inside the loop. I'll leave this as an exercise for the reader....

JpegConverter

A couple of weeks after I wrote and used ImageScaler, I ran into another problem where I needed a quick conversion utility. I had a series of BMP images that I wanted to convert to JPEG (in this case, because I needed a compressed image format). Once again I found myself writing a very quick program using ImageIO that did the job. In fact, I probably wrote and ran the program in less time than it would have taken me to find an appropriate utility out on the net, install it, and run it. And, once again, "it's another example of better living by doing it yourself".

This program takes a pointer to a directory (or uses "." by default), reads in all BMP image files in that directory (I constrained JpegConverter to convert only BMP files, but, like ImageScaler, this application could be easily extended to handle multiple formats) and then saves out a JPEG version of each file.

Here's the code:

import java.awt.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;

/**
 * JpegConverter
 *
 * This class loads all BMP images in a given directory and saves each as a JPEG
 * file in the same directory.  This code is specific to BMP, but it could be
 * easily extended to read images of any type that ImageIO handles.
 */
public class JpegConverter {
    
    public static void main(String args[]) {
        // Default directory is current directory, overridden by -dir parameter
        String imagesDir = ".";
        for (int i = 0; i < args.length; ++i) {
            if (args[i].equals("-dir") && ((i + 1) < args.length)) {
                imagesDir = args[++i];
            }
        }
        // directory that holds original images
        File cwd = new File(imagesDir);
        File files[] = cwd.listFiles();
        for (int i = 0; i < files.length; ++i) {
            String fileName = files[i].getName();
            // converstion to lower case just for ease of replacing the
            // filename extension
            String fileNameLC = fileName.toLowerCase();
            if (fileName.endsWith("bmp")) {
                try {
                    // Replace original "bmp" filename extension with "jpg"
                    int extensionIndex = fileNameLC.lastIndexOf("bmp");
                    String fileNameBase = fileName.substring(0, extensionIndex);
                    BufferedImage img = ImageIO.read(files[i]);
                    // create new JPEG file
                    File convertedImgFile = 
                            new File(imagesDir + File.separator + 
                                     fileNameBase + "jpg");
                    // store original file out in JPEG format
                    ImageIO.write(img, "jpeg", convertedImgFile);
                } catch (Exception e) {
                    System.out.println("Problem with " + files[i]);
                }
            }
        }
    }
}

Notes:

  • Some of the notes for ImageScaler above apply to JpegConverter as well, so consider this note as a dereference to the notes above...

And so, in conclusion

Some readers might wonder: "What's the big deal?" After all, these applications are pretty simple, so why all the fuss?

Exactly my point; thanks for clarifying it. It is easy. So easy that I would encourage everyone to think about using ImageIO next time you have a simple (or hard) image input/output problem come up; see if the ImageIO API doesn't make solving that problem just a tad easier.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • It's even better with java.awt.image
    That's great to hear. There is a lot of power in some pretty simple stuff in the APIs.

    On a related note, Dmitri and I are trying to finish up an article based on our Game Development talk at this year's JavaOne. Part of the particle will be a posting of the simple demo we wrote for that session. And part of that demo is some code that Jim Graham wrote for doing some simple 2D effects like you're talking about (he called it "Zoomy" and demoed it at JavaOne2003, if that rings a bell with anyone).

    I'm not sure of the posting date or forum for the article and code, but I'll at least post a blog here when the code is posted.

    Posted by: chet on July 19, 2004 at 07:29 AM

  • B&W Images
    When downscaling, I usually add a check to see if the image is bi-level. While black and white images rarely show up in graphics design, they are out in the wild alot (scanned images, faxes, etc). Trying to downscale a bilevel image tends to produce horrid results. I hunted around and found the "subsamplebinarytogray" operator which does just what the name implies. It produces a grey-scale image using gray levels averaged from surrounding bi-level pixels. This operator seems to work well for reductions to about 20% of normal size. If I have to go beyond this, I introduce a normal Affine scaling operation after the sub-sampling. I only mention this because it was a real bugar to figure out how to effectively down-scale bi-level images.

    Posted by: tlaurenzo on July 19, 2004 at 11:16 AM

  • It's even better with java.awt.image
    I was looking the other day at the psychedelic display in iTunes and wondered, how do they do that effect? I mean, I knew it was a continually updated radial blur, but I didn't know how to do it. I started browsing for demoz sites with tutorials, but everything I came up with was using low leve techniques in C like byteshifting and lookup tables. I didn't want to go through all that. I just wanted a proof of concept up and running soon.

    Then I remembered the java.awt.image routines that do image filtering and compositing. About half an hour later I had a simple radial blur effect animating at 10fps. No low level coding. No lookup tables. Just a convolve and composite. That's the power of well designed APIs.

    I'll have a demo up in another blog soon.

    - J

    Posted by: joshy on July 19, 2004 at 02:23 PM

  • More than the APIs...
    Sun has finally offered a good set of plugins for the API (a separate download from the JAI website). Now TIFF files are as easy to work with as others. I recently had to create an image manipulation program to support filing incoming faxes. I was easily able to amaze the .NET folks by rolling my own image manipulation components (thumbnails, enhancers, viewers) in less time than it would have taken them to fill out the purchase order to purchase a package for their environment, much less actually trying to use it. It still amazes me how FAST the JAI stuff is, too. JAI plus the ImageIO libraries are some of the best APIs in J2SE, IMHO.

    Posted by: tlaurenzo on July 19, 2004 at 06:11 PM

  • Scaling: up or down matters
    When it comes to wanting the smoothest result when scaling I've found
    that you have to ask [i]scaling up or scaling down[/i]? When making images
    larger, what Chet does is great (although I'm using 1.5 and I don't
    see an improvement with BICUBIC versus BILINEAR). But when making
    images smaller, It seems the following gives the best results:
    [code]
    Image result = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
    [/code]
    Have the Java 2D people gotten around to updating this? As far as I
    can tell, the above method is still using code from java.awt.Image.
    Is there a simple Java 2D way to get area averaging? I'd like to stick with
    BufferedImages and avoid the above method...

    Posted by: drlaszlojamf on July 19, 2004 at 11:47 PM

  • The Unix way..
    The unix way is that you have many small applications that do some small thing very well.
    This is contrary to your example application that some fellow needs to create a macro for etc; which we are very used to in the Windows world.

    This is what Java also delivers now; many great APIs that you can leveredge to stick all those options together.

    ps. I guess I would have used the unix command 'convert' (from the imagemagick package) which does this, and probably gives better results :)

    Posted by: zander on July 20, 2004 at 12:37 AM

  • The Unix way..
    Right - I guess what I was espousing was creating small utilities when you needed to. In this particular instance (image reading/writing/converting) the utilities were easier to create than to find/install/run from the net.

    In my case, I was on using Windows; there is no "convert" utility (or anything similar) by default. So I just created my own....

    Posted by: chet on July 20, 2004 at 02:17 AM

  • The Unix way..
    Oh; the 'ps' above was not a critique, it means that in the Java library there are many such small utilities that are also available under unix. Sticking these java-libraries together is much like writing a bash script on unix.

    In other words; you really did what I feel is the Unix way; sticking known-working libraries together to create your solution.

    Its a good solution when you don't have the installation software of debian that allows you to search for, download and install apps/docs/manuals for this kind of thing in 30 seconds.

    Posted by: zander on July 20, 2004 at 04:40 AM

  • JHLabs
    On the subject of filters and fills and things, I find Jerry Huxtable's Imaging code very useful:

    http://www.jhlabs.com/ip/index.html

    His LayoutManagers are very good too, in fact most of the stuff on his site can be described as very good.

    Posted by: uhf on July 20, 2004 at 08:50 PM

  • Scaling: up or down matters
    JAI as a subsample average function that works extremely well. It is actually a combo of a couple different atomic transforms, I think.

    Posted by: bwy on July 28, 2004 at 12:59 AM

  • Scaling: up or down matters
    The result of a scaling operation can depend very much on both the image you are scaling, the method you are using (BILINEAR, etc.), the direction of the scale, and the interpretation of the viewer. BICUBIC, for example, can be very subtly better than BILINEAR, but it depends entirely upon what you are trying to scale and what results you want whether the results you get are worth the extra computation involved in BICUBIC.

    The getScaledInstance() method above does go through a different code path. And the Area Averaging Filter is very different from either the BILINEAr or BICUBIC approach; it takes all pixels into account that are within the area of the scaled image. So, for example, if you downsize by 10x, each pixel in the resulting image will be the result of a 10x10 grid around that pixel. Note that this is much more expensive than the simple BILINEAR approach, which only looks at the neighboring pixels.

    It would be nice for you to use the operations in Graphics to perform scaling that suits your needs instead of using the old getScaledInstance() method. You might look into using Graphics.drawImage(BILINEAR) a couple of times for any sizable downscale; the cumulative effect of more than one biliear operation could provide similar effects to that of the AAF in getScaledInstance().

    Posted by: chet on August 12, 2004 at 11:38 AM

  • The Unix way..
    Using BeanShell for this type of script is also really neat - an interactive shell with full access to the java apis.

    Posted by: armando on August 26, 2004 at 06:51 AM

  • r u sure that ur JpegConverter.java is working fine it just simply change the extension,by just changinh tge extension to jpg from bmp u can't convert that image file,
    i never expecting that i found such a supid code here,
    i disappoint,after reading the code poor logic man.

    Posted by: panshow on May 18, 2006 at 02:00 AM

  • If you are too stupid to understand the code, don't criticise it. It does in fact reformat the image with the ImageIO.write() function... Just look at the second argument. Thank you Chet for the very helpful demos!

    Posted by: markdcc on December 03, 2006 at 02:25 PM

  • I like the examples of ImageIO but have one additional suggestion. You are using File.separator and some String concatenations in

    String scaledImagesDir = imagesDir + File.separator + "scaled";
    File subdir = new File(scaledImagesDir);

    Maybe you want to use the constructor File(String,String) or File(File,String) and code

    File subdir = new File(imagesDir, scaledImagesDir);

    - Christian Ullenboom

    Posted by: ullenboom on December 11, 2006 at 05:27 AM

  • What about the aspect ratio ?

    Posted by: aledes on April 11, 2007 at 05:10 AM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds