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();
+ }
}