From commits-return-13426-apmail-sis-commits-archive=sis.apache.org@sis.apache.org Thu Apr 2 20:45:37 2020 Return-Path: X-Original-To: apmail-sis-commits-archive@www.apache.org Delivered-To: apmail-sis-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [207.244.88.153]) by minotaur.apache.org (Postfix) with SMTP id 6CA961936C for ; Thu, 2 Apr 2020 20:45:37 +0000 (UTC) Received: (qmail 2946 invoked by uid 500); 2 Apr 2020 20:45:37 -0000 Delivered-To: apmail-sis-commits-archive@sis.apache.org Received: (qmail 2923 invoked by uid 500); 2 Apr 2020 20:45:36 -0000 Mailing-List: contact commits-help@sis.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: sis-dev@sis.apache.org Delivered-To: mailing list commits@sis.apache.org Received: (qmail 2914 invoked by uid 99); 2 Apr 2020 20:45:36 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 02 Apr 2020 20:45:36 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 907E4819F7; Thu, 2 Apr 2020 20:45:36 +0000 (UTC) Date: Thu, 02 Apr 2020 20:45:36 +0000 To: "commits@sis.apache.org" Subject: [sis] branch geoapi-4.0 updated: Interpolation should be done on floating point values when they exist. This is required for handling correctly the missing values. MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Message-ID: <158586033653.31661.598481579152862525@gitbox.apache.org> From: desruisseaux@apache.org X-Git-Host: gitbox.apache.org X-Git-Repo: sis X-Git-Refname: refs/heads/geoapi-4.0 X-Git-Reftype: branch X-Git-Oldrev: bfe55833ff7f9641fec438fe4c02f54626933496 X-Git-Newrev: cf9962ab9b3e6d71fb03e5868027b5475abed9a8 X-Git-Rev: cf9962ab9b3e6d71fb03e5868027b5475abed9a8 X-Git-NotificationType: ref_changed_plus_diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated 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 The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new cf9962a Interpolation should be done on floating point values when they exist. This is required for handling correctly the missing values. cf9962a is described below commit cf9962ab9b3e6d71fb03e5868027b5475abed9a8 Author: Martin Desruisseaux AuthorDate: Thu Apr 2 15:40:56 2020 +0200 Interpolation should be done on floating point values when they exist. This is required for handling correctly the missing values. --- .../sis/coverage/grid/ConvertedGridCoverage.java | 2 +- .../sis/coverage/grid/GridCoverageProcessor.java | 42 +++++++++++---- .../org/apache/sis/coverage/grid/GridGeometry.java | 33 +++++++----- .../sis/coverage/grid/ResampledGridCoverage.java | 60 +++++++++++++++++++++- .../java/org/apache/sis/image/ImageProcessor.java | 16 ++++-- 5 files changed, 125 insertions(+), 28 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java index 9a655b2..6ce1f11 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java @@ -59,7 +59,7 @@ final class ConvertedGridCoverage extends GridCoverage { * The coverage containing source values. * Sample values will be converted from that coverage using the {@link #converters}. */ - private final GridCoverage source; + final GridCoverage source; /** * Conversions from {@link #source} values to converted values. diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java index 6423318..8b9f477 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -58,26 +58,27 @@ public class GridCoverageProcessor implements Cloneable { } /** - * The image processor to use for resampling operations. + * The processor to use for operations on two-dimensional slices. */ - private final ImageProcessor imageProcessor; + protected final ImageProcessor imageProcessor; /** - * Creates a new set of grid coverage operations with default configuration. + * Creates a new processor with default configuration. */ public GridCoverageProcessor() { imageProcessor = new ImageProcessor(); } /** - * Creates a new set of grid coverage operations with the given configuration. + * Creates a new processor initialized to the given configuration. * - * @param processor the processor to use for image operations. + * @param processor the processor to use for operations on two-dimensional slices. */ public GridCoverageProcessor(final ImageProcessor processor) { ArgumentChecks.ensureNonNull("processor", processor); - imageProcessor = processor; + imageProcessor = processor.clone(); } + /** * Returns the interpolation method to use for resampling operations. * @@ -120,6 +121,8 @@ public class GridCoverageProcessor implements Cloneable { * * * The interpolation method can be specified by {@link #setInterpolation(Interpolation)}. + * If the grid coverage values are themselves interpolated, this method tries to use the + * original data. The intent is to avoid adding interpolations on top of other interpolations. * * @param source the grid coverage to resample. * @param target the desired geometry of returned grid coverage. May be incomplete. @@ -131,15 +134,35 @@ public class GridCoverageProcessor implements Cloneable { public GridCoverage resample(GridCoverage source, final GridGeometry target) throws TransformException { ArgumentChecks.ensureNonNull("source", source); ArgumentChecks.ensureNonNull("target", target); + final boolean isConverted = source == source.forConvertedValues(true); /* * If the source coverage is already the result of a previous "resample" operation, * use the original data in order to avoid interpolating values that are already interpolated. */ - if (source instanceof ResampledGridCoverage) { - source = ((ResampledGridCoverage) source).source; + for (;;) { + if (ResampledGridCoverage.equivalent(source.getGridGeometry(), target)) { + return source; + } else if (source instanceof ResampledGridCoverage) { + source = ((ResampledGridCoverage) source).source; + } else if (source instanceof ConvertedGridCoverage) { + source = ((ConvertedGridCoverage) source).source; + } else { + break; + } + } + /* + * The projection are usually applied on floating-point values, in order + * to gets maximal precision and to handle correctly the special case of + * NaN values. However, we can apply the projection on integer values if + * the interpolation type is "nearest neighbor" since this is not really + * an interpolation. + */ + if (!Interpolation.NEAREST.equals(imageProcessor.getInterpolation())) { + source = source.forConvertedValues(true); } + final GridCoverage resampled; try { - return ResampledGridCoverage.create(source, target, imageProcessor); + resampled = ResampledGridCoverage.create(source, target, imageProcessor); } catch (IllegalGridGeometryException e) { final Throwable cause = e.getCause(); if (cause instanceof TransformException) { @@ -150,6 +173,7 @@ public class GridCoverageProcessor implements Cloneable { } catch (FactoryException e) { throw new TransformException(e); } + return resampled.forConvertedValues(isConverted); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java index bc2d7c8..c96e82c 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java @@ -1388,7 +1388,6 @@ public class GridGeometry implements LenientComparable, Serializable { * @since 1.1 */ @Override - @SuppressWarnings("fallthrough") public boolean equals(final Object object, final ComparisonMode mode) { if (object == this) { return true; @@ -1407,17 +1406,7 @@ public class GridGeometry implements LenientComparable, Serializable { Utilities.deepEquals(getCoordinateReferenceSystem(envelope), getCoordinateReferenceSystem(othenv), mode)) { - if (envelope != null) { - for (int i=envelope.getDimension(); --i >= 0;) { - final double ε = (resolution != null) ? resolution[i] * Numerics.COMPARISON_THRESHOLD : 0; - if (!MathFunctions.epsilonEqual(envelope.getLower(i), othenv.getLower(i), ε) || - !MathFunctions.epsilonEqual(envelope.getUpper(i), othenv.getUpper(i), ε)) - { - return false; - } - } - } - return true; + return equalsApproximately(othenv); } } } @@ -1425,6 +1414,26 @@ public class GridGeometry implements LenientComparable, Serializable { } /** + * Returns whether the given envelope is equal to this grid geometry envelope with a tolerance threshold + * computed from grid resolution. If this grid geometry has no envelope, then this method arbitrarily + * returns {@code true} (this unusual behavior is required by {@link #equals(Object, ComparisonMode)}). + */ + final boolean equalsApproximately(final ImmutableEnvelope othenv) { + if (envelope != null) { + for (int i=envelope.getDimension(); --i >= 0;) { + // Arbitrary threshold of ½ pixel. + final double ε = (resolution != null) ? resolution[i] * 0.5 : 0; + if (!MathFunctions.epsilonEqual(envelope.getLower(i), othenv.getLower(i), ε) || + !MathFunctions.epsilonEqual(envelope.getUpper(i), othenv.getUpper(i), ε)) + { + return false; + } + } + } + return true; + } + + /** * Returns the default set of flags to use for {@link #toString()} implementations. * Current implementation returns all flags, but future implementation may omit some * flags if experience suggests that they are too verbose in practice. diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java index d8b149a..1fe621d 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java @@ -41,6 +41,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS; import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.referencing.CRS; import org.apache.sis.util.ComparisonMode; +import org.apache.sis.util.Utilities; /** @@ -177,7 +178,64 @@ final class ResampledGridCoverage extends GridCoverage { } /** + * Checks if two grid geometries are equal, ignoring unspecified properties. If a geometry + * has no extent or no {@code gridToCRS} transform, the missing property is not compared. + * Same applies for the grid extent. + * + * @return {@code true} if the two geometries are equal, ignoring unspecified properties. + */ + static boolean equivalent(final GridGeometry sourceGG, final GridGeometry targetGG) { + return (!isDefined(sourceGG, targetGG, GridGeometry.EXTENT) + || Utilities.equalsIgnoreMetadata(sourceGG.getExtent(), + targetGG.getExtent())) + && (!isDefined(sourceGG, targetGG, GridGeometry.CRS) + || Utilities.equalsIgnoreMetadata(sourceGG.getCoordinateReferenceSystem(), + targetGG.getCoordinateReferenceSystem())) + && (!isDefined(sourceGG, targetGG, GridGeometry.GRID_TO_CRS) + || Utilities.equalsIgnoreMetadata(sourceGG.getGridToCRS(PixelInCell.CELL_CORNER), + targetGG.getGridToCRS(PixelInCell.CELL_CORNER)) + || Utilities.equalsIgnoreMetadata(sourceGG.getGridToCRS(PixelInCell.CELL_CENTER), // Its okay if only one is equal. + targetGG.getGridToCRS(PixelInCell.CELL_CENTER))) + && (!isDefined(sourceGG, targetGG, GridGeometry.ENVELOPE) + || isDefined(sourceGG, targetGG, GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS) // Compare only if not inferred. + || sourceGG.equalsApproximately(targetGG.envelope)); + } + + /** + * Returns whether the given property is defined in both grid geometries. + * + * @param property one of {@link GridGeometry} constants. + */ + private static boolean isDefined(final GridGeometry sourceGG, final GridGeometry targetGG, final int property) { + return targetGG.isDefined(property) && sourceGG.isDefined(property); + } + + /** * Implementation of {@link GridCoverageProcessor#resample(GridCoverage, GridGeometry)}. + * This method computes the inverse of the transform from Source Grid + * to Target Grid. That transform will be computed using the following path: + * + *
Target Grid ⟶ Target CRS ⟶ Source CRS ⟶ Source Grid
+ * + * If the target {@link GridGeometry} is incomplete, this method provides default + * values for the missing properties. The following cases may occur: + * + *
    + *
  • + * User provided no {@link GridExtent}. This method will construct a "grid to CRS" transform + * preserving (at least approximately) axis directions and resolutions at the point of interest. + * Then a grid extent will be created with a size large enough for containing the original grid + * transformed by above Source GridTarget Grid transform. + *
  • + * User provided only a {@link GridExtent}. This method will compute an envelope large enough + * for containing the projected coordinates, then a "grid to CRS" transform will be derived + * from the grid and the georeferenced envelope with an attempt to preserve axis directions + * at least approximately. + *
  • + * User provided only a "grid to CRS" transform. This method will transform the projected envelope + * to "grid units" using the specified transform and create a grid extent large enough to hold the + * result.
  • + *
* * @param source the grid coverage to resample. * @param target the desired geometry of returned grid coverage. May be incomplete. @@ -201,7 +259,7 @@ final class ResampledGridCoverage extends GridCoverage { if (sourceGG.isDefined(GridGeometry.ENVELOPE) && target.isDefined(GridGeometry.ENVELOPE)) { changeOfCRS = Envelopes.findOperation(sourceGG.getEnvelope(), target.getEnvelope()); } - if (changeOfCRS == null) try { + if (changeOfCRS == null && sourceCRS != null && targetCRS != null) try { DefaultGeographicBoundingBox areaOfInterest = null; if (sourceGG.isDefined(GridGeometry.ENVELOPE)) { areaOfInterest = new DefaultGeographicBoundingBox(); diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java index 364a29a..1d93555 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java @@ -47,6 +47,10 @@ import org.apache.sis.referencing.operation.transform.MathTransforms; * *
    *
  • + * {@linkplain #setInterpolation(Interpolation) Interpolation method} to use during resampling operations. + *
  • + * {@linkplain #setFillValues(Number...) Fill values} to use for pixels that can not be computed. + *
  • * Whether operations can be executed in parallel. By default operations on unknown * {@link RenderedImage} implementations are executed sequentially in the caller thread, for safety reasons. * Some operations can be parallelized, but it should be enabled only if the {@link RenderedImage} is known @@ -58,9 +62,6 @@ import org.apache.sis.referencing.operation.transform.MathTransforms; * By default errors during calculation are propagated as an {@link ImagingOpException}, * in which case no result is available. But errors can also be notified as a {@link LogRecord} instead, * in which case partial results may be available. - *
  • - * Interpolation methods to use during resampling operations and fill values to use for pixels that can not - * be computed. *
  • *
* @@ -211,7 +212,7 @@ public class ImageProcessor implements Cloneable { } /** - * Creates a new set of image operations with default configuration. + * Creates a new processor with default configuration. * The execution mode is initialized to {@link Mode#DEFAULT} and the error action to {@link ErrorAction#THROW}. */ public ImageProcessor() { @@ -448,7 +449,7 @@ public class ImageProcessor implements Cloneable { * *

If the given source is an instance of {@link ResampledImage} or {@link AnnotatedImage}, * then this method will use {@linkplain PlanarImage#getSources() the source} of the given source. - * The intent is to avoid resampling a resampled image and try to work on the original data instead.

+ * The intent is to avoid resampling a resampled image; instead this method tries to work on the original data.

* * @param bounds domain of pixel coordinates of resampled image. * @param toSource conversion of pixel coordinates of this image to pixel coordinates of {@code source} image. @@ -500,6 +501,11 @@ public class ImageProcessor implements Cloneable { * Computes all tiles immediately, then return an image will all tiles ready. * Computations will use many threads if {@linkplain #getExecutionMode() execution mode} is parallel. * + *
Note: + * current implementation ignores the {@linkplain #getErrorAction() error action} because we do not yet + * have a mechanism for specifying which tile to produce in replacement of tiles that can not be computed. + * This behavior may be changed in a future version.
+ * * @param source the image to compute immediately (may be {@code null}). * @return image with all tiles computed, or {@code null} if the given image was null. * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)} call.