All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 */ }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy