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.addSource(source);
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_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
map.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
RenderingHints hints = new RenderingHints(map);
params.add(interpolation);
// 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();
jpegEncodeParam.setQuality(DEFAULT_SAMPLING_QUALITY);
// 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;
jpegEncodeParam.setRestartInterval(restartInterval);
// done messing with the image. Send the bytes to the
// outputstream.
encoder = ImageCodec.createImageEncoder("JPEG", out, jpegEncodeParam);
break;
case png:
PNGEncodeParam.RGB pngEncodeParam = new PNGEncodeParam.RGB();
encoder = ImageCodec.createImageEncoder("PNG", out, pngEncodeParam);
break;
default:
throw new IllegalArgumentException("Unrecognised image");
}
PlanarImage planarImage = alteredImage.getRendering();
encoder.encode(planarImage);
The difference is actually pretty startling.
![]() |
![]() |
Resized using “scale” with bicubic interpolation | Resized using “SubsampleAverage” |