sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/05: Specify an area of interest when computing statistics.
Date Mon, 15 Jun 2020 17:31:08 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 41e9f4410d0412001227bcbaf1b8700d9f8bd21f
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Jun 15 16:14:18 2020 +0200

    Specify an area of interest when computing statistics.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  2 +-
 .../org/apache/sis/gui/coverage/RenderingData.java | 20 ++++-
 .../java/org/apache/sis/image/AnnotatedImage.java  | 85 +++++++++++++++++-----
 .../java/org/apache/sis/image/DefaultIterator.java | 14 ++++
 .../java/org/apache/sis/image/ImageProcessor.java  | 37 +++++++---
 .../java/org/apache/sis/image/PixelIterator.java   | 13 ++++
 .../java/org/apache/sis/image/PlanarImage.java     |  4 +-
 .../java/org/apache/sis/image/RecoloredImage.java  |  5 +-
 .../org/apache/sis/image/StatisticsCalculator.java | 49 ++++++-------
 .../apache/sis/image/StatisticsCalculatorTest.java | 19 ++++-
 10 files changed, 181 insertions(+), 67 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 95f1d30..1ddcbe2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -414,7 +414,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                 resampledToDisplay = data.getTransform(objectiveToDisplay);
             }
             if (filteredImage == null) {
-                filteredImage = data.filter(resampledImage);
+                filteredImage = data.filter(resampledImage, displayBounds);
             }
             prefetchedImage = data.prefetch(filteredImage, resampledToDisplay, displayBounds);
         }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index 42a3e72..c269bb2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -24,6 +24,7 @@ import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Rectangle2D;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.CoordinateOperation;
@@ -268,9 +269,10 @@ final class RenderingData implements Cloneable {
      * Applies the image operation (if any) on the given resampled image, than stretches
the color ramp.
      *
      * @param  resampledImage  the image computed by {@link #resample(CoordinateReferenceSystem,
LinearTransform)}.
+     * @param  displayBounds   size and location of the display device, in pixel units.
      * @return image with operation applied and color ramp stretched. May be the same instance
than given image.
      */
-    final RenderedImage filter(RenderedImage resampledImage) {
+    final RenderedImage filter(RenderedImage resampledImage, final Rectangle2D displayBounds)
{
         /*
          * If the operation is not `NONE` but following call to `apply(…)` returns `resampledImage`
unchanged,
          * it means that the operation can not be applied on that image. We should reset
operation to `NONE`,
@@ -289,9 +291,21 @@ final class RenderingData implements Cloneable {
              */
             if (selectedDerivative.operation == ImageOperation.NONE) {
                 if (statistics == null) {
-                    statistics = processor.getStatistics(data);
+                    statistics = processor.getStatistics(data, null);
                 }
                 modifiers.put("statistics", statistics);
+            } else {
+                /*
+                 * If an operation is applied, compute statistics only for currently visible
region.
+                 * This is necessary because zoomed images may be very large. This is usually
not a
+                 * problem because only requested tiles are computed, but statistics requested
without
+                 * bounds would cause all those tiles to be computed.
+                 *
+                 * Inconvenient is that visual appareance is not stable: the color ramp may
change
+                 * after every zoom, or may not fit data anymore after a pan. Since we need
to revisit
+                 * the coverage operation framework anyway, we live with that problem for
now.
+                 */
+                modifiers.put("areaOfInterest", displayBounds.getBounds());
             }
             if (selectedDerivative.styling == Stretching.AUTOMATIC) {
                 modifiers.put("MultStdDev", 3);
@@ -305,7 +319,7 @@ final class RenderingData implements Cloneable {
      * Computes immediately, possibly using many threads, the tiles that are going to be
displayed.
      * The returned instance should be used only for current rendering event; it should not
be cached.
      *
-     * @param  filteredImage       the image computed by {@link #filter(RenderedImage)}.
+     * @param  filteredImage       the image computed by {@link #filter(RenderedImage, Rectangle2D)}.
      * @param  resampledToDisplay  the transform computed by {@link #getTransform(LinearTransform)}.
      * @param  displayBounds       size and location of the display device, in pixel units.
      * @return a temporary image with tiles intersecting the display region already computed.
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 57b707c..0b868cd 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
@@ -17,12 +17,14 @@
 package org.apache.sis.image;
 
 import java.util.Locale;
+import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.logging.Filter;
 import java.util.stream.Collector;
 import java.awt.Image;
+import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
@@ -33,6 +35,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
 
 /**
@@ -99,6 +102,25 @@ abstract class AnnotatedImage extends ImageAdapter {
     private final Cache<String,Object> cache;
 
     /**
+     * Pixel coordinates of the region for which to compute the values, or {@code null} for
the whole image.
+     * If non-null, the {@link Shape#contains(double, double)} method may be invoked for
testing if a pixel
+     * shall be included in the computation or not.
+     *
+     * <p>This shape should not be modified, either by this class or by the caller
who provided the shape.
+     * The {@code Shape} implementation shall be thread-safe, assuming its state stay unmodified,
unless
+     * the {@link #parallel} argument specified to the constructor was {@code false}.</p>
+     */
+    protected final Shape areaOfInterest;
+
+    /**
+     * Bounds of {@link #areaOfInterest} intersected with image bounds, or {@code null} for
the whole image.
+     * If the area of interest fully contains those bounds, then {@link #areaOfInterest}
is set to the same
+     * reference than {@code boundsOfInterest}. Subclasses can use {@code areaOfInterest
== boundsOfInterest}
+     * for quickly testing if the area of interest is rectangular.
+     */
+    protected final Rectangle boundsOfInterest;
+
+    /**
      * The errors that occurred while computing the result, or {@code null} if none or not
yet determined.
      * This field is never set if {@link #failOnException} is {@code true}.
      */
@@ -120,11 +142,24 @@ abstract class AnnotatedImage extends ImageAdapter {
      * The annotations are the additional properties computed by the subclass.
      *
      * @param  source           the image to wrap for adding properties (annotations).
+     * @param  areaOfInterest   pixel coordinates of AOI, or {@code null} for the whole image.
      * @param  parallel         whether parallel execution is authorized.
      * @param  failOnException  whether errors occurring during computation should be propagated.
      */
-    protected AnnotatedImage(RenderedImage source, final boolean parallel, final boolean
failOnException) {
+    protected AnnotatedImage(RenderedImage source, Shape areaOfInterest,
+                             final boolean parallel, final boolean failOnException)
+    {
         super(source);
+        if (areaOfInterest != null) {
+            boundsOfInterest = areaOfInterest.getBounds();
+            ImageUtilities.clipBounds(source, boundsOfInterest);
+            if (areaOfInterest.contains(boundsOfInterest)) {
+                areaOfInterest = boundsOfInterest;
+            }
+        } else {
+            boundsOfInterest = null;
+        }
+        this.areaOfInterest  = areaOfInterest;
         this.parallel        = parallel;
         this.failOnException = failOnException;
         /*
@@ -188,8 +223,7 @@ abstract class AnnotatedImage extends ImageAdapter {
     /**
      * Gets a property from this image or from its source. If the given name is for the property
      * to be computed by this class and if that property has not been computed before, then
this
-     * method invokes {@link #computeProperty(Rectangle)} with a {@code null} "area of interest"
-     * argument value. That {@code computeProperty(…)} result will be cached.
+     * method invokes {@link #computeProperty()} and caches its result.
      *
      * @param  name  name of the property to get.
      * @return the property for the given name ({@code null} is a valid result),
@@ -212,7 +246,7 @@ abstract class AnnotatedImage extends ImageAdapter {
                 try {
                     value = handler.peek();
                     if (value == null) {
-                        value = computeProperty(null);
+                        value = computeProperty();
                         if (value == null) value = NULL;
                         success = (errors == null);
                     }
@@ -305,21 +339,15 @@ abstract class AnnotatedImage extends ImageAdapter {
      *       and the area of interest covers at least two tiles, then this method distributes
      *       calculation on many threads using the functions provided by the collector.
      *       See {@link #collector()} Javadoc for more information.</li>
-     *   <li>Otherwise this method delegates to {@link #computeSequentially(Rectangle)}.</li>
+     *   <li>Otherwise this method delegates to {@link #computeSequentially()}.</li>
      * </ul>
      *
-     * The {@code areaOfInterest} argument is {@code null} by default, which means to calculate
-     * the property on all tiles. This argument exists for allowing subclasses to override
this
-     * method and invoke {@code super.computeProperty(…)} with a sub-region to compute.
-     *
-     * @param  areaOfInterest  pixel coordinates of the region of interest, or {@code null}
for the whole image.
-     *         It is caller responsibility to ensure that this rectangle is fully included
inside image bounds.
      * @return the computed property value. Note that {@code null} is a valid result.
      * @throws Exception if an error occurred while computing the property.
      */
-    protected Object computeProperty(final Rectangle areaOfInterest) throws Exception {
+    protected Object computeProperty() throws Exception {
         if (parallel) {
-            final TileOpExecutor executor = new TileOpExecutor(source, areaOfInterest);
+            final TileOpExecutor executor = new TileOpExecutor(source, boundsOfInterest);
             if (executor.isMultiTiled()) {
                 final Collector<? super Raster,?,?> collector = collector();
                 if (collector != null) {
@@ -327,7 +355,7 @@ abstract class AnnotatedImage extends ImageAdapter {
                 }
             }
         }
-        return computeSequentially(areaOfInterest);
+        return computeSequentially();
     }
 
     /**
@@ -339,13 +367,10 @@ abstract class AnnotatedImage extends ImageAdapter {
      * returned {@code null}), or when it is not worth to parallelize (image has only one
tile), or when
      * the {@linkplain #source} image may be non-thread safe ({@link #parallel} is {@code
false}).</p>
      *
-     * @param  areaOfInterest  pixel coordinates of the region of interest, or {@code null}
for the whole image.
-     *         This is the argument given to {@link #computeProperty(Rectangle)} and can
usually be ignored
-     *         (because always {@code null}) if that method has not been overridden.
      * @return the computed property value. Note that {@code null} is a valid result.
      * @throws Exception if an error occurred while computing the property.
      */
-    protected abstract Object computeSequentially(Rectangle areaOfInterest) throws Exception;
+    protected abstract Object computeSequentially() throws Exception;
 
     /**
      * Returns the function to execute for computing the property value, together with other
required functions
@@ -408,4 +433,28 @@ abstract class AnnotatedImage extends ImageAdapter {
         buffer.append('"').append(key).append('"');
         return AnnotatedImage.class;
     }
+
+    /**
+     * Returns a hash code value for this image. This method should be quick;
+     * it should not compute the hash code from sample values.
+     *
+     * @return a hash code value based on a description of the operation performed by this
image.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hashCode(areaOfInterest);
+    }
+
+    /**
+     * 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.
+     *
+     * @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) {
+        return super.equals(object) && Objects.equals(areaOfInterest, ((AnnotatedImage)
object).areaOfInterest);
+        // The `boundsOfInterest` is omitted because it is derived from `areaOfInterest`.
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/DefaultIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/DefaultIterator.java
index e5daa80..35f0506 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/DefaultIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/DefaultIterator.java
@@ -20,6 +20,7 @@ import java.util.Optional;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
+import java.awt.Shape;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -180,6 +181,19 @@ class DefaultIterator extends WritablePixelIterator {
     }
 
     /**
+     * Returns {@code true} if current iterator position is inside the given shape.
+     * Current version does not verify if iteration started or finished
+     * (this method is non-public for that reason).
+     *
+     * @param  domain  the shape for which to test inclusion.
+     * @return whether current iterator position is inside the given shape.
+     */
+    @Override
+    boolean isInside(final Shape domain) {
+        return domain.contains(x, y);
+    }
+
+    /**
      * Moves the pixel iterator to the given column (x) and row (y) indices.
      *
      * @param  px  the column index of the pixel to make current.
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 f339abf..82a80e6 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
@@ -22,6 +22,7 @@ import java.util.Arrays;
 import java.util.Objects;
 import java.util.logging.Filter;
 import java.util.logging.LogRecord;
+import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
@@ -65,6 +66,13 @@ import org.apache.sis.measure.Units;
  *   </li>
  * </ul>
  *
+ * <h2>Area of interest</h2>
+ * Some operations accept an optional <cite>area of interest</cite> argument
specified as a {@link Shape} instance in
+ * pixel coordinates. If a shape is given, it should not be modified after {@code ImageProcessor}
method call because
+ * the given object may be retained directly (i.e. the {@code Shape} is not always cloned;
it depends on its class).
+ * In addition, the {@code Shape} implementation shall be thread-safe (assuming its state
stay unmodified)
+ * unless the execution mode is set to {@link Mode#PARALLEL}.
+ *
  * <h2>Error handling</h2>
  * If an exception occurs during the computation of a tile, then the {@code ImageProcessor}
behavior
  * is controlled by the {@link #getErrorAction() errorAction} property:
@@ -428,23 +436,25 @@ public class ImageProcessor implements Cloneable {
 
     /**
      * Returns statistics (minimum, maximum, mean, standard deviation) on each bands of the
given image.
-     * Invoking this method is equivalent to invoking {@link #statistics(RenderedImage)}
and extracting
-     * immediately the statistics property value, except that errors are handled by the
+     * Invoking this method is equivalent to invoking {@link #statistics(RenderedImage, Shape)}
and
+     * extracting immediately the statistics property value, except that errors are handled
by the
      * {@linkplain #getErrorAction() error handler}.
      *
-     * @param  source  the image for which to compute statistics.
+     * @param  source          the image for which to compute statistics.
+     * @param  areaOfInterest  pixel coordinates of the area of interest, or {@code null}
for the whole image.
      * @return the statistics of sample values in each band.
      * @throws ImagingOpException if an error occurred during calculation
      *         and the error handler is {@link ErrorAction#THROW}.
      *
-     * @see #statistics(RenderedImage)
+     * @see #statistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
      */
-    public Statistics[] getStatistics(final RenderedImage source) {
+    public Statistics[] getStatistics(final RenderedImage source, final Shape areaOfInterest)
{
         ArgumentChecks.ensureNonNull("source", source);
         Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
         if (!(property instanceof Statistics[])) {
-            final StatisticsCalculator calculator = new StatisticsCalculator(source, parallel(source),
failOnException());
+            final StatisticsCalculator calculator = new StatisticsCalculator(
+                    source, areaOfInterest, parallel(source), failOnException());
             property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
             calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener());
         }
@@ -457,16 +467,17 @@ public class ImageProcessor implements Cloneable {
      * then that image is returned as-is. Otherwise this method returns a new image having
that property.
      * The property value will be computed when first requested (it is not computed by this
method).
      *
-     * @param  source  the image for which to provide statistics (may be {@code null}).
+     * @param  source          the image for which to provide statistics (may be {@code null}).
+     * @param  areaOfInterest  pixel coordinates of the area of interest, or {@code null}
for the whole image.
      * @return an image with an {@value StatisticsCalculator#STATISTICS_KEY} property.
      *         May be {@code image} if the given argument is null or already has a statistics
property.
      *
-     * @see #getStatistics(RenderedImage)
+     * @see #getStatistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
      */
-    public RenderedImage statistics(final RenderedImage source) {
+    public RenderedImage statistics(final RenderedImage source, final Shape areaOfInterest)
{
         return (source == null) || ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)
-                ? source : unique(new StatisticsCalculator(source, parallel(source), failOnException()));
+                ? source : unique(new StatisticsCalculator(source, areaOfInterest, parallel(source),
failOnException()));
     }
 
     /**
@@ -496,7 +507,7 @@ public class ImageProcessor implements Cloneable {
      * Returns an image with the same sample values than the given image, but with its color
ramp stretched between
      * automatically determined bounds. This is the same operation than {@link #stretchColorRamp(RenderedImage,
      * double, double) stretchColorRamp(…)} except that the minimum and maximum values
are determined by
-     * {@linkplain #getStatistics(RenderedImage) statistics} on the image:
+     * {@linkplain #getStatistics(RenderedImage, Shape) statistics} on the image:
      * a range of value is determined first from the {@linkplain Statistics#minimum() minimum}
and
      * {@linkplain Statistics#maximum() maximum} values found in the image, optionally narrowed
to an interval
      * of some {@linkplain Statistics#standardDeviation(boolean) standard deviations} around
the mean value.
@@ -526,6 +537,10 @@ public class ImageProcessor implements Cloneable {
      *     <td>{@code "statistics"}</td>
      *     <td>Statistics or image from which to get statistics.</td>
      *     <td>{@link Statistics} or {@link RenderedImage}</td>
+     *   </tr><tr>
+     *     <td>{@code "areaOfInterest"}</td>
+     *     <td>Pixel coordinates of the region for which to compute statistics.</td>
+     *     <td>{@link Shape}</td>
      *   </tr>
      * </table>
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index f3e53fd..6cdcca8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -22,6 +22,7 @@ import java.nio.Buffer;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
+import java.awt.Shape;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
@@ -546,6 +547,18 @@ public abstract class PixelIterator {
     public abstract Point getPosition();
 
     /**
+     * Returns {@code true} if current iterator position is inside the given shape.
+     * Current version does not require implementations to check if iteration started or
finished
+     * (this method is non-public for that reason).
+     *
+     * @param  domain  the shape for which to test inclusion.
+     * @return whether current iterator position is inside the given shape.
+     */
+    boolean isInside(final Shape domain) {
+        return domain.contains(getPosition());
+    }
+
+    /**
      * Moves the pixel iterator to the given column (x) and row (y) indices. After this method
invocation,
      * the iterator state is as if the {@link #next()} method has been invoked just before
to reach the
      * specified position.
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 4344a71..32547ba 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
@@ -145,10 +145,10 @@ public abstract class PlanarImage implements RenderedImage {
      *
      * <p>Values should be instances of <code>{@linkplain org.apache.sis.math.Statistics}[]</code>.
      * The array length should be the number of bands. If this property is not provided,
Apache SIS
-     * may have to {@linkplain ImageProcessor#statistics(RenderedImage) compute statistics
itself}
+     * may have to {@linkplain ImageProcessor#statistics(RenderedImage, Shape) compute statistics
itself}
      * (by iterating over pixel values) when needed.</p>
      *
-     * @see ImageProcessor#statistics(RenderedImage)
+     * @see ImageProcessor#statistics(RenderedImage, Shape)
      */
     public static final String STATISTICS_KEY = "org.apache.sis.Statistics";
 
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 c70970e..fbc9993 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
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.util.Map;
+import java.awt.Shape;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
@@ -134,7 +135,9 @@ final class RecoloredImage extends ImageAdapter {
         if (visibleBand >= 0) {
             if (statistics == null) {
                 if (statsAllBands == null) {
-                    statsAllBands = processor.getStatistics(statsSource);
+                    final Object areaOfInterest = modifiers.get("areaOfInterest");
+                    statsAllBands = processor.getStatistics(statsSource,
+                            (areaOfInterest instanceof Shape) ? (Shape) areaOfInterest :
null);
                 }
                 if (statsAllBands != null && visibleBand < statsAllBands.length)
{
                     statistics = statsAllBands[visibleBand];
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 e413429..b0af678 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
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.image;
 
-import java.awt.Rectangle;
+import java.awt.Shape;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.ImagingOpException;
@@ -40,13 +40,14 @@ final class StatisticsCalculator extends AnnotatedImage {
      * Creates a new calculator.
      *
      * @param  image            the image for which to compute statistics.
+     * @param  areaOfInterest   pixel coordinates of AOI, or {@code null} for the whole image.
      * @param  parallel         whether parallel execution is authorized.
      * @param  failOnException  whether errors occurring during computation should be propagated.
      */
-    StatisticsCalculator(final RenderedImage image,
+    StatisticsCalculator(final RenderedImage image, final Shape areaOfInterest,
                          final boolean parallel, final boolean failOnException)
     {
-        super(image, parallel, failOnException);
+        super(image, areaOfInterest, parallel, failOnException);
     }
 
     /**
@@ -75,44 +76,38 @@ final class StatisticsCalculator extends AnnotatedImage {
      * This method is invoked in both sequential and parallel case. In the sequential case
it
      * is invoked for the whole image; in the parallel case it is invoked for only one tile.
      *
+     * <p>This method may be invoked concurrently by many threads.
+     * Fields used by this method shall be thread-safe when not modified.</p>
+     *
      * @param accumulator  where to accumulate the statistics results.
      * @param it           the iterator on a raster or on the whole image.
      */
-    private static void compute(final Statistics[] accumulator, final PixelIterator it) {
+    private void compute(final Statistics[] accumulator, final PixelIterator it) {
         double[] samples = null;
         while (it.next()) {
-            samples = it.getPixel(samples);                 // Get values in all bands.
-            for (int i=0; i<accumulator.length; i++) {
-                accumulator[i].accept(samples[i]);
+            if (areaOfInterest == null || it.isInside(areaOfInterest)) {
+                samples = it.getPixel(samples);                 // Get values in all bands.
+                for (int i=0; i<accumulator.length; i++) {
+                    accumulator[i].accept(samples[i]);
+                }
             }
         }
     }
 
     /**
-     * Computes statistics on the given image in a sequential way (everything computed in
current thread).
-     * This is used for testing purposes, or when the image has only one tile, or when the
implementation
-     * of {@link RenderedImage#getTile(int, int)} may be non thread-safe.
-     *
-     * @param  source  the image on which to compute statistics.
-     * @return statistics on the given image computed sequentially.
+     * Computes the statistics on the image using a single thread. This is used for testing
purposes, or when
+     * the image has only one tile, or when the implementation of {@link RenderedImage#getTile(int,
int)} may
+     * be non thread-safe.
      */
-    static Statistics[] computeSequentially(final RenderedImage source) {
-        final PixelIterator it = PixelIterator.create(source);
+    @Override
+    protected Object computeSequentially() {
+        final PixelIterator it = new PixelIterator.Builder().setRegionOfInterest(boundsOfInterest).create(source);
         final Statistics[] accumulator = createAccumulator(it.getNumBands());
         compute(accumulator, it);
         return accumulator;
     }
 
     /**
-     * Computes the statistics on the whole image using a single thread. This method is invoked
when it is
-     * not worth to parallelize (image has only one tile), or when the source image may be
non-thread safe.
-     */
-    @Override
-    protected Object computeSequentially(Rectangle areaOfInterest) {
-        return computeSequentially(source);
-    }
-
-    /**
      * Invoked when a property of the given name has been requested and that property is
cached.
      * The property should be cloned before to be returned to the user in order to protect
this image state.
      */
@@ -131,7 +126,7 @@ final class StatisticsCalculator extends AnnotatedImage {
      */
     @Override
     protected Collector<Raster, Statistics[], Statistics[]> collector() {
-        return Collector.of(this::createAccumulator, StatisticsCalculator::compute, StatisticsCalculator::combine);
+        return Collector.of(this::createAccumulator, this::compute, StatisticsCalculator::combine);
     }
 
     /**
@@ -170,7 +165,7 @@ final class StatisticsCalculator extends AnnotatedImage {
      * @param  tile         the tile on which to perform a computation.
      * @throws RuntimeException if the calculation failed.
      */
-    private static void compute(final Statistics[] accumulator, final Raster tile) {
-        compute(accumulator, new PixelIterator.Builder().create(tile));
+    private void compute(final Statistics[] accumulator, final Raster tile) {
+        compute(accumulator, new PixelIterator.Builder().setRegionOfInterest(boundsOfInterest).create(tile));
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index 48cd363..02d206a 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.image;
 
 import java.util.Random;
 import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.math.Statistics;
@@ -76,6 +77,16 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
     }
 
     /**
+     * Computes statistics on the given image in a sequential way (everything computed in
current thread).
+     *
+     * @param  source  the image on which to compute statistics.
+     * @return statistics on the given image computed sequentially.
+     */
+    private static Statistics[] computeSequentially(final RenderedImage source) {
+        return (Statistics[]) new StatisticsCalculator(source, null, false, true).computeSequentially();
+    }
+
+    /**
      * Tests with parallel execution. The result of sequential execution is used as a reference.
      */
     @Test
@@ -83,8 +94,8 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         final ImageProcessor operations = new ImageProcessor();
         operations.setExecutionMode(ImageProcessor.Mode.PARALLEL);
         final TiledImageMock image = createImage();
-        final Statistics[] expected = StatisticsCalculator.computeSequentially(image);
-        final Statistics[] actual = operations.getStatistics(image);
+        final Statistics[] expected = computeSequentially(image);
+        final Statistics[] actual = operations.getStatistics(image, null);
         for (int i=0; i<expected.length; i++) {
             final Statistics e = expected[i];
             final Statistics a = actual  [i];
@@ -105,7 +116,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase
{
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(-8739538736973900203L));
         try {
-            operations.getStatistics(image);
+            operations.getStatistics(image, null);
             fail("Expected ImagingOpException.");
         } catch (ImagingOpException e) {
             final String message = e.getMessage();
@@ -125,7 +136,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase
{
         operations.setErrorAction(ImageProcessor.ErrorAction.LOG);
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(8004277484984714811L));
-        final Statistics[] stats = operations.getStatistics(image);
+        final Statistics[] stats = operations.getStatistics(image, null);
         for (final Statistics a : stats) {
             assertTrue(a.count() > 0);
         }


Mime
View raw message