You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/04/01 12:18:25 UTC

[sis] branch geoapi-4.0 updated: Make a best effort for reusing existing computation results, for avoiding spending CPU for e.g. resampling twice the same image.

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 983c69f  Make a best effort for reusing existing computation results, for avoiding spending CPU for e.g. resampling twice the same image.
983c69f is described below

commit 983c69f56c24a9373dc2bc953b03b41fc1203d7f
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Apr 1 14:17:34 2020 +0200

    Make a best effort for reusing existing computation results, for avoiding spending CPU for e.g. resampling twice the same image.
---
 .../sis/coverage/grid/GridCoverageProcessor.java   |  9 ++-
 .../sis/coverage/grid/ResampledGridCoverage.java   |  2 +-
 .../java/org/apache/sis/image/AnnotatedImage.java  |  2 +-
 .../java/org/apache/sis/image/ImageAdapter.java    | 40 ++++++++++++-
 .../java/org/apache/sis/image/ImageProcessor.java  | 69 +++++++++++++++++++---
 .../java/org/apache/sis/image/RecoloredImage.java  | 23 ++++++--
 .../java/org/apache/sis/image/ResampledImage.java  | 56 +++++++++++++++++-
 7 files changed, 181 insertions(+), 20 deletions(-)

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 2f1b843..abc1f15 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
@@ -102,9 +102,16 @@ public class GridCoverageProcessor {
      *         It may be the source CRS, the source extent, <i>etc.</i> depending on context.
      * @throws TransformException if some coordinates can not be transformed to the specified target.
      */
-    public GridCoverage resample(final GridCoverage source, GridGeometry target) throws TransformException {
+    public GridCoverage resample(GridCoverage source, final GridGeometry target) throws TransformException {
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("target", target);
+        /*
+         * 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;
+        }
         try {
             return ResampledGridCoverage.create(source, target, interpolation);
         } catch (IllegalGridGeometryException e) {
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 a05ddf2..8639e1c 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
@@ -65,7 +65,7 @@ final class ResampledGridCoverage extends GridCoverage {
     /**
      * The coverage to resample.
      */
-    private final GridCoverage source;
+    final GridCoverage source;
 
     /**
      * The transform from cell coordinates in this coverage to cell coordinates in {@linkplain #source} coverage.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index e8e7016..761995c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -399,7 +399,7 @@ abstract class AnnotatedImage extends ImageAdapter {
      * after the class name and before the string representation of the wrapped image.
      */
     @Override
-    final Class<AnnotatedImage> stringStart(final StringBuilder buffer) {
+    final Class<AnnotatedImage> appendStringContent(final StringBuilder buffer) {
         final String key = getComputedPropertyName();
         if (cache.containsKey(key)) {
             buffer.append("Cached ");
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
index 5d5871f..b119af5 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
@@ -24,6 +24,7 @@ import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -52,6 +53,7 @@ abstract class ImageAdapter extends PlanarImage {
      * @param  source  the image to wrap.
      */
     protected ImageAdapter(final RenderedImage source) {
+        ArgumentChecks.ensureNonNull("source", source);
         this.source = source;
     }
 
@@ -118,12 +120,46 @@ abstract class ImageAdapter extends PlanarImage {
     @Override public final WritableRaster copyData(WritableRaster r) {return source.copyData(r);}
 
     /**
+     * Compares the given object with this image for equality. This method should be quick and compare
+     * how images compute their values from their sources; it should not compare the actual pixel values.
+     *
+     * <p>The default implementation returns {@code true} if the given object is non-null, is an instance
+     * of the exact same class than this image and the {@linkplain #source} of both images are equal.
+     * Subclasses should override this method if more properties need to be compared.</p>
+     *
+     * @param  object  the object to compare with this image.
+     * @return {@code true} if the given object is an image performing the same calculation than this image.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object != null && object.getClass().equals(getClass())) {
+            return source.equals(((ImageAdapter) object).source);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this image. This method should be quick, for example using
+     * only a description of the operation to be done (e.g. implementation class, parameters).
+     * This method should not compute the hash code from sample values.
+     *
+     * <p>The default implementation computes a hash code based on the {@link #source} hash code and
+     * this image class. Subclasses should override this method if more properties need to be hashed.</p>
+     *
+     * @return a hash code value based on a description of the operation performed by this image.
+     */
+    @Override
+    public int hashCode() {
+        return source.hashCode() ^ getClass().hashCode();
+    }
+
+    /**
      * Returns a string representation of this image for debugging purpose.
      */
     @Override
     public String toString() {
         final StringBuilder buffer = new StringBuilder(100);
-        final Class<?> subtype = stringStart(buffer.append('['));
+        final Class<?> subtype = appendStringContent(buffer.append('['));
         return buffer.insert(0, subtype.getSimpleName()).append(" on ").append(source).append(']').toString();
     }
 
@@ -134,5 +170,5 @@ abstract class ImageAdapter extends PlanarImage {
      * @param  buffer  where to start writing content of {@link #toString()} representation.
      * @return name of the class to show in the {@link #toString()} representation.
      */
-    abstract Class<? extends ImageAdapter> stringStart(StringBuilder buffer);
+    abstract Class<? extends ImageAdapter> appendStringContent(StringBuilder buffer);
 }
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 67f44c1..cf77c77 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
@@ -16,9 +16,13 @@
  */
 package org.apache.sis.image;
 
+import java.util.List;
+import java.util.Objects;
 import java.util.logging.Filter;
 import java.util.logging.LogRecord;
 import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
 import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
@@ -28,10 +32,12 @@ import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.TiledImage;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 
 
 /**
@@ -80,6 +86,22 @@ import org.apache.sis.internal.coverage.j2d.TiledImage;
  */
 public class ImageProcessor {
     /**
+     * Cache of previously created images. We use this cache only for images of known implementations,
+     * especially the ones which may be costly to compute. Reusing an existing instance avoid to repeat
+     * the computation.
+     */
+    private static final WeakHashSet<RenderedImage> CACHE = new WeakHashSet<>(RenderedImage.class);
+
+    /**
+     * Returns an unique instance of the given image. This method should be invoked only for images
+     * of known implementation, especially the ones which are costly to compute. The implementation
+     * shall override {@link Object#equals(Object)} and {@link Object#hashCode()} methods.
+     */
+    private static RenderedImage unique(final RenderedImage image) {
+        return CACHE.unique(image);
+    }
+
+    /**
      * Interpolation to use during resample operations.
      *
      * @see #getInterpolation()
@@ -365,7 +387,7 @@ public class ImageProcessor {
         }
         final int visibleBand = ImageUtilities.getVisibleBand(source);
         if (visibleBand >= 0) {
-            return RecoloredImage.rescale(source, visibleBand, minimum, maximum);
+            return unique(RecoloredImage.rescale(source, visibleBand, minimum, maximum));
         }
         return source;
     }
@@ -404,7 +426,7 @@ public class ImageProcessor {
                     final double minimum = Math.max(s.minimum(), mean - deviations);
                     final double maximum = Math.min(s.maximum(), mean + deviations);
                     if (minimum < maximum) {
-                        return RecoloredImage.rescale(source, visibleBand, minimum, maximum);
+                        return unique(RecoloredImage.rescale(source, visibleBand, minimum, maximum));
                     }
                 }
             }
@@ -420,21 +442,52 @@ public class ImageProcessor {
      * in the new image are set to {@linkplain #getFillValues() fill values}. Otherwise sample values are interpolated
      * using the method given by {@link #getInterpolation()}.
      *
+     * <p>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.</p>
+     *
      * @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.
      * @param  source    the image to be resampled.
      * @return resampled image (may be {@code source}).
      */
-    public RenderedImage resample(final Rectangle bounds, final MathTransform toSource, final RenderedImage source) {
+    public RenderedImage resample(final Rectangle bounds, MathTransform toSource, RenderedImage source) {
         ArgumentChecks.ensureNonNull("bounds",   bounds);
         ArgumentChecks.ensureNonNull("toSource", toSource);
         ArgumentChecks.ensureNonNull("source",   source);
-        if (toSource.isIdentity() && bounds.x == source.getMinX() && bounds.y == source.getMinY()
-                && bounds.width == source.getWidth() && bounds.height == source.getHeight())
-        {
-            return source;
+        final ColorModel  cm = source.getColorModel();
+        final SampleModel sm = source.getSampleModel();
+        boolean isIdentity = toSource.isIdentity();
+        RenderedImage resampled = null;
+        for (;;) {
+            if (isIdentity && bounds.x == source.getMinX() && bounds.y == source.getMinY() &&
+                    bounds.width == source.getWidth() && bounds.height == source.getHeight())
+            {
+                resampled = source;
+                break;
+            }
+            if (Objects.equals(sm, source.getSampleModel())) {
+                if (source instanceof ImageAdapter) {
+                    source = ((ImageAdapter) source).source;
+                    continue;
+                }
+                if (source instanceof ResampledImage) {
+                    final List<RenderedImage> sources = source.getSources();
+                    if (sources != null && sources.size() == 1) {                         // Paranoiac check.
+                        toSource   = MathTransforms.concatenate(toSource, ((ResampledImage) source).toSource);
+                        isIdentity = toSource.isIdentity();
+                        source     = sources.get(0);
+                        continue;
+                    }
+                }
+            }
+            resampled = new ResampledImage(bounds, toSource, source, interpolation, fillValues);
+            break;
+        }
+        if (cm != null && !cm.equals(resampled.getColorModel())) {
+            resampled = new RecoloredImage(resampled, cm);
         }
-        return new ResampledImage(bounds, toSource, source, interpolation, fillValues);
+        return unique(resampled);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
index 7dd00e0..b1709bf 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -42,7 +42,7 @@ final class RecoloredImage extends ImageAdapter {
     /**
      * Creates a new recolored image with the given colors.
      */
-    private RecoloredImage(final RenderedImage source, final ColorModel colors) {
+    RecoloredImage(final RenderedImage source, final ColorModel colors) {
         super(source);
         this.colors = colors;
     }
@@ -64,8 +64,7 @@ final class RecoloredImage extends ImageAdapter {
         for (;;) {
             if (colors.equals(source.getColorModel())) {
                 return source;
-            }
-            if (source instanceof RecoloredImage) {
+            } else if (source instanceof RecoloredImage) {
                 source = ((RecoloredImage) source).source;
             } else {
                 break;
@@ -83,11 +82,27 @@ final class RecoloredImage extends ImageAdapter {
     }
 
     /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        return super.equals(object) && colors.equals(((RecoloredImage) object).colors);
+    }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 37*colors.hashCode();
+    }
+
+    /**
      * Appends a content to show in the {@link #toString()} representation,
      * after the class name and before the string representation of the wrapped image.
      */
     @Override
-    final Class<RecoloredImage> stringStart(final StringBuilder buffer) {
+    final Class<RecoloredImage> appendStringContent(final StringBuilder buffer) {
         buffer.append(colors.getColorSpace());
         return RecoloredImage.class;
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index e5adcf4..f9f1b05 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.util.Arrays;
+import java.util.Objects;
 import java.nio.DoubleBuffer;
 import java.awt.Dimension;
 import java.awt.Rectangle;
@@ -87,6 +88,16 @@ public class ResampledImage extends ComputedImage {
     protected final MathTransform toSource;
 
     /**
+     * Same as {@link #toSource} but with the addition of a shift for taking in account the number of pixels required
+     * for interpolations. For example if a bicubic interpolation needs 4×4 pixels, then the source coordinates that
+     * we need are not the coordinates of the pixel we want to interpolate, but 1 or 2 pixels before for making room
+     * for interpolation support.
+     *
+     * @see #interpolationSupportOffset(int)
+     */
+    private final MathTransform toSourceSupport;
+
+    /**
      * The object to use for performing interpolations.
      */
     protected final Interpolation interpolation;
@@ -134,6 +145,7 @@ public class ResampledImage extends ComputedImage {
          * pixels of source image. Supplemental coordinates can be used for selecting an image in a
          * n-dimensional data cube.
          */
+        this.toSource = toSource;
         int numDim = toSource.getSourceDimensions();
         if (numDim != BIDIMENSIONAL || (numDim = toSource.getTargetDimensions()) < BIDIMENSIONAL) {
             throw new IllegalArgumentException(Errors.format(
@@ -150,7 +162,7 @@ public class ResampledImage extends ComputedImage {
         final double[] offset = new double[numDim];
         offset[0] = interpolationSupportOffset(s.width);
         offset[1] = interpolationSupportOffset(s.height);
-        this.toSource = MathTransforms.concatenate(toSource, MathTransforms.translation(offset));
+        toSourceSupport = MathTransforms.concatenate(toSource, MathTransforms.translation(offset));
         final int numBands = ImageUtilities.getNumBands(source);
         /*
          * Copy the `fillValues` either as an `int[]` or `double[]` array, depending on
@@ -198,6 +210,8 @@ public class ResampledImage extends ComputedImage {
      *
      * @param  span  the width or height of the support region for interpolations.
      * @return relative index of the first pixel needed on the left or top sides, as a value ≤ 0.
+     *
+     * @see #toSourceSupport
      */
     private static double interpolationSupportOffset(final int span) {
         return -Math.max(0, (span - 1) / 2);        // Round toward 0.
@@ -305,7 +319,7 @@ public class ResampledImage extends ComputedImage {
         final int tileMinY = tile.getMinY();
         final int tileMaxX = Math.addExact(tileMinX, scanline);
         final int tileMaxY = Math.addExact(tileMinY, tile.getHeight());
-        final int tgtDim   = toSource.getTargetDimensions();
+        final int tgtDim   = toSourceSupport.getTargetDimensions();
         final double[] coordinates = new double[scanline * Math.max(BIDIMENSIONAL, tgtDim)];
         /*
          * Compute the bounds of pixel coordinates that we can use for setting iterator positions in the source image.
@@ -381,7 +395,7 @@ public class ResampledImage extends ComputedImage {
                 coordinates[ci++] = tx;
                 coordinates[ci++] = ty;
             }
-            toSource.transform(coordinates, 0, coordinates, 0, scanline);
+            toSourceSupport.transform(coordinates, 0, coordinates, 0, scanline);
             /*
              * Special case for nearest-neighbor.
              */
@@ -483,4 +497,40 @@ public class ResampledImage extends ComputedImage {
         }
         return tile;
     }
+
+    /**
+     * Compares the given object with this image for equality. This method returns {@code true}
+     * if the given object is non-null, is an instance of the exact same class than this image,
+     * has equal sources and do the same resampling operation (same interpolation method,
+     * same fill values, same coordinates).
+     *
+     * @param  object  the object to compare with this image.
+     * @return {@code true} if the given object is an image performing the same resampling than this image.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object != null && object.getClass().equals(getClass())) {
+            final ResampledImage other = (ResampledImage) object;
+            return minX   == other.minX &&
+                   minY   == other.minY &&
+                   width  == other.width &&
+                   height == other.height &&
+                   interpolation.equals(other.interpolation) &&
+                   Objects.deepEquals(fillValues, other.fillValues) &&
+                   toSource.equals(other.toSource) &&
+                   getSources().equals(other.getSources());
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this image.
+     *
+     * @return a hash code value based on a description of the operation performed by this image.
+     */
+    @Override
+    public int hashCode() {
+        return minX + 31*(minY + 31*(width + 31*height)) + interpolation.hashCode()
+                + toSource.hashCode() + getSources().hashCode();
+    }
 }