High-Quality Image Resize with Java

Learn a trick to shrink images using the Java2D API, which creates high-quality image icons for your applications.
24-Feb-2008 18:36 GMT

Image resize with java isn't a new topic. What I want to explain here is a technique to create small high-quality images using the Java2D API, since I couldn't find any reasonable solution on the Internet. The are some solutions available - like this one - but the final quality is low.

You can use this approach for different purposes, but the best example I can think of is the implementation of small avatars on web applications (see Nabble, DZone, etc.) or desktop software (e.g., Skype, MSN, etc.).

In order to show the contribution of this technique, I have to explain image resize from the beginning. We will improve the algorithm as we go and I also assume you have some background with Java2D.

Reading and Writing the Image

Since this is a simple example, I will read an image from the file system, resize it and save it back. The image I am using is below. Note that I am using a JPG file, which is the common format for pictures.

To read this image, we can use the ImageIO class:

BufferedImage image = ImageIO.read(new File("c:\picture.jpg"));

First Attempt

Now we have to find a way to resize this image. Our first attempt is to use the drawImage() method of the Graphics interface:

private static BufferedImage resize(BufferedImage image, int width, int height) {
BufferedImage resizedImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = resizedImage.createGraphics();
g.drawImage(image, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}

With this method, we can create a 24x24 avatar by calling:

Image resized = resize(image, 24, 24);

The last step is to save the image so that we can see the results. The ImageIO class can do this job:

ImageIO.write(resized, "png", new File("c:\picture1.png"));

Note that the second parameter is the format of the saved image. I used "png" because it is simpler. If you want to save it as JPG, you will have to use the JPEGImageEncoder class (and the result is the same, trust me).

The result of this method is a low-quality image:

Pay attention to the details of the image (look closer). There is no anti-alias and the face is distorted. Would you add such algorithm to your web application? I don't think so. Users could get scared by looking at those images.

Second Attempt

We noticed that the main problem of the resize() method above is the fact that it doesn't have anti-aliasing. So we could use the Rendering Hints of the Java2D API as an attempt to solve that problem:

private static BufferedImage resize(BufferedImage image, int width, int height) {
int type = image.getType() == 0? BufferedImage.TYPE_INT_ARGB : image.getType();
BufferedImage resizedImage = new BufferedImage(width, height, type);
Graphics2D g = resizedImage.createGraphics();
g.setComposite(AlphaComposite.Src);

g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);

g.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

g.drawImage(image, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}

The result is below (right image). If you pay attention to the details, it is somewhat better, but still a crap:

The Trick

Now I will explain how to improve the quality of this image. Basically, the trick is to blur the image before resizing. The code that blurs an image in java is:

public static BufferedImage blurImage(BufferedImage image) {
float ninth = 1.0f/9.0f;
float[] blurKernel = {
ninth, ninth, ninth,
ninth, ninth, ninth,
ninth, ninth, ninth
};

Map map = new HashMap();

map.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);

map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

RenderingHints hints = new RenderingHints(map);
BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, hints);
return op.filter(image, null);
}

Since the original image is in the JPG format, we can't blur it directly. We have to create a compatible image first:

private static BufferedImage createCompatibleImage(BufferedImage image) {
GraphicsConfiguration gc = BufferedImageGraphicsConfig.getConfig(image);
int w = image.getWidth();
int h = image.getHeight();
BufferedImage result = gc.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
Graphics2D g2 = result.createGraphics();
g2.drawRenderedImage(image, null);
g2.dispose();
return result;
}

Blurring the image before resizing it isn't the only trick. You have to do this when the width or height of the image is 100px. Since small avatars are usually a square (width = height), you can resize it to 100x100, call the blur() method and resize again to 24x24:

private static BufferedImage resizeTrick(BufferedImage image, int width, int height) {
image = createCompatibleImage(image);
image = resize(image, 100, 100);
image = blurImage(image);
image = resize(image, width, height);
return image;
}

(the resize() method is the one described in the second attempt)

Now you can compare the three results:

You have to look closer, but the last result is much better than the others. Now you can let users upload avatars on your java applications!

Conclusion

This was my contribution to the tough problem of shrinking images using the Java2D API. I would like to remember that this isn't the ultimate algorithm to achieve that goal, but it is good enough considering its simplicity. If you have comments or feedback, I would like to hear them.

The source code of the example is available here.


Stumble it!      Dzone.com      Digg.com      Reddit.com      Del.icio.us

Comments

Total: 39 comments
1 2 Next Last
Stefan Krause
24-Feb-2008 09:00 GMT
Okay, but why not use a real high quality (and fast) algorithm?
http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
Hugo Teixeira
24-Feb-2008 10:30 GMT
@Stefan Krause
Hi Stefan, thanks for commenting.
This approach is good because it is simple.
I don't use any fancy algorithm that could impact on performance (like the one you suggested, which seems to be the right direction).
For web servers that handle millions of requests every day, a simple approach like this might fit well.
I am aware that this isn't a perfect solution, but the performance is good and the final image is nice for most cases.
So why not consider it?
Regards,
Hugo Teixeira
marcelo camanho
25-Feb-2008 13:30 GMT
nice trick!
Romain Guy
26-Feb-2008 18:41 GMT
This is an interesting approach but unfortunately flawed. First of all you setup a bunch of useless rendering hints (like antialiasing). But the real problem is that a blur requires more operations than a bilinear resize. An easy and fast way to produce good looking thumbnails is to scale the image by half its size, using BILINEAR, until you reach the final size you want.

I won't go further into details but I highly recommend you to read Chris Campbell's article on the matter (that Stefan already posted): http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html

The algorithm proposed in Chris' article is *not* fancy at all. And it's pretty fast. Chet and I timed the various approaches and discussed them at length in our book Filthy Rich Client and in a couple of JavaOne presentations and the progressive bilinear approach is very very fast.
Hugo Teixeira
26-Feb-2008 20:41 GMT
@Romain Guy
Hi Romain, thanks for your nice feedback.
I agree with your comments and your expertise is very important at this moment.
I just want to clarify some points that might have been misunderstood:

1 - The rendering hints in the resize() method aren't that useless. They do produce better results (compared to the same method without them), although not as good as a high-quality resize (the hints in the blur() method might be useless though).

2 - You are right in the sense that a blur requires more operations than a bilinear resize, but I don't run the blur in the original image. First I reduce the size to height/width = 100 and then I run the blur. This is a completely different situation and creates a whole new scenario for performance analysis. So the question is: Is a blur in a small image faster than having a bilinear resize run several times? The fact is, we don't know the answer and it depends on the situation. So "flawed" might be a strong word in this sense.

Anyway, I am not fully concerned about the fate of this approach since we have a correct way of doing things. But I have to admit that it was nice to develop it and funny at the same time due to the unusual implementation.

Best regards,
Hugo Teixeira
Fred
29-Feb-2008 10:25 GMT
Thanks a lot, your code works very well.

the best time i've got is 31ms for an 5k image, it
is pretty good for a dev machine
(pentuim IV 2.4Ghz 1.5G ram Tomcat Eclipse Postgres launched)

Regards
FM
Sreedhar
07-Mar-2008 11:32 GMT
Very nice. I will stop my search here and thank you.......
Tom
20-Mar-2008 16:33 GMT
Thanks for your approach. The resize works fine for me with a .jpg, but not createCompatibleImage(...). When i write the picture, i have a black picture, with the right dimension. i don't understand why. Have you got an idea ?
Cory von Wallenstein
31-Mar-2008 22:04 GMT
Hugo,

Thanks a ton for this article. I've been wrestling with image quality for resizing JPEG's in Java for a bit now, and your trick helped a ton.

I documented my results here:

http://www.byfolio.com/roller/blog/entry/hugo_saves_the_day_for

Thanks!

Cory
vpetreski
30-Apr-2008 12:36 GMT
Try this ;)

http://www.comesolvego.com/2008/04/29/resize-images-with-java-high-quality-and-working-solution/
rsk
24-May-2008 12:10 GMT
guys... have been strugling a lot. need to resize the image since the source of image is remote place. i dont know the size of incoming image and sadly i have limited space on my swing app to display it... best solution to resize it is ?? heeeeeeeeeeelp
Ashara Shrestha(Nepal)
04-Jun-2008 05:04 GMT
hey, this is simple and the best. i have been trying to search this for a long time. Finaly I got it. Thanx a lot.
vkboss
15-Jul-2008 22:08 GMT
This solution worked pretty well for me.
thx a lot
KAMEL HADDAD
17-Jul-2008 10:21 GMT



I test the giving code but it give a bad quality, i founded an other java code can give you a more better quality




package com.diwansoft.resizer;

import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import javax.swing.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.Kernel;
import java.awt.image.ConvolveOp;

public class ImageUtil {

public static void resize(File originalFile, File resizedFile, int newWidth, float quality) throws IOException {

if (quality 1) {
throw new IllegalArgumentException("Quality has to be between 0 and 1");
}

ImageIcon ii = new ImageIcon(originalFile.getCanonicalPath());
Image i = ii.getImage();
Image resizedImage = null;

int iWidth = i.getWidth(null);
int iHeight = i.getHeight(null);

if (iWidth > iHeight) {
resizedImage = i.getScaledInstance(newWidth, (newWidth * iHeight) / iWidth, Image.SCALE_SMOOTH);
} else {
resizedImage = i.getScaledInstance((newWidth * iWidth) / iHeight, newWidth, Image.SCALE_SMOOTH);
}

// This code ensures that all the pixels in the image are loaded.
Image temp = new ImageIcon(resizedImage).getImage();

// Create the buffered image.
BufferedImage bufferedImage = new BufferedImage(temp.getWidth(null), temp.getHeight(null),
BufferedImage.TYPE_INT_RGB);

// Copy image to buffered image.
Graphics g = bufferedImage.createGraphics();

// Clear background and paint the image.
g.setColor(Color.white);
g.fillRect(0, 0, temp.getWidth(null), temp.getHeight(null));
g.drawImage(temp, 0, 0, null);
g.dispose();

// Soften.
float softenFactor = 0.05f;
float[] softenArray = {0, softenFactor, 0, softenFactor, 1-(softenFactor*4), softenFactor, 0, softenFactor, 0};
Kernel kernel = new Kernel(3, 3, softenArray);
ConvolveOp cOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
bufferedImage = cOp.filter(bufferedImage, null);

// Write the jpeg to a file.
FileOutputStream out = new FileOutputStream(resizedFile);

// Encodes image as a JPEG data stream
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);

JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bufferedImage);

param.setQuality(quality, true);

encoder.setJPEGEncodeParam(param);
encoder.encode(bufferedImage);
}

// Example usage
public static void main(String[] args) throws IOException {
File originalImage = new File("C:\Documents and Settings\Kamal Haddad\workspace\Flex\WebContent\projects\test\snapshots\fullsize\1.jpeg");
resize(originalImage, new File("C:\Documents and Settings\Kamal Haddad\workspace\Flex\WebContent\projects\test\snapshots\fullsize\1-7.jpeg"), 400, 0.7f);
resize(originalImage, new File("C:\Documents and Settings\Kamal Haddad\workspace\Flex\WebContent\projects\test\snapshots\fullsize\1-1.jpeg"), 400, 1f);
}

}
Michael
30-Sep-2008 20:50 GMT
Hi
Can you not simply use the 'getScaledInstance' method of the Image (extended to BufferedImage) class. You can select different quality levels.. e.g. Image.SCALE_SMOOTH. I don't think it's the fastest but I read somewhere it is high quality output? I'd be interested in what you think about this method.
Ray
05-Nov-2008 17:36 GMT
I have another suggestion for you, use SVG. You can use the batik libraries, or svgSalamander - a very lightweight SVG rendering Java component. What's even cooler is you can also animate the image with the SVG animation tools.
Jamie
12-Nov-2008 19:36 GMT
@Michael
Hi Michael,
The Image.getScaledInstance is VERY slow.. I was using that initially and was disappointed. The quality is good, but because it's done all in software it is very slow.

I finally decided upon:
org.jdesktop.swingx.graphics.GraphicUtilities
and the GraphicUtilities.createThumbnail() method.
It is part of the SwingX download from http://swinglabs.org/

It uses multiple intermediate bi-linear resizes from a managed bufferedimage's drawImage to resize in steps to preserve image quality.


See the following bug report with good comments about the getScaledImage() method http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6196792
Devella
04-Feb-2009 23:01 GMT
I have developed a library called ThumbMaster which is specifically targeted at Java programmers who need to resize images with JAI -- and get high quality results. It is implemented as a new JAI interpolation, and so the standard built-in operators (and their native acceleration) can all be used. Image quality and file size are both nearly identical to the results of ImageMagick, as they are both based heavily on the same image processing algorithms.

For more information and a trial download, visit http://www.devella.net/thumbmaster

guya
12-Apr-2009 13:39 GMT
this solution is very helpful
thank you
Nathan
22-Apr-2009 15:00 GMT
Very helpful. Thank you.
1 2 Next Last

Name

Enter your full name.

Your Comment

Please keep your comment relevant to the subject of the story.
HTML tags are not allowed. Use [b] and [/b] for bold text.
14 + 5 =