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 2d122d3cdf9deedb5d1a7029159ef6ca50534f26 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Aug 13 18:37:01 2021 +0200 Add a `GridDerivation.maximumSubsampling(…)` method and use it in `TiledGridResource` for disabling subsamplings on axes where it is not supported. --- .../apache/sis/coverage/grid/GridDerivation.java | 223 +++++++++++++-------- .../sis/coverage/grid/GridDerivationTest.java | 32 ++- .../sis/internal/storage/TiledGridResource.java | 16 +- .../apache/sis/test/storage/SubsampledImage.java | 63 +++--- 4 files changed, 224 insertions(+), 110 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java index 0c9c26a..6ff2ce0 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java @@ -126,6 +126,13 @@ public class GridDerivation { */ private int[] chunkSize; + /** + * The maximum subsampling values (inclusive), or {@code null} if none. + * + * @see #maximumSubsampling(int...) + */ + private int[] maximumSubsampling; + // ──────── FIELDS COMPUTED BY METHODS IN THIS CLASS ────────────────────────────────────────────────────────────── /** @@ -308,20 +315,8 @@ public class GridDerivation { * @see GridExtent#expand(long...) */ public GridDerivation margin(final int... cellCounts) { - ArgumentChecks.ensureNonNull("cellCounts", cellCounts); ensureSubgridNotSet(); - int[] margin = null; - for (int i=cellCounts.length; --i >= 0;) { - final int n = cellCounts[i]; - if (n != 0) { - ArgumentChecks.ensurePositive("cellCounts", n); - if (margin == null) { - margin = new int[i+1]; - } - margin[i] = n; - } - } - this.margin = margin; // Set only on success. We want null if all margin values are 0. + margin = validateCellCounts("cellCounts", cellCounts, 0); return this; } @@ -351,22 +346,63 @@ public class GridDerivation { * @since 1.1 */ public GridDerivation chunkSize(final int... cellCounts) { - ArgumentChecks.ensureNonNull("cellCounts", cellCounts); ensureSubgridNotSet(); - int[] chunkSize = null; - for (int i=cellCounts.length; --i >= 0;) { - final int n = cellCounts[i]; - if (n != 1) { - ArgumentChecks.ensureStrictlyPositive("cellCounts", n); - if (chunkSize == null) { - chunkSize = new int[i+1]; - Arrays.fill(chunkSize, 1); + chunkSize = validateCellCounts("cellCounts", cellCounts, 1); + return this; + } + + /** + * Specifies the maximum subsampling values (inclusive) for each dimension. + * If a subsampling value is greater than a specified value in the corresponding dimension, + * the subsampling will be clamped to the specified maximal value. + * Setting a maximum value of 1 in a dimension is equivalent to disabling subsampling in that dimension. + * + * <p>If this method is never invoked, then there is no maximum value. + * If this method is invoked too late, an {@link IllegalStateException} is thrown. + * If the {@code cellCounts} array length is shorter than the grid dimension, + * then all missing dimensions have no maximum value.</p> + * + * @param subsampling maximal subsampling values (inclusive). + * @return {@code this} for method call chaining. + * @throws IllegalArgumentException if a value is zero or negative. + * @throws IllegalStateException if {@link #subgrid(Envelope, double...)} or {@link #slice(DirectPosition)} + * has already been invoked. + * + * @since 1.1 + */ + public GridDerivation maximumSubsampling(final int... subsampling) { + ensureSubgridNotSet(); + maximumSubsampling = validateCellCounts("subsampling", subsampling, Integer.MAX_VALUE); + return this; + } + + /** + * Returns a copy of the {@code values} array with trailing {@code defaultValue} trimmed. + * Returns {@code null} if all values are trimmed. This method verifies that values are valid. + * + * @param property argument name to use in error message in case of errors. + * @param values user-supplied values. + * @return values to save in {@link GridDerivation}. + */ + private static int[] validateCellCounts(final String property, final int[] values, final int defaultValue) { + ArgumentChecks.ensureNonNull(property, values); + int[] copy = null; + for (int i=values.length; --i >= 0;) { + final int n = values[i]; + if (n != defaultValue) { + if (defaultValue == 0) { + ArgumentChecks.ensurePositive(property, n); + } else { + ArgumentChecks.ensureStrictlyPositive(property, n); + } + if (copy == null) { + copy = new int[i+1]; + Arrays.fill(copy, defaultValue); } - chunkSize[i] = n; + copy[i] = n; } } - this.chunkSize = chunkSize; // Set only on success. We want null if all size values are 1. - return this; + return copy; } /** @@ -531,44 +567,45 @@ public class GridDerivation { if (areaOfInterest.isEnvelopeOnly()) { return subgrid(areaOfInterest.envelope, (double[]) null); } + final double[] scales; if (areaOfInterest.isExtentOnly()) { - int[] subsampling = null; - if (areaOfInterest.resolution != null) { - /* - * In principle `resolution` is always null here because it is computed from `gridToCRS`, - * which is null (otherwise `isExtentOnly()` would have been false). However an exception - * to this rule happens if `areaOfInterest` has been computed by another `GridDerivation`, - * in which case the resolution requested by user is saved even when `gridToCRS` is null. - * In that case the resolution is relative to the base grid of the other `GridDerivation`. - * Note however that the `resolution` field is only an approximation (the exact transform - * would have been stored in `gridToCRS` if it was non-null) and the subsampling offsets - * are lost (they would also have been stored in `gridToCRS`). - */ - subsampling = new int[areaOfInterest.resolution.length]; - for (int i=0; i<subsampling.length; i++) { - subsampling[i] = roundSubsampling(areaOfInterest.resolution[i], i); - } + if (baseExtent != null) { + baseExtent = baseExtent.intersect(areaOfInterest.extent); + subGridSetter = "subgrid"; } - return subgrid(areaOfInterest.extent, subsampling); - } - subGridSetter = "subgrid"; - if (base.equals(areaOfInterest)) { - return this; - } - final MathTransform mapCenters; - final GridExtent domain = areaOfInterest.getExtent(); // May throw IncompleteGridGeometryException. - try { - final CoordinateOperationFinder finder = new CoordinateOperationFinder(areaOfInterest, base); - final MathTransform mapCorners = finder.gridToGrid(); - finder.setAnchor(PixelInCell.CELL_CENTER); - finder.nowraparound(); - mapCenters = finder.gridToGrid(); // We will use only the scale factors. - setBaseExtentClipped(domain.toCRS(mapCorners, mapCenters, null)); - } catch (FactoryException | TransformException e) { - throw new IllegalGridGeometryException(e, "areaOfInterest"); - } - if (baseExtent != base.extent && baseExtent.equals(areaOfInterest.extent)) { - baseExtent = areaOfInterest.extent; // Share common instance. + scales = areaOfInterest.resolution; + /* + * In principle `resolution` is always null here because it is computed from `gridToCRS`, + * which is null (otherwise `isExtentOnly()` would have been false). However an exception + * to this rule happens if `areaOfInterest` has been computed by another `GridDerivation`, + * in which case the resolution requested by user is saved even when `gridToCRS` is null. + * In that case the resolution is relative to the base grid of the other `GridDerivation`. + * Note however that the `resolution` field is only an approximation (the exact transform + * would have been stored in `gridToCRS` if it was non-null) and the subsampling offsets + * are lost (they would also have been stored in `gridToCRS`). + */ + } else { + if (base.equals(areaOfInterest)) { + return this; + } + final MathTransform mapCenters; + final GridExtent domain = areaOfInterest.getExtent(); // May throw IncompleteGridGeometryException. + try { + final CoordinateOperationFinder finder = new CoordinateOperationFinder(areaOfInterest, base); + final MathTransform mapCorners = finder.gridToGrid(); + finder.setAnchor(PixelInCell.CELL_CENTER); + finder.nowraparound(); + mapCenters = finder.gridToGrid(); // We will use only the scale factors. + setBaseExtentClipped(domain.toCRS(mapCorners, mapCenters, null)); + subGridSetter = "subgrid"; + } catch (FactoryException | TransformException e) { + throw new IllegalGridGeometryException(e, "areaOfInterest"); + } + if (baseExtent != base.extent && baseExtent.equals(areaOfInterest.extent)) { + baseExtent = areaOfInterest.extent; // Share common instance. + } + // The `domain` extent must be the source of the `mapCenters` transform. + scales = GridGeometry.resolution(mapCenters, domain); } /* * The subsampling will determine the scale factors in the transform from the given desired grid geometry @@ -577,8 +614,6 @@ public class GridDerivation { * the way transforms are concatenated) as the ratio between the resolutions of the `areaOfInterest` and * `base` grid geometries, computed in the center of the area of interest. */ - // The `domain` extent must be the source of the `mapCenters` transform. - final double[] scales = GridGeometry.resolution(mapCenters, domain); if (scales == null) { return this; } @@ -732,8 +767,16 @@ public class GridDerivation { if (s > 1) { // Also for skipping NaN values. scaled = true; final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k; - final int accuracy = Math.max(0, Math.getExponent(indices.getSpan(i))) + 1; // Power of 2. - s = Math.scalb(Math.rint(Math.scalb(s, accuracy)), -accuracy); + if (chunkSize != null) { + s = roundSubsampling(s, i); // Include clamp to `maximumSubsampling`. + } else { + final int accuracy = Math.max(0, Math.getExponent(indices.getSpan(i))) + 1; // Power of 2. + s = Math.scalb(Math.rint(Math.scalb(s, accuracy)), -accuracy); + if (maximumSubsampling != null && i < maximumSubsampling.length) { + final double max = maximumSubsampling[i]; + if (s > max) s = max; + } + } indices.setRange(i, indices.getLower(i) / s, indices.getUpper(i) / s); } @@ -873,7 +916,7 @@ public class GridDerivation { * * @since 1.1 */ - public GridDerivation subgrid(final GridExtent areaOfInterest, final int... subsampling) { + public GridDerivation subgrid(final GridExtent areaOfInterest, int... subsampling) { ensureSubgridNotSet(); final int n = base.getDimension(); if (areaOfInterest != null) { @@ -883,11 +926,20 @@ public class GridDerivation { Errors.Keys.MismatchedDimension_3, "extent", n, actual)); } } - subGridSetter = "subgrid"; if (areaOfInterest != null && baseExtent != null) { baseExtent = baseExtent.intersect(areaOfInterest); + subGridSetter = "subgrid"; + } + if (subsampling == null) { + return this; } - return (subsampling != null) ? subsample(subsampling) : this; + if (chunkSize != null || maximumSubsampling != null) { + subsampling = subsampling.clone(); + for (int i=0; i<subsampling.length; i++) { + subsampling[i] = roundSubsampling(subsampling[i], i); + } + } + return subsample(subsampling); } /** @@ -903,7 +955,11 @@ public class GridDerivation { * </ul> * * The {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform is scaled accordingly - * in order to map approximately to the same {@linkplain GridGeometry#getEnvelope() envelope}. + * + * <h4>Preconditions</h4> + * This method assumes that subsampling are divisors of {@linkplain #chunkSize(int...) chunk sizes} + * and are not greater than the {@linkplain #maximumSubsampling(int...) maximum subsampling}. + * It is caller responsibility to ensure that those preconditions are met. * * @param subsampling the subsampling to apply on each grid dimension. All values shall be greater than zero. * If the array length is shorter than the number of dimensions, missing values are assumed to be 1. @@ -919,7 +975,6 @@ public class GridDerivation { */ @Deprecated // TODO: make private (do not delete) after next SIS release. - // This method assumes that subsampling are divisors of chunk sizes. public GridDerivation subsample(final int... subsampling) { ArgumentChecks.ensureNonNull("subsampling", subsampling); if (toBase != null) { @@ -1276,6 +1331,7 @@ public class GridDerivation { /** * Rounds a subsampling value according the current {@link RoundingMode}. + * If {@link #maximumSubsampling} values have been specified, then subsampling is clamped if needed. * If a {@link #chunkSize} has been specified, then the subsampling will be a divisor of that size. * This is necessary for avoiding a drift of subsampled pixel coordinates computed from tile coordinates. * @@ -1302,13 +1358,18 @@ public class GridDerivation { * @param dimension the dimension of the scale factor to round. */ private int roundSubsampling(final double scale, final int dimension) { - final int subsampling; + int subsampling; switch (rounding) { default: throw new AssertionError(rounding); case NEAREST: subsampling = (int) Math.min(Math.round(scale), Integer.MAX_VALUE); break; case CONTAINED: subsampling = (int) Math.ceil(scale - tolerance(dimension)); break; case ENCLOSING: subsampling = (int) (scale + tolerance(dimension)); break; } + int max = Integer.MAX_VALUE; + if (maximumSubsampling != null && dimension < maximumSubsampling.length) { + max = maximumSubsampling[dimension]; + if (subsampling > max) subsampling = max; + } if (subsampling <= 1) { return 1; } @@ -1326,22 +1387,16 @@ public class GridDerivation { * It is better to know now if there is any problem here. */ int s = divisors[i-1]; - if (i < divisors.length) { - switch (rounding) { - case CONTAINED: { - s = divisors[i]; - break; - } - case NEAREST: { - final int above = divisors[i]; - if (above - r < r - s) { - s = above; - } - break; + final int offset = subsampling - r; + if (rounding != GridRoundingMode.ENCLOSING && i < divisors.length) { + final int above = divisors[i]; + if (max == Integer.MAX_VALUE || above <= max - offset) { + if (rounding == GridRoundingMode.CONTAINED || above - r < r - s) { + s = above; } } } - return s + (subsampling - r); + return s + offset; } } return subsampling; diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java index 28887b0..17932ba 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java @@ -23,6 +23,7 @@ import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.apache.sis.geometry.Envelope2D; @@ -337,7 +338,14 @@ public final strictfp class GridDerivationTest extends TestCase { final GridDerivation derivation = grid.derive().margin(4, 3).chunkSize(5, 10); grid = derivation.subgrid(envelope, 2, 1).build(); assertExtentEquals(new long[] {55, 0}, new long[] {204, 39}, derivation.getIntersection()); - assertExtentEquals(new long[] {14, 0}, new long[] { 50, 9}, grid.getExtent()); + assertExtentEquals(new long[] {11, 0}, new long[] { 40, 7}, grid.getExtent()); + assertArrayEquals(new double[] {2.5, 1.25}, grid.getResolution(false), STRICT); + /* + * Without chunk size, the resolution would have been {2,1} which correspond to a subsampling of {4,4}. + * But because of the chunk size, the subsampling have been rounded to {5,5} which correspond to above + * resolution. The grid extent, which would have been x:[14 … 50] and y:[0 … 9], is also affected by + * the subsampling adjustment. + */ } /** @@ -374,6 +382,28 @@ public final strictfp class GridDerivationTest extends TestCase { } /** + * Tests {@link GridDerivation#subgrid(GridGeometry)} with a maximum subsampling value. + */ + @Test + public void testSubgridWithMaximumSubsampling() { + GridGeometry query = grid( 80, -3, 110, 55, 100, -300); // Same as above test. + GridGeometry base = grid(2000, -1000, 9000, 8000, 2, -1); + GridDerivation change = base.derive().chunkSize(390, 70).maximumSubsampling(25, 100).subgrid(query); + GridGeometry result = change.build(); + Matrix toCRS = MathTransforms.getMatrix(result.getGridToCRS(PixelInCell.CELL_CORNER)); + /* + * Subsampling values checked below shall be equal or smaller + * than the values given to `maximumSubsampling(…)`. + */ + assertArrayEquals(new int[] {15, 84}, change.getSubsampling()); + assertArrayEquals(new int[] { 0, -70}, change.getSubsamplingOffsets()); + assertMatrixEquals("gridToCRS", new Matrix3( + 30, 0, 200, + 0, -84, 570, + 0, 0, 1), toCRS, STRICT); + } + + /** * Tests {@link GridDerivation#subgrid(GridExtent)} with a null "grid to CRS" transform. */ @Test diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java index e5d4c10..9fdf1a2 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java @@ -162,6 +162,19 @@ public abstract class TiledGridResource extends AbstractGridResource { } /** + * Returns the maximal subsampling supported in the given dimension. + * The default implementation puts no limit if {@code getAtomSize(dimension)} is 1, + * and disables subsampling otherwise. + * + * @param dimension the dimension to test. + * @return the maximal subsampling supported in the given dimension. + * @throws DataStoreException if an error occurred while fetching the sample model. + */ + protected int getMaximumSubsampling(final int dimension) throws DataStoreException { + return getAtomSize(dimension) == 1 ? Integer.MAX_VALUE : 1; + } + + /** * Returns {@code true} if the reader can load only the requested bands and skip the other bands, * or {@code false} if the reader must load all bands. This value controls the amount of data to * be loaded by {@link #read(GridGeometry, int...)}: @@ -356,7 +369,8 @@ public abstract class TiledGridResource extends AbstractGridResource { if (tileWidth >= sourceExtent.getSize(0)) {tileWidth = getAtomSize(0); sharedCache = false;} if (tileHeight >= sourceExtent.getSize(1)) {tileHeight = getAtomSize(1); sharedCache = false;} final GridDerivation target = gridGeometry.derive().chunkSize(tileWidth, tileHeight) - .rounding(GridRoundingMode.ENCLOSING).subgrid(domain); + .maximumSubsampling(getMaximumSubsampling(0), getMaximumSubsampling(1)) + .rounding(GridRoundingMode.ENCLOSING).subgrid(domain); domain = target.build(); readExtent = target.getIntersection(); subsampling = target.getSubsampling(); diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/SubsampledImage.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/SubsampledImage.java index 088cb0b..fa4af0b 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/SubsampledImage.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/SubsampledImage.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Vector; import java.awt.image.Raster; import java.awt.image.ColorModel; +import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; @@ -36,7 +37,7 @@ import static org.junit.Assert.*; * * <h2>Limitations</h2> * <ul> - * <li>Sample model must be an instance of {@link PixelInterleavedSampleModel}.</li> + * <li>Sample model must be an instance of {@link PixelInterleavedSampleModel} or {@link MultiPixelPackedSampleModel}.</li> * <li>Subsampling must be a divisor of tile size, except in dimensions having only one tile.</li> * <li>Conversion from source coordinates to target coordinates is a division only, without subsampling offsets.</li> * </ul> @@ -84,33 +85,47 @@ final class SubsampledImage extends PlanarImage { this.source = source; this.subX = subX; this.subY = subY; - final PixelInterleavedSampleModel sm = (PixelInterleavedSampleModel) source.getSampleModel(); - final int pixelStride = sm.getPixelStride(); - final int scanlineStride = sm.getScanlineStride(); - final int offset = pixelStride*offX + scanlineStride*offY; - final int[] offsets = sm.getBandOffsets(); - /* - * Conversion from subsampled coordinate x′ to full resolution x is: - * - * x = (x′ × subsampling) + offset - * - * We simulate the offset addition by adding the value in the offset bands. - * PixelInterleavedSampleModel uses that value for computing array index as below: - * - * y*scanlineStride + x*pixelStride + bandOffsets[b] - */ - for (int i=0; i<offsets.length; i++) { - offsets[i] += offset; + final SampleModel sourceModel = source.getSampleModel(); + if (sourceModel instanceof PixelInterleavedSampleModel) { + final PixelInterleavedSampleModel sm = (PixelInterleavedSampleModel) sourceModel; + final int pixelStride = sm.getPixelStride(); + final int scanlineStride = sm.getScanlineStride(); + final int offset = pixelStride*offX + scanlineStride*offY; + final int[] offsets = sm.getBandOffsets(); + /* + * Conversion from subsampled coordinate x′ to full resolution x is: + * + * x = (x′ × subsampling) + offset + * + * We simulate the offset addition by adding the value in the offset bands. + * PixelInterleavedSampleModel uses that value for computing array index as below: + * + * y*scanlineStride + x*pixelStride + bandOffsets[b] + */ + for (int i=0; i<offsets.length; i++) { + offsets[i] += offset; + } + model = new PixelInterleavedSampleModel(sm.getDataType(), + divExclusive(sm.getWidth(), subX), + divExclusive(sm.getHeight(), subY), + pixelStride*subX, scanlineStride*subY, offsets); + } else if (sourceModel instanceof MultiPixelPackedSampleModel) { + final MultiPixelPackedSampleModel sm = (MultiPixelPackedSampleModel) sourceModel; + assertEquals("Subsampling on the X axis is not supported.", 1, subX); + model = new MultiPixelPackedSampleModel(sm.getDataType(), + divExclusive(sm.getWidth(), subX), + divExclusive(sm.getHeight(), subY), + sm.getPixelBitStride(), + sm.getScanlineStride() * subY, + sm.getDataBitOffset()); + } else { + throw new AssertionError("Unsupported sample model: " + sourceModel); } - model = new PixelInterleavedSampleModel(sm.getDataType(), - divExclusive(sm.getWidth(), subX), - divExclusive(sm.getHeight(), subY), - pixelStride*subX, scanlineStride*subY, offsets); /* * Conditions documented in class javadoc. */ - if (getNumXTiles() > 1) assertEquals(0, sm.getWidth() % subX); - if (getNumYTiles() > 1) assertEquals(0, sm.getHeight() % subY); + if (getNumXTiles() > 1) assertEquals(0, sourceModel.getWidth() % subX); + if (getNumYTiles() > 1) assertEquals(0, sourceModel.getHeight() % subY); } /**