sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Apply a more uniform way to define and handle RenderedImage properties. Prepare CoverageCanvas to handle resampled GridCoverage by executing image operations in a single Process internal class (for making easier to control the chain of operations).
Date Wed, 06 May 2020 21:49:20 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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 78e494d  Apply a more uniform way to define and handle RenderedImage properties. Prepare CoverageCanvas to handle resampled GridCoverage by executing image operations in a single Process internal class (for making easier to control the chain of operations).
78e494d is described below

commit 78e494daf39e343753d0e1f76f71c2a82307c325
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed May 6 23:47:06 2020 +0200

    Apply a more uniform way to define and handle RenderedImage properties.
    Prepare CoverageCanvas to handle resampled GridCoverage by executing image operations in a single Process internal class (for making easier to control the chain of operations).
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    | 271 +++++++++++++++------
 .../apache/sis/gui/coverage/CoverageControls.java  |   7 +-
 .../apache/sis/internal/gui/ImageRenderings.java   |   5 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |  87 +++++--
 .../java/org/apache/sis/image/PlanarImage.java     |  35 ++-
 .../java/org/apache/sis/image/ResampledImage.java  |  69 +++++-
 .../org/apache/sis/image/StatisticsCalculator.java |   9 +-
 .../coverage/j2d/BandedSampleConverter.java        |  16 ++
 .../apache/sis/image/StatisticsCalculatorTest.java |   6 +-
 .../operation/matrix/AffineTransforms2D.java       |  23 ++
 .../src/main/java/org/apache/sis/util/Numbers.java |  20 +-
 11 files changed, 427 insertions(+), 121 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 d38fba4..2492e7f 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
@@ -36,6 +36,7 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.gui.ImageRenderings;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.gui.map.MapCanvasAWT;
@@ -78,14 +79,14 @@ public class CoverageCanvas extends MapCanvasAWT {
     /**
      * Different ways to represent the data. The {@link #data} field shall be one value from this map.
      *
-     * @see #setImage(Stretching, RenderedImage)
+     * @see #setDerivedImage(Stretching, RenderedImage)
      */
-    private final EnumMap<Stretching,RenderedImage> dataAlternatives;
+    private final EnumMap<Stretching,RenderedImage> stretchedColorRamps;
 
     /**
-     * Key of the currently selected alternative in {@link #dataAlternatives} map.
+     * Key of the currently selected alternative in {@link #stretchedColorRamps} map.
      *
-     * @see #setImage(Stretching, RenderedImage)
+     * @see #setDerivedImage(Stretching, RenderedImage)
      */
     private Stretching currentDataAlternative;
 
@@ -99,7 +100,8 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * The {@link GridGeometry#getGridToCRS(PixelInCell)} conversion of rendered {@linkplain #data}
-     * as an affine transform. This is often an immutable instance.
+     * as an affine transform. This is often an immutable instance. A null value is synonymous to
+     * identity transform.
      */
     private AffineTransform gridToCRS;
 
@@ -110,7 +112,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         super(Locale.getDefault());
         coverageProperty       = new SimpleObjectProperty<>(this, "coverage");
         sliceExtentProperty    = new SimpleObjectProperty<>(this, "sliceExtent");
-        dataAlternatives       = new EnumMap<>(Stretching.class);
+        stretchedColorRamps    = new EnumMap<>(Stretching.class);
         currentDataAlternative = Stretching.NONE;
         coverageProperty   .addListener((p,o,n) -> onImageSpecified());
         sliceExtentProperty.addListener((p,o,n) -> onImageSpecified());
@@ -118,10 +120,10 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * Returns the data which are the source of all alternative images that may be stored in the
-     * {@link #dataAlternatives} map. All alternative images are computed from this source.
+     * {@link #stretchedColorRamps} map. All alternative images are computed from this source.
      */
     private RenderedImage getSourceData() {
-        return dataAlternatives.get(Stretching.NONE);
+        return stretchedColorRamps.get(Stretching.NONE);
     }
 
     /**
@@ -193,90 +195,147 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * Invoked when a new coverage has been specified or when the slice extent changed.
-     * This method starts loading in a background thread.
+     * This method fetches the image (which may imply data loading) in a background thread.
      */
     private void onImageSpecified() {
-        data = null;
-        dataAlternatives.clear();
         final GridCoverage coverage = getCoverage();
         if (coverage == null) {
             clear();
         } else {
-            final GridExtent sliceExtent = getSliceExtent();
-            execute(new Task<RenderedImage>() {
-                /** Invoked in background thread for fetching the image. */
-                @Override protected RenderedImage call() {
-                    return coverage.render(sliceExtent);
-                }
-
-                /** Invoked in JavaFX thread on success. */
-                @Override protected void succeeded() {
-                    if (coverage.equals(getCoverage()) && Objects.equals(sliceExtent, getSliceExtent())) {
-                        setImage(getValue(), coverage.getGridGeometry(), sliceExtent);
-                    }
-                }
-            });
+            execute(new Process(coverage, currentDataAlternative));
         }
     }
 
     /**
-     * Invoked when the user selected a new color stretching mode. Also invoked {@linkplain #onImageSpecified after
-     * loading a new image or a new slice} for switching the new image to the same type of range as previously selected.
-     * If the image for the specified type is not already available, then this method computes the image in a background
-     * thread and refreshes the view after the computation completed.
+     * Invoked when the user selected a new color stretching mode. Also invoked {@linkplain #setRawImage after
+     * loading a new image or a new slice} for switching the new image to the same type of range as previously
+     * selected. If the image for the specified type is not already available, then this method computes the
+     * image in a background thread and refreshes the view after the computation completed.
      */
     final void setStretching(final Stretching type) {
         currentDataAlternative = type;
-        final RenderedImage alt = dataAlternatives.get(type);
+        final RenderedImage alt = stretchedColorRamps.get(type);
         if (alt != null) {
-            setImage(type, alt);
+            setDerivedImage(type, alt);
         } else {
             final RenderedImage source = getSourceData();
             if (source != null) {
-                execute(new Task<RenderedImage>() {
-                    /** Invoked in background thread for fetching the image. */
-                    @Override protected RenderedImage call() {
-                        switch (type) {
-                            case VALUE_RANGE: return ImageRenderings.valueRangeStretching(source);
-                            case AUTOMATIC:   return ImageRenderings. automaticStretching(source);
-                            default:          return source;
-                        }
-                    }
-
-                    /** Invoked in JavaFX thread on success. */
-                    @Override protected void succeeded() {
-                        if (source.equals(getSourceData())) {
-                            setImage(type, getValue());
-                        }
-                    }
-                });
+                execute(new Process(source, type));
             }
         }
     }
 
     /**
-     * Invoked in JavaFX thread for setting the image to show. The given image should be a slice
-     * produced by current value of {@link #coverageProperty} (should be verified by the caller).
+     * Loads or resample images before to show them in the canvas. This class performs some or all of
+     * the following tasks, in order. It is possible to skip the first tasks if they are already done,
+     * but after the work started at some point all remaining points are executed:
      *
-     * @param  type  the type of range used for scaling the color ramp of given image.
-     * @param  alt   the image or alternative image to show (can be {@code null}).
+     * <ol>
+     *   <li>Loads the image.</li>
+     *   <li>Compute statistics on sample values (if needed).</li>
+     *   <li>Reproject the image (if needed).</li>
+     * </ol>
      */
-    private void setImage(final Stretching type, RenderedImage alt) {
-        /*
-         * Store the result but do not necessarily show it because maybe the user changed the
-         * `Stretching` during the time the background thread was working. If the user did not
-         * changed the type, then the `alt` variable below will stay unchanged.
+    private final class Process extends Task<RenderedImage> {
+        /**
+         * The coverage from which to fetch an image, or {@code null} if the {@link #source} is already known.
          */
-        dataAlternatives.put(type, alt);
-        alt = dataAlternatives.get(currentDataAlternative);
-        if (!Objects.equals(alt, data)) {
-            data = alt;
-            requestRepaint();
+        private final GridCoverage coverage;
+
+        /**
+         * The {@linkplain #coverage} slice to fetch, or {@code null} if {@link #coverage} is null
+         * or for loading the whole coverage extent.
+         */
+        private final GridExtent sliceExtent;
+
+        /**
+         * The source image, or {@code null} if it will be the result of fetching an image from
+         * the {@linkplain #coverage}. If non-null then it should be {@link #getSourceData()}.
+         */
+        private RenderedImage source;
+
+        /**
+         * The color ramp stretching to apply, or {@link Stretching#NONE} if none.
+         */
+        private final Stretching stretching;
+
+        /**
+         * Creates a new process which will load data from the specified coverage.
+         */
+        Process(final GridCoverage coverage, final Stretching stretching) {
+            this.coverage    = coverage;
+            this.sliceExtent = getSliceExtent();
+            this.stretching  = stretching;
+        }
+
+        /**
+         * Creates a new process which will resample the given image.
+         */
+        Process(final RenderedImage source, final Stretching stretching) {
+            this.coverage    = null;
+            this.sliceExtent = null;
+            this.source      = source;
+            this.stretching  = stretching;
+        }
+
+        /**
+         * Invoked in background thread for fetching the image, stretching the color ramp or resampling.
+         * This method performs some or all steps documented in class Javadoc, with possibility to skip
+         * the first step is required source image is already loaded.
+         */
+        @Override protected RenderedImage call() {
+            if (source == null) {
+                source = coverage.render(sliceExtent);
+            }
+            final RenderedImage derived;
+            switch (stretching) {
+                case VALUE_RANGE: derived = ImageRenderings.valueRangeStretching(source); break;
+                case AUTOMATIC:   derived = ImageRenderings. automaticStretching(source); break;
+                default:          derived = source; break;
+            }
+            return derived;
+        }
+
+        /**
+         * Invoked in JavaFX thread on success. This method stores the computation results, provided that
+         * the settings ({@link #coverage}, source image, <i>etc.</i>) are still the ones for which the
+         * computation has been launched.
+         */
+        @Override protected void succeeded() {
+            /*
+             * The image is shown only if the coverage and extent did not changed during the time we were
+             * loading in background thread (if they changed, another thread is probably running for them).
+             * After `setRawImage(…)` execution, `getSourceData()` should return the given `source`.
+             */
+            if (coverage != null && coverage.equals(getCoverage()) && Objects.equals(sliceExtent, getSliceExtent())) {
+                setRawImage(source, coverage.getGridGeometry(), sliceExtent);
+            }
+            /*
+             * The stretching result is stored only if the user did not changed the image while we were computing
+             * statistics in background thread. This method does not verify if user changed the stretching mode;
+             * this check will be done by `setDerivedImage(…)`.
+             */
+            if (source.equals(getSourceData())) {
+                setDerivedImage(stretching, getValue());
+            }
+        }
+
+        /**
+         * Invoked when an error occurred while loading an image or processing it.
+         * This method popups the dialog box immediately because it is considered
+         * an important error.
+         */
+        @Override protected void failed() {
+            final Throwable ex = getException();
+            errorOccurred(ex);
+            ExceptionReporter.canNotUseResource(ex);
         }
     }
 
     /**
-     * Invoked when a new image has been successfully loaded.
+     * Invoked when a new image has been successfully loaded. The given image must the the "raw" image,
+     * without resampling and without color ramp stretching. The {@link #setDerivedImage} method may
+     * be invoked after this method for specifying image derived from this raw image.
      *
      * @todo Needs to handle non-affine transform.
      *
@@ -284,29 +343,83 @@ public class CoverageCanvas extends MapCanvasAWT {
      * @param  geometry     the grid geometry of the coverage that produced the image.
      * @param  sliceExtent  the extent that was requested.
      */
-    private void setImage(final RenderedImage image, final GridGeometry geometry, final GridExtent sliceExtent) {
-        setImage(Stretching.NONE, image);
-        setStretching(currentDataAlternative);
+    private void setRawImage(final RenderedImage image, final GridGeometry geometry, GridExtent sliceExtent) {
+        data = null;
+        stretchedColorRamps.clear();
+        setDerivedImage(Stretching.NONE, image);
         try {
             gridToCRS = AffineTransforms2D.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER));
         } catch (RuntimeException e) {                      // Conversion not defined or not affine.
             gridToCRS = null;
             errorOccurred(e);
         }
+        /*
+         * If the user did not specified a sub-region, set the initial visible area to the envelope
+         * of the whole coverage. The `setObjectiveBounds(…)` method will take care of computing an
+         * initial "objective to display" transform from that information.
+         */
         Envelope visibleArea = null;
-        if (gridToCRS != null && geometry.isDefined(GridGeometry.ENVELOPE)) {
-            visibleArea = geometry.getEnvelope();
-        } else if (geometry.isDefined(GridGeometry.EXTENT)) try {
-            final GridExtent extent = geometry.getExtent();
-            visibleArea = extent.toEnvelope(MathTransforms.identity(extent.getDimension()));
-        } catch (TransformException e) {
-            // Should never happen because we asked for an identity transform.
-            errorOccurred(e);
+        if (sliceExtent == null) {
+            if (gridToCRS != null && geometry.isDefined(GridGeometry.ENVELOPE)) {
+                // This envelope is valid only if we are able to use the `gridToCRS`.
+                visibleArea = geometry.getEnvelope();
+            }
+            if (geometry.isDefined(GridGeometry.EXTENT)) {
+                sliceExtent = geometry.getExtent();
+            }
+        }
+        /*
+         * If geospatial area declared in grid geometry can not be used, compute it from grid extent.
+         * It is the case for example when only a sub-region has been fetched.
+         */
+        if (sliceExtent != null) {
+            if (visibleArea == null) try {
+                visibleArea = sliceExtent.toEnvelope((gridToCRS != null)
+                                ? AffineTransforms2D.toMathTransform(gridToCRS)
+                                : MathTransforms.identity(sliceExtent.getDimension()));
+            } catch (TransformException e) {
+                // Should never happen because we used an affine transform.
+                errorOccurred(e);
+            }
+            /*
+             * Coordinate (0,0) in the image corresponds to the lowest coordinates requested.
+             * For taking that offset in account, we need to apply a translation.
+             */
+            if (gridToCRS != null) {
+                final int[] dimensions = sliceExtent.getSubspaceDimensions(BIDIMENSIONAL);
+                final long tx = sliceExtent.getLow(dimensions[0]);
+                final long ty = sliceExtent.getLow(dimensions[1]);
+                if ((tx | ty) != 0) {
+                    gridToCRS = new AffineTransform(gridToCRS);
+                    gridToCRS.translate(tx, ty);
+                }
+            }
         }
         setObjectiveBounds(visibleArea);
     }
 
     /**
+     * Invoked in JavaFX thread for setting the image to show. The given image should be a slice
+     * produced by current value of {@link #coverageProperty} (should be verified by the caller).
+     *
+     * @param  type  the type of range used for scaling the color ramp of given image.
+     * @param  alt   the image or alternative image to show (can be {@code null}).
+     */
+    private void setDerivedImage(final Stretching type, RenderedImage alt) {
+        /*
+         * Store the result but do not necessarily show it because maybe the user changed the
+         * `Stretching` during the time the background thread was working. If the user did not
+         * changed the type, then the `alt` variable below will stay unchanged.
+         */
+        stretchedColorRamps.put(type, alt);
+        alt = stretchedColorRamps.get(currentDataAlternative);
+        if (!Objects.equals(alt, data)) {
+            data = alt;
+            requestRepaint();
+        }
+    }
+
+    /**
      * Invoked in JavaFX thread for creating a renderer to be executed in a background thread.
      * This method prepares the information needed but does not start the rendering itself.
      * The rendering will be done later by a call to {@link Renderer#paint(Graphics2D)}.
@@ -333,4 +446,14 @@ public class CoverageCanvas extends MapCanvasAWT {
             }
         };
     }
+
+    /**
+     * Removes the image shown and releases memory.
+     */
+    @Override
+    protected void clear() {
+        data = null;
+        stretchedColorRamps.clear();
+        super.clear();
+    }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index e4308d2..52f92b3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -26,7 +26,6 @@ import javafx.scene.layout.GridPane;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import javafx.beans.property.ObjectProperty;
-import javafx.beans.value.ObservableValue;
 import javafx.scene.control.ChoiceBox;
 import javafx.scene.paint.Color;
 import org.opengis.referencing.ReferenceSystem;
@@ -89,7 +88,7 @@ final class CoverageControls extends Controls {
          */
         final VBox displayPane;
         {   // Block for making variables locale to this scope.
-            final ChoiceBox<ReferenceSystem> systems = referenceSystems.createChoiceBox(this::onReferenceSystemSelected);
+            final ChoiceBox<ReferenceSystem> systems = referenceSystems.createChoiceBox((p,o,n) -> onReferenceSystemSelected(n));
             systems.setMaxWidth(Double.POSITIVE_INFINITY);
             referenceSystem = systems.valueProperty();
             final Label systemLabel = new Label(vocabulary.getLabel(Vocabulary.Keys.ReferenceSystem));
@@ -143,9 +142,7 @@ final class CoverageControls extends Controls {
     /**
      * Invoked when a new coordinate reference system is selected.
      */
-    private void onReferenceSystemSelected(final ObservableValue<? extends ReferenceSystem> property,
-                                           final ReferenceSystem oldValue, ReferenceSystem newValue)
-    {
+    private void onReferenceSystemSelected(final ReferenceSystem newValue) {
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
index 7ccd729..48de1c9 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.gui;
 
+import java.util.Collections;
 import java.awt.image.RenderedImage;
 import org.apache.sis.image.ImageProcessor;
 
@@ -55,7 +56,7 @@ public final class ImageRenderings {
      * @return the stretched image.
      */
     public static RenderedImage valueRangeStretching(final RenderedImage image) {
-        return PROCESSOR.automaticColorRamp(image, Double.POSITIVE_INFINITY);
+        return PROCESSOR.stretchColorRamp(image, null);
     }
 
     /**
@@ -67,6 +68,6 @@ public final class ImageRenderings {
      * @return the stretched image.
      */
     public static RenderedImage automaticStretching(final RenderedImage image) {
-        return PROCESSOR.automaticColorRamp(image, 3);
+        return PROCESSOR.stretchColorRamp(image, Collections.singletonMap("MultStdDev", 3));
     }
 }
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 7bc3b56..ed6933d 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,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
@@ -31,6 +32,7 @@ import java.awt.image.ImagingOpException;
 import java.awt.image.RasterFormatException;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.math.Statistics;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.WeakHashSet;
@@ -351,20 +353,45 @@ 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
+     * {@linkplain #getErrorAction() error handler}.
      *
      * @param  source  the image for which to compute statistics.
      * @return the statistics of sample values in each band.
-     * @throws ImagingOpException if an error occurred during calculation and {@code failOnException} is {@code true}.
+     * @throws ImagingOpException if an error occurred during calculation
+     *         and the error handler is {@link ErrorAction#THROW}.
+     *
+     * @see #statistics(RenderedImage)
+     * @see StatisticsCalculator#STATISTICS_KEY
      */
-    public Statistics[] statistics(final RenderedImage source) {
+    public Statistics[] getStatistics(final RenderedImage source) {
         ArgumentChecks.ensureNonNull("source", source);
-        final StatisticsCalculator calculator = new StatisticsCalculator(source, parallel(source), failOnException());
-        final Object property = calculator.getProperty(StatisticsCalculator.PROPERTY_NAME);
-        calculator.logAndClearError(ImageProcessor.class, "statistics", errorListener());
-        if (property instanceof Statistics[]) {
-            return (Statistics[]) property;
+        Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
+        if (!(property instanceof Statistics[])) {
+            final StatisticsCalculator calculator = new StatisticsCalculator(source, parallel(source), failOnException());
+            property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
+            calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener());
         }
-        return null;
+        return (Statistics[]) property;
+    }
+
+    /**
+     * Returns an image with statistics (minimum, maximum, mean, standard deviation) on each bands.
+     * If the given image already contains an {@value StatisticsCalculator#STATISTICS_KEY} property,
+     * 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.
+     * @return an image with an {@value StatisticsCalculator#STATISTICS_KEY} property.
+     *
+     * @see #getStatistics(RenderedImage)
+     * @see StatisticsCalculator#STATISTICS_KEY
+     */
+    public RenderedImage statistics(final RenderedImage source) {
+        ArgumentChecks.ensureNonNull("source", source);
+        return ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)
+                ? source : unique(new StatisticsCalculator(source, parallel(source), failOnException()));
     }
 
     /**
@@ -399,9 +426,10 @@ 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 rescaleColorRamp(…)}
-     * except that the minimum and maximum values are determined by {@linkplain #statistics(RenderedImage) statistics}
-     * on the image: a range of value is determined first from the {@linkplain Statistics#minimum() minimum} and
+     * 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:
+     * 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.
      *
@@ -411,18 +439,41 @@ public class ImageProcessor implements Cloneable {
      * values for the color ramp because a single value very far from other values is sufficient for making the colors
      * difficult to distinguish for 99.9% of the data.</p>
      *
-     * @param  source      the image to recolor (may be {@code null}).
-     * @param  deviations  multiple of standard deviations around the mean, of {@link Double#POSITIVE_INFINITY}
-     *                     for not using standard deviation for narrowing the range of values.
-     *                     Some values giving good results for a Gaussian distribution are 1.5, 2 or 3.
+     * <p>The range of values for the color ramp can be narrowed with following modifiers
+     * (a {@link Map} is used for allowing addition of more modifiers in future Apache SIS versions).
+     * All unrecognized modifiers are silently ignored. If no modifier is specified, then the color ramp
+     * will be stretched from minimum to maximum values.</p>
+     *
+     * <table>
+     *   <caption>Value range modifiers</caption>
+     *   <tr>
+     *     <th>Key</th>
+     *     <th>Purpose</th>
+     *     <th>Examples</th>
+     *   </tr><tr>
+     *     <td>{@code MultStdDev}</td>
+     *     <td>Multiple of the standard deviation.</td>
+     *     <td>1.5, 2 or 3.</td>
+     *   </tr>
+     * </table>
+     *
+     * @param  source     the image to recolor (may be {@code null}).
+     * @param  modifiers  modifiers for narrowing the range of values, or {@code null} if none.
      * @return the image with color ramp stretched between the automatic bounds,
      *         or {@code image} unchanged if the operation can not be applied on the given image.
      */
-    public RenderedImage automaticColorRamp(final RenderedImage source, double deviations) {
-        ArgumentChecks.ensureStrictlyPositive("deviations", deviations);
+    public RenderedImage stretchColorRamp(final RenderedImage source, final Map<String,Number> modifiers) {
+        double deviations = Double.POSITIVE_INFINITY;
+        if (modifiers != null) {
+            Number value = modifiers.get("MultStdDev");
+            if (value != null) {
+                deviations = value.doubleValue();
+                ArgumentChecks.ensureStrictlyPositive("MultStdDev", deviations);
+            }
+        }
         final int visibleBand = ImageUtilities.getVisibleBand(source);
         if (visibleBand >= 0) {
-            final Statistics[] statistics = statistics(source);
+            final Statistics[] statistics = getStatistics(source);
             if (statistics != null && visibleBand < statistics.length) {
                 final Statistics s = statistics[visibleBand];
                 if (s != null) {
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 254db3b..ceab89d 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
@@ -109,13 +109,30 @@ public abstract class PlanarImage implements RenderedImage {
      * Key of a property defining the resolutions of sample values in each band. This property is recommended
      * for images having sample values as floating point numbers. For example if sample values were computed by
      * <var>value</var> = <var>integer</var> × <var>scale factor</var>, then the resolution is the scale factor.
-     * This information is used for choosing the number of fraction digits to show when writing sample values in
-     * text format.
+     * This information can be used for choosing the number of fraction digits to show when writing sample values
+     * in text format.
      *
      * <p>Values should be instances of {@code float[]} or {@code double[]}.
-     * The array length should be the number of bands.</p>
+     * The array length should be the number of bands. This property may be computed automatically during
+     * {@linkplain org.apache.sis.coverage.grid.GridCoverage#forConvertedValues(boolean) conversions from
+     * integer values to floating point values}.</p>
      */
-    public static final String SAMPLE_RESOLUTIONS_KEY = "SampleResolution";
+    public static final String SAMPLE_RESOLUTIONS_KEY = "org.apache.sis.SampleResolution";
+
+    /**
+     * Key of property providing statistics on sample values in each band. Providing a value for this key
+     * is recommended when those statistics are known in advance (for example if they are provided in some
+     * metadata of a raster format). Statistics are useful for stretching a color palette over the values
+     * actually used in an image.
+     *
+     * <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}
+     * (by iterating over pixel values) when needed.</p>
+     *
+     * @see ImageProcessor#statistics(RenderedImage)
+     */
+    public static final String STATISTICS_KEY = "org.apache.sis.Statistics";
 
     /**
      * Creates a new rendered image.
@@ -140,16 +157,18 @@ public abstract class PlanarImage implements RenderedImage {
     }
 
     /**
-     * Gets a property from this image.
-     * The property to get is identified by the specified key. Some keys supported by Apache SIS are:
+     * Gets a property from this image. The property to get is identified by the specified key.
+     * The set of available keys is given by {@link #getPropertyNames()} and depends on the image instance.
+     * The following table gives examples of keys recognized by some Apache SIS {@link RenderedImage} instances:
      *
      * <table class="sis">
-     *   <caption>Recognized property keys</caption>
+     *   <caption>Examples of property keys</caption>
      *   <tr><th>Keys</th>                             <th>Values</th></tr>
      *   <tr><td>{@value #SAMPLE_RESOLUTIONS_KEY}</td> <td>Resolutions of sample values in each band.</td></tr>
+     *   <tr><td>{@value #STATISTICS_KEY}</td>         <td>Minimum, maximum and mean values for each band.</td></tr>
      * </table>
      *
-     * This method returns {@link Image#UndefinedProperty} if the specified property is not defined.
+     * This method shall return {@link Image#UndefinedProperty} if the specified property is not defined.
      * The default implementation returns {@link Image#UndefinedProperty} in all cases.
      *
      * @param  key  the name of the property to get.
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 98eeffc..6418239 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
@@ -16,8 +16,10 @@
  */
 package org.apache.sis.image;
 
+import java.util.Set;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.Collections;
 import java.nio.DoubleBuffer;
 import java.awt.Dimension;
 import java.awt.Rectangle;
@@ -33,6 +35,7 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
@@ -69,6 +72,14 @@ import org.apache.sis.measure.NumberRange;
  */
 public class ResampledImage extends ComputedImage {
     /**
+     * The properties to forwards to source image in calls to {@link #getProperty(String)}.
+     * This list may be augmented in any future Apache SIS version.
+     *
+     * @see #getProperty(String)
+     */
+    private static final Set<String> FILTERED_PROPERTIES = Collections.singleton(SAMPLE_RESOLUTIONS_KEY);
+
+    /**
      * The {@value} value for identifying code expecting exactly 2 dimensions.
      */
     private static final int BIDIMENSIONAL = 2;
@@ -233,7 +244,7 @@ public class ResampledImage extends ComputedImage {
         if (error == null && toSource instanceof MathTransform2D) try {
             final Rectangle bounds = getBounds();
             final Rectangle2D tb = Shapes2D.transform((MathTransform2D) toSource, bounds, bounds);
-            if (!ImageUtilities.getBounds(getSource(0)).intersects(tb)) {
+            if (!ImageUtilities.getBounds(getSource()).intersects(tb)) {
                 return "toSource";
             }
         } catch (TransformException e) {
@@ -244,13 +255,65 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
+     * Returns the unique source of this resampled image.
+     */
+    private RenderedImage getSource() {
+        return getSource(0);
+    }
+
+    /**
      * Returns the same color model than the source image.
      *
      * @return the color model, or {@code null} if unspecified.
      */
     @Override
     public ColorModel getColorModel() {
-        return getSource(0).getColorModel();
+        return getSource().getColorModel();
+    }
+
+    /**
+     * Gets a property from this image. Current default implementation forwards the following property requests
+     * to the source image (more properties may be added to this list in future Apache SIS versions):
+     *
+     * <ul>
+     *   <li>{@value #SAMPLE_RESOLUTIONS_KEY}</li>
+     * </ul>
+     *
+     * Above listed properties are selected because they should have approximately the same values before and after
+     * resampling. {@linkplain #STATISTICS_KEY Statistics} are not in this list because, while minimum and maximum
+     * values should stay approximately the same, the average value and standard deviation may be quite different.
+     */
+    @Override
+    public Object getProperty(final String key) {
+        if (FILTERED_PROPERTIES.contains(key)) {
+            return getSource().getProperty(key);
+        } else {
+            return super.getProperty(key);
+        }
+    }
+
+    /**
+     * Returns the names of all recognized properties, or {@code null} if this image has no properties.
+     * The returned array contains the properties listed in {@link #getProperty(String)} if the source
+     * image has those properties.
+     *
+     * @return names of all recognized properties, or {@code null} if none.
+     */
+    @Override
+    public String[] getPropertyNames() {
+        final String[] names = getSource().getPropertyNames();      // Array should be a copy, so we don't copy again.
+        if (names != null) {
+            int n = 0;
+            for (final String name : names) {
+                if (FILTERED_PROPERTIES.contains(name)) {
+                    names[n++] = name;
+                }
+            }
+            if (n != 0) {
+                return ArraysExt.resize(names, n);
+            }
+        }
+        return null;
     }
 
     /**
@@ -334,7 +397,7 @@ public class ResampledImage extends ComputedImage {
         final PixelIterator it;
         {   // For keeping temporary variables locale.
             final Dimension support = interpolation.getSupportSize();
-            it = new PixelIterator.Builder().setWindowSize(support).create(getSource(0));
+            it = new PixelIterator.Builder().setWindowSize(support).create(getSource());
             final Rectangle domain = it.getDomain();    // Source image bounds.
             xmin = domain.getMinX();                    // We will tolerate 0.5 pixels before (from center to border).
             ymin = domain.getMinY();
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 535f284..e413429 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
@@ -27,7 +27,7 @@ import org.apache.sis.util.resources.Vocabulary;
 
 /**
  * Computes statistics on all pixel values of an image. The results are stored in an array
- * of {@link Statistics} objects (one per band) in a property named {@value #PROPERTY_NAME}.
+ * of {@link Statistics} objects (one per band) in a property named {@value #STATISTICS_KEY}.
  * The statistics can be computed in parallel or sequentially for non thread-safe images.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -37,11 +37,6 @@ import org.apache.sis.util.resources.Vocabulary;
  */
 final class StatisticsCalculator extends AnnotatedImage {
     /**
-     * Name of the property computed by this class.
-     */
-    static final String PROPERTY_NAME = "org.apache.sis.image.statistics";
-
-    /**
      * Creates a new calculator.
      *
      * @param  image            the image for which to compute statistics.
@@ -59,7 +54,7 @@ final class StatisticsCalculator extends AnnotatedImage {
      */
     @Override
     protected String getComputedPropertyName() {
-        return PROPERTY_NAME;
+        return STATISTICS_KEY;
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
index 156212c..6975c84 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BandedSampleConverter.java
@@ -26,13 +26,16 @@ import java.awt.image.BandedSampleModel;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.TileObserver;
+import java.lang.reflect.Array;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.image.ComputedImage;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.NumberRange;
 
 
@@ -99,6 +102,8 @@ public class BandedSampleConverter extends ComputedImage {
          */
         boolean hasResolutions = false;
         final double[] resolutions = new double[converters.length];
+        final Object sr = source.getProperty(SAMPLE_RESOLUTIONS_KEY);
+        final int n = (sr != null && Numbers.isNumber(sr.getClass().getComponentType())) ? Array.getLength(sr) : 0;
         for (int i=0; i<resolutions.length; i++) {
             /*
              * Get the sample value in the middle of the range of valid values for the current band.
@@ -132,6 +137,17 @@ public class BandedSampleConverter extends ComputedImage {
             } catch (TransformException e) {
                 r = Double.NaN;
             }
+            /*
+             * The implicit source resolution if 1 on the assumption that we are converting from
+             * integer values. But if the source image specifies a resolution, use the specified
+             * value instead than the implicit 1 value.
+             */
+            if (i < n) {
+                final Number v = (Number) Array.get(sr, i);
+                if (v != null) {
+                    r *= (v instanceof Float) ? DecimalFunctions.floatToDouble(v.floatValue()) : v.doubleValue();
+                }
+            }
             resolutions[i] = r;
             hasResolutions |= Double.isFinite(r);
         }
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 38021df..b45d8f1 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
@@ -84,7 +84,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         operations.setExecutionMode(ImageProcessor.Mode.PARALLEL);
         final TiledImageMock image = createImage();
         final Statistics[] expected = StatisticsCalculator.computeSequentially(image);
-        final Statistics[] actual = operations.statistics(image);
+        final Statistics[] actual = operations.getStatistics(image);
         for (int i=0; i<expected.length; i++) {
             final Statistics e = expected[i];
             final Statistics a = actual  [i];
@@ -105,7 +105,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(-8739538736973900203L));
         try {
-            operations.statistics(image);
+            operations.getStatistics(image);
             fail("Expected ImagingOpException.");
         } catch (ImagingOpException e) {
             final String message = e.getMessage();
@@ -125,7 +125,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.statistics(image);
+        final Statistics[] stats = operations.getStatistics(image);
         for (final Statistics a : stats) {
             assertTrue(a.count() > 0);
         }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
index 781c76b..04da43b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
@@ -27,6 +27,7 @@ import java.awt.geom.NoninvertibleTransformException;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.ArgumentChecks;
@@ -57,6 +58,8 @@ public final class AffineTransforms2D extends Static {
      * @param  transform  the transform to convert, or {@code null}.
      * @return the transform argument if it can be safely casted (including {@code null} argument) or converted.
      * @throws IllegalArgumentException if the given transform can not be caster or converted.
+     *
+     * @see #toMathTransform(AffineTransform)
      */
     public static AffineTransform castOrCopy(final MathTransform transform) throws IllegalArgumentException {
         if (transform == null || transform instanceof AffineTransform) {
@@ -106,6 +109,26 @@ public final class AffineTransforms2D extends Static {
     }
 
     /**
+     * Creates a math transform from the given affine transform.
+     * This method is the converse of {@link #castOrCopy(MathTransform)}.
+     *
+     * @param  transform  the affine transform to cast or copy as a {@link MathTransform}, or {@code null}.
+     * @return a {@link MathTransform} doing the same operation than the given {@link AffineTransform},
+     *         or {@code null} if the given transform was null.
+     *
+     * @see #castOrCopy(MathTransform)
+     *
+     * @since 1.1
+     */
+    public static LinearTransform toMathTransform(final AffineTransform transform) {
+        if (transform == null || transform instanceof LinearTransform) {
+            return (LinearTransform) transform;
+        } else {
+            return new AffineTransform2D(transform);
+        }
+    }
+
+    /**
      * Transforms the given shape.
      * This method is similar to {@link AffineTransform#createTransformedShape(Shape)} except that:
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java b/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
index eea7c88..bdc01ec 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
@@ -39,7 +39,7 @@ import static java.lang.Double.doubleToLongBits;
  * Static methods working with {@link Number} objects, and a few primitive types by extension.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see org.apache.sis.math.MathFunctions
  *
@@ -181,6 +181,24 @@ public final class Numbers extends Static {
     }
 
     /**
+     * Returns {@code true} if the given {@code type} is a floating point or an integer type.
+     * This method returns {@code true} if either {@link #isFloat(Class)} or {@link #isInteger(Class)}
+     * returns {@code true} for the given argument.
+     *
+     * @param  type  the primitive type or wrapper class to test (can be {@code null}).
+     * @return {@code true} if {@code type} is a floating point or an integer type.
+     *
+     * @see #isFloat(Class)
+     * @see #isInteger(Class)
+     *
+     * @since 1.1
+     */
+    public static boolean isNumber(final Class<?> type) {
+        final Numbers mapping = MAPPING.get(type);
+        return (mapping != null) && (mapping.isInteger | mapping.isFloat);
+    }
+
+    /**
      * Returns the number of bits used by primitive of the specified type.
      * The given type must be a primitive type or its wrapper class.
      *


Mime
View raw message