com.twelvemonkeys.image.ResampleOp Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
*******************************************************************************
*
* Based on example code found in Graphics Gems III, Filtered Image Rescaling
* (filter_rcg.c), available from http://www.acm.org/tog/GraphicsGems/.
*
* Public Domain 1991 by Dale Schumacher. Mods by Ray Gardener
*
* Original by Dale Schumacher (fzoom)
*
* Additional changes by Ray Gardener, Daylon Graphics Ltd.
* December 4, 1999
*
*******************************************************************************
*
* Aditional changes inspired by ImageMagick's resize.c.
*
*******************************************************************************
*
* Java port and additional changes/bugfixes by Harald Kuhr, Twelvemonkeys.
* February 20, 2006
*
*******************************************************************************
*/
package com.twelvemonkeys.image;
import com.twelvemonkeys.lang.SystemUtil;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
/**
* Resamples (scales) a {@code BufferedImage} to a new width and height, using
* high performance and high quality algorithms.
* Several different interpolation algorithms may be specifed in the
* constructor, either using the
* filter type constants, or one of the
* {@code RendereingHints}.
*
* For fastest results, use {@link #FILTER_POINT} or {@link #FILTER_BOX}.
* In most cases, {@link #FILTER_TRIANGLE} will produce acceptable results, while
* being relatively fast.
* For higher quality output, use more sophisticated interpolation algorithms,
* like {@link #FILTER_MITCHELL} or {@link #FILTER_LANCZOS}.
*
* Example:
*
* BufferedImage image;
*
* //...
*
* ResampleOp resampler = new ResampleOp(100, 100, ResampleOp.FILTER_TRIANGLE);
* BufferedImage thumbnail = resampler.filter(image, null);
*
*
* If your imput image is very large, it's possible to first resample using the
* very fast {@code FILTER_POINT} algorithm, then resample to the wanted size,
* using a higher quality algorithm:
*
* BufferedImage verylLarge;
*
* //...
*
* int w = 300;
* int h = 200;
*
* BufferedImage temp = new ResampleOp(w * 2, h * 2, FILTER_POINT).filter(verylLarge, null);
*
* BufferedImage scaled = new ResampleOp(w, h).filter(temp, null);
*
*
* For maximum performance, this class will use native code, through
* JMagick, when available.
* Otherwise, the class will silently fall back to pure Java mode.
* Native code may be disabled globally, by setting the system property
* {@code com.twelvemonkeys.image.accel} to {@code false}.
* To allow debug of the native code, set the system property
* {@code com.twelvemonkeys.image.magick.debug} to {@code true}.
*
* This {@code BufferedImageOp} is based on C example code found in
* Graphics Gems III,
* Filtered Image Rescaling, by Dale Schumacher (with additional improvments by
* Ray Gardener).
* Additional changes are inspired by
* ImageMagick and
* Marco Schmidt's Java Imaging Utilities
* (which are also adaptions of the same original code from Graphics Gems III).
*
* For a description of the various interpolation algorithms, see
* General Filtered Image Rescaling in Graphics Gems III,
* Academic Press, 1994.
*
* @author Harald Kuhr
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ResampleOp.java#1 $
* @see #ResampleOp(int,int,int)
* @see #ResampleOp(int,int,java.awt.RenderingHints)
* @see BufferedImage
* @see RenderingHints
* @see AffineTransformOp
*/
// TODO: Consider using AffineTransformOp for more operations!?
public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ {
// NOTE: These MUST correspond to ImageMagick filter types, for the
// MagickAccelerator to work consistently (see magick.FilterType).
/**
* Undefined interpolation, filter method will use default filter
*/
public final static int FILTER_UNDEFINED = 0;
/**
* Point interpolation (also known as "nearest neighbour").
* Very fast, but low quality
* (similar to {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR}
* and {@link Image#SCALE_REPLICATE}).
*/
public final static int FILTER_POINT = 1;
/**
* Box interpolation. Fast, but low quality.
*/
public final static int FILTER_BOX = 2;
/**
* Triangle interpolation (also known as "linear" or "bilinear").
* Quite fast, with acceptable quality
* (similar to {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} and
* {@link Image#SCALE_AREA_AVERAGING}).
*/
public final static int FILTER_TRIANGLE = 3;
/**
* Hermite interpolation.
*/
public final static int FILTER_HERMITE = 4;
/**
* Hanning interpolation.
*/
public final static int FILTER_HANNING = 5;
/**
* Hamming interpolation.
*/
public final static int FILTER_HAMMING = 6;
/**
* Blackman interpolation..
*/
public final static int FILTER_BLACKMAN = 7;
/**
* Gaussian interpolation.
*/
public final static int FILTER_GAUSSIAN = 8;
/**
* Quadratic interpolation.
*/
public final static int FILTER_QUADRATIC = 9;
/**
* Cubic interpolation.
*/
public final static int FILTER_CUBIC = 10;
/**
* Catrom interpolation.
*/
public final static int FILTER_CATROM = 11;
/**
* Mitchell interpolation. High quality.
*/
public final static int FILTER_MITCHELL = 12;// IM default scale with palette or alpha, or scale up
/**
* Lanczos interpolation. High quality.
*/
public final static int FILTER_LANCZOS = 13;// IM default
/**
* Blackman-Bessel interpolation. High quality.
*/
public final static int FILTER_BLACKMAN_BESSEL = 14;
/**
* Blackman-Sinc interpolation. High quality.
*/
public final static int FILTER_BLACKMAN_SINC = 15;
/**
* RenderingHints.Key specifying resampling interpolation algorithm.
*/
public final static RenderingHints.Key KEY_RESAMPLE_INTERPOLATION = new Key("ResampleInterpolation");
/**
* @see #FILTER_POINT
*/
public final static Object VALUE_INTERPOLATION_POINT =
new Value(KEY_RESAMPLE_INTERPOLATION, "Point", FILTER_POINT);
/**
* @see #FILTER_BOX
*/
public final static Object VALUE_INTERPOLATION_BOX =
new Value(KEY_RESAMPLE_INTERPOLATION, "Box", FILTER_BOX);
/**
* @see #FILTER_TRIANGLE
*/
public final static Object VALUE_INTERPOLATION_TRIANGLE =
new Value(KEY_RESAMPLE_INTERPOLATION, "Triangle", FILTER_TRIANGLE);
/**
* @see #FILTER_HERMITE
*/
public final static Object VALUE_INTERPOLATION_HERMITE =
new Value(KEY_RESAMPLE_INTERPOLATION, "Hermite", FILTER_HERMITE);
/**
* @see #FILTER_HANNING
*/
public final static Object VALUE_INTERPOLATION_HANNING =
new Value(KEY_RESAMPLE_INTERPOLATION, "Hanning", FILTER_HANNING);
/**
* @see #FILTER_HAMMING
*/
public final static Object VALUE_INTERPOLATION_HAMMING =
new Value(KEY_RESAMPLE_INTERPOLATION, "Hamming", FILTER_HAMMING);
/**
* @see #FILTER_BLACKMAN
*/
public final static Object VALUE_INTERPOLATION_BLACKMAN =
new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman", FILTER_BLACKMAN);
/**
* @see #FILTER_GAUSSIAN
*/
public final static Object VALUE_INTERPOLATION_GAUSSIAN =
new Value(KEY_RESAMPLE_INTERPOLATION, "Gaussian", FILTER_GAUSSIAN);
/**
* @see #FILTER_QUADRATIC
*/
public final static Object VALUE_INTERPOLATION_QUADRATIC =
new Value(KEY_RESAMPLE_INTERPOLATION, "Quadratic", FILTER_QUADRATIC);
/**
* @see #FILTER_CUBIC
*/
public final static Object VALUE_INTERPOLATION_CUBIC =
new Value(KEY_RESAMPLE_INTERPOLATION, "Cubic", FILTER_CUBIC);
/**
* @see #FILTER_CATROM
*/
public final static Object VALUE_INTERPOLATION_CATROM =
new Value(KEY_RESAMPLE_INTERPOLATION, "Catrom", FILTER_CATROM);
/**
* @see #FILTER_MITCHELL
*/
public final static Object VALUE_INTERPOLATION_MITCHELL =
new Value(KEY_RESAMPLE_INTERPOLATION, "Mitchell", FILTER_MITCHELL);
/**
* @see #FILTER_LANCZOS
*/
public final static Object VALUE_INTERPOLATION_LANCZOS =
new Value(KEY_RESAMPLE_INTERPOLATION, "Lanczos", FILTER_LANCZOS);
/**
* @see #FILTER_BLACKMAN_BESSEL
*/
public final static Object VALUE_INTERPOLATION_BLACKMAN_BESSEL =
new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Bessel", FILTER_BLACKMAN_BESSEL);
/**
* @see #FILTER_BLACKMAN_SINC
*/
public final static Object VALUE_INTERPOLATION_BLACKMAN_SINC =
new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Sinc", FILTER_BLACKMAN_SINC);
// Member variables
// Package access, to allow access from MagickAccelerator
int mWidth;
int mHeight;
int mFilterType;
private static final boolean TRANSFORM_OP_BICUBIC_SUPPORT = SystemUtil.isFieldAvailable(AffineTransformOp.class.getName(), "TYPE_BICUBIC");
/**
* RendereingHints.Key implementation, works only with Value values.
*/
// TODO: Move to abstract class AbstractBufferedImageOp?
static class Key extends RenderingHints.Key {
static int sIndex = 10000;
private final String mName;
public Key(final String pName) {
super(sIndex++);
mName = pName;
}
public boolean isCompatibleValue(Object pValue) {
return pValue instanceof Value && ((Value) pValue).isCompatibleKey(this);
}
public String toString() {
return mName;
}
}
/**
* RenderingHints value implementaion, works with Key keys.
*/
// TODO: Extract abstract Value class, and move to AbstractBufferedImageOp
static final class Value {
final private RenderingHints.Key mKey;
final private String mName;
final private int mType;
public Value(final RenderingHints.Key pKey, final String pName, final int pType) {
mKey = pKey;
mName = pName;
validateFilterType(pType);
mType = pType;// TODO: test for duplicates
}
public boolean isCompatibleKey(Key pKey) {
return pKey == mKey;
}
public int getFilterType() {
return mType;
}
public String toString() {
return mName;
}
}
/**
* Creates a {@code ResampleOp} that will resample input images to the
* given width and height, using the default interpolation filter.
*
* @param pWidth width of the resampled image
* @param pHeight height of the resampled image
*/
public ResampleOp(int pWidth, int pHeight) {
this(pWidth, pHeight, FILTER_UNDEFINED);
}
/**
* Creates a {@code ResampleOp} that will resample input images to the
* given width and height, using the interpolation filter specified by
* the given hints.
* If using {@code RenderingHints}, the hints are mapped as follows:
*
* - {@code KEY_RESAMPLE_INTERPOLATION} takes precedence over any
* standard {@code java.awt} hints, and dictates interpolation
* directly, see
* {@code RenderingHints} constants.
*
* - {@code KEY_INTERPOLATION} takes precedence over other hints.
*
* - {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR} specifies
* {@code FILTER_POINT}
* - {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} specifies
* {@code FILTER_TRIANGLE}
* - {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} specifies
* {@code FILTER_QUADRATIC}
*
*
*
* - {@code KEY_RENDERING} or {@code KEY_COLOR_RENDERING}
*
* - {@link RenderingHints#VALUE_RENDER_SPEED} specifies
* {@code FILTER_POINT}
* - {@link RenderingHints#VALUE_RENDER_QUALITY} specifies
* {@code FILTER_MITCHELL}
*
*
*
* Other hints have no effect on this filter.
*
* @param pWidth width of the resampled image
* @param pHeight height of the resampled image
* @param pHints rendering hints, affecting interpolation algorithm
* @see #KEY_RESAMPLE_INTERPOLATION
* @see RenderingHints#KEY_INTERPOLATION
* @see RenderingHints#KEY_RENDERING
* @see RenderingHints#KEY_COLOR_RENDERING
*/
public ResampleOp(int pWidth, int pHeight, RenderingHints pHints) {
this(pWidth, pHeight, getFilterType(pHints));
}
/**
* Creates a {@code ResampleOp} that will resample input images to the
* given width and height, using the given interpolation filter.
*
* @param pWidth width of the resampled image
* @param pHeight height of the resampled image
* @param pFilterType interpolation filter algorithm
* @see filter type constants
*/
public ResampleOp(int pWidth, int pHeight, int pFilterType) {
if (pWidth <= 0 || pHeight <= 0) {
// NOTE: w/h == 0 makes the Magick DLL crash and the JVM dies.. :-P
throw new IllegalArgumentException("width and height must be positive");
}
mWidth = pWidth;
mHeight = pHeight;
validateFilterType(pFilterType);
mFilterType = pFilterType;
}
private static void validateFilterType(int pFilterType) {
switch (pFilterType) {
case FILTER_UNDEFINED:
case FILTER_POINT:
case FILTER_BOX:
case FILTER_TRIANGLE:
case FILTER_HERMITE:
case FILTER_HANNING:
case FILTER_HAMMING:
case FILTER_BLACKMAN:
case FILTER_GAUSSIAN:
case FILTER_QUADRATIC:
case FILTER_CUBIC:
case FILTER_CATROM:
case FILTER_MITCHELL:
case FILTER_LANCZOS:
case FILTER_BLACKMAN_BESSEL:
case FILTER_BLACKMAN_SINC:
break;
default:
throw new IllegalArgumentException("Unknown filter type: " + pFilterType);
}
}
/**
* Gets the filter type specified by the given hints.
*
* @param pHints rendering hints
* @return a filter type constant
*/
private static int getFilterType(RenderingHints pHints) {
if (pHints == null) {
return FILTER_UNDEFINED;
}
if (pHints.containsKey(KEY_RESAMPLE_INTERPOLATION)) {
Object value = pHints.get(KEY_RESAMPLE_INTERPOLATION);
// NOTE: Workaround for a bug in RenderingHints constructor (Bug id# 5084832)
if (!KEY_RESAMPLE_INTERPOLATION.isCompatibleValue(value)) {
throw new IllegalArgumentException(value + " incompatible with key " + KEY_RESAMPLE_INTERPOLATION);
}
return value != null ? ((Value) value).getFilterType() : FILTER_UNDEFINED;
}
else
if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))
|| (!pHints.containsKey(RenderingHints.KEY_INTERPOLATION)
&& (RenderingHints.VALUE_RENDER_SPEED.equals(pHints.get(RenderingHints.KEY_RENDERING))
|| RenderingHints.VALUE_COLOR_RENDER_SPEED.equals(pHints.get(RenderingHints.KEY_COLOR_RENDERING))))) {
// Nearest neighbour, or prioritze speed
return FILTER_POINT;
}
else
if (RenderingHints.VALUE_INTERPOLATION_BILINEAR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) {
// Triangle equals bi-linear interpolation
return FILTER_TRIANGLE;
}
else
if (RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) {
return FILTER_QUADRATIC;// No idea if this is correct..?
}
else
if (RenderingHints.VALUE_RENDER_QUALITY.equals(pHints.get(RenderingHints.KEY_RENDERING))
|| RenderingHints.VALUE_COLOR_RENDER_QUALITY.equals(pHints.get(RenderingHints.KEY_COLOR_RENDERING))) {
// Prioritize quality
return FILTER_MITCHELL;
}
// NOTE: Other hints are ignored
return FILTER_UNDEFINED;
}
/**
* Resamples (scales) the image to the size, and using the algorithm
* specified in the constructor.
*
* @param pInput The {@code BufferedImage} to be filtered
* @param pOutput The {@code BufferedImage} in which to store the resampled
* image
* @return The resampled {@code BufferedImage}.
* @throws NullPointerException if {@code pInput} is {@code null}
* @throws IllegalArgumentException if {@code pInput == pOutput}.
* @see #ResampleOp(int,int,int)
*/
public final BufferedImage filter(final BufferedImage pInput, final BufferedImage pOutput) {
if (pInput == null) {
throw new NullPointerException("Input == null");
}
if (pInput == pOutput) {
throw new IllegalArgumentException("Output image cannot be the same as the input image");
}
InterpolationFilter filter;
// Special case for POINT, TRIANGLE and QUADRATIC filter, as standard
// Java implementation is very fast (possibly H/W accellerated)
switch (mFilterType) {
case FILTER_POINT:
return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
case FILTER_TRIANGLE:
return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR);
case FILTER_QUADRATIC:
if (TRANSFORM_OP_BICUBIC_SUPPORT) {
return fastResample(pInput, pOutput, mWidth, mHeight, 3); // AffineTransformOp.TYPE_BICUBIC
}
// Fall through
default:
filter = createFilter(mFilterType);
// NOTE: Workaround for filter throwing exceptions when input or output is less than support...
if (Math.min(pInput.getWidth(), pInput.getHeight()) <= filter.support() || Math.min(mWidth, mHeight) <= filter.support()) {
return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR);
}
// Fall through
}
// Try to use native ImageMagick code
BufferedImage result = MagickAccelerator.filter(this, pInput, pOutput);
if (result != null) {
return result;
}
// Otherwise, continue in pure Java mode
// TODO: What if pOutput != null and wrong size? Create new? Render on only a part? Document?
// If filter type != POINT or BOX an input has IndexColorModel, convert
// to true color, with alpha reflecting that of the original colormodel.
BufferedImage input;
ColorModel cm;
if (mFilterType != FILTER_BOX && (cm = pInput.getColorModel()) instanceof IndexColorModel) {
// TODO: OPTIMIZE: If colormodel has only b/w or gray, we could skip color info
input = ImageUtil.toBuffered(pInput, cm.hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
}
else {
input = pInput;
}
// Create or convert output to a suitable image
// TODO: OPTIMIZE: Don't really need to convert all types to same as input
result = pOutput != null ? /*pOutput*/ ImageUtil.toBuffered(pOutput, input.getType()) : createCompatibleDestImage(input, null);
resample(input, result, filter);
// If pOutput != null and needed to be converted, draw it back
if (pOutput != null && pOutput != result) {
//pOutput.setData(output.getRaster());
ImageUtil.drawOnto(pOutput, result);
result = pOutput;
}
return result;
}
/*
private static BufferedImage pointResample(final BufferedImage pInput, final BufferedImage pOutput, final int pWidth, final int pHeight) {
double xScale = pWidth / (double) pInput.getWidth();
double yScale = pHeight / (double) pInput.getHeight();
// NOTE: This is extremely fast, native, possibly H/W accelerated code
AffineTransform transform = AffineTransform.getScaleInstance(xScale, yScale);
AffineTransformOp scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scale.filter(pInput, pOutput);
}
*/
/*
// TODO: This idea from Chet and Romain is actually not too bad..
// It reuses the image/raster/graphics...
// However, they forget to end with a halve operation..
private static BufferedImage getFasterScaledInstance(BufferedImage img,
int targetWidth, int targetHeight, Object hint,
boolean progressiveBilinear) {
int type = (img.getTransparency() == Transparency.OPAQUE) ?
BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage ret = img;
BufferedImage scratchImage = null;
Graphics2D g2 = null;
int w, h;
int prevW = ret.getWidth();
int prevH = ret.getHeight();
boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE;
if (progressiveBilinear) {
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = img.getWidth();
h = img.getHeight();
} else {
// Use one-step technique: scale directly from original
// size to target size with a single drawImage() call
w = targetWidth;
h = targetHeight;
}
do {
if (progressiveBilinear && w > targetWidth) {
w /= 2;
if (w < targetWidth) {
w = targetWidth;
}
}
if (progressiveBilinear && h > targetHeight) {
h /= 2;
if (h < targetHeight) {
h = targetHeight;
}
}
if (scratchImage == null || isTranslucent) {
// Use a single scratch buffer for all iterations
// and then copy to the final, correctly-sized image
// before returning
scratchImage = new BufferedImage(w, h, type);
g2 = scratchImage.createGraphics();
}
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
g2.drawImage(ret, 0, 0, w, h, 0, 0, prevW, prevH, null);
prevW = w;
prevH = h;
ret = scratchImage;
} while (w != targetWidth || h != targetHeight);
if (g2 != null) {
g2.dispose();
}
// If we used a scratch buffer that is larger than our target size,
// create an image of the right size and copy the results into it
if (targetWidth != ret.getWidth() || targetHeight != ret.getHeight()) {
scratchImage = new BufferedImage(targetWidth, targetHeight, type);
g2 = scratchImage.createGraphics();
g2.drawImage(ret, 0, 0, null);
g2.dispose();
ret = scratchImage;
}
return ret;
}
*/
private static BufferedImage fastResample(final BufferedImage pInput, final BufferedImage pOutput, final int pWidth, final int pHeight, final int pType) {
BufferedImage temp = pInput;
double xScale;
double yScale;
AffineTransform transform;
AffineTransformOp scale;
if (pType > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) {
// Initially scale so all remaining operations will halve the image
if (pWidth < pInput.getWidth() || pHeight < pInput.getHeight()) {
int w = pWidth;
int h = pHeight;
while (w < pInput.getWidth() / 2) {
w *= 2;
}
while (h < pInput.getHeight() / 2) {
h *= 2;
}
xScale = w / (double) pInput.getWidth();
yScale = h / (double) pInput.getHeight();
//System.out.println("First scale by x=" + xScale + ", y=" + yScale);
transform = AffineTransform.getScaleInstance(xScale, yScale);
scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
temp = scale.filter(temp, null);
}
}
scale = null;// NOTE: This resets!
xScale = pWidth / (double) temp.getWidth();
yScale = pHeight / (double) temp.getHeight();
if (pType > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) {
// TODO: Test skipping first scale (above), and instead scale once
// more here, and a little less than .5 each time...
// That would probably make the scaling smoother...
while (xScale < 0.5 || yScale < 0.5) {
if (xScale >= 0.5) {
//System.out.println("Halving by y=" + (yScale * 2.0));
transform = AffineTransform.getScaleInstance(1.0, 0.5);
scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
yScale *= 2.0;
}
else if (yScale >= 0.5) {
//System.out.println("Halving by x=" + (xScale * 2.0));
transform = AffineTransform.getScaleInstance(0.5, 1.0);
scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
xScale *= 2.0;
}
else {
//System.out.println("Halving by x=" + (xScale * 2.0) + ", y=" + (yScale * 2.0));
xScale *= 2.0;
yScale *= 2.0;
}
if (scale == null) {
transform = AffineTransform.getScaleInstance(0.5, 0.5);
scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
}
temp = scale.filter(temp, null);
}
}
//System.out.println("Rest to scale by x=" + xScale + ", y=" + yScale);
transform = AffineTransform.getScaleInstance(xScale, yScale);
scale = new AffineTransformOp(transform, pType);
return scale.filter(temp, pOutput);
}
/**
* Returns the current filter type constant.
*
* @return the current filter type constant.
* @see filter type constants
*/
public int getFilterType() {
return mFilterType;
}
private static InterpolationFilter createFilter(int pFilterType) {
// TODO: Select correct filter based on scale up or down, if undefined!
if (pFilterType == FILTER_UNDEFINED) {
pFilterType = FILTER_LANCZOS;
}
switch (pFilterType) {
//case FILTER_POINT: // Should never happen
case FILTER_BOX:
return new BoxFilter();
case FILTER_TRIANGLE:
return new TriangleFilter();
case FILTER_HERMITE:
return new HermiteFilter();
case FILTER_HANNING:
return new HanningFilter();
case FILTER_HAMMING:
return new HammingFilter();
case FILTER_BLACKMAN:
return new BlacmanFilter();
case FILTER_GAUSSIAN:
return new GaussianFilter();
case FILTER_QUADRATIC:
return new QuadraticFilter();
case FILTER_CUBIC:
return new CubicFilter();
case FILTER_CATROM:
return new CatromFilter();
case FILTER_MITCHELL:
return new MitchellFilter();
case FILTER_LANCZOS:
return new LanczosFilter();
case FILTER_BLACKMAN_BESSEL:
return new BlackmanBesselFilter();
case FILTER_BLACKMAN_SINC:
return new BlackmanSincFilter();
default:
throw new IllegalStateException("Unknown filter type: " + pFilterType);
}
}
public final BufferedImage createCompatibleDestImage(final BufferedImage pInput, final ColorModel pModel) {
if (pInput == null) {
throw new NullPointerException("pInput == null");
}
ColorModel cm = pModel != null ? pModel : pInput.getColorModel();
// TODO: Might not work with all colormodels..
// If indexcolormodel, we probably don't want to use that...
// NOTE: Either BOTH or NONE of the images must have ALPHA
return new BufferedImage(cm, ImageUtil.createCompatibleWritableRaster(pInput, cm, mWidth, mHeight),
cm.isAlphaPremultiplied(), null);
}
public RenderingHints getRenderingHints() {
Object value;
switch (mFilterType) {
case FILTER_UNDEFINED:
return null;
case FILTER_POINT:
value = VALUE_INTERPOLATION_POINT;
break;
case FILTER_BOX:
value = VALUE_INTERPOLATION_BOX;
break;
case FILTER_TRIANGLE:
value = VALUE_INTERPOLATION_TRIANGLE;
break;
case FILTER_HERMITE:
value = VALUE_INTERPOLATION_HERMITE;
break;
case FILTER_HANNING:
value = VALUE_INTERPOLATION_HANNING;
break;
case FILTER_HAMMING:
value = VALUE_INTERPOLATION_HAMMING;
break;
case FILTER_BLACKMAN:
value = VALUE_INTERPOLATION_BLACKMAN;
break;
case FILTER_GAUSSIAN:
value = VALUE_INTERPOLATION_GAUSSIAN;
break;
case FILTER_QUADRATIC:
value = VALUE_INTERPOLATION_QUADRATIC;
break;
case FILTER_CUBIC:
value = VALUE_INTERPOLATION_CUBIC;
break;
case FILTER_CATROM:
value = VALUE_INTERPOLATION_CATROM;
break;
case FILTER_MITCHELL:
value = VALUE_INTERPOLATION_MITCHELL;
break;
case FILTER_LANCZOS:
value = VALUE_INTERPOLATION_LANCZOS;
break;
case FILTER_BLACKMAN_BESSEL:
value = VALUE_INTERPOLATION_BLACKMAN_BESSEL;
break;
case FILTER_BLACKMAN_SINC:
value = VALUE_INTERPOLATION_BLACKMAN_SINC;
break;
default:
throw new IllegalStateException("Unknown filter type: " + mFilterType);
}
return new RenderingHints(KEY_RESAMPLE_INTERPOLATION, value);
}
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle(mWidth, mHeight);
}
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
// TODO: This is wrong...
// How can I possible know how much one point is scaled, without first knowing the ration?!
// TODO: Maybe set all points outside of bounds, inside?
// TODO: Assume input image of Integer.MAX_VAL x Integer.MAX_VAL?! ;-)
if (dstPt == null) {
if (srcPt instanceof Point2D.Double) {
dstPt = new Point2D.Double();
}
else {
dstPt = new Point2D.Float();
}
dstPt.setLocation(srcPt);
}
return dstPt;
}
/* -- Java port of filter_rcg.c below... -- */
/*
* filter function definitions
*/
static interface InterpolationFilter {
double filter(double t);
double support();
}
static class HermiteFilter implements InterpolationFilter {
public final double filter(double t) {
/* f(t) = 2|t|^3 - 3|t|^2 + 1, -1 <= t <= 1 */
if (t < 0.0) {
t = -t;
}
if (t < 1.0) {
return (2.0 * t - 3.0) * t * t + 1.0;
}
return 0.0;
}
public final double support() {
return 1.0;
}
}
static class PointFilter extends BoxFilter {
public PointFilter() {
super(0.0);
}
}
static class BoxFilter implements InterpolationFilter {
private final double mSupport;
public BoxFilter() {
mSupport = 0.5;
}
protected BoxFilter(double pSupport) {
mSupport = pSupport;
}
public final double filter(final double t) {
//if ((t > -0.5) && (t <= 0.5)) {
if ((t >= -0.5) && (t < 0.5)) {// ImageMagick resample.c
return 1.0;
}
return 0.0;
}
public final double support() {
return mSupport;
}
}
static class TriangleFilter implements InterpolationFilter {
public final double filter(double t) {
if (t < 0.0) {
t = -t;
}
if (t < 1.0) {
return 1.0 - t;
}
return 0.0;
}
public final double support() {
return 1.0;
}
}
static class QuadraticFilter implements InterpolationFilter {
// AKA Bell
public final double filter(double t)/* box (*) box (*) box */ {
if (t < 0) {
t = -t;
}
if (t < .5) {
return .75 - (t * t);
}
if (t < 1.5) {
t = (t - 1.5);
return .5 * (t * t);
}
return 0.0;
}
public final double support() {
return 1.5;
}
}
static class CubicFilter implements InterpolationFilter {
// AKA B-Spline
public final double filter(double t)/* box (*) box (*) box (*) box */ {
final double tt;
if (t < 0) {
t = -t;
}
if (t < 1) {
tt = t * t;
return (.5 * tt * t) - tt + (2.0 / 3.0);
}
else if (t < 2) {
t = 2 - t;
return (1.0 / 6.0) * (t * t * t);
}
return 0.0;
}
public final double support() {
return 2.0;
}
}
private static double sinc(double x) {
x *= Math.PI;
if (x != 0.0) {
return Math.sin(x) / x;
}
return 1.0;
}
static class LanczosFilter implements InterpolationFilter {
// AKA Lanczos3
public final double filter(double t) {
if (t < 0) {
t = -t;
}
if (t < 3.0) {
return sinc(t) * sinc(t / 3.0);
}
return 0.0;
}
public final double support() {
return 3.0;
}
}
private final static double B = 1.0 / 3.0;
private final static double C = 1.0 / 3.0;
private final static double P0 = (6.0 - 2.0 * B) / 6.0;
private final static double P2 = (-18.0 + 12.0 * B + 6.0 * C) / 6.0;
private final static double P3 = (12.0 - 9.0 * B - 6.0 * C) / 6.0;
private final static double Q0 = (8.0 * B + 24.0 * C) / 6.0;
private final static double Q1 = (-12.0 * B - 48.0 * C) / 6.0;
private final static double Q2 = (6.0 * B + 30.0 * C) / 6.0;
private final static double Q3 = (-1.0 * B - 6.0 * C) / 6.0;
static class MitchellFilter implements InterpolationFilter {
public final double filter(double t) {
if (t < -2.0) {
return 0.0;
}
if (t < -1.0) {
return Q0 - t * (Q1 - t * (Q2 - t * Q3));
}
if (t < 0.0) {
return P0 + t * t * (P2 - t * P3);
}
if (t < 1.0) {
return P0 + t * t * (P2 + t * P3);
}
if (t < 2.0) {
return Q0 + t * (Q1 + t * (Q2 + t * Q3));
}
return 0.0;
}
public final double support() {
return 2.0;
}
}
private static double j1(final double t) {
final double[] pOne = {
0.581199354001606143928050809e+21,
-0.6672106568924916298020941484e+20,
0.2316433580634002297931815435e+19,
-0.3588817569910106050743641413e+17,
0.2908795263834775409737601689e+15,
-0.1322983480332126453125473247e+13,
0.3413234182301700539091292655e+10,
-0.4695753530642995859767162166e+7,
0.270112271089232341485679099e+4
};
final double[] qOne = {
0.11623987080032122878585294e+22,
0.1185770712190320999837113348e+20,
0.6092061398917521746105196863e+17,
0.2081661221307607351240184229e+15,
0.5243710262167649715406728642e+12,
0.1013863514358673989967045588e+10,
0.1501793594998585505921097578e+7,
0.1606931573481487801970916749e+4,
0.1e+1
};
double p = pOne[8];
double q = qOne[8];
for (int i = 7; i >= 0; i--) {
p = p * t * t + pOne[i];
q = q * t * t + qOne[i];
}
return p / q;
}
private static double p1(final double t) {
final double[] pOne = {
0.352246649133679798341724373e+5,
0.62758845247161281269005675e+5,
0.313539631109159574238669888e+5,
0.49854832060594338434500455e+4,
0.2111529182853962382105718e+3,
0.12571716929145341558495e+1
};
final double[] qOne = {
0.352246649133679798068390431e+5,
0.626943469593560511888833731e+5,
0.312404063819041039923015703e+5,
0.4930396490181088979386097e+4,
0.2030775189134759322293574e+3,
0.1e+1
};
double p = pOne[5];
double q = qOne[5];
for (int i = 4; i >= 0; i--) {
p = p * (8.0 / t) * (8.0 / t) + pOne[i];
q = q * (8.0 / t) * (8.0 / t) + qOne[i];
}
return p / q;
}
private static double q1(final double t) {
final double[] pOne = {
0.3511751914303552822533318e+3,
0.7210391804904475039280863e+3,
0.4259873011654442389886993e+3,
0.831898957673850827325226e+2,
0.45681716295512267064405e+1,
0.3532840052740123642735e-1
};
final double[] qOne = {
0.74917374171809127714519505e+4,
0.154141773392650970499848051e+5,
0.91522317015169922705904727e+4,
0.18111867005523513506724158e+4,
0.1038187585462133728776636e+3,
0.1e+1
};
double p = pOne[5];
double q = qOne[5];
for (int i = 4; i >= 0; i--) {
p = p * (8.0 / t) * (8.0 / t) + pOne[i];
q = q * (8.0 / t) * (8.0 / t) + qOne[i];
}
return p / q;
}
static double besselOrderOne(double t) {
double p, q;
if (t == 0.0) {
return 0.0;
}
p = t;
if (t < 0.0) {
t = -t;
}
if (t < 8.0) {
return p * j1(t);
}
q = Math.sqrt(2.0 / (Math.PI * t)) * (p1(t) * (1.0 / Math.sqrt(2.0) * (Math.sin(t) - Math.cos(t))) - 8.0 / t * q1(t) *
(-1.0 / Math.sqrt(2.0) * (Math.sin(t) + Math.cos(t))));
if (p < 0.0) {
q = -q;
}
return q;
}
private static double bessel(final double t) {
if (t == 0.0) {
return Math.PI / 4.0;
}
return besselOrderOne(Math.PI * t) / (2.0 * t);
}
private static double blackman(final double t) {
return 0.42 + 0.50 * Math.cos(Math.PI * t) + 0.08 * Math.cos(2.0 * Math.PI * t);
}
static class BlacmanFilter implements InterpolationFilter {
public final double filter(final double t) {
return blackman(t);
}
public final double support() {
return 1.0;
}
}
static class CatromFilter implements InterpolationFilter {
public final double filter(double t) {
if (t < 0) {
t = -t;
}
if (t < 1.0) {
return 0.5 * (2.0 + t * t * (-5.0 + t * 3.0));
}
if (t < 2.0) {
return 0.5 * (4.0 + t * (-8.0 + t * (5.0 - t)));
}
return 0.0;
}
public final double support() {
return 2.0;
}
}
static class GaussianFilter implements InterpolationFilter {
public final double filter(final double t) {
return Math.exp(-2.0 * t * t) * Math.sqrt(2.0 / Math.PI);
}
public final double support() {
return 1.25;
}
}
static class HanningFilter implements InterpolationFilter {
public final double filter(final double t) {
return 0.5 + 0.5 * Math.cos(Math.PI * t);
}
public final double support() {
return 1.0;
}
}
static class HammingFilter implements InterpolationFilter {
public final double filter(final double t) {
return 0.54 + 0.46 * Math.cos(Math.PI * t);
}
public final double support() {
return 1.0;
}
}
static class BlackmanBesselFilter implements InterpolationFilter {
public final double filter(final double t) {
return blackman(t / support()) * bessel(t);
}
public final double support() {
return 3.2383;
}
}
static class BlackmanSincFilter implements InterpolationFilter {
public final double filter(final double t) {
return blackman(t / support()) * sinc(t);
}
public final double support() {
return 4.0;
}
}
/*
* image rescaling routine
*/
class Contributor {
int pixel;
double weight;
}
class ContributorList {
int n;/* number of contributors (may be < p.length) */
Contributor[] p;/* pointer to list of contributions */
}
/*
round()
Round an FP value to its closest int representation.
General routine; ideally belongs in general math lib file.
*/
static int round(double d) {
// NOTE: This code seems to be faster than Math.round(double)...
// Version that uses no function calls at all.
int n = (int) d;
double diff = d - (double) n;
if (diff < 0) {
diff = -diff;
}
if (diff >= 0.5) {
if (d < 0) {
n--;
}
else {
n++;
}
}
return n;
}/* round */
/*
calcXContrib()
Calculates the filter weights for a single target column.
contribX->p must be freed afterwards.
Returns -1 if error, 0 otherwise.
*/
private ContributorList calcXContrib(double xscale, double fwidth, int srcwidth, InterpolationFilter pFilter, int i) {
// TODO: What to do when fwidth > srcwidyj or dstwidth
double width;
double fscale;
double center;
double weight;
ContributorList contribX = new ContributorList();
if (xscale < 1.0) {
/* Shrinking image */
width = fwidth / xscale;
fscale = 1.0 / xscale;
if (width <= .5) {
// Reduce to point sampling.
width = .5 + 1.0e-6;
fscale = 1.0;
}
//contribX.n = 0;
contribX.p = new Contributor[(int) (width * 2.0 + 1.0)];
center = (double) i / xscale;
int left = (int) Math.ceil(center - width);// Note: Assumes width <= .5
int right = (int) Math.floor(center + width);
double density = 0.0;
for (int j = left; j <= right; j++) {
weight = center - (double) j;
weight = pFilter.filter(weight / fscale) / fscale;
int n;
if (j < 0) {
n = -j;
}
else if (j >= srcwidth) {
n = (srcwidth - j) + srcwidth - 1;
}
else {
n = j;
}
/**/
if (n >= srcwidth) {
n = n % srcwidth;
}
else if (n < 0) {
n = srcwidth - 1;
}
/**/
int k = contribX.n++;
contribX.p[k] = new Contributor();
contribX.p[k].pixel = n;
contribX.p[k].weight = weight;
density += weight;
}
if ((density != 0.0) && (density != 1.0)) {
//Normalize.
density = 1.0 / density;
for (int k = 0; k < contribX.n; k++) {
contribX.p[k].weight *= density;
}
}
}
else {
/* Expanding image */
//contribX.n = 0;
contribX.p = new Contributor[(int) (fwidth * 2.0 + 1.0)];
center = (double) i / xscale;
int left = (int) Math.ceil(center - fwidth);
int right = (int) Math.floor(center + fwidth);
for (int j = left; j <= right; j++) {
weight = center - (double) j;
weight = pFilter.filter(weight);
int n;
if (j < 0) {
n = -j;
}
else if (j >= srcwidth) {
n = (srcwidth - j) + srcwidth - 1;
}
else {
n = j;
}
/**/
if (n >= srcwidth) {
n = n % srcwidth;
}
else if (n < 0) {
n = srcwidth - 1;
}
/**/
int k = contribX.n++;
contribX.p[k] = new Contributor();
contribX.p[k].pixel = n;
contribX.p[k].weight = weight;
}
}
return contribX;
}/* calcXContrib */
/*
resample()
Resizes bitmaps while resampling them.
Returns -1 if error, 0 if success.
*/
private BufferedImage resample(BufferedImage pSource, BufferedImage pDest, InterpolationFilter pFilter) {
// TODO: Don't work... Could fix by creating a temporary image in filter method
final int dstWidth = pDest.getWidth();
final int dstHeight = pDest.getHeight();
final int srcWidth = pSource.getWidth();
final int srcHeight = pSource.getHeight();
/* create intermediate column to hold horizontal dst column zoom */
final ColorModel cm = pSource.getColorModel();
final WritableRaster work = cm.createCompatibleWritableRaster(1, srcHeight);
double xscale = (double) dstWidth / (double) srcWidth;
double yscale = (double) dstHeight / (double) srcHeight;
ContributorList[] contribY = new ContributorList[dstHeight];
for (int i = 0; i < contribY.length; i++) {
contribY[i] = new ContributorList();
}
// TODO: What to do when fwidth > srcHeight or dstHeight
double fwidth = pFilter.support();
if (yscale < 1.0) {
double width = fwidth / yscale;
double fscale = 1.0 / yscale;
if (width <= .5) {
// Reduce to point sampling.
width = .5 + 1.0e-6;
fscale = 1.0;
}
for (int i = 0; i < dstHeight; i++) {
//contribY[i].n = 0;
contribY[i].p = new Contributor[(int) (width * 2.0 + 1)];
double center = (double) i / yscale;
int left = (int) Math.ceil(center - width);
int right = (int) Math.floor(center + width);
double density = 0.0;
for (int j = left; j <= right; j++) {
double weight = center - (double) j;
weight = pFilter.filter(weight / fscale) / fscale;
int n;
if (j < 0) {
n = -j;
}
else if (j >= srcHeight) {
n = (srcHeight - j) + srcHeight - 1;
}
else {
n = j;
}
/**/
if (n >= srcHeight) {
n = n % srcHeight;
}
else if (n < 0) {
n = srcHeight - 1;
}
/**/
int k = contribY[i].n++;
contribY[i].p[k] = new Contributor();
contribY[i].p[k].pixel = n;
contribY[i].p[k].weight = weight;
density += weight;
}
if ((density != 0.0) && (density != 1.0)) {
//Normalize.
density = 1.0 / density;
for (int k = 0; k < contribY[i].n; k++) {
contribY[i].p[k].weight *= density;
}
}
}
}
else {
for (int i = 0; i < dstHeight; ++i) {
//contribY[i].n = 0;
contribY[i].p = new Contributor[(int) (fwidth * 2 + 1)];
double center = (double) i / yscale;
double left = Math.ceil(center - fwidth);
double right = Math.floor(center + fwidth);
for (int j = (int) left; j <= right; ++j) {
double weight = center - (double) j;
weight = pFilter.filter(weight);
int n;
if (j < 0) {
n = -j;
}
else if (j >= srcHeight) {
n = (srcHeight - j) + srcHeight - 1;
}
else {
n = j;
}
/**/
if (n >= srcHeight) {
n = n % srcHeight;
}
else if (n < 0) {
n = srcHeight - 1;
}
/**/
int k = contribY[i].n++;
contribY[i].p[k] = new Contributor();
contribY[i].p[k].pixel = n;
contribY[i].p[k].weight = weight;
}
}
}
final Raster raster = pSource.getRaster();
final WritableRaster out = pDest.getRaster();
// TODO: This is not optimal for non-byte-packed rasters...
// (What? Maybe I implemented the fix, but forgot to remove the qTODO?)
final int numChannels = raster.getNumBands();
final int[] channelMax = new int[numChannels];
for (int k = 0; k < numChannels; k++) {
channelMax[k] = (1 << pSource.getColorModel().getComponentSize(k)) - 1;
}
for (int xx = 0; xx < dstWidth; xx++) {
ContributorList contribX = calcXContrib(xscale, fwidth, srcWidth, pFilter, xx);
/* Apply horz filter to make dst column in tmp. */
for (int k = 0; k < srcHeight; k++) {
for (int channel = 0; channel < numChannels; channel++) {
double weight = 0.0;
boolean bPelDelta = false;
// TODO: This line throws index out of bounds, if the image
// is smaller than filter.support()
double pel = raster.getSample(contribX.p[0].pixel, k, channel);
for (int j = 0; j < contribX.n; j++) {
double pel2 = j == 0 ? pel : raster.getSample(contribX.p[j].pixel, k, channel);
if (pel2 != pel) {
bPelDelta = true;
}
weight += pel2 * contribX.p[j].weight;
}
weight = bPelDelta ? round(weight) : pel;
if (weight < 0) {
weight = 0;
}
else if (weight > channelMax[channel]) {
weight = channelMax[channel];
}
work.setSample(0, k, channel, weight);
}
}/* next row in temp column */
/* The temp column has been built. Now stretch it vertically into dst column. */
for (int i = 0; i < dstHeight; i++) {
for (int channel = 0; channel < numChannels; channel++) {
double weight = 0.0;
boolean bPelDelta = false;
double pel = work.getSample(0, contribY[i].p[0].pixel, channel);
for (int j = 0; j < contribY[i].n; j++) {
// TODO: This line throws index out of bounds, if the image
// is smaller than filter.support()
double pel2 = j == 0 ? pel : work.getSample(0, contribY[i].p[j].pixel, channel);
if (pel2 != pel) {
bPelDelta = true;
}
weight += pel2 * contribY[i].p[j].weight;
}
weight = bPelDelta ? round(weight) : pel;
if (weight < 0) {
weight = 0;
}
else if (weight > channelMax[channel]) {
weight = channelMax[channel];
}
out.setSample(xx, i, channel, weight);
}
}/* next dst row */
}/* next dst column */
return pDest;
}/* resample */
}