blob: c46779b481a1998136f5dd1d4a24063b09369282 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.operation.transform;
import java.util.Arrays;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.referencing.privy.DirectPositionView;
import org.apache.sis.referencing.privy.ExtendedPrecisionMatrix;
import org.apache.sis.referencing.privy.Formulas;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.internal.Arithmetic;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.privy.Numerics;
import org.apache.sis.math.Fraction;
/**
* A usually affine, or otherwise a projective transform for the generic cases.
* This implementation is used for cases other than identity, translation only,
* scale only, 1D transform, 2D transform or axis swapping.
*
* <p>A projective transform is capable of mapping an arbitrary quadrilateral into another arbitrary quadrilateral,
* while preserving the straightness of lines. In the special case where the transform is affine, the parallelism of
* lines in the source is preserved in the output.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
*
* @see java.awt.geom.AffineTransform
*/
class ProjectiveTransform extends AbstractLinearTransform implements ExtendedPrecisionMatrix {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -4813507361303377148L;
/**
* The number of rows.
*/
private final int numRow;
/**
* The number of columns.
*/
private final int numCol;
/**
* Number of columns for coefficients other than scales and shears in the {@link #elt} array.
* There is one column for translation terms and one column for denominators.
*/
private static final int NON_SCALE_COLUMNS = 2;
/**
* Elements of the matrix augmented with one column containing common denominators.
* Column indices vary fastest.
*
* <h4>Denominator column</h4>
* An additional column is appended after the translation column.
* That column contains a denominator inferred from fractional values found on the row.
* All elements in the matrix row shall be multiplied by that denominator.
* The intend is to increase the chances that matrix elements are integer values.
* If no fractional value is found, the default denominator value is 1.
*/
private final double[] elt;
/**
* Same numbers as {@link #elt} excluding the denominators and with potentially extended precision.
* Zero values <em>shall</em> be represented by null elements.
*/
private final Number[] numbers;
/**
* Constructs a transform from the specified matrix.
* The matrix is usually square and affine, but this is not enforced.
* Non-affine transforms (e.g. projective transforms) are accepted but may not be invertible.
*
* @param matrix the matrix containing the coefficients of this projective transform.
*/
protected ProjectiveTransform(final Matrix matrix) {
numRow = matrix.getNumRow();
numCol = matrix.getNumCol();
/*
* Get the matrix elements as `Number` instances if possible.
* Those instances allow better precision than `double` values.
* Those numbers are available only through SIS-specific API.
*/
final boolean hasNumbers;
if (matrix instanceof ExtendedPrecisionMatrix) {
// Use `writable = true` because we need a copy protected from changes.
numbers = ((ExtendedPrecisionMatrix) matrix).getElementAsNumbers(true);
hasNumbers = true;
} else if (matrix instanceof MatrixSIS) {
final MatrixSIS m = (MatrixSIS) matrix;
numbers = new Number[numRow * numCol];
for (int i=0; i<numbers.length; i++) {
final Number e = m.getNumber(i / numCol, i % numCol);
if (!ExtendedPrecisionMatrix.isZero(e)) {
numbers[i] = e;
}
}
hasNumbers = true;
} else {
numbers = new Number[numRow * numCol];
hasNumbers = false;
}
/*
* Get the matrix elements as `double` values through the standard matrix API.
* We do that as a way to preserve negative zero, which is lost in `numbers`.
* The `numbers` array is either completed or compared for consistency.
*/
final int dstDim = numRow - 1; // Last row is [0 0 … 1] in an affine transform.
final int rowStride = numCol + 1; // The `elt` array has an extra column for denominators.
elt = new double[numRow * rowStride - 1]; // We don't need the denominator of the [0 0 … 1] row.
for (int k=0,i=0; i < numRow; i++) {
for (int j=0; j < numCol; j++) {
final double e = matrix.getElement(i, j);
elt[k++] = e; // May be negative zero.
if (hasNumbers) {
assert epsilonEqual(e, numbers[i*numCol + j]);
} else if (e != 0) {
final int v = (int) e; // Check if we can store as integer.
numbers[i*numCol + j] = (v == e) ? Integer.valueOf(v) : Double.valueOf(e);
}
}
if (i != dstDim) {
elt[k++] = 1;
} else {
assert k == elt.length;
}
}
/*
* At this point, this `ProjectiveTransform` is initialized and valid.
* Optionally update the elements values for reducing rounding errors
* when a denominator can be identified. This is where the denominator
* column in the `elt` array may get values different than 1.
*/
if (hasNumbers) {
for (int row=0; row < dstDim; row++) {
final int lower = numCol * row;
final int upper = numCol + lower;
final Integer denominator;
try {
Fraction sum = null;
for (int i=lower; i<upper; i++) {
final Number element = numbers[i];
if (element instanceof Fraction) {
final Fraction f = (Fraction) element;
sum = (sum != null) ? sum.add(f) : f;
}
}
if (sum == null) {
continue;
}
denominator = sum.denominator;
} catch (ArithmeticException e) {
continue;
}
int k = row * rowStride;
for (int i=lower; i<upper; i++) {
final Number element = Arithmetic.multiply(numbers[i], denominator);
if (element != null) {
elt[k] = element.doubleValue();
}
k++;
}
elt[k] = denominator.doubleValue();
}
}
}
/**
* Returns whether the given number are equal, with a tolerance of 1 ULP.
* A null {@code Number} is interpreted as zero.
*/
private static boolean epsilonEqual(final double e, final Number v) {
return Numerics.epsilonEqual(e, (v != null) ? v.doubleValue() : 0, Math.ulp(e));
}
/**
* If a more efficient implementation of this math transform can be used, returns it.
* Otherwise returns {@code this} unchanged.
*/
final LinearTransform optimize() {
if (numCol < numRow) {
return this;
}
final int n = (numRow - 1) * numCol;
for (int i = 0; i < numCol;) {
if (!isIdentity(numbers[n + i], ++i == numCol)) {
return this; // Transform is not affine (ignoring if square or not).
}
}
/*
* Note: we could check for CopyTransform case here, but this check is rather done in
* MathTransforms.linear(Matrix) in order to avoid ProjectiveTransform instantiation.
*
* Check which elements are non-zero. For ScaleTransform, they must be on the diagonal.
* For TranslationTransform, they must be in the last column. Note that the transform
* should not be identity (except for testing purpose) since the identity case should
* have been handled by MathTransforms.linear(Matrix) before to reach this point.
*/
boolean isScale = true; // ScaleTransform accepts non-square matrix.
boolean isTranslation = (numRow == numCol); // TranslationTransform is restricted to square matrix.
final int lastColumn = numCol - 1;
for (int i=0; i<n; i++) {
final Number element = numbers[i];
final int row = (i / numCol);
final int col = (i % numCol);
if (col != row) isScale &= (element == null);
if (col != lastColumn) isTranslation &= isIdentity(element, col == row);
if (!(isScale | isTranslation)) {
return this;
}
}
if (isTranslation) {
return new TranslationTransform(numRow, numbers);
} else {
return new ScaleTransform(numRow, numCol, numbers);
}
}
/**
* Returns {@code true} if the given element has the expected value of an identity matrix.
*
* @param element the element to test.
* @param diagonal whether the element is on the diagonal.
* @return whether the given element is an element of an identity matrix.
*
* @see #isIdentity()
*/
private static boolean isIdentity(final Number element, final boolean diagonal) {
return diagonal ? (element != null && element.doubleValue() == 1) : (element == null);
}
/**
* Gets the dimension of input points.
*/
@Override
public final int getSourceDimensions() {
return numCol - 1;
}
/**
* Gets the dimension of output points.
*/
@Override
public final int getTargetDimensions() {
return numRow - 1;
}
/**
* Gets the number of rows in the matrix.
*/
@Override
public final int getNumRow() {
return numRow;
}
/**
* Gets the number of columns in the matrix.
*/
@Override
public final int getNumCol() {
return numCol;
}
/**
* Returns a copy of all matrix elements in a flat, row-major array. Zero values <em>shall</em> be null.
* Callers can write in the returned array if and only if the {@code writable} argument is {@code true}.
*/
@Override
public final Number[] getElementAsNumbers(final boolean writable) {
return writable ? numbers.clone() : numbers;
}
/**
* Retrieves the value at the specified row and column of the matrix.
* If the value is zero, then this method <em>shall</em> return {@code null}.
*/
@Override
public final Number getElementOrNull(final int row, final int column) {
ArgumentChecks.ensureBetween("row", 0, numRow - 1, row);
ArgumentChecks.ensureBetween("column", 0, numCol - 1, column);
return numbers[row * numCol + column];
}
/**
* Returns the matrix element at the given index.
*/
@Override
public final double getElement(final int row, final int column) {
ArgumentChecks.ensureBetween("row", 0, numRow - 1, row);
ArgumentChecks.ensureBetween("column", 0, numCol - 1, column);
final Number element = numbers[row * numCol + column];
if (element != null) {
return element.doubleValue();
}
/*
* Fallback on the `elt` array only for 0 values for avoiding the need to divide by the denominator.
* Do not return a hard-coded 0 value in order to preserve the sign of negative zero.
*/
final int rowStride = numCol + 1;
return elt[row * rowStride + column];
}
/**
* Tests whether this transform does not move any points.
*
* <h4>Note</h4>
* This method should always returns {@code false}, because
* {@code MathTransforms.linear(…)} should have created specialized implementations for identity cases.
* Nevertheless we perform the full check as a safety, in case someone instantiated this class directly
* instead of using a factory method.
*/
@Override
public final boolean isIdentity() {
if (numRow != numCol) {
return false;
}
for (int i=0; i < numbers.length; i++) {
if (!isIdentity(numbers[i], (i / numCol) == (i % numCol))) {
return false;
}
}
return true;
}
/**
* Converts a single coordinate tuple in a list of coordinate values,
* and optionally computes the derivative at that location.
*
* @return {@inheritDoc}
*/
@Override
public final Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate)
{
if (!derivate) {
transform(srcPts, srcOff, dstPts, dstOff, 1);
return null;
}
// A non-null position is required in case this transform is non-affine.
Matrix derivative = derivative(new DirectPositionView.Double(srcPts, srcOff, getSourceDimensions()));
transform(srcPts, srcOff, dstPts, dstOff, 1);
return derivative;
}
/**
* Transforms an array of floating point coordinates by this matrix. Point coordinates must have a dimension
* equal to <code>{@link Matrix#getNumCol}-1</code>. For example, for square matrix of size 4×4, coordinate
* points are three-dimensional and stored in the arrays starting at the specified offset ({@code srcOff}) in
* the order
* <code>[x₀, y₀, z₀,
* x₁, y₁, z₁...,
* x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>.
*
* @param srcPts the array containing the source point coordinates.
* @param srcOff the offset to the first point to be transformed in the source array.
* @param dstPts the array into which the transformed point coordinates are returned.
* @param dstOff the offset to the location of the first transformed point that is stored in the
* destination array. The source and destination array sections can overlap.
* @param numPts the number of points to be transformed.
*/
@Override
public final void transform(double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) {
final int srcDim, dstDim;
int srcInc = srcDim = numCol - 1; // The last coordinate will be assumed equal to 1.
int dstInc = dstDim = numRow - 1;
if (srcPts == dstPts) {
switch (IterationStrategy.suggest(srcOff, srcDim, dstOff, dstDim, numPts)) {
case ASCENDING: {
break;
}
case DESCENDING: {
srcOff += (numPts - 1) * srcDim;
dstOff += (numPts - 1) * dstDim;
srcInc = -srcInc;
dstInc = -dstInc;
break;
}
default: {
srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcDim);
srcOff = 0;
break;
}
}
}
final double[] buffer = new double[numRow];
while (--numPts >= 0) {
int mix = 0;
for (int j=0; j<numRow; j++) {
double sum = elt[mix + srcDim]; // Initialize to translation term.
for (int i=0; i<srcDim; i++) {
final double e = elt[mix++];
if (e != 0) {
/*
* The purpose of the test for non-zero value is not performance (it is actually more likely
* to slow down the calculation), but to get a valid sum even if some source coordinates are NaN.
* This occurs when the ProjectiveTransform is used for excluding some dimensions, for example
* getting 2D points from 3D points. In such case, the fact that the excluded dimensions had
* NaN values should not force the retained dimensions to get NaN values.
*/
if (Formulas.USE_FMA) {
sum = Math.fma(srcPts[srcOff + i], e, sum);
} else {
sum += srcPts[srcOff + i] * e;
}
}
}
buffer[j] = sum;
mix += NON_SCALE_COLUMNS; // Skip the translation column and the denominator column.
}
int k = numCol;
final int rowStride = numCol + 1;
final double w = buffer[dstDim];
for (int j=0; j<dstDim; j++) {
// `w` is equal to 1 if the transform is affine.
dstPts[dstOff + j] = buffer[j] / (w * elt[k]);
k += rowStride;
}
srcOff += srcInc;
dstOff += dstInc;
}
}
/**
* Transforms an array of floating point coordinates by this matrix. Point coordinates must have a dimension
* equal to <code>{@link Matrix#getNumCol()} - 1</code>. For example, for square matrix of size 4×4, coordinate
* points are three-dimensional and stored in the arrays starting at the specified offset ({@code srcOff})
* in the order
* <code>[x₀, y₀, z₀,
* x₁, y₁, z₁...,
* x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>.
*
* @param srcPts the array containing the source point coordinates.
* @param srcOff the offset to the first point to be transformed in the source array.
* @param dstPts the array into which the transformed point coordinates are returned.
* @param dstOff the offset to the location of the first transformed point that is stored in the
* destination array. The source and destination array sections can overlap.
* @param numPts the number of points to be transformed.
*/
@Override
public final void transform(float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) {
final int srcDim, dstDim;
int srcInc = srcDim = numCol - 1;
int dstInc = dstDim = numRow - 1;
if (srcPts == dstPts) {
switch (IterationStrategy.suggest(srcOff, srcDim, dstOff, dstDim, numPts)) {
case ASCENDING: {
break;
}
case DESCENDING: {
srcOff += (numPts - 1) * srcDim;
dstOff += (numPts - 1) * dstDim;
srcInc = -srcInc;
dstInc = -dstInc;
break;
}
default: {
srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcDim);
srcOff = 0;
break;
}
}
}
final double[] buffer = new double[numRow];
while (--numPts >= 0) {
int mix = 0;
for (int j=0; j<numRow; j++) {
double sum = elt[mix + srcDim];
for (int i=0; i<srcDim; i++) {
final double e = elt[mix++];
if (e != 0) { // See comment in transform(double[], ...)
if (Formulas.USE_FMA) {
sum = Math.fma(srcPts[srcOff + i], e, sum);
} else {
sum += srcPts[srcOff + i] * e;
}
}
}
buffer[j] = sum;
mix += NON_SCALE_COLUMNS;
}
int k = numCol;
final int rowStride = numCol + 1;
final double w = buffer[dstDim];
for (int j=0; j<dstDim; j++) {
dstPts[dstOff + j] = (float) (buffer[j] / (w * elt[k]));
k += rowStride;
}
srcOff += srcInc;
dstOff += dstInc;
}
}
/**
* Transforms an array of floating point coordinates by this matrix.
*
* @param srcPts the array containing the source point coordinates.
* @param srcOff the offset to the first point to be transformed in the source array.
* @param dstPts the array into which the transformed point coordinates are returned.
* @param dstOff the offset to the location of the first transformed point that is stored in the destination array.
* @param numPts the number of points to be transformed.
*/
@Override
public final void transform(final double[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) {
final int srcDim = numCol - 1;
final int dstDim = numRow - 1;
final double[] buffer = new double[numRow];
while (--numPts >= 0) {
int mix = 0;
for (int j=0; j<numRow; j++) {
double sum = elt[mix + srcDim];
for (int i=0; i<srcDim; i++) {
final double e = elt[mix++];
if (e != 0) { // See comment in transform(double[], ...)
if (Formulas.USE_FMA) {
sum = Math.fma(srcPts[srcOff + i], e, sum);
} else {
sum += srcPts[srcOff + i] * e;
}
}
}
buffer[j] = sum;
mix += NON_SCALE_COLUMNS;
}
int k = numCol;
final int rowStride = numCol + 1;
final double w = buffer[dstDim];
for (int j=0; j<dstDim; j++) {
dstPts[dstOff++] = (float) (buffer[j] / (w * elt[k]));
k += rowStride;
}
srcOff += srcDim;
}
}
/**
* Transforms an array of floating point coordinates by this matrix.
*
* @param srcPts the array containing the source point coordinates.
* @param srcOff the offset to the first point to be transformed in the source array.
* @param dstPts the array into which the transformed point coordinates are returned.
* @param dstOff the offset to the location of the first transformed point that is stored in the destination array.
* @param numPts the number of points to be transformed.
*/
@Override
public final void transform(final float[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) {
final int srcDim = numCol - 1;
final int dstDim = numRow - 1;
final double[] buffer = new double[numRow];
while (--numPts >= 0) {
int mix = 0;
for (int j=0; j<numRow; j++) {
double sum = elt[mix + srcDim];
for (int i=0; i<srcDim; i++) {
final double e = elt[mix++];
if (e != 0) { // See comment in transform(double[], ...)
if (Formulas.USE_FMA) {
sum = Math.fma(srcPts[srcOff + i], e, sum);
} else {
sum += srcPts[srcOff + i] * e;
}
}
}
buffer[j] = sum;
mix += NON_SCALE_COLUMNS;
}
int k = numCol;
final int rowStride = numCol + 1;
final double w = buffer[dstDim];
for (int j=0; j<dstDim; j++) {
dstPts[dstOff++] = buffer[j] / (w * elt[k]);
k += rowStride;
}
srcOff += srcDim;
}
}
/**
* Gets the derivative of this transform at a point. For an affine transform,
* the derivative is the same everywhere and the given point is ignored.
* For a perspective transform, the given point is used.
*
* @param point the coordinate tuple where to evaluate the derivative.
*/
@Override
public final Matrix derivative(final DirectPosition point) {
final int srcDim = numCol - 1;
final int dstDim = numRow - 1;
final int rowStride = numCol + 1;
/*
* In the `transform(…)` method, all coordinate values are divided by a `w` coefficient
* which depends on the position. We need to reproduce that division here. Note that `w`
* coefficient is different than 1 only if the transform is non-affine.
*/
int mix = dstDim * rowStride;
double w = elt[mix + srcDim]; // `w` is equal to 1 if the transform is affine.
for (int i=0; i<srcDim; i++) {
final double e = elt[mix++];
if (e != 0) { // For avoiding NullPointerException if affine.
w += point.getCoordinate(i) * e;
}
}
/*
* In the usual affine case (w=1), the derivative is a copy of this matrix
* with last row and last column omitted.
*/
mix = 0;
int k = numCol;
final MatrixSIS matrix = Matrices.createZero(dstDim, srcDim);
for (int j=0; j<dstDim; j++) {
final double r = w * elt[k];
for (int i=0; i<srcDim; i++) {
matrix.setElement(j, i, elt[mix++] / r);
}
mix += NON_SCALE_COLUMNS; // Skip the translation column and the denominator column.
k += rowStride;
}
return matrix;
}
/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
protected int computeHashCode() {
return Arrays.hashCode(elt) + 31 * super.computeHashCode();
}
/**
* Compares this math transform with an object which is known to be an instance of the same class.
*/
@Override
protected boolean equalsSameClass(final Object object) {
final ProjectiveTransform that = (ProjectiveTransform) object;
return numRow == that.numRow &&
numCol == that.numCol &&
Arrays.equals(elt, that.elt) &&
Arrays.equals(numbers, that.numbers);
}
}