/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.referencing.operation.transform; import java.util.Arrays; import java.util.Objects; import java.io.Serializable; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import javax.media.jai.Warp; import javax.media.jai.WarpOpImage; import javax.media.jai.WarpPolynomial; import javax.media.jai.operator.WarpDescriptor; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; import org.geotoolkit.lang.Workaround; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.ArgumentChecks; import org.geotoolkit.parameter.Parameter; import org.geotoolkit.parameter.ParameterGroup; import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D; import org.apache.sis.referencing.operation.transform.IterationStrategy; import static org.geotoolkit.util.Utilities.hash; import static org.geotoolkit.referencing.operation.provider.WarpPolynomial.*; /** * Wraps an arbitrary {@link Warp} object as a {@linkplain MathTransform2D two-dimensional transform}. * Calls to {@linkplain #transform(float[],int,float[],int,int) transform} methods are forwarded to * the {@link Warp#warpPoint(int,int,float[]) warpPoint} method, or something equivalent. This * implies that source coordinates may be rounded to nearest integers before the transformation * is applied. * <p> * This transform is typically used with {@linkplain org.geotoolkit.coverage.processing.operation.Resample * grid coverage "Resample" operation} for reprojecting an image. Source and destination coordinates * are usually pixel coordinates in source and target image, which is why this transform may use * integer arithmetic. * <p> * This math transform can be created alone (by invoking its public constructors directly), or it can * be created by a factory like {@link org.geotoolkit.referencing.operation.builder.LocalizationGrid}. * <p> * For more information on image warp, see * <A HREF="http://java.sun.com/products/java-media/jai/forDevelopers/jai1_0_1guide-unc/Geom-image-manip.doc.html">Geometric * Image Manipulation</A> in the <cite>Programming in Java Advanced Imaging</cite> guide. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Alessio Fabiani (Geosolutions) * @version 3.18 * * @see org.geotoolkit.referencing.operation.builder.LocalizationGrid#getPolynomialTransform(int) * @see Warp * @see WarpOpImage * @see WarpDescriptor * * @since 1.2 * @module */ public class WarpTransform2D extends AbstractMathTransform2D implements Serializable { /** * Serial number for inter-operability with different versions. */ private static final long serialVersionUID = -7949539694656719923L; /** * The maximal polynomial degree allowed. * * @since 2.4 */ public static final int MAX_DEGREE = 7; /** * The warp object. Transformations will be applied using the * {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. */ private final Warp warp; /** * The inverse math transform. */ private final WarpTransform2D inverse; /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using a polynomial warp * with the degree supplied in argument. The <em>minimal</em> number of points required for * each degree of warp are as follows: * <p> * <table border="1"> * <tr align="center" bgcolor="#EEEEFF"><th>Degree of Warp</th><th>Number of Points</th></tr> * <tr align="center"><td>1</td><td>3</td></tr> * <tr align="center"><td>2</td><td>6</td></tr> * <tr align="center"><td>3</td><td>10</td></tr> * <tr align="center"><td>4</td><td>15</td></tr> * <tr align="center"><td>5</td><td>21</td></tr> * <tr align="center"><td>6</td><td>28</td></tr> * <tr align="center"><td>7</td><td>36</td></tr> * </table> * * @param srcCoords Source coordinates. * @param dstCoords Destination coordinates. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Point2D[] srcCoords, final Point2D[] dstCoords, final int degree) { this(null, srcCoords, 0, null, dstCoords, 0, Math.min(srcCoords.length, dstCoords.length), degree); } /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using some polynomial warp * with the degree supplied in argument. * * @param srcBounds Bounding box of source coordinates, or {@code null} if unknown. * @param srcCoords Source coordinates. * @param srcOffset The initial entry of {@code srcCoords} to be used. * @param dstBounds Bounding box of destination coordinates, or {@code null} if unknown. * @param dstCoords Destination coordinates. * @param dstOffset The initial entry of {@code destCoords} to be used. * @param numCoords The number of coordinates from {@code srcCoords} and {@code destCoords} to be used. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Rectangle2D srcBounds, final Point2D[] srcCoords, final int srcOffset, final Rectangle2D dstBounds, final Point2D[] dstCoords, final int dstOffset, final int numCoords, final int degree) { this(srcBounds, toFloat(srcCoords, srcOffset, numCoords), 0, dstBounds, toFloat(dstCoords, dstOffset, numCoords), 0, numCoords, degree, false); } /** * Converts an array of points into an array of floats. * This is used internally for the above constructor only. */ private static float[] toFloat(final Point2D[] points, int offset, final int numCoords) { final float[] array = new float[numCoords * 2]; for (int i=0; i<array.length;) { final Point2D point = points[offset++]; array[i++] = (float) point.getX(); array[i++] = (float) point.getY(); } return array; } /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using some polynomial warp * with the degree supplied in argument. * * @param srcBounds Bounding box of source coordinates, or {@code null} if unknown. * @param srcCoords Source coordinates with <var>x</var> and <var>y</var> alternating. * @param srcOffset The initial entry of {@code srcCoords} to be used. * @param dstBounds Bounding box of destination coordinates, or {@code null} if unknown. * @param dstCoords Destination coordinates with <var>x</var> and <var>y</var> alternating. * @param dstOffset The initial entry of {@code destCoords} to be used. * @param numCoords The number of coordinates from {@code srcCoords} and {@code destCoords} to be used. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Rectangle2D srcBounds, final float[] srcCoords, final int srcOffset, final Rectangle2D dstBounds, final float[] dstCoords, final int dstOffset, final int numCoords, final int degree) { this(srcBounds, srcCoords, srcOffset, dstBounds, dstCoords, dstOffset, numCoords, degree, true); } /** * Work around for a bug in WarpPolynomial.createWarp(...). This constructor should move in * the one above when the {@code cloneCoords} argument will no longer be needed (after the * JAI bug get fixed). */ @Workaround(library="JAI", version="1.1.3") private WarpTransform2D(final Rectangle2D srcBounds, float[] srcCoords, int srcOffset, final Rectangle2D dstBounds, float[] dstCoords, int dstOffset, final int numCoords, final int degree, boolean cloneCoords) { final float preScaleX, preScaleY, postScaleX, postScaleY; if (srcBounds != null) { preScaleX = (float) srcBounds.getWidth(); preScaleY = (float) srcBounds.getHeight(); } else { preScaleX = getWidth(srcCoords, srcOffset , numCoords); preScaleY = getWidth(srcCoords, srcOffset+1, numCoords); } if (dstBounds != null) { postScaleX = (float) dstBounds.getWidth(); postScaleY = (float) dstBounds.getHeight(); } else { postScaleX = getWidth(dstCoords, dstOffset , numCoords); postScaleY = getWidth(dstCoords, dstOffset+1, numCoords); } /* * Workaround for a bug in WarpPolynomial.create(...): the later scale coordinates * according the scale values, but the 'preScale' and 'postScale' are interchanged. * When JAI bug will be fixed, delete all the following block until the next comment. */ final double scaleX = preScaleX / postScaleX; final double scaleY = preScaleY / postScaleY; if (scaleX!=1 || scaleY!=1) { final int n = numCoords*2; if (cloneCoords) { srcCoords = Arrays.copyOfRange(srcCoords, srcOffset, srcOffset + n); srcOffset = 0; dstCoords = Arrays.copyOfRange(dstCoords, dstOffset, dstOffset + n); dstOffset = 0; } for (int i=0; i<n;) { srcCoords[srcOffset + i ] /= scaleX; dstCoords[dstOffset + i++] *= scaleX; srcCoords[srcOffset + i ] /= scaleY; dstCoords[dstOffset + i++] *= scaleY; } } /* * Note: Warp semantic (transforms coordinates from destination to source) is the * opposite of MathTransform semantic (transforms coordinates from source to * destination). We have to interchange source and destination arrays for the * direct transform. */ warp = WarpPolynomial.createWarp(dstCoords, dstOffset, srcCoords, srcOffset, numCoords, 1/preScaleX, 1/preScaleY, postScaleX, postScaleY, degree); inverse = new WarpTransform2D( WarpPolynomial.createWarp(srcCoords, srcOffset, dstCoords, dstOffset, numCoords, 1/postScaleX, 1/postScaleY, preScaleX, preScaleY, degree), this); } /** * Returns the maximum minus the minimum ordinate in the specified array. * This is used internally for the above constructor only. */ private static float getWidth(final float[] array, int offset, int num) { float min = Float.POSITIVE_INFINITY; float max = Float.NEGATIVE_INFINITY; while (--num >= 0) { float value = array[offset]; if (value < min) min = value; if (value > max) max = value; offset += 2; } return max - min; } /** * Constructs a transform using the specified warp object. Transformations will be applied * using the {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. * * @param warp The image warp to wrap into a math transform. * @param inverse An image warp to uses for the {@linkplain #inverse inverse transform}, * or {@code null} in none. * * @see #create */ protected WarpTransform2D(final Warp warp, final Warp inverse) { ArgumentChecks.ensureNonNull("warp", warp); this.warp = warp; this.inverse = (inverse != null) ? new WarpTransform2D(inverse, this) : null; } /** * Constructs a transform using the specified warp object. This private constructor is used * for the construction of {@link #inverse} transform only. */ private WarpTransform2D(final Warp warp, final WarpTransform2D inverse) { this.warp = warp; this.inverse = inverse; } /** * Returns a transform using the specified warp object. Transformations will be applied * using the {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. * * @param warp The image warp to wrap into a math transform. * @return The transform for the given warp. */ public static MathTransform2D create(final Warp warp) { if (warp instanceof WarpAdapter) { return ((WarpAdapter) warp).getTransform(); } return new WarpTransform2D(warp, (Warp) null); } /** * Returns the {@link Warp} wrapped by this transform. Its {@link Warp#warpPoint(int,int,float[]) * Warp.warpPoint} method transforms coordinates from source to target CRS. Note that JAI's * {@linkplain WarpDescriptor warp operation} needs a warp object with the opposite semantic * (i.e. the image warp must transforms coordinates from target to source CRS). Consequently, * consider invoking <code>{@linkplain #inverse}.getWarp()</code> if the warp object is going * to be used in an image reprojection. * * @return The image warp from source to target CRS. */ public Warp getWarp() { return warp; } /** * Returns the parameter descriptors for this math transform. */ @Override public ParameterDescriptorGroup getParameterDescriptors() { return (warp instanceof WarpPolynomial) ? PARAMETERS : null; } /** * Returns the parameter values for this math transform. */ @Override public ParameterValueGroup getParameterValues() { if (warp instanceof WarpPolynomial) { final WarpPolynomial poly = (WarpPolynomial) warp; final ParameterValue<?>[] p = new ParameterValue<?>[7]; int c = 0; p[c++] = new Parameter<>(DEGREE, poly.getDegree()); p[c++] = new Parameter<>(X_COEFFS, poly.getXCoeffs()); p[c++] = new Parameter<>(Y_COEFFS, poly.getYCoeffs()); float s; if ((s=poly.getPreScaleX ()) != 1) p[c++] = new Parameter<>(PRE_SCALE_X, s); if ((s=poly.getPreScaleY ()) != 1) p[c++] = new Parameter<>(PRE_SCALE_Y, s); if ((s=poly.getPostScaleX()) != 1) p[c++] = new Parameter<>(POST_SCALE_X, s); if ((s=poly.getPostScaleY()) != 1) p[c++] = new Parameter<>(POST_SCALE_Y, s); return new ParameterGroup(getParameterDescriptors(), ArraysExt.resize(p, c)); } else { return null; } } /** * Tests if this transform is the identity transform. */ @Override public boolean isIdentity() { return false; } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). * * @param ptSrc the specified coordinate point to be transformed. * @param ptDst the specified coordinate point that stores the result of transforming * {@code ptSrc}, or {@code null}. * @return the coordinate point after transforming {@code ptSrc} and storing the result in * {@code ptDst}. */ @Override public Point2D transform(Point2D ptSrc, Point2D ptDst) { /* * We have to copy the coordinate in a temporary point object because we don't know * neither the ptSrc or ptDst type. Since mapDestPoint returns a clone of the point * given in argument, giving ptSrc directly would result in a lost of precision if * its type is java.awt.Point (for example), even if ptDst had a greater precision. * * There is also an other reason for creating a temporary object: * JAI's Warp is designed for mapping pixel coordinates in J2SE's image. In JAI, pixel * coordinates map by definition to the pixel's upper left corner. But for interpolation * purpose, JAI needs to map pixel's center. This introduce a shift of 0.5, which is * documented (for example) in WarpAffine.mapDestPoint(Point2D). */ ptSrc = new PointDouble(ptSrc.getX() - 0.5, ptSrc.getY() - 0.5); final Point2D result = warp.mapDestPoint(ptSrc); result.setLocation(result.getX() + 0.5, result.getY() + 0.5); if (ptDst == null) { // Do not returns 'result' directly, since it has tricked 'clone()' method. ptDst = new Point2D.Float(); } ptDst.setLocation(result); return ptDst; } @Override public Matrix derivative(Point2D point) throws TransformException { throw new TransformException("Derivative not implemented yet."); } /** * Transforms a single source coordinate (usually pixel indices) into destination coordinate * (usually "real world" coordinates). * * @since 3.20 (derived from 3.00) */ @Override public Matrix transform(final double[] srcPts, final int srcOff, final double[] dstPts, final int dstOff, final boolean derivate) throws TransformException { final Matrix derivative = derivate ? derivative( new Point2D.Double(srcPts[srcOff], srcPts[srcOff+1])) : null; if (dstPts != null) { transform(srcPts, srcOff, dstPts, dstOff, 1); } return derivative; } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ @Override public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) { int postIncrement = 0; if (srcPts == dstPts) { switch (IterationStrategy.suggest(srcOff, 2, dstOff, 2, numPts)) { case ASCENDING: { break; } case DESCENDING: { srcOff += (numPts-1) * 2; dstOff += (numPts-1) * 2; postIncrement = -4; break; } default: { srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*2); srcOff = 0; break; } } } final Point2D.Double ptSrc = new PointDouble(); while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = result.getX() + 0.5; dstPts[dstOff++] = result.getY() + 0.5; dstOff += postIncrement; } } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ @Override public void transform(float[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) { int postIncrement = 0; if (srcPts == dstPts) { switch (IterationStrategy.suggest(srcOff, 2, dstOff, 2, numPts)) { case ASCENDING: { break; } case DESCENDING: { srcOff += (numPts-1) * 2; dstOff += (numPts-1) * 2; postIncrement = -4; break; } default: { srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*2); srcOff = 0; break; } } } final Point2D.Float ptSrc = new PointFloat(); while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5f; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5f; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = (float) (result.getX() + 0.5); dstPts[dstOff++] = (float) (result.getY() + 0.5); dstOff += postIncrement; } } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ @Override public void transform(double[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) { final Point2D.Double ptSrc = new PointDouble(); while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = (float) (result.getX() + 0.5); dstPts[dstOff++] = (float) (result.getY() + 0.5); } } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ @Override public void transform(float[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) { final Point2D.Double ptSrc = new PointDouble(); while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = result.getX() + 0.5; dstPts[dstOff++] = result.getY() + 0.5; } } /** * Returns the inverse transform. * * @throws NoninvertibleTransformException if no inverse warp were specified at construction time. */ @Override public MathTransform2D inverse() throws NoninvertibleTransformException { if (inverse != null) { return inverse; } else { return super.inverse(); } } /** * {@inheritDoc} */ @Override protected int computeHashCode() { return hash(warp, super.computeHashCode()); } /** * Compares this transform with the specified object for equality. */ @Override public boolean equals(final Object object, final ComparisonMode mode) { if (object == this) { // Slight optimization return true; } if (super.equals(object, mode)) { final WarpTransform2D that = (WarpTransform2D) object; return Objects.equals(this.warp, that.warp); } return false; } /** * A {@code Point2D.Float} that returns itself when {@link #clone} is invoked. * This trick is used for avoiding the creation of thousands of temporary objects * when transforming an array of points using {@link Warp#mapDestPoint}. */ @SuppressWarnings("serial") private static final class PointFloat extends Point2D.Float { @Override public PointFloat clone() { return this; } } /** * A {@code Point2D.Double} that returns itself when {@link #clone} is invoked. * This trick is used for avoiding the creation of thousands of temporary objects * when transforming an array of points using {@link Warp#mapDestPoint}. */ @SuppressWarnings("serial") private static final class PointDouble extends Point2D.Double { public PointDouble() { super(); } public PointDouble(double x, double y) { super(x,y); } @Override public PointDouble clone() { return this; } } }