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 <[email protected]>
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.
*