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 1ec1320dda6fe3cc00847488510b1cce0d7d36c4
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Nov 7 18:03:22 2021 +0100

    Specify the "no data" values when computing statistics for stretching the 
color ramp of an image.
    It makes a difference for image having no "quantitative" categories, 
because those images are not
    converted to units of measurement, which implies that their "no data" 
values were not set to NaN.
---
 .../org/apache/sis/gui/coverage/RenderingData.java |  8 +-
 .../apache/sis/internal/gui/ImageConverter.java    |  6 +-
 .../org/apache/sis/internal/gui/PropertyView.java  |  2 +
 .../java/org/apache/sis/image/ImageProcessor.java  |  4 +
 .../java/org/apache/sis/image/RecoloredImage.java  | 40 ++++++----
 .../java/org/apache/sis/image/Visualization.java   |  4 +-
 .../sis/internal/coverage/SampleDimensions.java    | 87 ++++++++++++++++++++++
 .../apache/sis/internal/coverage/package-info.java |  2 +-
 8 files changed, 133 insertions(+), 20 deletions(-)

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 09699c9..859d3bc 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
@@ -20,7 +20,6 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.Future;
-import java.util.function.DoubleUnaryOperator;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.awt.Graphics2D;
@@ -48,6 +47,7 @@ import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.ErrorHandler;
 import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.internal.coverage.SampleDimensions;
 import org.apache.sis.internal.coverage.j2d.ColorModelType;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.referencing.WraparoundApplicator;
@@ -189,7 +189,9 @@ final class RenderingData implements Cloneable {
 
     /**
      * Statistics on pixel values of current {@link #data}, or {@code null} if 
none or not yet computed.
-     * There is one {@link Statistics} instance per band.
+     * There is one {@link Statistics} instance per band. This is a cache for 
stretching the color ramp
+     * of the image to view. The {@link #recolor()} method uses statistics on 
the source image instead
+     * of statistics on the shown image in order to have stable colors during 
pans or zooms.
      *
      * @see #recolor()
      */
@@ -290,7 +292,7 @@ final class RenderingData implements Cloneable {
         if (selectedDerivative != Stretching.NONE) {
             final Map<String,Object> modifiers = new HashMap<>(4);
             if (statistics == null) {
-                statistics = processor.valueOfStatistics(image, null, 
(DoubleUnaryOperator[]) null);
+                statistics = processor.valueOfStatistics(image, null, 
SampleDimensions.toSampleFilters(processor, dataRanges));
             }
             modifiers.put("statistics", statistics);
             if (selectedDerivative == Stretching.AUTOMATIC) {
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
index 6868f29..a407d7f 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
@@ -108,6 +108,8 @@ final class ImageConverter extends Task<Statistics[]> {
 
     /**
      * Prepares the ARGB values to be written in the JavaFX image.
+     * The task opportunistically returns statistics on all bands of the 
source image.
+     * Those statistics were used for stretching the color ramp before to 
paint the JavaFX image.
      */
     @Override
     protected Statistics[] call() {
@@ -125,7 +127,9 @@ final class ImageConverter extends Task<Statistics[]> {
         height = (int) Math.round(scale * bounds.height);
         final AffineTransform toCanvas = 
AffineTransform.getScaleInstance(scale, scale);
         toCanvas.translate(-bounds.x, -bounds.y);
-
+        /*
+         * Stretch color ramp using statistics on the source image before to 
pain on JavaFX image.
+         */
         final ImageProcessor processor  = new ImageProcessor();
         final Statistics[]   statistics = processor.valueOfStatistics(source, 
bounds, (DoubleUnaryOperator[]) null);
         final RenderedImage  image      = processor.stretchColorRamp(source, 
JDK9.mapOf("multStdDev", 3, "statistics", statistics));
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
index 9543886..50879d1 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
@@ -323,6 +323,8 @@ public final class PropertyView extends 
CompoundFormat<Object> {
 
     /**
      * Invoked when {@link #runningTask} completed its work, either 
successfully or with a failure.
+     *
+     * @param  statistics  statistics for each band of the source image 
(before conversion to JavaFX).
      */
     private void taskCompleted(final Statistics[] statistics) {
         runningTask  = null;
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 4585a76..d6d6b39 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
@@ -754,6 +754,10 @@ public class ImageProcessor implements Cloneable {
      *     <td>Pixel coordinates of the region for which to compute 
statistics.</td>
      *     <td>{@link Shape}</td>
      *   </tr><tr>
+     *     <td>{@code "nodataValues"}</td>
+     *     <td>Values to ignore in statistics.</td>
+     *     <td>{@link Number} or {@code Number[]}</td>
+     *   </tr><tr>
      *     <td>{@code "sampleDimensions"}</td>
      *     <td>Meaning of pixel values.</td>
      *     <td>{@link SampleDimension}</td>
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 9a6dafc..e0a8c78 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
@@ -43,7 +43,7 @@ import org.apache.sis.measure.NumberRange;
  * for {@link ImageProcessor}, defined here for reducing {@link 
ImageProcessor} size.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -116,13 +116,15 @@ final class RecoloredImage extends ImageAdapter {
          * Main use case: color model is (probably) an IndexColorModel or 
ScaledColorModel instance,
          * or something we can handle in the same way.
          */
-        RenderedImage statsSource   = source;
-        Statistics[]  statsAllBands = null;
-        Statistics    statistics    = null;
-        double        minimum       = Double.NaN;
-        double        maximum       = Double.NaN;
-        double        deviations    = Double.POSITIVE_INFINITY;
-        SampleDimension range       = null;
+        RenderedImage   statsSource    = source;
+        Statistics[]    statsAllBands  = null;
+        Statistics      statistics     = null;
+        Shape           areaOfInterest = null;
+        Number[]        nodataValues   = null;
+        SampleDimension range          = null;
+        double          minimum        = Double.NaN;
+        double          maximum        = Double.NaN;
+        double          deviations     = Double.POSITIVE_INFINITY;
         /*
          * Extract and validate parameter values.
          * No calculation started at this stage.
@@ -142,7 +144,18 @@ final class RecoloredImage extends ImageAdapter {
                     ArgumentChecks.ensureStrictlyPositive("multStdDev", 
deviations);
                 }
             }
-            Object value = modifiers.get("statistics");
+            areaOfInterest = Containers.property(modifiers, "areaOfInterest", 
Shape.class);
+            Object value = modifiers.get("nodataValues");
+            if (value != null) {
+                if (value instanceof Number) {
+                    nodataValues = new Number[] {(Number) value};
+                } else if (value instanceof Number[]) {
+                    nodataValues = (Number[]) value;
+                } else {
+                    throw illegalPropertyType(modifiers, "nodataValues", 
value);
+                }
+            }
+            value = modifiers.get("statistics");
             if (value != null) {
                 if (value instanceof RenderedImage) {
                     statsSource = (RenderedImage) value;
@@ -178,10 +191,9 @@ final class RecoloredImage extends ImageAdapter {
         if (Double.isNaN(minimum) || Double.isNaN(maximum)) {
             if (statistics == null) {
                 if (statsAllBands == null) {
-                    final Object areaOfInterest = 
modifiers.get("areaOfInterest");
-                    statsAllBands = processor.valueOfStatistics(statsSource,
-                            (areaOfInterest instanceof Shape) ? (Shape) 
areaOfInterest : null,
-                            (DoubleUnaryOperator[]) null);
+                    final DoubleUnaryOperator[] sampleFilters = new 
DoubleUnaryOperator[visibleBand + 1];
+                    sampleFilters[visibleBand] = 
processor.filterNodataValues(nodataValues);
+                    statsAllBands = processor.valueOfStatistics(statsSource, 
areaOfInterest, sampleFilters);
                 }
                 if (statsAllBands != null && visibleBand < 
statsAllBands.length) {
                     statistics = statsAllBands[visibleBand];
@@ -198,7 +210,7 @@ final class RecoloredImage extends ImageAdapter {
             return source;
         }
         /*
-         * finished to collect information. Derive a new color model from the 
existing one.
+         * Finished to collect information. Derive a new color model from the 
existing one.
          */
         final ColorModel cm;
         if (source.getColorModel() instanceof IndexColorModel) {
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 8ba574b..4e593eb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -39,6 +39,7 @@ import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.internal.coverage.SampleDimensions;
 import org.apache.sis.internal.coverage.CompoundTransform;
 import org.apache.sis.internal.coverage.j2d.Colorizer;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
@@ -244,7 +245,8 @@ final class Visualization extends ResampledImage {
                  * If none of above Colorizer configurations worked, use 
statistics in last resort. We do that
                  * after we reduced the image to a single band, in order to 
reduce the amount of calculations.
                  */
-                final Statistics statistics = 
processor.valueOfStatistics(source, null, (DoubleUnaryOperator[]) 
null)[VISIBLE_BAND];
+                final DoubleUnaryOperator[] sampleFilters = 
SampleDimensions.toSampleFilters(processor, sourceBands);
+                final Statistics statistics = 
processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND];
                 colorizer.initialize(statistics.minimum(), 
statistics.maximum());
             }
             /*
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
new file mode 100644
index 0000000..1881373
--- /dev/null
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage;
+
+import java.util.List;
+import java.util.function.DoubleUnaryOperator;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.util.Static;
+
+
+/**
+ * Utility methods working on {@link SampleDimension} instances.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public final class SampleDimensions extends Static {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private SampleDimensions() {
+    }
+
+    /**
+     * Returns the {@code sampleFilters} arguments to use in a call to
+     * {@link ImageProcessor#statistics ImageProcessor.statistics(…)} for 
excluding no-data values.
+     * If the given sample dimensions are {@linkplain 
SampleDimension#converted() converted to units of measurement},
+     * then all "no data" values are already NaN values and this method 
returns an array of {@code null} operators.
+     * Otherwise this method returns an array of operators that covert "no 
data" values to {@link Double#NaN}.
+     *
+     * <p>This method is not in public API because it partially duplicates the 
work
+     * of {@linkplain SampleDimension#getTransferFunction() transfer 
function}.</p>
+     *
+     * @param  processor  the processor to use for creating {@link 
DoubleUnaryOperator}.
+     * @param  bands      the sample dimensions for which to create {@code 
sampleFilters}, or {@code null}.
+     * @return the filters, or {@code null} if {@code bands} was null. The 
array may contain null elements.
+     */
+    public static DoubleUnaryOperator[] toSampleFilters(final ImageProcessor 
processor, final List<SampleDimension> bands) {
+        if (bands == null) {
+            return null;
+        }
+        final DoubleUnaryOperator[] sampleFilters = new 
DoubleUnaryOperator[bands.size()];
+        for (int i = 0; i < sampleFilters.length; i++) {
+            final SampleDimension band = bands.get(i);
+            if (band != null) {
+                final List<Category> categories = band.getCategories();
+                final Number[] nodataValues = new Number[categories.size()];
+                for (int j = 0; j < nodataValues.length; j++) {
+                    final Category category = categories.get(j);
+                    if (!category.isQuantitative()) {
+                        final NumberRange<?> range = category.getSampleRange();
+                        final Number value;
+                        if (range.isMinIncluded()) {
+                            value = range.getMinValue();
+                        } else if (range.isMaxIncluded()) {
+                            value = range.getMaxValue();
+                        } else {
+                            continue;
+                        }
+                        nodataValues[j] = value;
+                    }
+                }
+                sampleFilters[i] = processor.filterNodataValues(nodataValues);
+            }
+        }
+        return sampleFilters;
+    }
+}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
index 46043f4..533312c 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/package-info.java
@@ -24,7 +24,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */

Reply via email to