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: 51 comments
First Previous 1 2 3 Next Last
andi
23-Apr-2009 22:36 GMT
lol the VALUE_ANTIALIAS_ON is only for text
Networker
14-Jun-2009 12:06 GMT
That's helpful but so far, there is no good solution for image resize in Java. I guess I have to go the server and resize it manually
Rudrajit Das
02-Jul-2009 05:56 GMT
This is a nice article i have modified this method as follows to work with images directly. This also works fine..

private Image scaleImage(Image p_image, int p_width, int p_height) {
Image final_image = null;

if (p_image != null || p_width > 0 || p_height > 0) {

try {

Image image = p_image;
final_image = null;
int thumbWidth = p_width;
int thumbHeight = p_height;

// Make sure the aspect ratio is maintained, so the image is not
// skewed
double thumbRatio = (double) thumbWidth / (double) thumbHeight;
int imageWidth = image.getWidth(null);
int imageHeight = image.getHeight(null);
double imageRatio = (double) imageWidth / (double) imageHeight;
if (thumbRatio < imageRatio) {
thumbHeight = (int) (thumbWidth / imageRatio);
} else {
thumbWidth = (int) (thumbHeight * imageRatio);
}

BufferedImage thumbImage = new BufferedImage(thumbWidth,
thumbHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = thumbImage.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, thumbWidth, thumbHeight, null);
g.dispose();
final_image = (Image) thumbImage;

} catch (Exception e) {

}

}

return final_image;
}

Remis B
09-Jul-2009 11:11 GMT
Article on java.net http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html is relly good, but would like to pay attention to anyone who going to use it, this method only does image scaling down. On scale up operation it goes to infinity loop. It's not hard to fix that, but still something nice to be warned.
ankit
05-Oct-2009 09:30 GMT
thanks u very much for this wonderful code..
it has really helped me a lot..
Indy
12-Oct-2009 11:35 GMT
thank you for the code... i tried it out, changed the resulting picture to be a JPG (not a png) but the resulting picture has a red tint. I tried out many things, but it doesn't get better... Can you help me with that?
Wayne
20-Nov-2009 21:54 GMT
yes, I got same issue with Indy. :(
Shinta
03-Dec-2009 03:19 GMT
I resized my jpg image using the code above, but when I tried to display it on the screen, it doesn't work. Here's my code:
public void showImage(String filePath,HttpServletResponse response){
ImageOutputStream imageOut = null;
BufferedImage image = null;
try {
//in = new FileInputStream(filePath);
image = ImageIO.read(new File(filePath));
imageOut = ImageIO.createImageOutputStream(response.getOutputStream());

if (imageOut != null){
if (filePath.endsWith(".jpg") || filePath.endsWith(".JPG")){
ImageIO.write(image,"jpg",imageOut);
} else if (filePath.endsWith(".bmp") || filePath.endsWith(".BMP")){
ImageIO.write(image,"bmp",imageOut);
} else {//tiff
ImageIO.write(image,"tiff",imageOut);
}
}
} catch (FileNotFoundException ffe) {
logger.error("FileNotFoundException when trying to create instream showImage() " + ffe);
} catch (IOException ioe) {
logger.error("IOException when trying to create instream showImage()) " + ioe);
} catch (Exception e){
logger.error("Exception when trying to create instream showImage()) " + e);
} finally{
try {
if(image != null){
image.flush();
}
if(imageOut != null){
imageOut.close();
}
} catch (Exception e) {
logger.error("Exception thrown in finally block " + e);
}
}
}

Any help will be appreciated. Thanks
haedlnal
16-Dec-2009 07:17 GMT
Very helpful. Thank you.
deep
21-Dec-2009 14:51 GMT
import java.awt.*;
public class BlurImage {


public image blur_image(image i, int radius) {

int count3=0;
int redarray[][] = new int[i.rows][i.columns];
int greenarray[][] = new int[i.rows][i.columns];
int bluearray[][] = new int[i.rows][i.columns];
int l=0,m=0,p=0,q=0;
float sum1 = 0, sum2 = 0, sum3 = 0;
int red[][] = new int[i.rows][i.columns];
int green[][] = new int[i.rows][i.columns];
int blue[][] = new int[i.rows][i.columns];
if(i.rows>> 16 ) & 0xFF;
System.out.println("red"+red[j][k]);

green[j][k] = ( i.data[j][k] >>> 8 ) & 0xFF;

System.out.println("green"+green[j][k]);

blue[j][k] = i.data[j][k] & 0xFF;

System.out.println("blue"+blue[j][k]);
}
}
for(int j=0;j=i.rows)
l = i.rows-1;
if(m>=i.columns)
m = i.columns-1;
p = j-radius;

q = k-radius;

if(p<0)
p=0;
if(q<0)
q=0;
for(int a=p;a<=l;a++)
{
for(int b=q;b<=m;b++)
{
++count3;

}
}


for(int y=p;y<=l;y++)
{
for(int z=q;z<=m;z++)
{
sum1 = sum1 + red[y][z];
sum2 = sum2 + green[y][z];
sum3 = sum3 + blue[y][z];

}
}
sum1 = sum1/count3;

System.out.println("sum1 "+sum1);
sum2 = sum2/count3;
System.out.println("sum2 "+sum2);
sum3 = sum3/count3;
System.out.println("sum3 "+sum3);
redarray[j][k] = (int)sum1;
greenarray[j][k] = (int)sum2;
bluearray[j][k] = (int)sum3;
System.out.println("red array = "+redarray[j][k]);
System.out.println("green array = "+greenarray[j][k]);
System.out.println("blue array = "+bluearray[j][k]);
count3 = 0;
sum1 = 0;
sum2 = 0;
sum3 = 0;
}
}
for(int j=0;j {
for(int k=0;k {
red[j][k] = redarray[j][k];
green[j][k] = greenarray[j][k];
blue[j][k] = bluearray[j][k];
System.out.println( red[j][k]);
System.out.println( green[j][k]);
System.out.println( blue[j][k]);
Color c = new Color(red[j][k],green[j][k],blue[j][k]);

String s = Integer.toHexString( c.getRGB() & 0x00ffffff );
System.out.println(s);





}
}
return i;

}

public static void main(String[] args) {
//TestCase 1
try {
image i = new image();
i.rows = 5;
i.columns = 3;
i.data = new int[][]{{6, 12, 18}, {5, 11, 17}, {4, 10, 16}, {3, 9, 15}, {2, 8, 14}};

BlurImage obj = new BlurImage();
image res = obj.blur_image(i, 2);
System.out.println("TestCase 1");
if (res != null) {
for (int k = 0; k < i.rows; k++) {
System.out.println();
for (int j = 0; j < i.columns; j++) {
System.out.print(res.data[k][j] + ",");
}
}
} else
System.out.println("Null");
}
catch (Exception e) {
e.printStackTrace();
}
//TestCase 2
try {
image i = new image();
i.rows = 3;
i.columns = 5;
i.data = new int[][]{{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038},
{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038},
{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038}};

BlurImage obj = new BlurImage();
image res = obj.blur_image(i, 1);
System.out.println(" TestCase 2");
if (res != null) {
for (int k = 0; k < i.rows; k++) {
System.out.println();
for (int j = 0; j < i.columns; j++) {
System.out.print("0x"+res.data[k][j] + ",");
}
}
} else
System.out.println("NULL");
}
catch (Exception e) {
e.printStackTrace();
}
//TestCase 3
try {
image i = new image();
i.rows = 3;
i.columns = 5;
i.data = new int[][]{{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038},
{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038},
{0x5a0060, 0x6a0050, 0x6a0050, 0x6a0050, 0x7f0038}};

BlurImage obj = new BlurImage();
image res = obj.blur_image(i, 4);
System.out.println(" TestCase 3");
if (res != null) {
for (int k = 0; k < i.rows; k++) {
System.out.println();
for (int j = 0; j < i.columns; j++) {
System.out.print(res.data[k][j] + ",");
}
}
} else
System.out.println("NULL");
}
catch (Exception e) {
e.printStackTrace();
}
}
}

please help in this code to get correct output for testcases as in the same procedure given in the code with the image class.
Anonymous
20-Jan-2010 00:10 GMT
In theory, to get better results, you have to use a Gaussian kernel. I suppose that there is no optimizations according to the kernel, then it could be better to use it.

It could be easily understand : With a Gaussian kernel, you don't give the same height to all kernel pixel, then you obtain a result more natural. To obtain more scientist informations => PSF (Point Spread Function) Gaussian
Rafael Sanches
02-Mar-2010 09:21 GMT
Valeu, excelente artigo. vou botar na minha app android. Acho que esse romain guy 'e um dos mentores do projeto android hehe
McTrafik
07-Apr-2010 22:33 GMT
Hey there... I was actually working on resizing images myself, and frankly, I don't see why all this complication is necessary.

For my purposes I needed down-sized square images, but I still think you might find my algorithm good. It's a simple matter of down-sampling with 2x anti-aliasing.

Aside from simple array operations, only one buffered image is created with a couple of calls to it. No Graphics2D objects or anything like that. Works really fast on my old laptop.
/*************************************/
int size = 24;
String location = "hugo.jpg";

File file = new File(location);
BufferedImage img = ImageIO.read(file);
width = img.getWidth();
height = img.getHeight();
data = new int[width * height];
img.getRGB(0,0, width, height, data, 0, width);

int[] resized = new int[size * size];
float ratio = (width > height) ? (float) height / size : (float) width / size;
float hr = ratio / 2;
int i, j, k, l, m;
for (int x = 0; x > 2) & m;
}
}
}

BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
img.setRGB(0, 0, size, size, resized, 0, size);
ImageIO.write(img, "jpg", file);
McTrafik
07-Apr-2010 22:36 GMT
Hm.. the code didn't paste right:

int size = 24;
String location = "hugo.jpg";

File file = new File(location);
BufferedImage img = ImageIO.read(file);
width = img.getWidth();
height = img.getHeight();
data = new int[width * height];
img.getRGB(0,0, width, height, data, 0, width);

int[] resized = new int[size * size];
float ratio = (width > height) ? (float) height / size : (float) width / size;
float hr = ratio / 2;
int i, j, k, l, m;
for (int x = 0; x > 2) & m;
}
}
}

BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
img.setRGB(0, 0, size, size, resized, 0, size);
ImageIO.write(img, "jpg", file);
McTrafik
07-Apr-2010 22:38 GMT
@Whoever runs this joint
This comment box doesn't know how to handle LESS THAN sign...
McTrafik
07-Apr-2010 22:39 GMT
Trying to put the code here one more time :D

int size = 24;
String location = "hugo.jpg";

File file = new File(location);
BufferedImage img = ImageIO.read(file);
width = img.getWidth();
height = img.getHeight();
data = new int[width * height];
img.getRGB(0,0, width, height, data, 0, width);

int[] resized = new int[size * size];
float ratio = (width > height) ? (float) height / size : (float) width / size;
float hr = ratio / 2;
int i, j, k, l, m;
for (int x = 0; x < size; x ++) {
for (int y = 0; y < size; y ++) {
i = (int) (x * ratio);
j = (int) (y * ratio);
k = (int) (i + hr);
l = (int) (j + hr);
for (int p = 0; p < 3; p ++) {
m = 0xFF << (p * 8);
resized[x + y * size] |= (
(data[i + j * width] & m) +
(data[k + j * width] & m) +
(data[i + l * width] & m) +
(data[k + l * width] & m) >> 2) & m;
}
}
}

BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
img.setRGB(0, 0, size, size, resized, 0, size);
ImageIO.write(img, "jpg", file);
123
21-Apr-2010 05:37 GMT
4123
Ilya
11-Jun-2010 12:25 GMT
Thank you very much! :)
Steve
21-Jun-2010 14:53 GMT
Thanks, appreciate the code!
Mitul
17-Sep-2010 12:33 GMT
Your trick is working but it is not giving the desired output as user wants , we are not getting the exact replica of our image if we do not consider it size width and height.But Thanks Any Way...
First Previous 1 2 3 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.
94 + 0 =