This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0a53a466e34dcb3ab390f4f1313aff3408215985 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Feb 28 12:17:02 2025 +0100 Add a safety against singular matrices read from GeoTIFF files. --- .../sis/referencing/operation/matrix/Matrices.java | 62 ++++++++++++++++++---- .../referencing/operation/matrix/MatricesTest.java | 16 ++++++ .../geotiff/reader/GridGeometryBuilder.java | 13 ++++- .../sis/storage/geotiff/writer/GeoEncoder.java | 6 ++- 4 files changed, 84 insertions(+), 13 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java index f058efe7bf..85960cec2e 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java @@ -75,7 +75,7 @@ import org.opengis.coordinate.MismatchedDimensionException; * </ul> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.4 + * @version 1.5 * * @see org.apache.sis.parameter.TensorParameters * @@ -891,6 +891,46 @@ public final class Matrices extends Static { return changed; } + /** + * Forces the matrix to have at least one non-zero coefficient in every row, assuming an affine transform. + * The last column (the translation terms) and the last row (the [0 0 … 1] terms) are ignored. + * If a row contains only zero values (ignoring the translation term), + * then this method sets one element of that row to the given {@code defaultValue}. + * That modification occurs in the first free column, i.e. a column having no non-zero value. + * If no such free column is found, then this method stops the operation and returns {@code false}. + * + * @param matrix the matrix in which to force non-zero scale factors. Will be modified in-place. + * @param defaultValue the scale factor to assign to rows that do not have a non-zero scale factor. + * @return {@code true} on success (including when this method does nothing), or + * {@code false} if this method cannot complete the operation. + * + * @since 1.5 + */ + public static boolean forceNonZeroScales(final Matrix matrix, final double defaultValue) { + final int numCol = matrix.getNumCol() - 1; + final int numRow = matrix.getNumRow() - 1; + int freeColumn = 0; +okay: for (int j=0; j<numRow; j++) { + for (int i=0; i<numCol; i++) { + if (matrix.getElement(j, i) != 0) { + continue okay; + } + } +search: while (freeColumn < numCol) { + for (int i=0; i<numRow; i++) { + if (matrix.getElement(i, freeColumn) != 0) { + freeColumn++; + continue search; + } + } + matrix.setElement(j, freeColumn++, defaultValue); + continue okay; + } + return false; + } + return true; + } + /** * Returns {@code true} if the given matrix is likely to use extended precision. * A value of {@code true} is not a guarantee that the matrix uses extended precision, @@ -1000,7 +1040,7 @@ public final class Matrices extends Static { if (numRow == numCol) { return Solver.inverse(matrix); } - final NonSquareMatrix result = new NonSquareMatrix(numRow, numCol, false); + final var result = new NonSquareMatrix(numRow, numCol, false); result.setMatrix(matrix); return result.inverse(); } @@ -1221,13 +1261,13 @@ public final class Matrices extends Static { public static String toString(final Matrix matrix) { final int numRow = matrix.getNumRow(); final int numCol = matrix.getNumCol(); - final String[] elements = new String [numCol * numRow]; // String representation of matrix values. - final boolean[] noFractionDigits = new boolean[numCol * numRow]; // Whether to remove the trailing ".0" for a given number. - final boolean[] hasDecimalSeparator = new boolean[numCol]; // Whether the column has at least one number where fraction digits are shown. - final byte[] maximumFractionDigits = new byte [numCol]; // The greatest number of fraction digits found in a column. - final byte[] maximumPaddingZeros = new byte [numCol * numRow]; // Maximal number of zeros that we can append before to exceed the IEEE 754 accuracy. - final byte[] widthBeforeFraction = new byte [numCol]; // Number of characters before the fraction digits: spacing + ('-') + integerDigits + '.' - final byte[] columnWidth = new byte [numCol]; // Total column width. + final var elements = new String [numCol * numRow]; // String representation of matrix values. + final var noFractionDigits = new boolean[numCol * numRow]; // Whether to remove the trailing ".0" for a given number. + final var hasDecimalSeparator = new boolean[numCol]; // Whether the column has at least one number where fraction digits are shown. + final var maximumFractionDigits = new byte [numCol]; // The greatest number of fraction digits found in a column. + final var maximumPaddingZeros = new byte [numCol * numRow]; // Maximal number of zeros that we can append before to exceed the IEEE 754 accuracy. + final var widthBeforeFraction = new byte [numCol]; // Number of characters before the fraction digits: spacing + ('-') + integerDigits + '.' + final var columnWidth = new byte [numCol]; // Total column width. int totalWidth = 1; /* * Create now the string representation of all matrix elements and measure the width @@ -1290,9 +1330,9 @@ public final class Matrices extends Static { * Now append the formatted elements with the appropriate number of spaces before each value, * and trailling zeros after each value except ±0, ±1, NaN and infinities. */ - final String lineSeparator = System.lineSeparator(); + final String lineSeparator = System.lineSeparator(); final CharSequence whiteLine = CharSequences.spaces(totalWidth); - final StringBuilder buffer = new StringBuilder((totalWidth + 2 + lineSeparator.length()) * (numRow + 2)); + final var buffer = new StringBuilder((totalWidth + 2 + lineSeparator.length()) * (numRow + 2)); buffer.append('┌').append(whiteLine).append('┐').append(lineSeparator); int flatIndex = 0; for (int j=0; j<numRow; j++) { diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java index 29b28f520f..252ae8862c 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java @@ -442,6 +442,22 @@ public final class MatricesTest extends TestCase { }), matrix); } + /** + * Tests {@link Matrices#forceNonZeroScales(Matrix, double)}. + */ + public void testForceNonZeroScales() { + MatrixSIS matrix = Matrices.create(4, 4, new double[] { + 2, 0, 0, 8, + 0, 0, 4, 7, + 0, 0, 0, 6, + 0, 0, 0, 1 + }); + MatrixSIS expected = matrix.clone(); + expected.setElement(2, 1, 3); + assertTrue(Matrices.forceNonZeroScales(matrix, 3)); + assertEquals(expected, matrix); + } + /** * Tests {@link Matrices#equals(Matrix, Matrix, double, boolean)}. */ diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java index bb64c6df01..7a7366e899 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java @@ -25,6 +25,7 @@ import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.referencing.operation.TransformException; @@ -76,6 +77,14 @@ import org.apache.sis.math.Vector; * @author Martin Desruisseaux (Geomatys) */ public final class GridGeometryBuilder extends GeoKeysLoader { + /** + * Default scale factory to apply if a row in the model transformation contains only zero values. + * The matrix in a GeoTIFF file is always of size 4×4 even if the <abbr>CRS</abbr> is two-dimensional. + * In the latter case, the matrix row for the third dimension has only zero values. That row should be + * discarded when building the final {@link GridGeometry}, but there is sometime inconsistency between + * the number of <abbr>CRS</abbr> dimensions and which matrix rows have been assigned non-zero values. + */ + private static final double DEFAULT_SCALE_FACTOR = 1; // ╔════════════════════════════════════════════════════════════════════════════════╗ // ║ ║ @@ -300,7 +309,9 @@ public final class GridGeometryBuilder extends GeoKeysLoader { try { MathTransform gridToCRS = null; if (affine != null) { - gridToCRS = factory.createAffineTransform(Matrices.resizeAffine(affine, ++n, n)); + final Matrix m = Matrices.resizeAffine(affine, ++n, n); + Matrices.forceNonZeroScales(m, DEFAULT_SCALE_FACTOR); + gridToCRS = factory.createAffineTransform(m); } else if (modelTiePoints != null) { pixelIsPoint = true; gridToCRS = Localization.nonLinear(modelTiePoints); diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java index fc61bfaef5..a0d4db0378 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java @@ -97,7 +97,11 @@ public final class GeoEncoder { /** * Size of the model transformation matrix, in number of rows and columns. - * This size is fixed by the GeoTIFF specification. + * This size is fixed by the GeoTIFF specification. If some rows need to + * be added for filling the space, they will be filled with zero values + * (i.e., the matrix will be singular until the reader removes those rows). + * + * @see org.apache.sis.storage.geotiff.reader.GridGeometryBuilder#DEFAULT_SCALE_FACTOR */ private static final int MATRIX_SIZE = 4;