All entries for Monday 14 September 2009

September 14, 2009

Using subsample averaging instead of scaling in JAI to get better results

I was looking over this press release about using parallel computing with Xboxes this morning and was struck by just how rubbish the resized images look in it. We use JAI to do image resizing in pure Java in our CMS, and obviously it’s not coming out very well. We’re turning a high quality source image into a very low quality thumbnail.

I tried fiddling with the interpolation on our operation, from Bilinear to Bicubic or Nearest-Neighbour but nothing seemed to make a noticable difference. In the end, however, I stumbled upon this which suggested using Subsample Averaging instead of Scaling as the operation in JAI. Success!

// We have sourceSS, a SeekableStream, and an OutputStream, out

// Load the image from a source stream
RenderedOp source = JAI.create("stream", sourceSS);

// scale the image
float width = source.getWidth();
float height = source.getHeight();

// assume no resizing at first
double scale = 1;

// if the image is too wide, scale down
if (shouldResizeWidth(source, maxWidth)) {
    scale = maxWidth / width;

// if the image is too hight, scale down
// IF that makes it smaller than maxWidth has done already
if (shouldResizeHeight(source, maxHeight)) {
    float heightScale = maxHeight / height;
    if (heightScale < scale) {
scale = heightScale;

ParameterBlock params = new ParameterBlock();
params.add(scale);// x scale factor
params.add(scale);// y scale factor
params.add(0.0F);// x translate
params.add(0.0F);// y translate

Map<RenderingHints.Key, Object> map = Maps.newHashMap();
map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

RenderingHints hints = new RenderingHints(map);


// Here's the important bit - use "SubsampleAverage" instead of "scale" 
RenderedOp alteredImage = JAI.create("SubsampleAverage", params, hints);

ImageEncoder encoder;

switch (fileType) {
    case gif:
    case jpg:
        // now re-encode
        JPEGEncodeParam jpegEncodeParam = new JPEGEncodeParam();
        // who knows what all this could possibly mean ?
        jpegEncodeParam.setHorizontalSubsampling(0, 1);
        jpegEncodeParam.setHorizontalSubsampling(1, 2);
        jpegEncodeParam.setHorizontalSubsampling(2, 2);
        jpegEncodeParam.setVerticalSubsampling(0, 1);
        jpegEncodeParam.setVerticalSubsampling(1, 1);
        jpegEncodeParam.setVerticalSubsampling(2, 1);
        final int restartInterval = 64;

        // done messing with the image. Send the bytes to the
        // outputstream.
        encoder = ImageCodec.createImageEncoder("JPEG", out, jpegEncodeParam);

    case png:
        PNGEncodeParam.RGB pngEncodeParam = new PNGEncodeParam.RGB();
        encoder = ImageCodec.createImageEncoder("PNG", out, pngEncodeParam);
        throw new IllegalArgumentException("Unrecognised image");

PlanarImage planarImage = alteredImage.getRendering();

The difference is actually pretty startling.

Resized using “scale” with bicubic interpolation Resized using “SubsampleAverage”

<about />


I’m a Web Developer in e-lab, part of IT Services at the University of Warwick.

<input type="search" />

<ol id="recentComments">

  • First commit was around 12 months ago, but there was a long period where it was put on the back burn… by Mathew Mannion on this entry
  • So, how long did it take?! by Phil Wilson on this entry
  • Hi Matthew, having a problem putting favourites into folders, it only seems to let me put them in on… by Rupert Elder on this entry
  • I wrote one entry in Chinese. It published the content as lots of questions marks. by Hongfeng Sun on this entry
  • I'm sure there's a setting somewhere that'd filter it out. by Nick on this entry

<ol id="archive">


Am I still fat?

Not signed in
Sign in

Powered by BlogBuilder