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 */