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 be6db459ff344fe46d560a5f838d3eaff52528e4 Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon Apr 4 23:02:47 2022 +0200 Preserve information about how the color model of an image has been stretched. This is not needed when the stretching is computed from a sample dimension, but become necessary for RGB images with empty `SampleDimension` objects. --- .../java/org/apache/sis/image/ImageProcessor.java | 9 +- .../java/org/apache/sis/image/RecoloredImage.java | 97 +++++++++++++++++++--- .../java/org/apache/sis/image/Visualization.java | 12 ++- .../sis/internal/map/coverage/RenderingData.java | 2 +- 4 files changed, 105 insertions(+), 15 deletions(-) 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 ae21cc9cee..834248a0b2 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 @@ -941,7 +941,7 @@ public class ImageProcessor implements Cloneable { ArgumentChecks.ensureNonNull("bounds", bounds); ArgumentChecks.ensureNonNull("toSource", toSource); ensureNonEmpty(bounds); - final ColorModel cm = source.getColorModel(); + final RenderedImage colored = source; final SampleModel sm = source.getSampleModel(); boolean isIdentity = toSource.isIdentity(); RenderedImage resampled = null; @@ -986,7 +986,12 @@ public class ImageProcessor implements Cloneable { bounds, toSource, interpolation, fillValues, positionalAccuracyHints)); break; } - return RecoloredImage.create(resampled, cm); + /* + * Preserve the color model of the original image, including information about how it + * has been constructed. If the source image was not an instance of `RecoloredImage`, + * then above call should return `resampled` unchanged. + */ + return RecoloredImage.applySameColors(resampled, colored); } /** 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 2237ee644b..a4f7dcfbd0 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 @@ -29,6 +29,7 @@ import org.apache.sis.coverage.Category; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.internal.coverage.j2d.ColorModelFactory; import org.apache.sis.internal.coverage.j2d.ImageUtilities; +import org.apache.sis.internal.util.Numerics; import org.apache.sis.util.collection.Containers; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.ArgumentChecks; @@ -55,32 +56,89 @@ final class RecoloredImage extends ImageAdapter { */ private final ColorModel colors; + /** + * The minimum and maximum values used for computing the color model. + * This is used for preserving color ramp stretching when a new color ramp is applied. + * + * <p>Current implementation can only describes a uniform stretching between a minimum and maximum value. + * Future version may allow more sophisticated ways to redistribute the colors. The possibilities are + * determined by {@link #stretchColorRamp(ImageProcessor, RenderedImage, Map)} implementation.</p> + */ + final double minimum, maximum; + /** * Creates a new recolored image with the given colors. + * + * @param source the image to wrap. + * @param colors the new color model. + * @param minimum the minimal sample value used for computing the color model. + * @param maximum the maximal sample value used for computing the color model. */ - private RecoloredImage(final RenderedImage source, final ColorModel colors) { + private RecoloredImage(final RenderedImage source, final ColorModel colors, final double minimum, final double maximum) { super(source); - this.colors = colors; + this.colors = colors; + this.minimum = minimum; + this.maximum = maximum; } /** - * Returns a recolored image with the given colors. This method may return - * an existing ancestor if one is found with the specified color model. + * Returns a recolored image with the same colors than the given image. + * This method may return an existing ancestor if one is found with the desired color model. + * + * @param source the image to wrap. + * @param colored the image from which to preserve the color model. */ - static RenderedImage create(RenderedImage source, final ColorModel colors) { + static RenderedImage applySameColors(RenderedImage source, RenderedImage colored) { + final ColorModel colors = colored.getColorModel(); if (colors == null) { return source; } + /* + * Find the image which contains the minimum and maximum values that we want to keep. + * We can skip `ImageAdapter` because those images may modify properties but not sample values. + */ + RecoloredImage expected = null; + while (colored instanceof ImageAdapter) { + if (colored instanceof RecoloredImage) { + expected = (RecoloredImage) colored; + break; + } + colored = ((ImageAdapter) colored).source; + } + /* + * Verify if the given image, or one of its sources, has the expected color model. + * We explore only the sources that are themselves `RecoloredImage` instances, + * because other kind of images are result of operations that we want to keep. + */ for (;;) { if (colors.equals(source.getColorModel())) { + if (expected != null && source instanceof RecoloredImage) { + final RecoloredImage actual = (RecoloredImage) source; + if (!(Numerics.equals(expected.minimum, actual.minimum) && + Numerics.equals(expected.maximum, actual.maximum))) + { + continue; + } + } return source; - } else if (source instanceof RecoloredImage) { + } + if (source instanceof RecoloredImage) { source = ((RecoloredImage) source).source; } else { break; } } - return ImageProcessor.unique(new RecoloredImage(source, colors)); + /* + * At this point we found no existing image with the desired color model, + * or the minimum/maximum information would be lost. Create a new image. + */ + final RecoloredImage image; + if (expected != null) { + image = new RecoloredImage(source, colors, expected.minimum, expected.maximum); + } else { + image = new RecoloredImage(source, colors, Double.NaN, Double.NaN); + } + return ImageProcessor.unique(image); } /** @@ -100,7 +158,7 @@ final class RecoloredImage extends ImageAdapter { * * @see ImageProcessor#stretchColorRamp(RenderedImage, Map) */ - static RenderedImage stretchColorRamp(final ImageProcessor processor, final RenderedImage source, + static RenderedImage stretchColorRamp(final ImageProcessor processor, RenderedImage source, final Map<String,?> modifiers) { /* @@ -215,7 +273,7 @@ final class RecoloredImage extends ImageAdapter { final ColorModel cm; if (source.getColorModel() instanceof IndexColorModel) { /* - * Get the range of indices of RGB values than can be used for interpolations. + * Get the range of indices of RGB values that can be used for interpolations. * We want to exclude qualitative categories (no data, clouds, forests, etc.). * In the vast majority of cases, we have at most one quantitative category. * But if there is 2 or more, then we select the one having largest intersection @@ -266,7 +324,26 @@ final class RecoloredImage extends ImageAdapter { final SampleModel sm = source.getSampleModel(); cm = ColorModelFactory.createGrayScale(sm.getDataType(), sm.getNumBands(), visibleBand, minimum, maximum); } - return create(source, cm); + /* + * Verify if an existing ancestor already have the specified color model. + * If not, built the new `RecoloredImage` here. + */ + for (;;) { + if (cm.equals(source.getColorModel())) { + if (source instanceof RecoloredImage) { + final RecoloredImage colored = (RecoloredImage) source; + if (colored.minimum != minimum || colored.maximum != maximum) { + continue; + } + } + return source; + } else if (source instanceof RecoloredImage) { + source = ((RecoloredImage) source).source; + } else { + break; + } + } + return ImageProcessor.unique(new RecoloredImage(source, cm, minimum, maximum)); } /** 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 f749ebf56d..7aee2c936c 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 @@ -234,8 +234,16 @@ final class Visualization extends ResampledImage { * There is no call to `rescaleMainRange(…)` because the following code already uses the range * specified by the ColorModel, if available. */ - initialized = colorizer.initialize(source.getColorModel()) || - colorizer.initialize(source.getSampleModel(), visibleBand); + initialized = colorizer.initialize(source.getColorModel()); + if (!initialized) { + if (source instanceof RecoloredImage) { + final RecoloredImage colored = (RecoloredImage) source; + colorizer.initialize(colored.minimum, colored.maximum); + initialized = true; + } else { + initialized = colorizer.initialize(source.getSampleModel(), visibleBand); + } + } } } source = BandSelectImage.create(source, new int[] {visibleBand}); // Make single-banded. diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java index e746df5395..18039f9f14 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java @@ -576,7 +576,7 @@ public class RenderingData implements Cloneable { */ if (CREATE_INDEX_COLOR_MODEL) { final ColorModelType ct = ColorModelType.find(recoloredImage.getColorModel()); - if (ct.isSlow || (processor.getCategoryColors() != null && ct.useColorRamp)) { + if (ct.isSlow || (ct.useColorRamp && processor.getCategoryColors() != null)) { return processor.visualize(recoloredImage, bounds, displayToCenter, dataRanges); } }
