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 2022/06/01 15:07:26 UTC

[sis] branch geoapi-4.0 updated: Improvement in the cache of `RenderedImage` instances: - Revisit the `equals(Object)` and `hashCode()` methods. - Reuse existing `RenderedImage` instances for a given `SliceExtent`.

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 4d0e134cc8 Improvement in the cache of `RenderedImage` instances: - Revisit the `equals(Object)` and `hashCode()` methods. - Reuse existing `RenderedImage` instances for a given `SliceExtent`.
4d0e134cc8 is described below

commit 4d0e134cc83977c18a485bb92de1e74070bbab68
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 1 16:25:54 2022 +0200

    Improvement in the cache of `RenderedImage` instances:
    - Revisit the `equals(Object)` and `hashCode()` methods.
    - Reuse existing `RenderedImage` instances for a given `SliceExtent`.
    
    The latter item is helpful even if the rendered images are cheap to create
    (some of them are just wrappers around existing `DataBuffer`) because the
    image may be the starting point of a chain of operations with statistics,
    raster resampling, etc. Not reusing an existing image means discarding
    all the derivated information.
---
 .../sis/coverage/grid/BufferedGridCoverage.java    | 79 ++++++++++++++++++++--
 .../sis/coverage/grid/ConvertedGridCoverage.java   |  2 +
 .../org/apache/sis/coverage/grid/GridCoverage.java |  2 +-
 .../apache/sis/coverage/grid/GridEvaluator.java    |  2 +
 .../org/apache/sis/coverage/grid/GridExtent.java   |  2 +
 .../apache/sis/coverage/grid/ReshapedImage.java    | 31 ++++++++-
 .../java/org/apache/sis/image/AnnotatedImage.java  |  9 ++-
 .../java/org/apache/sis/image/BandSelectImage.java | 23 ++++++-
 .../apache/sis/image/BandedSampleConverter.java    | 44 +++++++++++-
 .../java/org/apache/sis/image/ComputedImage.java   | 27 ++++++++
 .../java/org/apache/sis/image/ImageAdapter.java    | 11 +--
 .../main/java/org/apache/sis/image/MaskImage.java  | 28 +++++++-
 .../java/org/apache/sis/image/MaskedImage.java     |  9 +--
 .../java/org/apache/sis/image/PlanarImage.java     | 10 +++
 .../sis/image/PositionalConsistencyImage.java      | 23 ++++++-
 .../java/org/apache/sis/image/PrefetchedImage.java | 30 +++++++-
 .../java/org/apache/sis/image/RecoloredImage.java  |  8 ++-
 .../java/org/apache/sis/image/ResampledImage.java  |  9 ++-
 .../org/apache/sis/image/SourceAlignedImage.java   | 28 +++++++-
 .../org/apache/sis/image/StatisticsCalculator.java |  2 +-
 .../java/org/apache/sis/image/Visualization.java   | 26 ++++++-
 .../org/apache/sis/internal/netcdf/Raster.java     | 29 +++-----
 .../sis/internal/storage/TiledGridCoverage.java    |  4 +-
 .../apache/sis/test/storage/SubsampledImage.java   | 27 +++++++-
 24 files changed, 407 insertions(+), 58 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
index 0304be0419..8f20214e49 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.List;
+import java.util.function.Function;
 import java.awt.image.DataBuffer;
 import java.awt.image.DataBufferByte;
 import java.awt.image.DataBufferDouble;
@@ -28,11 +29,13 @@ import java.awt.image.RasterFormatException;
 import java.awt.image.RenderedImage;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.Cache;
 import org.apache.sis.image.DataType;
 
 // Branch-specific imports
@@ -75,7 +78,7 @@ import org.opengis.coverage.PointOutsideCoverageException;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -90,6 +93,41 @@ public class BufferedGridCoverage extends GridCoverage {
      */
     protected final DataBuffer data;
 
+    /**
+     * Cache of rendered images produced by calls to {@link #render(GridExtent)}.
+     * Those images are cached because, even if they are cheap to create,
+     * they may become the source of a chain of operations for statistics,
+     * {@linkplain org.apache.sis.image.ResampledImage image resampling}, <i>etc.</i>
+     * Caching the source image preserves not only the {@link RenderedImage} instance created by the
+     * {@link #render(GridExtent)} method, but also the chain of operations potentially derived from that image.
+     *
+     * <h4>Usage</h4>
+     * Implementation of {@link #render(GridExtent)} method can be like below:
+     *
+     * {@preformat java
+     *     &#64;Override
+     *     public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
+     *         if (sliceExtent == null) {
+     *             sliceExtent = gridGeometry.getExtent();
+     *         }
+     *         // Do some other verification if needed…
+     *         // … then get or compute the image.
+     *         try {
+     *             return cachedRenderings.computeIfAbsent(sliceExtent, (slice) -> {
+     *                 val renderer = new ImageRenderer(this, slice);
+     *                 renderer.setData(data);
+     *                 return renderer.createImage();
+     *             });
+     *         } catch (IllegalGridGeometryException | MismatchedDimensionException e) {
+    *              throw e;
+     *         } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) {
+     *             throw new CannotEvaluateException(e.getMessage(), e);
+     *         }
+     *     }
+     * }
+     */
+    private final Cache<GridExtent,RenderedImage> cachedRenderings;
+
     /**
      * Constructs a grid coverage using the specified grid geometry, sample dimensions and data buffer.
      * This method stores the given buffer by reference (no copy). The bands in the given buffer can be
@@ -139,6 +177,7 @@ public class BufferedGridCoverage extends GridCoverage {
             throw new IllegalGridGeometryException(Resources.format(
                     Resources.Keys.InsufficientBufferCapacity_3, b, numBands, expectedSize - bufferSize));
         }
+        cachedRenderings = new Cache<>();
     }
 
     /**
@@ -163,6 +202,7 @@ public class BufferedGridCoverage extends GridCoverage {
             case DataBuffer.TYPE_DOUBLE: data = new DataBufferDouble(n); break;
             default: throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, dataType));
         }
+        cachedRenderings = new Cache<>();
     }
 
     /**
@@ -204,19 +244,48 @@ public class BufferedGridCoverage extends GridCoverage {
      * Returns a two-dimensional slice of grid data as a rendered image.
      * This method returns a view; sample values are not copied.
      *
+     * <p>The default implementation prepares an {@link ImageRenderer},
+     * then invokes {@link #configure(ImageRenderer)} for allowing subclasses
+     * to complete the renderer configuration before to create the image.</p>
+     *
      * @return the grid slice as a rendered image.
      */
     @Override
-    public RenderedImage render(final GridExtent sliceExtent) {
-        final ImageRenderer renderer = new ImageRenderer(this, sliceExtent);
+    public RenderedImage render(GridExtent sliceExtent) {
+        if (sliceExtent == null) {
+            sliceExtent = gridGeometry.extent;
+        }
         try {
-            renderer.setData(data);
-            return renderer.createImage();
+            return cachedRenderings.computeIfAbsent(sliceExtent, (slice) -> {
+                ImageRenderer renderer = new ImageRenderer(this, slice);
+                renderer.setData(data);
+                return renderer.createImage();
+            });
+        } catch (IllegalGridGeometryException | MismatchedDimensionException e) {
+            throw e;
         } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) {
             throw new CannotEvaluateException(e.getMessage(), e);
         }
     }
 
+    /**
+     * Invoked by the default implementation of {@link #render(GridExtent)}
+     * for completing the renderer configuration before to create an image.
+     * The default implementation does nothing.
+     *
+     * <p>Some example of methods that subclasses may want to use are:</p>
+     * <ul>
+     *   <li>{@link ImageRenderer#setCategoryColors(Function)}</li>
+     *   <li>{@link ImageRenderer#setVisibleBand(int)}</li>
+     * </ul>
+     *
+     * @param  renderer  the renderer to configure before to create an image.
+     *
+     * @since 1.3
+     */
+    protected void configure(final ImageRenderer renderer) {
+    }
+
     /**
      * Implementation of evaluator returned by {@link #evaluator()}.
      */
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 c2f6b8870a..8c30d9b63d 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
@@ -332,6 +332,8 @@ final class ConvertedGridCoverage extends GridCoverage {
         RenderedImage image = source.render(sliceExtent);
         /*
          * That image should never be null. But if an implementation wants to do so, respect that.
+         * We do not cache the image because caching is already handled by `ImageProcessor`,
+         * assuming that `source` returned an image from its own cache.
          */
         if (image != null) {
             image = convert(image, bandType, converters, processor);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index c4f7115e92..bd0425eedb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -75,7 +75,7 @@ public abstract class GridCoverage extends BandedCoverage {
      *
      * @see #getGridGeometry()
      */
-    final GridGeometry gridGeometry;
+    protected final GridGeometry gridGeometry;
 
     /**
      * List of sample dimension (band) information for the grid coverage. Information include such things
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
index d79883df73..6a40742dbb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
@@ -208,6 +208,8 @@ public class GridEvaluator implements GridCoverage.Evaluator {
      * @param  slice  the default slice where to perform evaluation, or an empty map if none.
      * @throws IllegalArgumentException if the map contains an illegal dimension or grid coordinate value.
      *
+     * @see GridExtent#getSliceCoordinates()
+     *
      * @since 1.3
      */
     public void setDefaultSlice(Map<Integer,Long> slice) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 2616661c94..838e443331 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -841,6 +841,8 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
      *
      * @return grid coordinates for all dimensions where the grid has a size of 1.
      *
+     * @see GridEvaluator#setDefaultSlice(Map)
+     *
      * @since 1.3
      */
     public SortedMap<Integer,Long> getSliceCoordinates() {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReshapedImage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReshapedImage.java
index 9500a8edf2..0a7efb56ff 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReshapedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ReshapedImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Vector;
+import java.util.Objects;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -42,7 +43,7 @@ import static java.lang.Math.toIntExact;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -305,4 +306,32 @@ final class ReshapedImage extends PlanarImage {
         if (getTileGridYOffset() != super.getTileGridYOffset()) return "tileGridYOffset";
         return super.verify();      // "width" and "height" properties should be checked last.
     }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(source, minX, minY, width, height);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object instanceof ReshapedImage) {
+            final ReshapedImage other = (ReshapedImage) object;
+            return source.equals(other.source) &&
+                    minX     == other.minX     &&
+                    minY     == other.minY     &&
+                    width    == other.width    &&
+                    height   == other.height   &&
+                    offsetX  == other.offsetX  &&
+                    offsetY  == other.offsetY  &&
+                    minTileX == other.minTileX &&
+                    minTileY == other.minTileY;
+        }
+        return false;
+    }
 }
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 3c1d7e7143..c6e4c07e50 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
@@ -540,7 +540,7 @@ abstract class AnnotatedImage extends ImageAdapter {
      * @return a hash code value based on a description of the operation performed by this image.
      */
     @Override
-    public int hashCode() {
+    public final int hashCode() {
         return super.hashCode() + Objects.hashCode(areaOfInterest) + Boolean.hashCode(failOnException);
     }
 
@@ -548,11 +548,16 @@ abstract class AnnotatedImage extends ImageAdapter {
      * 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.
      *
+     * <h4>Requirements for subclasses</h4>
+     * Subclasses should override {@link #getExtraParameter()} instead of this method.
+     *
      * @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.
+     *
+     * @see #getExtraParameter()
      */
     @Override
-    public boolean equals(final Object object) {
+    public final boolean equals(final Object object) {
         return super.equals(object) && equalParameters((AnnotatedImage) object);
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 53247c09c1..c3dfe1db09 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.util.Set;
+import java.util.Arrays;
 import java.util.Hashtable;
 import java.lang.reflect.Array;
 import java.awt.Image;
@@ -37,7 +38,7 @@ import org.apache.sis.internal.jdk9.JDK9;
  * it works by modifying the sample model and color model.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -174,4 +175,24 @@ final class BandSelectImage extends SourceAlignedImage {
          */
         return parent.createChild(x, y, parent.getWidth(), parent.getHeight(), x, y, bands);
     }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 97 * Arrays.hashCode(bands);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (super.equals(object)) {
+            final BandSelectImage other = (BandSelectImage) object;
+            return Arrays.equals(bands, other.bands);
+        }
+        return false;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
index e6b15c5b43..2f3fda058e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.image;
 
+import java.util.Arrays;
+import java.util.Objects;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
@@ -64,7 +66,7 @@ import static java.util.logging.Logger.getLogger;
  * In such case, writing converted values will cause the corresponding source values to be updated too.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -487,5 +489,45 @@ class BandedSampleConverter extends ComputedImage {
                 markDirtyTiles(executor.getTileIndices());
             }
         }
+
+        /**
+         * Restores the identity behavior for writable image,
+         * because it may have listeners attached to this specific instance.
+         */
+        @Override
+        public int hashCode() {
+            return System.identityHashCode(this);
+        }
+
+        /**
+         * Restores the identity behavior for writable image,
+         * because it may have listeners attached to this specific instance.
+         */
+        @Override
+        public boolean equals(final Object object) {
+            return object == this;
+        }
+    }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return hashCodeBase() + 37 * Arrays.hashCode(converters) + Objects.hashCode(colorModel);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (equalsBase(object)) {
+            final BandedSampleConverter other = (BandedSampleConverter) object;
+            return Arrays .equals(converters, other.converters) &&
+                   Objects.equals(colorModel, other.colorModel) &&
+                   Arrays .equals(sampleResolutions, other.sampleResolutions);
+        }
+        return false;
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 56fc18a4d5..879638cd36 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Vector;
+import java.util.Objects;
 import java.lang.ref.Reference;
 import java.awt.Insets;
 import java.awt.Point;
@@ -781,4 +782,30 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     public void dispose() {
         reference.dispose();
     }
+
+    /**
+     * Returns a hash code value based on the fields known to this base class.
+     * This is a helper method for {@link #hashCode()} implementation in subclasses.
+     * It should <strong>not</strong> be used by {@link WritableRenderedImage} implementations,
+     * because those images have listeners that are attached to a specific instance.
+     */
+    final int hashCodeBase() {
+        return Arrays.hashCode(sources) + 31*sampleModel.hashCode() + 37*Objects.hash(destination);
+    }
+
+    /**
+     * Compares the given object with this image for equality using the fields known to this base class.
+     * This is a helper method for {@link #equals(Object)} implementation in subclasses.
+     * It should <strong>not</strong> be used by {@link WritableRenderedImage} implementations,
+     * because those images have listeners that are attached to a specific instance.
+     */
+    final boolean equalsBase(final Object object) {
+        if (object != null && getClass().equals(object.getClass())) {
+            final ComputedImage other = (ComputedImage) object;
+            return Arrays .equals(sources,     other.sources) &&
+                   Objects.equals(destination, other.destination) &&
+                   sampleModel.equals(other.sampleModel);
+        }
+        return false;
+    }
 }
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 1482872c5d..e34393ebfc 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
@@ -33,13 +33,16 @@ import org.apache.sis.util.Disposable;
  * All {@link RenderedImage} methods related to coordinate systems (pixel coordinates or tile
  * indices), and all methods fetching tiles, delegate to the wrapped image.
  *
- * <div class="note"><b>Design note:</b>
+ * <h2>Design note</h2>
  * most non-abstract methods are final because {@link PixelIterator} (among others) relies
- * on the fact that it can unwrap this image and still get the same pixel values.</div>
+ * on the fact that it can unwrap this image and still get the same pixel values.
  *
- * <div class="note"><b>Relationship with other classes</b><br>
+ * <h2>Relationship with other classes</h2>
  * This class is similar to {@link SourceAlignedImage} except that it does not extend {@link ComputedImage}
- * and forward {@link #getTile(int, int)}, {@link #getData()} and other data methods to the source image.</div>
+ * and forward {@link #getTile(int, int)}, {@link #getData()} and other data methods to the source image.
+ *
+ * <h2>Requirements for subclasses</h2>
+ * All subclasses shall override {@link #equals(Object)} and {@link #hashCode()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
index c2284d4c60..de89515d62 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MaskImage.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.util.Objects;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import org.opengis.referencing.operation.MathTransform;
@@ -34,7 +35,7 @@ import static java.util.logging.Logger.getLogger;
  * This is the implementation of {@value ResampledImage#MASK_KEY} property value.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see ResampledImage#getProperty(String)
  * @see ResampledImage#MASK_KEY
@@ -47,7 +48,7 @@ final class MaskImage extends SourceAlignedImage {
      * Convert integer values to floating point values, or {@code null} if none.
      * This is needed since we use {@link Float#isNaN(float)} for identifying values to mask.
      */
-    private MathTransform converter;
+    private final MathTransform converter;
 
     /**
      * Creates a new instance for the given image.
@@ -55,12 +56,15 @@ final class MaskImage extends SourceAlignedImage {
     MaskImage(final ResampledImage image) {
         super(image, ColorModelFactory.createIndexColorModel(
                 1, ImageUtilities.getVisibleBand(image), new int[] {0, -1}, true, 0));
+
+        MathTransform converter = null;
         if (image.interpolation instanceof Visualization.InterpConvert) try {
             converter = ((Visualization.InterpConvert) image.interpolation).converter.inverse();
         } catch (NoninvertibleTransformException e) {
             // ResampledImage.getProperty("org.apache.sis.Mask") is the public caller of this constructor.
             Logging.unexpectedException(getLogger(Modules.RASTER), ResampledImage.class, "getProperty", e);
         }
+        this.converter = converter;
     }
 
     /**
@@ -120,4 +124,24 @@ final class MaskImage extends SourceAlignedImage {
         }
         return tile;
     }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 97 * Objects.hashCode(converter);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (super.equals(object)) {
+            final MaskImage other = (MaskImage) object;
+            return Objects.equals(converter, other.converter);
+        }
+        return false;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java
index ae15580b50..0b25ef0022 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.image;
 
-import java.util.Objects;
 import java.nio.ByteBuffer;
 import java.nio.LongBuffer;
 import java.awt.Rectangle;
@@ -470,11 +469,9 @@ complete:   for (int border = 0; ; border++) {
      */
     @Override
     public boolean equals(final Object object) {
-        if (object != null && object.getClass().equals(getClass())) {
+        if (super.equals(object)) {
             final MaskedImage other = (MaskedImage) object;
-            return clip.equals(other.clip) &&
-                   Objects.deepEquals(fillValues, other.fillValues) &&
-                   getSources().equals(other.getSources());
+            return clip.equals(other.clip) && fillValues.equals(other.fillValues);
         }
         return false;
     }
@@ -486,6 +483,6 @@ complete:   for (int border = 0; ; border++) {
      */
     @Override
     public int hashCode() {
-        return clip.hashCode() + Objects.hashCode(fillValues) + getSources().hashCode();
+        return super.hashCode() + clip.hashCode() + fillValues.hashCode();
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 3ac76d2e00..31a5937a41 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -646,4 +646,14 @@ colors: if (cm != null) {
         }
         return buffer.toString();
     }
+
+    /*
+     * Note on `equals(Object)` and `hashCode()` methods:
+     *
+     * Do not provide base implementation for those methods, because they can only be incomplete and it is too easy
+     * to forget to override those methods in subclasses. Furthermore we should override those methods only in final
+     * classes that are read-only images. Base classes of potentially writable images should continue to use identity
+     * comparisons, especially when some tiles have been acquired for writing and not yet released at the time the
+     * `equals(Object)` method is invoked.
+     */
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PositionalConsistencyImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PositionalConsistencyImage.java
index 370cb2fe14..69c0b3988b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PositionalConsistencyImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PositionalConsistencyImage.java
@@ -31,7 +31,7 @@ import org.apache.sis.internal.jdk9.JDK9;
  * This is the implementation of {@link ResampledImage#POSITIONAL_CONSISTENCY_KEY} property value.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -141,4 +141,25 @@ final class PositionalConsistencyImage extends SourceAlignedImage {
         }
         return tile;
     }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 67 * toSource.hashCode()
+                                + 97 * toTarget.hashCode();
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (super.equals(object)) {
+            final PositionalConsistencyImage other = (PositionalConsistencyImage) object;
+            return toSource.equals(other.toSource) && toTarget.equals(other.toTarget);
+        }
+        return false;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
index 989c58e112..5b52aa4e60 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.util.Vector;
+import java.util.Objects;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
@@ -38,7 +39,7 @@ import org.apache.sis.util.Disposable;
  * This image has the same coordinate systems than the source image.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see ImageProcessor#prefetch(RenderedImage, Rectangle)
  *
@@ -303,4 +304,31 @@ final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Exec
         return p.create(new Point(ImageUtilities.tileToPixelX(source, tileX),
                                   ImageUtilities.tileToPixelY(source, tileY)));
     }
+
+    /**
+     * Returns a hash code value for this image.
+     * Defined for consistency with {@link #equals(Object)}.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(source, minTileX, minTileY, numXTiles, numYTiles);
+    }
+
+    /**
+     * Compares the given object with this image for equality. This is defined as a matter of principle,
+     * but is a little bit useless for {@link PrefetchedImage} because tiles have already been computed
+     * in the constructor. So it is too late for caching for example.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object instanceof PrefetchedImage) {
+            final PrefetchedImage other = (PrefetchedImage) object;
+            return source.equals(other.source)  &&
+                   minTileX  == other.minTileX  &&
+                   minTileY  == other.minTileY  &&
+                   numXTiles == other.numXTiles &&
+                   numYTiles == other.numYTiles;
+        }
+        return false;
+    }
 }
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 a4f7dcfbd0..d03658e87a 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
@@ -369,7 +369,13 @@ final class RecoloredImage extends ImageAdapter {
      */
     @Override
     public boolean equals(final Object object) {
-        return super.equals(object) && colors.equals(((RecoloredImage) object).colors);
+        if (super.equals(object)) {
+            final RecoloredImage other = (RecoloredImage) object;
+            return Numerics.equals(minimum, other.minimum) &&
+                   Numerics.equals(maximum, other.maximum) &&
+                   colors.equals(other.colors);
+        }
+        return false;
     }
 
     /**
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 344a886265..2867b04cdb 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
@@ -907,7 +907,7 @@ public class ResampledImage extends ComputedImage {
      */
     @Override
     public boolean equals(final Object object) {
-        if (object != null && object.getClass().equals(getClass())) {
+        if (equalsBase(object)) {
             final ResampledImage other = (ResampledImage) object;
             return minX     == other.minX &&
                    minY     == other.minY &&
@@ -917,8 +917,7 @@ public class ResampledImage extends ComputedImage {
                    minTileY == other.minTileY &&
                    interpolation.equals(other.interpolation) &&
                    Objects.deepEquals(fillValues, other.fillValues) &&
-                   toSource.equals(other.toSource) &&
-                   getSources().equals(other.getSources());
+                   toSource.equals(other.toSource);
         }
         return false;
     }
@@ -930,7 +929,7 @@ public class ResampledImage extends ComputedImage {
      */
     @Override
     public int hashCode() {
-        return minX + 31*(minY + 31*(width + 31*height)) + interpolation.hashCode()
-                + toSource.hashCode() + getSources().hashCode();
+        return hashCodeBase() + minX + 31*(minY + 31*(width + 31*height))
+                + interpolation.hashCode() + toSource.hashCode();
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
index e0602ef256..907a701d1a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.util.Set;
+import java.util.Objects;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
@@ -43,9 +44,10 @@ import org.apache.sis.internal.jdk9.JDK9;
  * That method is invoked when a requested tile is not in the cache or needs to be updated.
  * All methods related to pixel and tile coordinates ({@link #getMinX()}, {@link #getMinTileX()},
  * <i>etc.</i>) are final and delegate to the source image.
+ * The {@link #equals(Object)} and {@link #hashCode()} methods should also be overridden.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -59,7 +61,7 @@ abstract class SourceAlignedImage extends ComputedImage {
             POSITIONAL_ACCURACY_KEY, ResampledImage.POSITIONAL_CONSISTENCY_KEY);
 
     /**
-     * The color model for this image.
+     * The color model for this image. May be {@code null}.
      */
     private final ColorModel colorModel;
 
@@ -205,4 +207,26 @@ abstract class SourceAlignedImage extends ComputedImage {
             return super.prefetch(tiles);
         }
     }
+
+    /**
+     * Returns a hash code value for this image.
+     * Subclasses should override this method.
+     */
+    @Override
+    public int hashCode() {
+        return hashCodeBase() + 37 * Objects.hashCode(colorModel);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     * Subclasses should override this method.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (equalsBase(object)) {
+            final SourceAlignedImage other = (SourceAlignedImage) object;
+            return Objects.equals(colorModel, other.colorModel);
+        }
+        return false;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
index 213501652f..dcb691f971 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
@@ -109,7 +109,7 @@ final class StatisticsCalculator extends AnnotatedImage {
      * @param  accumulator  where to accumulate the statistics results.
      * @return the accumulator optionally filtered.
      */
-    private final DoubleConsumer[] filtered(final Statistics[] accumulator) {
+    private DoubleConsumer[] filtered(final Statistics[] accumulator) {
         if (sampleFilters == null) {
             return accumulator;
         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 3695693b9d..7a69dd26f5 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -18,6 +18,8 @@ package org.apache.sis.image;
 
 import java.util.Map;
 import java.util.List;
+import java.util.Arrays;
+import java.util.Objects;
 import java.util.Collection;
 import java.util.function.Function;
 import java.util.function.DoubleUnaryOperator;
@@ -58,7 +60,7 @@ import org.apache.sis.util.collection.BackingStoreException;
  * {@link WritableRaster#setPixel(int, int, int[])} has more efficient implementations for integers.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -463,4 +465,26 @@ final class Visualization extends ResampledImage {
         Transferer.create(getSource(), tile).compute(converters);
         return tile;
     }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (super.equals(object)) {
+            final Visualization other = (Visualization) object;
+            return Arrays .equals(converters, other.converters) &&
+                   Objects.equals(colorModel, other.colorModel);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 67 *  Arrays.hashCode(converters)
+                                + 97 * Objects.hashCode(colorModel);
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
index 611452f19d..cc019c1cc4 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
@@ -21,12 +21,9 @@ import java.util.function.Function;
 import java.awt.Color;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
-import java.awt.image.RasterFormatException;
-import org.opengis.coverage.CannotEvaluateException;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
-import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.ImageRenderer;
 import org.apache.sis.coverage.grid.BufferedGridCoverage;
 
@@ -43,7 +40,7 @@ import org.apache.sis.coverage.grid.BufferedGridCoverage;
  * but it is {@link ImageRenderer} responsibility to perform this substitution as an optimization.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
@@ -103,24 +100,16 @@ final class Raster extends BufferedGridCoverage {
     }
 
     /**
-     * Returns a two-dimensional slice of grid data as a rendered image.
-     * This returns a view as much as possible; sample values are not copied.
+     * Configures a two-dimensional slice of grid data as a rendered image.
      */
     @Override
-    public RenderedImage render(final GridExtent target) {
-        final ImageRenderer renderer = new ImageRenderer(this, target);
-        try {
-            renderer.setData(data);
-            if (bandOffsets != null) {
-                renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets);
-            }
-            if (colors != null) {
-                renderer.setCategoryColors(colors);
-            }
-            renderer.setVisibleBand(visibleBand);
-            return renderer.createImage();
-        } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) {
-            throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label, e), e);
+    protected void configure(final ImageRenderer renderer) {
+        if (bandOffsets != null) {
+            renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets);
         }
+        if (colors != null) {
+            renderer.setCategoryColors(colors);
+        }
+        renderer.setVisibleBand(visibleBand);
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index 646aebf84d..4b01e6a7e5 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -382,7 +382,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
      */
     @Override
     public RenderedImage render(GridExtent sliceExtent) {
-        final GridExtent available = getGridGeometry().getExtent();
+        final GridExtent available = gridGeometry.getExtent();
         final int dimension = available.getDimension();
         if (sliceExtent == null) {
             sliceExtent = available;
@@ -433,7 +433,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
              *    - Two-dimensional conversion from pixel coordinates to "real world" coordinates.
              */
             final AOI iterator = new AOI(tileLower, tileUpper, offsetAOI, dimension);
-            final Map<String,Object> properties = DeferredProperty.forGridGeometry(getGridGeometry(), selectedDimensions);
+            final Map<String,Object> properties = DeferredProperty.forGridGeometry(gridGeometry, selectedDimensions);
             if (deferredTileReading) {
                 image = new TiledDeferredImage(imageSize, tileLower, properties, iterator);
             } else {
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 20aa540796..cb826be6a5 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
@@ -19,6 +19,7 @@ package org.apache.sis.test.storage;
 import java.awt.Point;
 import java.util.Arrays;
 import java.util.Vector;
+import java.util.Objects;
 import java.awt.image.Raster;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
@@ -45,7 +46,7 @@ import static org.junit.Assert.*;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -352,4 +353,28 @@ final class SubsampledImage extends PlanarImage {
         }
         return target;
     }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(source, subX, subY, offX, offY);
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object instanceof SubsampledImage) {
+            final SubsampledImage other = (SubsampledImage) object;
+            return source.equals(other.source) &&
+                   subX == other.subX &&
+                   subY == other.subY &&
+                   offX == other.offX &&
+                   offY == other.offY;
+        }
+        return false;
+    }
 }