You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/07/29 16:18:56 UTC
[sis] 03/10: At image rendering time,
convert floating point values to integer values that
IndexColorModel can use.
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 e4c85f06f20cdfc53c3098e415e74c5bce1461ff
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jul 28 13:01:15 2020 +0200
At image rendering time, convert floating point values to integer values that IndexColorModel can use.
---
.../apache/sis/gui/coverage/CoverageCanvas.java | 54 +++++------
.../sis/gui/coverage/ImagePropertyExplorer.java | 3 +-
.../org/apache/sis/gui/coverage/RenderingData.java | 69 ++++++++++++--
.../java/org/apache/sis/image/ImageProcessor.java | 5 +-
.../java/org/apache/sis/image/RecoloredImage.java | 11 ++-
.../sis/internal/coverage/j2d/ColorModelType.java | 100 +++++++++++++++++++++
.../sis/internal/coverage/j2d/Colorizer.java | 1 +
.../internal/coverage/j2d/ScaledColorModel.java | 18 ++++
8 files changed, 222 insertions(+), 39 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 8a0f55b..2e3fa64 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
@@ -18,6 +18,7 @@ package org.apache.sis.gui.coverage;
import java.util.Map;
import java.util.EnumMap;
+import java.util.List;
import java.util.Locale;
import java.awt.Graphics2D;
import java.awt.Rectangle;
@@ -41,6 +42,7 @@ import org.opengis.geometry.DirectPosition;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
@@ -373,7 +375,7 @@ public class CoverageCanvas extends MapCanvasAWT {
* Invoked in JavaFX thread for setting the image to the instance we just fetched.
*/
@Override protected void succeeded() {
- setRawImage(getValue(), imageGeometry);
+ setRawImage(getValue(), imageGeometry, coverage.getSampleDimensions());
}
});
}
@@ -383,13 +385,15 @@ public class CoverageCanvas extends MapCanvasAWT {
* Invoked when a new image has been successfully loaded. The given image must be the "raw" image,
* without resampling and without color ramp stretching. The call to this method is followed by a
* a repaint event, which will cause the image to be resampled in a background thread.
+ *
+ * <p>All arguments can be {@code null} for clearing the canvas.</p>
*/
- private void setRawImage(final RenderedImage image, final GridGeometry imageGeometry) {
+ private void setRawImage(final RenderedImage image, final GridGeometry domain, final List<SampleDimension> ranges) {
resampledImages.clear();
- data.setImage(image, imageGeometry);
+ data.setImage(image, domain, ranges);
Envelope bounds = null;
- if (imageGeometry != null && imageGeometry.isDefined(GridGeometry.ENVELOPE)) {
- bounds = imageGeometry.getEnvelope();
+ if (domain != null && domain.isDefined(GridGeometry.ENVELOPE)) {
+ bounds = domain.getEnvelope();
}
setObjectiveBounds(bounds);
requestRepaint(); // Cause `Worker` class to be executed.
@@ -442,9 +446,12 @@ public class CoverageCanvas extends MapCanvasAWT {
private final LinearTransform objectiveToDisplay;
/**
- * The source image after resampling.
+ * Whether the {@linkplain RenderingData#resample resampling operation} applied is different
+ * than the one used the last time that the image has been rendered, ignoring translations.
+ * Translations do not require new resampling operations because we can manage translations
+ * by changing {@link RenderedImage} coordinates.
*/
- private RenderedImage resampledImage;
+ private boolean resamplingChanged;
/**
* The resampled image after color ramp stretching and/or index color model applied.
@@ -459,8 +466,7 @@ public class CoverageCanvas extends MapCanvasAWT {
private RenderedImage prefetchedImage;
/**
- * Conversion from {@link #resampledImage} (also {@link #prefetchedImage})
- * pixel coordinates to display coordinates.
+ * Conversion from {@link #prefetchedImage} pixel coordinates to display coordinates.
*/
private AffineTransform resampledToDisplay;
@@ -485,7 +491,6 @@ public class CoverageCanvas extends MapCanvasAWT {
objectiveToDisplay = canvas.getObjectiveToDisplay();
displayBounds = canvas.getDisplayBounds();
if (data.validateCRS(objectiveCRS)) {
- resampledImage = canvas.resampledImages.get(Stretching.NONE);
recoloredImage = canvas.resampledImages.get(data.selectedDerivative);
}
}
@@ -506,7 +511,7 @@ public class CoverageCanvas extends MapCanvasAWT {
}
/**
- * Invoked in background thread for resampling the image or stretching the color ramp.
+ * Invoked in background thread for resampling the image and stretching the color ramp.
* This method performs some of the steps documented in class Javadoc, with possibility
* to skip the first step if the required source image is already resampled.
*/
@@ -515,19 +520,19 @@ public class CoverageCanvas extends MapCanvasAWT {
protected void render() throws TransformException {
final Long id = LogHandler.loadingStart(originator);
try {
- boolean isResampled = (resampledImage != null);
- if (isResampled) {
+ /*
+ * A new resampling is needed if the change compared to last rendering
+ * is anything else than identity or translation.
+ */
+ resamplingChanged = (recoloredImage == null);
+ if (!resamplingChanged) {
resampledToDisplay = data.getTransform(objectiveToDisplay);
- // Recompute if anything else than identity or translation.
- isResampled = (resampledToDisplay.getType()
- & ~(AffineTransform.TYPE_IDENTITY | AffineTransform.TYPE_TRANSLATION)) == 0;
+ resamplingChanged = (resampledToDisplay.getType() &
+ ~(AffineTransform.TYPE_IDENTITY | AffineTransform.TYPE_TRANSLATION)) != 0;
}
- if (!isResampled) {
- recoloredImage = null;
- resampledImage = data.resample(objectiveCRS, objectiveToDisplay);
+ if (resamplingChanged) {
+ final RenderedImage resampledImage = data.resample(objectiveCRS, objectiveToDisplay);
resampledToDisplay = data.getTransform(objectiveToDisplay);
- }
- if (recoloredImage == null) {
recoloredImage = data.recolor(resampledImage);
}
prefetchedImage = data.prefetch(recoloredImage, resampledToDisplay, displayBounds);
@@ -561,15 +566,12 @@ public class CoverageCanvas extends MapCanvasAWT {
*/
private void cacheRenderingData(final Worker worker) {
data = worker.data;
- final RenderedImage newValue = worker.resampledImage;
- final RenderedImage oldValue = resampledImages.put(Stretching.NONE, newValue);
- if (oldValue != newValue && oldValue != null) {
+ if (worker.resamplingChanged) {
/*
* If resampled image changed, then all derivative images (with stretched color ramp
* or other operation applied) are not valid anymore. We need to empty the cache.
*/
resampledImages.clear();
- resampledImages.put(Stretching.NONE, newValue);
}
resampledImages.put(data.selectedDerivative, worker.recoloredImage);
/*
@@ -636,7 +638,7 @@ public class CoverageCanvas extends MapCanvasAWT {
*/
@Override
protected void clear() {
- setRawImage(null, null);
+ setRawImage(null, null, null);
super.clear();
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
index a57ffb0..9fa669c 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImagePropertyExplorer.java
@@ -385,7 +385,8 @@ public class ImagePropertyExplorer extends Widget {
selectedImage = sources.getSelectionModel().selectedItemProperty();
sources.setCellFactory(SourceCell::new);
selectedImage.addListener((p,o,n) -> {
- final RenderedImage selected = n.getValue();
+ RenderedImage selected = null;
+ if (n != null) selected = n.getValue();
imageSelected(selected != null ? selected : image.get());
});
}
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 cb41d64..4d41337 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
@@ -18,12 +18,17 @@ package org.apache.sis.gui.coverage;
import java.util.Map;
import java.util.HashMap;
+import java.util.List;
+import java.util.function.Function;
+import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SampleDimension;
import org.opengis.util.FactoryException;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.CoordinateOperation;
@@ -36,6 +41,8 @@ import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.Shapes2D;
import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.internal.coverage.j2d.Colorizer;
+import org.apache.sis.internal.coverage.j2d.ColorModelType;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.coverage.j2d.PreferredSize;
import org.apache.sis.internal.system.Modules;
@@ -83,6 +90,13 @@ import org.apache.sis.util.logging.Logging;
*/
final class RenderingData implements Cloneable {
/**
+ * Whether to allow the creation of {@link java.awt.image.IndexColorModel}. This flag may be temporarily set
+ * to {@code false} for testing or debugging. If {@code false}, images may be only grayscale and may be much
+ * slower to render, but should still be visible.
+ */
+ private static final boolean CREATE_INDEX_COLOR_MODEL = false;
+
+ /**
* The data fetched from {@link GridCoverage#render(GridExtent)} for current {@code sliceExtent}.
* This rendered image may be tiled and fetching those tiles may require computations to be performed
* in background threads. Pixels in this {@code data} image are mapped to pixels in the display
@@ -93,6 +107,10 @@ final class RenderingData implements Cloneable {
* <li><code>{@linkplain #changeOfCRS}.getMathTransform()</code></li>
* <li>{@link CoverageCanvas#getObjectiveToDisplay()}</li>
* </ol>
+ *
+ * @see #dataGeometry
+ * @see #dataRanges
+ * @see #setImage(RenderedImage, GridGeometry, List)
*/
private RenderedImage data;
@@ -102,10 +120,24 @@ final class RenderingData implements Cloneable {
* to two dimensions and with a translation added for taking in account the requested {@code sliceExtent}.
* The coverage CRS is initially the same as the {@linkplain CoverageCanvas#getObjectiveCRS() objective CRS},
* but may become different later if user selects a different objective CRS.
+ *
+ * @see #data
+ * @see #dataRanges
+ * @see #setImage(RenderedImage, GridGeometry, List)
*/
private GridGeometry dataGeometry;
/**
+ * Ranges of sample values in each band of {@link #data}. This is used for determining on which sample values
+ * to apply colors when user asked to apply a color ramp. May be {@code null}.
+ *
+ * @see #data
+ * @see #dataGeometry
+ * @see #setImage(RenderedImage, GridGeometry, List)
+ */
+ private List<SampleDimension> dataRanges;
+
+ /**
* Conversion or transformation from {@linkplain #data} CRS to {@linkplain CoverageCanvas#getObjectiveCRS()
* objective CRS}, or {@code null} if not yet computed. This is an identity operation if the user did not
* selected a different CRS after the coverage has been shown.
@@ -139,6 +171,14 @@ final class RenderingData implements Cloneable {
private Statistics[] statistics;
/**
+ * The colors to apply for given categories, or {@code null} if none.
+ * A null value means that the original colors specified in the {@link #data} image are used unmodified.
+ *
+ * @see Colorizer#GRAYSCALE
+ */
+ private Function<Category,Color[]> colors;
+
+ /**
* The processor that we use for resampling image and stretching their color ramps.
*/
final ImageProcessor processor;
@@ -187,13 +227,16 @@ final class RenderingData implements Cloneable {
/**
* Sets the data to given image, which can be {@code null}.
+ * This method does not reset the {@link #colors} to null.
*/
- final void setImage(final RenderedImage data, final GridGeometry dataGeometry) {
+ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
+ final void setImage(final RenderedImage data, final GridGeometry domain, final List<SampleDimension> ranges) {
clearCRS();
displayToObjective = null;
statistics = null;
this.data = data;
- this.dataGeometry = dataGeometry;
+ this.dataGeometry = domain;
+ this.dataRanges = ranges; // Not cloned because already an unmodifiable list.
}
/**
@@ -253,13 +296,12 @@ final class RenderingData implements Cloneable {
}
/**
- * Applies image operation on the given resampled image.
- * In current implementation, the only operations are stretching the color ramp.
+ * Optionally stretches the color map, then optionally applies an index color model.
*
* @param resampledImage the image computed by {@link #resample(CoordinateReferenceSystem, LinearTransform)}.
* @return image with operation applied and color ramp stretched. May be the same instance than given image.
*/
- final RenderedImage recolor(final RenderedImage resampledImage) {
+ final RenderedImage recolor(RenderedImage resampledImage) {
if (selectedDerivative != Stretching.NONE) {
final Map<String,Object> modifiers = new HashMap<>(4);
/*
@@ -274,7 +316,22 @@ final class RenderingData implements Cloneable {
if (selectedDerivative == Stretching.AUTOMATIC) {
modifiers.put("multStdDev", 3);
}
- return processor.stretchColorRamp(resampledImage, modifiers);
+ resampledImage = processor.stretchColorRamp(resampledImage, modifiers);
+ }
+ /*
+ * Converts images of floating point values to integer values that we can use with IndexColorModel.
+ *
+ * TODO: if `colors` is null, instead than defaulting to `Colorizer.GRAYSCALE` we should get the colors
+ * from the current ColorModel. This work should be done in Colorizer by converting the ranges of
+ * sample values in source image to ranges of sample values in destination image, then query
+ * ColorModel.getRGB(Object) for increasing integer values in that range.
+ */
+ if (CREATE_INDEX_COLOR_MODEL) {
+ final ColorModelType ct = ColorModelType.find(resampledImage.getColorModel());
+ if (ct.isSlow || (colors != null && ct.useColorRamp)) {
+ resampledImage = processor.toIndexedColors(resampledImage,
+ dataRanges, (colors != null) ? colors : Colorizer.GRAYSCALE);
+ }
}
return resampledImage;
}
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 4e27f41..7b8c5b2 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
@@ -734,10 +734,7 @@ public class ImageProcessor implements Cloneable {
interpolation, fillValues, positionalAccuracyHints));
break;
}
- if (cm != null && !cm.equals(resampled.getColorModel())) {
- resampled = unique(new RecoloredImage(resampled, cm));
- }
- return resampled; // Do not cache user-provided image.
+ return RecoloredImage.create(resampled, cm);
}
/**
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 7c5ac5c..fb91497 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
@@ -64,7 +64,7 @@ final class RecoloredImage extends ImageAdapter {
/**
* Creates a new recolored image with the given colors.
*/
- RecoloredImage(final RenderedImage source, final ColorModel colors) {
+ private RecoloredImage(final RenderedImage source, final ColorModel colors) {
super(source);
this.colors = colors;
}
@@ -73,7 +73,10 @@ final class RecoloredImage extends ImageAdapter {
* Returns a recolored image with the given colors. This method may return
* an existing ancestor if one is found with the specified color model.
*/
- private static RenderedImage create(RenderedImage source, final ColorModel colors) {
+ static RenderedImage create(RenderedImage source, final ColorModel colors) {
+ if (colors == null) {
+ return source;
+ }
for (;;) {
if (colors.equals(source.getColorModel())) {
return source;
@@ -264,7 +267,11 @@ final class RecoloredImage extends ImageAdapter {
}
/*
* Sample values can not be reused as-is; we need to convert them to integers in [0 … 255] range.
+ * Skip any previous `RecoloredImage` since we are replacing the `ColorModel` by a new one.
*/
+ while (source instanceof RecoloredImage) {
+ source = ((RecoloredImage) source).source;
+ }
final ColorModel colorModel = colorizer.compactColorModel(1, 0); // Must be first.
final MathTransform1D converter = colorizer.getSampleToIndexValues();
final NumberRange<?> range = colorizer.getRepresentativeRange();
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelType.java
new file mode 100644
index 0000000..4c53565
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelType.java
@@ -0,0 +1,100 @@
+/*
+ * 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.j2d;
+
+import java.awt.image.ColorModel;
+import java.awt.image.DirectColorModel;
+import java.awt.image.IndexColorModel;
+
+
+/**
+ * Information about the color model. This enumeration provides an easier way of determining
+ * whether the color ramp <em>can</em> be replaced, and whether it <em>should</em> be replaced
+ * for performance reasons.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public enum ColorModelType {
+ /**
+ * Color model uses directly RGB colors.
+ * This model is efficient and should not be changed.
+ * Color palette can not be changed.
+ */
+ DIRECT(false, false),
+
+ /**
+ * Color model uses indexed colors.
+ * This model is efficient and does not need to be changed.
+ * Color palette can be changed.
+ */
+ INDEXED(true, false),
+
+ /**
+ * Color model use colors computed on the fly from floating point values.
+ * This model is inefficient and should be changed if possible.
+ */
+ SCALED(true, true),
+
+ /**
+ * Unrecognized color model.
+ */
+ OTHER(false, false);
+
+ /**
+ * Whether the color model uses a color palette.
+ * A {@code true} value implies that the color ramp is replaceable.
+ */
+ public final boolean useColorRamp;
+
+ /**
+ * Whether rendering with this color model is slow.
+ * In such case, the color model may need to be changed using {@link Colorizer}.
+ */
+ public final boolean isSlow;
+
+ /**
+ * Creates a new enumeration value.
+ */
+ private ColorModelType(final boolean useColorRamp, final boolean isSlow) {
+ this.useColorRamp = useColorRamp;
+ this.isSlow = isSlow;
+ }
+
+ /**
+ * Gets the type of given color model.
+ *
+ * @param model the color model (may be {@code null}).
+ * @return type of given color model (never {@code null}).
+ */
+ public static ColorModelType find(final ColorModel model) {
+ if (model != null) {
+ if (model instanceof DirectColorModel) {
+ return DIRECT;
+ }
+ if (model instanceof IndexColorModel) {
+ return INDEXED;
+ }
+ if (model.getColorSpace() instanceof ScaledColorSpace) {
+ return SCALED;
+ }
+ }
+ return OTHER;
+ }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
index 2161f3d..a8c7fce 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
@@ -62,6 +62,7 @@ import org.apache.sis.util.resources.Vocabulary;
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
+ * @see ColorModelType
* @see ColorModelFactory#createColorModel(int, int, int, Collection)
*
* @since 1.1
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java
index 4ec32ab..082535b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ScaledColorModel.java
@@ -137,4 +137,22 @@ final class ScaledColorModel extends ComponentColorModel {
final int c = Math.max(0, Math.min(MASK, (int) ((value - cs.offset) * cs.scale)));
return c | (c << Byte.SIZE) | (c << 2*Byte.SIZE) | (MASK << 3*Byte.SIZE);
}
+
+ /**
+ * Returns {@code true} if the given object is also an instance of {@link ScaledColorModel}
+ * with equals color space and same transfer type.
+ *
+ * <div class="note"><b>Note:</b>
+ * we have to override this method because the {@link ComponentColorModel#equals(Object)} implementation
+ * is confused by our overriding of {@link #getTransparency()} method. However we do not need to override
+ * {@link #hashCode()}.</div>
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof ScaledColorModel) {
+ final ScaledColorModel other = (ScaledColorModel) obj;
+ return transferType == other.getTransferType() && cs.equals(other.cs);
+ }
+ return false;
+ }
}