/* AffineTransformOp.java -- This class performs affine
transformation between two images or rasters in 2 dimensions.
- Copyright (C) 2004 Free Software Foundation
+ Copyright (C) 2004, 2006 Free Software Foundation
This file is part of GNU Classpath.
package java.awt.image;
import java.awt.Graphics2D;
+import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.util.Arrays;
/**
- * This class performs affine transformation between two images or
- * rasters in 2 dimensions.
+ * AffineTransformOp performs matrix-based transformations (translations,
+ * scales, flips, rotations, and shears).
+ *
+ * If interpolation is required, nearest neighbour, bilinear, and bicubic
+ * methods are available.
*
* @author Olga Rodimina (rodimina@redhat.com)
+ * @author Francis Kung (fkung@redhat.com)
*/
public class AffineTransformOp implements BufferedImageOp, RasterOp
{
*
* @param xform AffineTransform that will applied to the source image
* @param interpolationType type of interpolation used
+ * @throws ImagingOpException if the transform matrix is noninvertible
*/
public AffineTransformOp (AffineTransform xform, int interpolationType)
{
*
* @param xform AffineTransform that will applied to the source image
* @param hints rendering hints that will be used during transformation
+ * @throws ImagingOpException if the transform matrix is noninvertible
*/
public AffineTransformOp (AffineTransform xform, RenderingHints hints)
{
}
/**
- * Creates empty BufferedImage with the size equal to that of the
- * transformed image and correct number of bands. The newly created
+ * Creates a new BufferedImage with the size equal to that of the
+ * transformed image and the correct number of bands. The newly created
* image is created with the specified ColorModel.
- * If the ColorModel is equal to null, then image is created
- * with the ColorModel of the source image.
+ * If a ColorModel is not specified, an appropriate ColorModel is used.
*
- * @param src source image
- * @param destCM color model for the destination image
- * @return new compatible destination image
+ * @param src the source image.
+ * @param destCM color model for the destination image (can be null).
+ * @return a new compatible destination image.
*/
public BufferedImage createCompatibleDestImage (BufferedImage src,
ColorModel destCM)
{
+ if (destCM != null)
+ return new BufferedImage(destCM,
+ createCompatibleDestRaster(src.getRaster()),
+ src.isAlphaPremultiplied(), null);
+
+ // This behaviour was determined by Mauve testcases, and is compatible
+ // with the reference implementation
+ if (src.getType() == BufferedImage.TYPE_INT_ARGB_PRE
+ || src.getType() == BufferedImage.TYPE_4BYTE_ABGR
+ || src.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE)
+ return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
- // if destCm is not specified, use color model of the source image
-
- if (destCM == null)
- destCM = src.getColorModel ();
-
- return new BufferedImage (destCM,
- createCompatibleDestRaster (src.getRaster ()),
- src.isAlphaPremultiplied (),
- null);
-
+ else
+ return new BufferedImage(src.getWidth(), src.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
}
/**
- * Creates empty WritableRaster with the size equal to the transformed
- * source raster and correct number of bands
+ * Creates a new WritableRaster with the size equal to the transformed
+ * source raster and correct number of bands .
*
- * @param src source raster
- * @throws RasterFormatException if resulting width or height of raster is 0
- * @return new compatible raster
+ * @param src the source raster.
+ * @throws RasterFormatException if resulting width or height of raster is 0.
+ * @return a new compatible raster.
*/
public WritableRaster createCompatibleDestRaster (Raster src)
{
- Rectangle rect = (Rectangle) getBounds2D (src);
+ Rectangle2D rect = getBounds2D(src);
- // throw RasterFormatException if resulting width or height of the
- // transformed raster is 0
-
- if (rect.getWidth () == 0 || rect.getHeight () == 0)
+ if (rect.getWidth() == 0 || rect.getHeight() == 0)
throw new RasterFormatException("width or height is 0");
- return src.createCompatibleWritableRaster ((int) rect.getWidth (),
- (int) rect.getHeight ());
+ return src.createCompatibleWritableRaster((int) rect.getWidth(),
+ (int) rect.getHeight());
}
/**
* Transforms source image using transform specified at the constructor.
- * The resulting transformed image is stored in the destination image.
+ * The resulting transformed image is stored in the destination image if one
+ * is provided; otherwise a new BufferedImage is created and returned.
*
* @param src source image
* @param dst destination image
- * @return transformed source image
+ * @throws IllegalArgumentException if the source and destination image are
+ * the same
+ * @return transformed source image.
*/
public final BufferedImage filter (BufferedImage src, BufferedImage dst)
{
-
if (dst == src)
- throw new IllegalArgumentException ("src image cannot be the same as the dst image");
-
- // If the destination image is null, then BufferedImage is
- // created with ColorModel of the source image
+ throw new IllegalArgumentException("src image cannot be the same as "
+ + "the dst image");
+ // If the destination image is null, then use a compatible BufferedImage
if (dst == null)
- dst = createCompatibleDestImage(src, src.getColorModel ());
-
- // FIXME: Must check if color models of src and dst images are the same.
- // If it is not, then source image should be converted to color model
- // of the destination image
+ dst = createCompatibleDestImage(src, null);
- Graphics2D gr = (Graphics2D) dst.createGraphics ();
- gr.setRenderingHints (hints);
- gr.drawImage (src, transform, null);
+ Graphics2D gr = (Graphics2D) dst.createGraphics();
+ gr.setRenderingHints(hints);
+ gr.drawImage(src, transform, null);
return dst;
-
}
/**
* Transforms source raster using transform specified at the constructor.
- * The resulting raster is stored in the destination raster.
+ * The resulting raster is stored in the destination raster if it is not
+ * null, otherwise a new raster is created and returned.
*
* @param src source raster
* @param dst destination raster
- * @return transformed raster
+ * @throws IllegalArgumentException if the source and destination are not
+ * compatible
+ * @return transformed raster.
*/
- public final WritableRaster filter (Raster src, WritableRaster dst)
+ public final WritableRaster filter(Raster src, WritableRaster dst)
{
+ // Initial checks
if (dst == src)
throw new IllegalArgumentException("src image cannot be the same as"
- + " the dst image");
+ + " the dst image");
if (dst == null)
dst = createCompatibleDestRaster(src);
if (src.getNumBands() != dst.getNumBands())
throw new IllegalArgumentException("src and dst must have same number"
- + " of bands");
+ + " of bands");
- double[] dpts = new double[dst.getWidth() * 2];
- double[] pts = new double[dst.getWidth() * 2];
+ // Optimization for rasters that can be represented in the RGB colormodel:
+ // wrap the rasters in images, and let Cairo do the transformation
+ if (ColorModel.getRGBdefault().isCompatibleSampleModel(src.getSampleModel())
+ && ColorModel.getRGBdefault().isCompatibleSampleModel(dst.getSampleModel()))
+ {
+ WritableRaster src2 = Raster.createWritableRaster(src.getSampleModel(),
+ src.getDataBuffer(),
+ new Point(src.getMinX(),
+ src.getMinY()));
+ BufferedImage iSrc = new BufferedImage(ColorModel.getRGBdefault(),
+ src2, false, null);
+ BufferedImage iDst = new BufferedImage(ColorModel.getRGBdefault(), dst,
+ false, null);
+
+ return filter(iSrc, iDst).getRaster();
+ }
+
+ // Otherwise, we need to do the transformation in java code...
+ // Create arrays to hold all the points
+ double[] dstPts = new double[dst.getHeight() * dst.getWidth() * 2];
+ double[] srcPts = new double[dst.getHeight() * dst.getWidth() * 2];
+
+ // Populate array with all points in the *destination* raster
+ int i = 0;
for (int x = 0; x < dst.getWidth(); x++)
- {
- dpts[2 * x] = x + dst.getMinX();
- dpts[2 * x + 1] = x;
- }
- Rectangle srcbounds = src.getBounds();
- if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
- {
- for (int y = dst.getMinY(); y < dst.getMinY() + dst.getHeight(); y++)
- {
- try {
- transform.inverseTransform(dpts, 0, pts, 0, dst.getWidth() * 2);
- } catch (NoninvertibleTransformException e) {
- // Can't happen since the constructor traps this
- e.printStackTrace();
- }
-
- for (int x = 0; x < dst.getWidth(); x++)
- {
- if (!srcbounds.contains(pts[2 * x], pts[2 * x + 1]))
- continue;
- dst.setDataElements(x + dst.getMinX(), y,
- src.getDataElements((int)pts[2 * x],
- (int)pts[2 * x + 1],
- null));
- }
- }
- }
- else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
- {
- double[] tmp = new double[4 * src.getNumBands()];
- for (int y = dst.getMinY(); y < dst.getMinY() + dst.getHeight(); y++)
{
- try {
- transform.inverseTransform(dpts, 0, pts, 0, dst.getWidth() * 2);
- } catch (NoninvertibleTransformException e) {
- // Can't happen since the constructor traps this
- e.printStackTrace();
- }
-
- for (int x = 0; x < dst.getWidth(); x++)
- {
- if (!srcbounds.contains(pts[2 * x], pts[2 * x + 1]))
- continue;
- int xx = (int)pts[2 * x];
- int yy = (int)pts[2 * x + 1];
- double dx = (pts[2 * x] - xx);
- double dy = (pts[2 * x + 1] - yy);
-
- // TODO write this more intelligently
- if (xx == src.getMinX() + src.getWidth() - 1 ||
- yy == src.getMinY() + src.getHeight() - 1)
+ for (int y = 0; y < dst.getHeight(); y++)
{
- // bottom or right edge
- Arrays.fill(tmp, 0);
- src.getPixel(xx, yy, tmp);
+ dstPts[i++] = x;
+ dstPts[i++] = y;
}
- else
- {
- // Normal case
- src.getPixels(xx, yy, 2, 2, tmp);
- for (int b = 0; b < src.getNumBands(); b++)
- tmp[b] = dx * dy * tmp[b]
- + (1 - dx) * dy * tmp[b + src.getNumBands()]
- + dx * (1 - dy) * tmp[b + 2 * src.getNumBands()]
- + (1 - dx) * (1 - dy) * tmp[b + 3 * src.getNumBands()];
- }
- dst.setPixel(x, y, tmp);
- }
}
- }
- else
- {
- // Bicubic
- throw new UnsupportedOperationException("not implemented yet");
- }
+ Rectangle srcbounds = src.getBounds();
+
+ // Use an inverse transform to map each point in the destination to
+ // a point in the source. Note that, while all points in the destination
+ // matrix are integers, this is not necessarily true for points in the
+ // source (hence why interpolation is required)
+ try
+ {
+ AffineTransform inverseTx = transform.createInverse();
+ inverseTx.transform(dstPts, 0, srcPts, 0, dstPts.length / 2);
+ }
+ catch (NoninvertibleTransformException e)
+ {
+ // Shouldn't happen since the constructor traps this
+ throw new ImagingOpException(e.getMessage());
+ }
+
+ // Different interpolation methods...
+ if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
+ filterNearest(src, dst, dstPts, srcPts);
+ else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
+ filterBilinear(src, dst, dstPts, srcPts);
+
+ else // bicubic
+ filterBicubic(src, dst, dstPts, srcPts);
+
return dst;
}
*/
public final Rectangle2D getBounds2D (Raster src)
{
- // determine new size for the transformed raster.
- // Need to calculate transformed coordinates of the lower right
- // corner of the raster. The upper left corner is always (0,0)
-
- double x2 = (double) src.getWidth () + src.getMinX ();
- double y2 = (double) src.getHeight () + src.getMinY ();
- Point2D p2 = getPoint2D (new Point2D.Double (x2,y2), null);
-
- Rectangle2D rect = new Rectangle (0, 0, (int) p2.getX (), (int) p2.getY ());
- return rect.getBounds ();
+ return transform.createTransformedShape(src.getBounds()).getBounds2D();
}
/**
- * Returns interpolation type used during transformations
+ * Returns interpolation type used during transformations.
*
* @return interpolation type
*/
public final int getInterpolationType ()
{
- if(hints.containsValue (RenderingHints.VALUE_INTERPOLATION_BILINEAR))
+ if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
return TYPE_BILINEAR;
+
+ else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BICUBIC))
+ return TYPE_BICUBIC;
+
else
return TYPE_NEAREST_NEIGHBOR;
}
/**
* Returns rendering hints that are used during transformation.
*
- * @return rendering hints
+ * @return the rendering hints used in this Op.
*/
public final RenderingHints getRenderingHints ()
{
* Returns transform used in transformation between source and destination
* image.
*
- * @return transform
+ * @return the transform used in this Op.
*/
public final AffineTransform getTransform ()
{
return transform;
}
+
+ /**
+ * Perform nearest-neighbour filtering
+ *
+ * @param src the source raster
+ * @param dst the destination raster
+ * @param dpts array of points on the destination raster
+ * @param pts array of corresponding points on the source raster
+ */
+ private void filterNearest(Raster src, WritableRaster dst, double[] dpts,
+ double[] pts)
+ {
+ Rectangle srcbounds = src.getBounds();
+
+ // For all points on the destination raster, copy the value from the
+ // corrosponding (rounded) source point
+ for (int i = 0; i < dpts.length; i += 2)
+ {
+ int srcX = (int) Math.round(pts[i]) + src.getMinX();
+ int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
+
+ if (srcbounds.contains(srcX, srcY))
+ dst.setDataElements((int) dpts[i] + dst.getMinX(),
+ (int) dpts[i + 1] + dst.getMinY(),
+ src.getDataElements(srcX, srcY, null));
+ }
+ }
+
+ /**
+ * Perform bilinear filtering
+ *
+ * @param src the source raster
+ * @param dst the destination raster
+ * @param dpts array of points on the destination raster
+ * @param pts array of corresponding points on the source raster
+ */
+ private void filterBilinear(Raster src, WritableRaster dst, double[] dpts,
+ double[] pts)
+ {
+ Rectangle srcbounds = src.getBounds();
+
+ Object xyarr = null;
+ Object xp1arr = null;
+ Object yp1arr = null;
+ Object xyp1arr = null;
+
+ double xy;
+ double xp1;
+ double yp1;
+ double xyp1;
+
+ double[] result = new double[src.getNumBands()];
+
+ // For all points in the destination raster, use bilinear interpolation
+ // to find the value from the corrosponding source points
+ for (int i = 0; i < dpts.length; i += 2)
+ {
+ int srcX = (int) Math.round(pts[i]) + src.getMinX();
+ int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
+
+ if (srcbounds.contains(srcX, srcY))
+ {
+ // Corner case at the bottom or right edge; use nearest neighbour
+ if (pts[i] >= src.getWidth() - 1
+ || pts[i + 1] >= src.getHeight() - 1)
+ dst.setDataElements((int) dpts[i] + dst.getMinX(),
+ (int) dpts[i + 1] + dst.getMinY(),
+ src.getDataElements(srcX, srcY, null));
+
+ // Standard case, apply the bilinear formula
+ else
+ {
+ int x = (int) Math.floor(pts[i] + src.getMinX());
+ int y = (int) Math.floor(pts[i + 1] + src.getMinY());
+ double xdiff = pts[i] + src.getMinX() - x;
+ double ydiff = pts[i + 1] + src.getMinY() - y;
+
+ // Get surrounding pixels used in interpolation... optimized
+ // to use the smallest datatype possible.
+ if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+ || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+ {
+ xyarr = src.getPixel(x, y, (double[])xyarr);
+ xp1arr = src.getPixel(x+1, y, (double[])xp1arr);
+ yp1arr = src.getPixel(x, y+1, (double[])yp1arr);
+ xyp1arr = src.getPixel(x+1, y+1, (double[])xyp1arr);
+ }
+ else
+ {
+ xyarr = src.getPixel(x, y, (int[])xyarr);
+ xp1arr = src.getPixel(x+1, y, (int[])xp1arr);
+ yp1arr = src.getPixel(x, y+1, (int[])yp1arr);
+ xyp1arr = src.getPixel(x+1, y+1, (int[])xyp1arr);
+ }
+ // using
+ // array[] pixels = src.getPixels(x, y, 2, 2, pixels);
+ // instead of doing four individual src.getPixel() calls
+ // should be faster, but benchmarking shows that it's not...
+
+ // Run interpolation for each band
+ for (int j = 0; j < src.getNumBands(); j++)
+ {
+ // Pull individual sample values out of array
+ if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+ || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+ {
+ xy = ((double[])xyarr)[j];
+ xp1 = ((double[])xp1arr)[j];
+ yp1 = ((double[])yp1arr)[j];
+ xyp1 = ((double[])xyp1arr)[j];
+ }
+ else
+ {
+ xy = ((int[])xyarr)[j];
+ xp1 = ((int[])xp1arr)[j];
+ yp1 = ((int[])yp1arr)[j];
+ xyp1 = ((int[])xyp1arr)[j];
+ }
+
+ // If all four samples are identical, there's no need to
+ // calculate anything
+ if (xy == xp1 && xy == yp1 && xy == xyp1)
+ result[j] = xy;
+
+ // Run bilinear interpolation formula
+ else
+ result[j] = (xy * (1-xdiff) + xp1 * xdiff)
+ * (1-ydiff)
+ + (yp1 * (1-xdiff) + xyp1 * xdiff)
+ * ydiff;
+ }
+
+ dst.setPixel((int)dpts[i] + dst.getMinX(),
+ (int)dpts[i+1] + dst.getMinY(),
+ result);
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform bicubic filtering
+ * based on http://local.wasp.uwa.edu.au/~pbourke/colour/bicubic/
+ *
+ * @param src the source raster
+ * @param dst the destination raster
+ * @param dpts array of points on the destination raster
+ * @param pts array of corresponding points on the source raster
+ */
+ private void filterBicubic(Raster src, WritableRaster dst, double[] dpts,
+ double[] pts)
+ {
+ Rectangle srcbounds = src.getBounds();
+ double[] result = new double[src.getNumBands()];
+ Object pixels = null;
+
+ // For all points on the destination raster, perform bicubic interpolation
+ // from corrosponding source points
+ for (int i = 0; i < dpts.length; i += 2)
+ {
+ if (srcbounds.contains((int) Math.round(pts[i]) + src.getMinX(),
+ (int) Math.round(pts[i + 1]) + src.getMinY()))
+ {
+ int x = (int) Math.floor(pts[i] + src.getMinX());
+ int y = (int) Math.floor(pts[i + 1] + src.getMinY());
+ double dx = pts[i] + src.getMinX() - x;
+ double dy = pts[i + 1] + src.getMinY() - y;
+ Arrays.fill(result, 0);
+
+ for (int m = - 1; m < 3; m++)
+ for (int n = - 1; n < 3; n++)
+ {
+ // R(x) = ( P(x+2)^3 - 4 P(x+1)^3 + 6 P(x)^3 - 4 P(x-1)^3 ) / 6
+ double r1 = 0;
+ double r2 = 0;
+
+ // Calculate R(m - dx)
+ double rx = m - dx + 2;
+ r1 += rx * rx * rx;
+
+ rx = m - dx + 1;
+ if (rx > 0)
+ r1 -= 4 * rx * rx * rx;
+
+ rx = m - dx;
+ if (rx > 0)
+ r1 += 6 * rx * rx * rx;
+
+ rx = m - dx - 1;
+ if (rx > 0)
+ r1 -= 4 * rx * rx * rx;
+
+ r1 /= 6;
+
+ // Calculate R(dy - n);
+ rx = dy - n + 2;
+ if (rx > 0)
+ r2 += rx * rx * rx;
+
+ rx = dy - n + 1;
+ if (rx > 0)
+ r2 -= 4 * rx * rx * rx;
+
+ rx = dy - n;
+ if (rx > 0)
+ r2 += 6 * rx * rx * rx;
+
+ rx = dy - n - 1;
+ if (rx > 0)
+ r2 -= 4 * rx * rx * rx;
+
+ r2 /= 6;
+
+ // Calculate F(i+m, j+n) R(m - dx) R(dy - n)
+ // Check corner cases
+ int srcX = x + m;
+ if (srcX >= src.getMinX() + src.getWidth())
+ srcX = src.getMinX() + src.getWidth() - 1;
+ else if (srcX < src.getMinX())
+ srcX = src.getMinX();
+
+ int srcY = y + n;
+ if (srcY >= src.getMinY() + src.getHeight())
+ srcY = src.getMinY() + src.getHeight() - 1;
+ else if (srcY < src.getMinY())
+ srcY = src.getMinY();
+
+ // Calculate once for each band, using the smallest
+ // datatype possible
+ if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+ || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+ {
+ pixels = src.getPixel(srcX, srcY, (double[])pixels);
+ for (int j = 0; j < result.length; j++)
+ result[j] += ((double[])pixels)[j] * r1 * r2;
+ }
+ else
+ {
+ pixels = src.getPixel(srcX, srcY, (int[])pixels);
+ for (int j = 0; j < result.length; j++)
+ result[j] += ((int[])pixels)[j] * r1 * r2;
+ }
+ }
+
+ // Put it all together
+ dst.setPixel((int)dpts[i] + dst.getMinX(),
+ (int)dpts[i+1] + dst.getMinY(),
+ result);
+ }
+ }
+ }
}