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 c3ca073d5e90a798af9cad8b62352e9cbf41e23f Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Apr 4 17:24:20 2022 +0200 Bug fix: colors applied to ASCII Grid images were wrongly applied to other kind of images if they shared the same `ImageProcessor`. --- .../org/apache/sis/coverage/grid/GridCoverage.java | 4 +- .../sis/coverage/grid/GridCoverageBuilder.java | 2 +- .../apache/sis/coverage/grid/ImageRenderer.java | 3 +- .../java/org/apache/sis/image/Visualization.java | 4 +- .../internal/coverage/j2d/ColorModelFactory.java | 2 +- .../sis/internal/coverage/j2d/Colorizer.java | 74 ++++++++++++++++------ .../sis/internal/coverage/j2d/ColorsForRange.java | 61 +++++++++--------- .../sis/internal/coverage/j2d/ColorizerTest.java | 4 +- 8 files changed, 97 insertions(+), 57 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java index 8232a77d9f..2d1ade3610 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java @@ -282,7 +282,9 @@ public abstract class GridCoverage extends BandedCoverage { final int visibleBand = Math.max(0, ImageUtilities.getVisibleBand(source)); final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE); final ColorModel colors; - if (colorizer.initialize(sampleDimensions[visibleBand]) || colorizer.initialize(source.getColorModel())) { + if (colorizer.initialize(source.getSampleModel(), sampleDimensions[visibleBand]) || + colorizer.initialize(source.getColorModel())) + { colors = colorizer.createColorModel(bandType.toDataBufferType(), sampleDimensions.length, visibleBand); } else { colors = Colorizer.NULL_COLOR_MODEL; diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java index 5a83745739..1e437360e5 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java @@ -473,7 +473,7 @@ public class GridCoverageBuilder { final SampleModel sm = raster.getSampleModel(); final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE); final ColorModel colors; - if (colorizer.initialize(bands.get(visibleBand)) || colorizer.initialize(sm, visibleBand)) { + if (colorizer.initialize(sm, bands.get(visibleBand)) || colorizer.initialize(sm, visibleBand)) { colors = colorizer.createColorModel(ImageUtilities.getBandType(sm), bands.size(), visibleBand); } else { colors = Colorizer.NULL_COLOR_MODEL; diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java index bf2f424b91..9bb02bc94a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java @@ -731,7 +731,8 @@ public class ImageRenderer { final Raster raster = createRaster(); final Colorizer colorizer = new Colorizer(colors); final ColorModel colors; - if (colorizer.initialize(bands[visibleBand]) || colorizer.initialize(raster.getSampleModel(), visibleBand)) { + final SampleModel sm = raster.getSampleModel(); + if (colorizer.initialize(sm, bands[visibleBand]) || colorizer.initialize(sm, visibleBand)) { colors = colorizer.createColorModel(buffer.getDataType(), bands.length, visibleBand); } else { colors = Colorizer.NULL_COLOR_MODEL; 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 393f65fb1f..f749ebf56d 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 @@ -219,10 +219,10 @@ final class Visualization extends ResampledImage { * in various ways: sample dimensions, scaled color model, statistics in last resort. */ colorizer = new Colorizer(categoryColors); - initialized = (sourceBands != null) && colorizer.initialize(sourceBands.get(visibleBand)); + initialized = (sourceBands != null) && colorizer.initialize(source.getSampleModel(), sourceBands.get(visibleBand)); if (initialized) { /* - * If we have been able to configure Colorizer using the SampleModel, apply an adjustment based + * If we have been able to configure Colorizer using SampleDimension, apply an adjustment based * on the ScaledColorModel if it exists. Use case: an image is created with an IndexColorModel * determined by the SampleModel, then user enhanced contrast by a call to `stretchColorRamp(…)` * above. We want to preserve that contrast enhancement. diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java index bf0835fafb..c502db5fd5 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java @@ -398,7 +398,7 @@ public final class ColorModelFactory { final double lower, final double upper, final Color... colors) { return createPiecewise(dataType, numBands, visibleBand, new ColorsForRange[] { - new ColorsForRange(null, new NumberRange<>(Double.class, lower, true, upper, false), colors) + new ColorsForRange(null, new NumberRange<>(Double.class, lower, true, upper, false), colors, true) }); } 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 c39cadb2fd..715c521c2a 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 @@ -28,6 +28,7 @@ import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.SampleModel; +import org.opengis.util.InternationalString; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.apache.sis.referencing.operation.transform.MathTransforms; @@ -58,8 +59,8 @@ import org.apache.sis.util.Debug; * There is no {@code initialize(Raster)} or {@code initialize(RenderedImage)} method because if those methods * were present, users may expect them to iterate over sample values for finding minimum and maximum values. * We do not perform such iteration because they are potentially costly and give unstable results: - * the resulting color model varies from image to image, which is confusing when they are images of the same - * product as different depth or different time. + * the resulting color model varies from image to image, which is confusing when many images exist + * for the same product at different times or at different depths. * * @author Martin Desruisseaux (Geomatys) * @version 1.2 @@ -77,6 +78,21 @@ public final class Colorizer { */ public static final ColorModel NULL_COLOR_MODEL = null; + /** + * Names to use for the synthetic categories and sample dimensions created for visualization purposes. + * Transparent pixel is usually 0 and opaque pixels are in the range 1 to {@value #MAX_VALUE} inclusive. + * + * <p>For safety, we use names that are different than the default "No data" and "Data" names assigned by + * {@link SampleDimension.Builder}. The "[No] data" default names are often used by formats that are poor + * in metadata, for example ASCII Grid. If we were using the same names, a {@link #colors} function could + * confuse synthetic categories with "real" categories with uninformative name, and consequently apply + * wrong colors.</p> + */ + private static final InternationalString + TRANSPARENT = Vocabulary.formatInternational(Vocabulary.Keys.Transparent), + COLOR_INDEX = Vocabulary.formatInternational(Vocabulary.Keys.ColorIndex), + VISUAL = Vocabulary.formatInternational(Vocabulary.Keys.Visual); + /** * Maximal index value which can be used with a 8 bits {@link IndexColorModel}, inclusive. * Sample values must be in that range for enabling the use of {@link #TYPE_COMPACT}. @@ -192,20 +208,33 @@ public final class Colorizer { * the sample dimension, colors will be determined by a call to {@code colors.apply(category)} * where {@code colors} is the function specified at construction time. * + * @param model the sample model used with the data, or {@code null} if unknown. * @param source description of range of values in the source image, or {@code null}. * @return {@code true} on success, or {@code false} if no range of values has been found. * @throws IllegalStateException if a sample dimension is already defined on this colorizer. */ - public boolean initialize(final SampleDimension source) { + public boolean initialize(final SampleModel model, final SampleDimension source) { checkInitializationStatus(false); if (source != null) { this.source = source; final List<Category> categories = source.getCategories(); if (!categories.isEmpty()) { - final ColorsForRange[] entries = new ColorsForRange[categories.size()]; + boolean missingNodata = true; + ColorsForRange[] entries = new ColorsForRange[categories.size()]; for (int i=0; i<entries.length; i++) { final Category category = categories.get(i); - entries[i] = new ColorsForRange(category, category.getSampleRange(), colors.apply(category)); + entries[i] = new ColorsForRange(category, colors); + missingNodata &= category.isQuantitative(); + } + /* + * If the model uses floating point values and there is no "no data" category, add one. + * We force a "no data" category because floating point values may be NaN. + */ + if (missingNodata && (model == null || !ImageUtilities.isIntegerType(model))) { + final int count = entries.length; + entries = Arrays.copyOf(entries, count + 1); + entries[count] = new ColorsForRange(Vocabulary.formatInternational(Vocabulary.Keys.Nodata), + NumberRange.create(Float.class, Float.NaN), null, false); } // Leave `target` to null. It will be computed by `compact()` if needed. this.entries = entries; @@ -271,8 +300,9 @@ public final class Colorizer { * Applies colors on the given range of values. The 0 index will be reserved for NaN value, * and indices in the [1 … 255] will be mapped to the given range. * - * <p>This method is typically used as a last resort fallback when other {@code initialize(…)} - * methods failed or can not be applied.</p> + * <p>This method is typically used as a last resort fallback when all other {@code initialize(…)} + * methods failed or can not be applied. This method assumes that no {@link Category} information + * is available.</p> * * @param minimum minimum value, inclusive. * @param maximum maximum value, inclusive. @@ -284,15 +314,20 @@ public final class Colorizer { ArgumentChecks.ensureFinite("maximum", maximum); defaultRange = NumberRange.create(minimum, true, maximum, true); target = new SampleDimension.Builder() - .setBackground(null, 0) - .addQuantitative(null, NumberRange.create(1, true, MAX_VALUE, true), defaultRange).build(); - + .setBackground(TRANSPARENT, 0) + .addQuantitative(COLOR_INDEX, NumberRange.create(1, true, MAX_VALUE, true), defaultRange) + .setName(VISUAL).build(); + /* + * We created a synthetic `SampleDimension` with the specified range of values. + * Recompute `source` and `entries` fields for consistency with the new ranges. + * The `source` is recreated as a matter of principle, but will not be used by + * `compact()` because `target` will take precedence. + */ source = target.forConvertedValues(true); - final List<Category> categories = source.getCategories(); + final List<Category> categories = target.getCategories(); final ColorsForRange[] entries = new ColorsForRange[categories.size()]; for (int i=0; i<entries.length; i++) { - final Category category = categories.get(i); - entries[i] = new ColorsForRange(category, category.forConvertedValues(false).getSampleRange(), colors.apply(category)); + entries[i] = new ColorsForRange(categories.get(i), GRAYSCALE); } this.entries = entries; } @@ -417,13 +452,12 @@ reuse: if (source != null) { for (int i=0; i<count; i++) { final ColorsForRange entry = entries[i]; NumberRange<?> sourceRange = entry.sampleRange; - if (!entry.isData()) { + if (!entry.isData) { if (lower >= MAX_VALUE) { throw new IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives)); } final NumberRange<Integer> targetRange = NumberRange.create(lower, true, ++lower, false); if (mapper.put(targetRange, entry) == null) { - final CharSequence name = entry.name(); final double value = sourceRange.getMinDouble(); /* * In the usual case where we have a mix of quantitative and qualitative categories, @@ -433,12 +467,12 @@ reuse: if (source != null) { * computing a transfer function, but those categories should not be returned to user. */ if (Double.isNaN(value)) { - builder.mapQualitative(name, targetRange, (float) value); + builder.mapQualitative(entry.name, targetRange, (float) value); } else { if (value == entry.sampleRange.getMaxDouble()) { sourceRange = NumberRange.create(value - 0.5, true, value + 0.5, false); } - builder.addQuantitative(name, targetRange, sourceRange); + builder.addQuantitative(entry.name, targetRange, sourceRange); themes = (themes != null) ? themes.unionAny(sourceRange) : sourceRange; } } @@ -472,7 +506,7 @@ reuse: if (source != null) { span += sourceRange.getSpan(); final ColorsForRange[] tmp = Arrays.copyOf(entries, ++count); System.arraycopy(entries, deferred, tmp, ++deferred, count - deferred); - tmp[deferred-1] = new ColorsForRange(null, sourceRange, new Color[] {Color.BLACK, Color.WHITE}); + tmp[deferred-1] = new ColorsForRange(null, sourceRange, new Color[] {Color.BLACK, Color.WHITE}, true); entries = tmp; } } @@ -494,7 +528,7 @@ reuse: if (source != null) { } final NumberRange<Integer> samples = NumberRange.create(lower, true, upper, false); if (mapper.put(samples, entry) == null) { - builder.addQuantitative(entry.name(), samples, entry.sampleRange); + builder.addQuantitative(entry.name, samples, entry.sampleRange); } lower = upper; } @@ -505,7 +539,7 @@ reuse: if (source != null) { if (source != null) { builder.setName(source.getName()); } else { - builder.setName(Vocabulary.format(Vocabulary.Keys.Visual)); + builder.setName(VISUAL); } target = builder.build(); for (final Category category : target.getCategories()) { diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java index 3236904432..23a6bbd66b 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java @@ -18,6 +18,7 @@ package org.apache.sis.internal.coverage.j2d; import java.util.Map; import java.util.Collection; +import java.util.function.Function; import java.awt.Color; import java.awt.image.IndexColorModel; import org.apache.sis.coverage.Category; @@ -31,7 +32,7 @@ import org.apache.sis.util.ArraysExt; * the time needed for {@link ColorModelFactory#createColorModel(int, int, int, ColorsForRange[])}. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * * @see ColorModelFactory#createColorModel(int, int, int, ColorsForRange[]) * @@ -40,10 +41,10 @@ import org.apache.sis.util.ArraysExt; */ final class ColorsForRange implements Comparable<ColorsForRange> { /** - * If this {@code ColorsForRange} has been created for a category, that category. - * Otherwise {@code null}. + * A name identifying the range of values. the category name is used if available, + * otherwise this is a string representation of the range. */ - private final Category category; + final CharSequence name; /** * The range of sample values on which the colors will be applied. Shall never be null. @@ -58,18 +59,40 @@ final class ColorsForRange implements Comparable<ColorsForRange> { */ private final Color[] colors; + /** + * {@code true} if this entry should be taken as data, or {@code false} if it should be ignored. + * Entry to ignore are entries associated to NaN values. + */ + final boolean isData; + + /** + * Creates a new instance for the given category. + * + * @param category the category for which this {@code ColorsForRange} is created, or {@code null}. + * @param colors colors to apply on the category. + */ + ColorsForRange(final Category category, final Function<Category,Color[]> colors) { + final CharSequence name = category.getName(); + this.name = (name != null) ? name : sampleRange.toString(); + this.sampleRange = category.getSampleRange(); + this.colors = colors.apply(category); + this.isData = category.isQuantitative(); + } + /** * Creates a new instance for the given range of values. * - * @param category the category for which this {@code ColorsForRange} is created, or {@code null}. + * @param name a name identifying the range of values, or {@code null} for automatic. * @param sampleRange range of sample values on which the colors will be applied. * @param colors colors to apply on the range of sample values, or {@code null} for transparent. + * @param isData whether this entry should be taken as main data (not fill values). */ - ColorsForRange(final Category category, final NumberRange<?> sampleRange, final Color[] colors) { + ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, final Color[] colors, final boolean isData) { ArgumentChecks.ensureNonNull("sampleRange", sampleRange); - this.category = category; + this.name = (name != null) ? name : sampleRange.toString(); this.sampleRange = sampleRange; this.colors = colors; + this.isData = isData; } /** @@ -85,37 +108,17 @@ final class ColorsForRange implements Comparable<ColorsForRange> { final ColorsForRange[] entries = new ColorsForRange[colors.size()]; int n = 0; for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) { - entries[n++] = new ColorsForRange(null, entry.getKey(), entry.getValue()); + entries[n++] = new ColorsForRange(null, entry.getKey(), entry.getValue(), true); } return ArraysExt.resize(entries, n); // `resize` should not be needed, but we are paranoiac. } - /** - * Returns {@code true} if this entry should be taken as data, or {@code false} if it should be ignored. - * Entry to ignore are entries associated to NaN values. - */ - final boolean isData() { - return category == null || category.isQuantitative(); - } - - /** - * Returns a name identifying the range of values. the category name is used if available, - * otherwise a string representation of the range is created. - */ - final CharSequence name() { - if (category != null) { - final CharSequence name = category.getName(); - if (name != null) return name; - } - return sampleRange.toString(); - } - /** * Returns a string representation for debugging purposes. */ @Override public String toString() { - return name().toString(); + return name.toString(); } /** diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorizerTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorizerTest.java index 5d37bb214d..8fc4c8a49d 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorizerTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorizerTest.java @@ -38,7 +38,7 @@ import static org.junit.Assert.*; * Tests {@link Colorizer}. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ @@ -101,7 +101,7 @@ public final strictfp class ColorizerTest extends TestCase { .setName("Temperature").build(); final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE); - assertTrue("initialize", colorizer.initialize(sd)); + assertTrue("initialize", colorizer.initialize(null, sd)); final IndexColorModel cm = (IndexColorModel) colorizer.compactColorModel(1, 0); // Must be first. /* * Test conversion of a few sample values to packed values.