Marc, himself, his blogs, and you reading them.

November 30, 2005
My Ugly Hack at Java Image Resizing.

Ok, so I have done quite some Java, but most of that has been quite exclusively been pulled around the technical gravity centers Web, XML and derivatives... together with some more generic interest in sound OO design and patterns.

And yeah, I've been doing some image processing with The Gimp, which offers me some early understanding of the underlaying colormodels, transparency, unmask sharpness, feathers and antialliasing...

But when my home project to get a picture-album for daisy includes programmatic image resizing in Java I'm pretty much left to whatever the GoogleHarvest will bring me...

capetown-sa.jpg

When starting-off with the standard code-samples rescaling an image from 1600px width to 200px (still quite big for a thumbnail) I wasn't quite pleased with the result of the built in interpollation (even when set to 'bicubic' proclaimed as ensuring the better quality)

    public static BufferedImage scale(double scale, BufferedImage srcImg)
    {
        RenderingHints hints = new RenderingHints(null);
        hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
//        hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        
        BufferedImage target = new BufferedImage((int)(srcImg.getWidth()*scale),
                                                                      (int)(srcImg.getHeight()*scale), 
                                                                      BufferedImage.TYPE_INT_RGB);
        
        Graphics2D g = target.createGraphics();
        g.drawImage(srcImg, null, null);
         
        AffineTransform affineTransform = AffineTransform.getScaleInstance(scale, scale);
         
        g.setRenderingHints(hints);
        g.drawRenderedImage(srcImg, affineTransform);
        
        g.dispose();
        return target;
    }

More GoogleHunting showed me I wasn't alone in my frustration, but also revealed the best way to get better results was just to use the imagemagick 'convert' behind the scenes through some Runtime.exec(). Trying that out, and seeing the better quality (but slower processing) I was only left to believe the Java libs (Java2D or JAI for that mather) must be able to do the job as nicely...

j2d-capetown-sa-200.jpg

Digging in again I allowed to help myself with the more dirty groundwork.... and Hohoho, Lo and Behold: what do you think of this 2nd result? I find it far more acceptable then the first for sure.

But don't jump up and down yet though, since here is my hack:

        double fScale = Math.min(scaleX, scaleY);
        
        // hack to go into things step by step, 
        // slows down but avoids bad quality from doing all in one step
        while (fScale < 0.5f)
        {
            imgSrc = scale(0.5f, imgSrc);
            fScale *= 2;
        }
        
        return scale(fScale, imgSrc);

So, I'm puzzled. And not sure what is ugglier (the first image result or the latter code) The recursive re-size in steps hack isn't one I like since it's far more costly then to do it in one sweap (but not really worse then using imagemagick for the scale I need to cover here). Also, I would suspect every resize operation to degrade quality a bit, which only gets accumulated through this way of working. However a simple visual check on the resulting image kinda shows the opposite effect on actual image quality, so I'm really left to wonder how exactly the interpollation algorithms are sampling the image in the first place. (I really think they are sampling right next to the nearest point, rather then in the middle of the various nearest points, but would need to build me a good sample test image to assess that point) Any expert out there that can point out where I or the Java libs are missing the point or someone that has the ready made interpollation test-images up somewhere?

capetown-sa-thumbsmooth.jpg

update: Those in search of a solution should dig down to the comment from Andrey Akselrod. That gave me this final nice looking result without any awkward hack. Many thanks for sharing this, Andrey.

# Posted by mpo at 08:08 AM | TrackBack
Comments

We had a discussion about this topic a few weeks ago (http://www.jsp-develop.de/forum/view/37632/ [in German]) and came to the same conclusion: Either use ImageMagick or scale the image down in small steps.
So at least, you are not alone with this problem ;-)

Posted by: Michael at November 30, 2005 01:21 PM

I was in a similar position not so long ago. I had screeds of Java2D code that was ugly as heck and (thus) awkward to modify when I needed to add in image rotations. JAI saved the day. It performs as quick as, or quicker than Java2D (with or without the native libraries installed), and provides much more sensible image scaling quality. Plus it does it in a matter of 2-3 lines of code, instead of all that Graphics2D nonsense. Any time you put into figuring out JAI is well worth it IMO.

(only downside is that JAI needs a LOT more memory to get its job done - something about tiles/tile caches. I didn't have time to figure it out so just gave my Java process more memory)

Posted by: Richard at November 30, 2005 02:16 PM


from http://java.sun.com/j2se/1.5.0/docs/guide/2d/new_features.html#bi

The 2D implementation now supports bicubic interpolation and uses it whenever requested. Previously, the VALUE_INTERPOLATION_BICUBIC hint defined by the RenderingHints class wasn't honored, and bilinear interpolation was used instead.

were you using 1.5 or 1.4 ?

Posted by: johnt at November 30, 2005 04:27 PM

(@Johnt) I've just ran my test again, now based on JDK 1.5 and helas: same bad results. Thx for the suggestion though...

(@Richard) Sorry for not being clear in my original posting: I _did_ do the effort of getting into JAI and use that, but again, with similar awkward-looking results. (yeah, even on java 1.5 now)

Posted by: -marc= at December 2, 2005 05:13 PM

Try the folowing code:


public static void createThumbnail(File original, File resized, int maxSize) throws IOException, RTException
{
Image i = ImageIO.read(original);

Image resizedImage = null;
int iWidth = i.getWidth(null);
int iHeight = i.getHeight(null);
if (iWidth > iHeight)
resizedImage = i.getScaledInstance(maxSize,(maxSize*iHeight)/iWidth,Image.SCALE_SMOOTH);
else
resizedImage = i.getScaledInstance((maxSize*iWidth)/iHeight,maxSize,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.01f;
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);

// suffix of resized should always be jpg
String resizedpath = resized.getPath();
if (!resizedpath.endsWith("." + THUMBEXT))
{
resizedpath += "." + THUMBEXT;
resized = new File(resizedpath);
}

// write the jpeg to a file
ImageIO.write(bufferedImage, THUMBEXT, resized);
}

Posted by: Andrey Akselrod at December 2, 2005 10:50 PM
Post a comment









Remember personal info?





Please enter the security code you see here