This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 5ccba14513e03785f83bfdd4cd9d613fae20b13e Author: Martin Desruisseaux <[email protected]> AuthorDate: Thu Apr 23 17:44:34 2026 +0200 More systematic construction of gray scale instead of index color model. --- .../main/org/apache/sis/image/DataType.java | 2 +- .../image/internal/shared/ColorModelFactory.java | 150 +++++++++++++-------- 2 files changed, 95 insertions(+), 57 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java index 75782ed869..995d2ffb98 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java @@ -346,7 +346,7 @@ public enum DataType { * @since 1.3 */ public final int bytes() { - return size() >>> 3; // `size()` is never smaller than 8. + return size() / Byte.SIZE; // `size()` is never smaller than 8. } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorModelFactory.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorModelFactory.java index 3618a131b6..da40bae51a 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorModelFactory.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorModelFactory.java @@ -130,7 +130,10 @@ public final class ColorModelFactory { /** * The minimum (inclusive) and maximum (exclusive) sample values. - * This is used only if {@link #dataType} is not {@link DataBuffer#TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT}. + * If no category or color map was specified, then the minimum and maximum are set to the standard range of values. + * For integer types, this is the full range of values allowed by that type. For floating-point types, this is 0…1. + * + * @see #isStandardRange(int, double, double) */ private final double minimum, maximum; @@ -215,9 +218,21 @@ public final class ColorModelFactory { } } } + /* + * If no category or color map was specified, default to the standard range of values. + * For floating point types, the standard range of values is defined by Java2D as 0 … 1. + */ if (minimum >= maximum) { minimum = 0; maximum = 1; + if (ImageUtilities.isIntegerType(dataType)) { + if (dataType == DataBuffer.TYPE_SHORT) { + minimum = Short.MIN_VALUE; + maximum = Short.MAX_VALUE + 1; // Exclusive. + } else { + maximum = 1L << DataBuffer.getDataTypeSize(dataType); + } + } } /* * The length of `pieceStarts` may differ from the expected length if there is holes between categories. @@ -266,7 +281,7 @@ public final class ColorModelFactory { return true; } if (other instanceof ColorModelFactory) { - final ColorModelFactory that = (ColorModelFactory) other; + final var that = (ColorModelFactory) other; return this.dataType == that.dataType && this.numBands == that.numBands && this.visibleBand == that.visibleBand @@ -339,6 +354,17 @@ public final class ColorModelFactory { } } + /** + * Creates a color model for opaque images with sample values in the range defined by this factory. + * This method is invoked as a fallback when an {@link IndexColorModel} cannot be created. + * This is only a fallback because this method ignores the {@link #ARGB} colors. + * + * @return the color model for the range of values of this factory, ignoring {@link #ARGB} colors. + */ + private ColorModel createGrayScale() { + return createGrayScale(dataType, numBands, visibleBand, minimum, maximum); + } + /** * Constructs the color model from the {@link #ARGB} data. * This method is invoked the first time the color model is created, @@ -353,39 +379,44 @@ public final class ColorModelFactory { * But a future implementation may use them. */ if (dataType != DataBuffer.TYPE_BYTE && dataType != DataBuffer.TYPE_USHORT) { - return createGrayScale(dataType, numBands, visibleBand, minimum, maximum); + return createGrayScale(); + } + /* + * If no categories or ARGB colors were specified, construct a gray scale palette. + * If there is a single band, the performance should be fine since above condition + * verified that the data type is one of the types supported by Java2D. + * + * If there are more than one band, the color model may be very slow but we use it anyway + * because a gray scale gives directly the intensity while an index color model is indirect. + * It makes a difference during interpolations, which can be unsupported for indexed values + * because not all code paths resolve the indirection. + */ + final int categoryCount = pieceStarts.length - 1; + if (categoryCount <= 0) { + return createGrayScale(); } /* - * If there are no categories, construct a gray scale palette. + * Interpolates the colors in the color palette. Colors that do not fall + * in the range of a category will be set to a transparent color. + * A gray scale color palette */ - final int[] colorMap; int transparent = -1; - if (isGrayScale()) { - if (numBands == 1) { - final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); - final int[] numBits = { - DataBuffer.getDataTypeSize(dataType) - }; - return unique(new ComponentColorModel(cs, numBits, false, true, Transparency.OPAQUE, dataType)); - } - colorMap = new int[256]; - Arrays.setAll(colorMap, (i) -> 0xFF000000 | (i << 24) | (i << 16) | (i << 8) | i); - } else { - /* - * Interpolates the colors in the color palette. Colors that do not fall - * in the range of a category will be set to a transparent color. - */ - final int categoryCount = pieceStarts.length - 1; - colorMap = new int[pieceStarts[categoryCount]]; - for (int i=0; i<categoryCount; i++) { - final int[] colors = ARGB[i]; - final int lower = pieceStarts[i ]; - final int upper = pieceStarts[i+1]; - if (transparent < 0 && colors.length == 0) { - transparent = lower; - } - expand(colors, colorMap, lower, upper); + final int[] colorMap = new int[pieceStarts[categoryCount]]; + for (int i=0; i<categoryCount; i++) { + final int[] colors = ARGB[i]; + final int lower = pieceStarts[i ]; + final int upper = pieceStarts[i+1]; + if (transparent < 0 && colors.length == 0) { + transparent = lower; } + expand(colors, colorMap, lower, upper); + } + /* + * If the color palette appears to be gray scale, replace by a gray scale even if potentially slower. + * This is for the same reason as in previous comment: enable interpolations on sample values. + */ + if (colorMap.length == (1 << DataBuffer.getDataTypeSize(dataType)) && isGrayScale(colorMap)) { + return createGrayScale(); } return createIndexColorModel(null, 0, numBands, visibleBand, colorMap, true, transparent); } @@ -491,11 +522,12 @@ public final class ColorModelFactory { } /** - * Creates a color model for opaque images storing pixels as real numbers. + * Creates a color model for opaque images storing pixels as integers or floating-point numbers. * The color model can have an arbitrary number of bands, but in current implementation only one band is used. * - * <p><b>Warning:</b> the use of this color model may be very slow. - * It should be used only when no standard color model can be used.</p> + * <p><b>Warning:</b> the returned color model has good performance for data stored in a single band of bytes + * or unsigned short integers when the full range of values allowed by those types is used. For other cases, + * the color model is very slow and should be used only when no standard color model can be used.</p> * * @param dataType the color model type as one of {@code DataBuffer.TYPE_*} constants. * @param numComponents the number of components. @@ -539,10 +571,10 @@ public final class ColorModelFactory { static boolean isStandardRange(final int dataType, double minimum, final double maximum) { final boolean signed; switch (dataType) { - case DataBuffer.TYPE_BYTE: + case DataBuffer.TYPE_BYTE: // Fall through case DataBuffer.TYPE_USHORT: signed = false; break; - case DataBuffer.TYPE_SHORT: - case DataBuffer.TYPE_INT: signed = true; break; + case DataBuffer.TYPE_SHORT: signed = true; break; + case DataBuffer.TYPE_INT: signed = (minimum < 0); break; /* * The standard range of floating point types is [0 … 1], but we never return `true` * for those types even if the specified minimum and maximum values match that range @@ -568,26 +600,6 @@ public final class ColorModelFactory { return Math.abs(minimum) < 1 && Math.abs(maximum - (upper - 0.5)) < 1.5; } - /** - * Returns whether this factory is building a gray scale color model. - * A gray scale color model is preferred to an index color model because gray scale gives - * directly the intensity, while index color model is indirect (need to lookup in a table). - * Therefore, sample values associated to gray scale can be interpolated while sample values - * that are indexes cannot be interpolated easily. - */ - private boolean isGrayScale() { - if (ARGB == null) { - return true; - } - switch (ARGB.length) { - default: return false; - case 0: return true; - case 1: break; - } - final int[] codes = ARGB[0]; - return codes.length == 2 && codes[0] == 0xFF000000 && codes[1] == 0xFFFFFFFF; - } - /** * Creates a color model for opaque images storing pixels using the given sample model. * The color model can have an arbitrary number of bands, but in current implementation only one band is used. @@ -749,6 +761,32 @@ public final class ColorModelFactory { return Math.max(1, count); } + /** + * Returns {@code true} if the given <abbr>ARGB</abbr> values define a gray scale. + * The number of values should be the full range of byte or unsigned short values, + * but this method accepts any power of 2. + * + * <p>A gray scale color model is preferred to an index color model because gray scale gives + * directly the intensity, while index color model is indirect (need to lookup in a table). + * Therefore, sample values associated to gray scale can be interpolated while sample values + * that are indexes cannot be interpolated easily.</p> + */ + private static boolean isGrayScale(final int[] ARGB) { + int shift = Integer.numberOfTrailingZeros(ARGB.length); + if (ARGB.length != (1 << shift)) { + return false; + } + shift -= Byte.SIZE; + for (int i = ARGB.length; --i >= 0;) { + int c = i >>> shift; + c |= (c << 8) | (c << 16) | (c << 24) | 0xFF000000; + if (ARGB[i] != c) { + return false; + } + } + return true; + } + /** * Copies {@code colors} into {@code ARGB} array from index {@code lower} inclusive to index {@code upper} exclusive. * If {@code upper-lower} is not equal to the length of {@code colors} array, then colors will be interpolated.
