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 2023/04/04 10:55:49 UTC

[sis] branch geoapi-4.0 updated: Bug fix: `ColorModelBuilder` sometime created a "compact" color model when it was not desired.

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 da7281a6b0 Bug fix: `ColorModelBuilder` sometime created a "compact" color model when it was not desired.
da7281a6b0 is described below

commit da7281a6b058c6ca1b2146584f884b885fa09f1e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Apr 4 12:55:01 2023 +0200

    Bug fix: `ColorModelBuilder` sometime created a "compact" color model when it was not desired.
---
 .../org/apache/sis/coverage/SampleDimension.java   | 109 ++++++++++----------
 .../sis/coverage/grid/GridCoverageBuilder.java     |   2 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   2 +-
 .../java/org/apache/sis/image/BandSelectImage.java |   1 +
 .../apache/sis/image/BandedSampleConverter.java    |   3 +-
 .../main/java/org/apache/sis/image/Colorizer.java  |   2 +-
 .../java/org/apache/sis/image/Visualization.java   |  17 ++--
 .../internal/coverage/j2d/ColorModelBuilder.java   | 111 +++++++++++++++------
 .../coverage/j2d/ColorModelBuilderTest.java        |  10 +-
 9 files changed, 151 insertions(+), 106 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 8f105ffe50..f0a24e8c9e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -51,8 +51,8 @@ import org.apache.sis.util.Debug;
  * is a forest” and some other values for <cite>quantitative</cite> information like a temperature
  * measurements.
  *
- * <div class="note"><b>Example:</b>
- * an image of sea surface temperature (SST) could define the following categories:
+ * <h2>Example</h2>
+ * An image of sea surface temperature (SST) could define the following categories:
  * <table class="sis">
  *   <caption>Example of categories in a sample dimension</caption>
  *   <tr><th>Values range</th> <th>Meaning</th></tr>
@@ -62,7 +62,6 @@ import org.apache.sis.util.Debug;
  *   <tr><td>[10…210]</td>     <td>Temperature to be converted into Celsius degrees through a linear equation</td></tr>
  * </table>
  * In this example, sample values in range [10…210] define a quantitative category, while all others categories are qualitative.
- * </div>
  *
  * <h2>Relationship with metadata</h2>
  * This class provides the same information than ISO 19115 {@link org.opengis.metadata.content.SampleDimension},
@@ -650,8 +649,8 @@ public class SampleDimension implements Serializable {
          * Sets an identification of the sample dimension as a character sequence.
          * This is a convenience method for creating a {@link GenericName} from the given characters.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #setName(GenericName)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #setName(GenericName)}.
          *
          * @param  name  identification of the sample dimension.
          * @return {@code this}, for method call chaining.
@@ -664,8 +663,8 @@ public class SampleDimension implements Serializable {
          * Sets an identification of the sample dimension as a band number.
          * This method should be used only when no more descriptive name is available.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #setName(GenericName)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #setName(GenericName)}.
          *
          * @param  band  sequence identifier of the sample dimension to create.
          * @return {@code this}, for method call chaining.
@@ -780,13 +779,13 @@ public class SampleDimension implements Serializable {
          * Adds a qualitative category for samples of the given boolean value.
          * The {@code true} value is represented by 1 and the {@code false} value is represented by 0.
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -802,13 +801,13 @@ public class SampleDimension implements Serializable {
          * Adds a qualitative category for samples of the given tiny (8 bits) integer value.
          * The argument is treated as a signed integer ({@value Byte#MIN_VALUE} to {@value Byte#MAX_VALUE}).
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -823,13 +822,13 @@ public class SampleDimension implements Serializable {
          * Adds a qualitative category for samples of the given short (16 bits) integer value.
          * The argument is treated as a signed integer ({@value Short#MIN_VALUE} to {@value Short#MAX_VALUE}).
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -844,13 +843,13 @@ public class SampleDimension implements Serializable {
          * Adds a qualitative category for samples of the given integer value.
          * The argument is treated as a signed integer ({@value Integer#MIN_VALUE} to {@value Integer#MAX_VALUE}).
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -864,13 +863,13 @@ public class SampleDimension implements Serializable {
         /**
          * Adds a qualitative category for samples of the given floating-point value.
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -891,13 +890,13 @@ public class SampleDimension implements Serializable {
         /**
          * Adds a qualitative category for samples of the given double precision floating-point value.
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
          *                 or {@code null} for a default "no data" name.
@@ -918,13 +917,13 @@ public class SampleDimension implements Serializable {
         /**
          * Adds a qualitative category for samples in the given range of values.
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.
          *
          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
          *                  or {@code null} for a default "no data" name.
@@ -944,10 +943,10 @@ public class SampleDimension implements Serializable {
          * This is the most generic method for adding a qualitative category.
          * All other {@code addQualitative(name, …)} methods are convenience methods delegating their work to this method.
          *
-         * <div class="note"><b>Usage note:</b>
-         * the {@link #setBackground(CharSequence, Number)} method should be used instead of this method
+         * <h4>Usage note</h4>
+         * The {@link #setBackground(CharSequence, Number)} method should be used instead of this method
          * when the aim is to define a default "no data" category to use when the missing value cannot
-         * be categorized more precisely (cloud, instrument error, <i>etc</i>).</div>
+         * be categorized more precisely (cloud, instrument error, <i>etc</i>).
          *
          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
          *                  or {@code null} for a default "no data" name.
@@ -969,8 +968,8 @@ public class SampleDimension implements Serializable {
          * (not necessarily the {@link Float#NaN} canonical value)
          * and that value shall not have been used by another category.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #mapQualitative(CharSequence, NumberRange, float)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #mapQualitative(CharSequence, NumberRange, float)}.
          *
          * @param  name       the category name as a {@link String} or {@link InternationalString} object,
          *                    or {@code null} for a default "no data" name.
@@ -1037,8 +1036,8 @@ public class SampleDimension implements Serializable {
          * <p><b>Warning:</b> this method is provided for convenience when the scale and offset factors are not explicitly specified.
          * If those factor are available, then the other {@code addQuantitative(name, samples, …)} methods are more reliable.</p>
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.
          *
          * @param  name        the category name as a {@link String} or {@link InternationalString} object,
          *                     or {@code null} for a default "data" name.
@@ -1076,8 +1075,8 @@ public class SampleDimension implements Serializable {
          * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
          * in the given units of measurement. The transfer function is set to identity.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.
          *
          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
          *                  or {@code null} for a default "data" name.
@@ -1095,8 +1094,8 @@ public class SampleDimension implements Serializable {
          * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
          * in the given units of measurement. The transfer function is set to identity.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.
          *
          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
          *                  or {@code null} for a default "data" name.
@@ -1118,8 +1117,8 @@ public class SampleDimension implements Serializable {
          *
          * Results of above conversion are measurements in the units specified by the {@code units} argument.
          *
-         * <div class="note"><b>Implementation note:</b>
-         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+         * <h4>Default implementation</h4>
+         * This convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.
          *
          * @param  name    the category name as a {@link String} or {@link InternationalString} object.
          *                 or {@code null} for a default "data" name.
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 5e46bc3419..c6b048c31c 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
@@ -470,7 +470,7 @@ public class GridCoverageBuilder {
                  */
                 bands = GridCoverage2D.defaultIfAbsent(bands, null, raster.getNumBands());
                 final SampleModel sm = raster.getSampleModel();
-                final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
+                final var colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null, false);
                 final ColorModel colors;
                 if (colorizer.initialize(sm, bands.get(visibleBand)) || colorizer.initialize(sm, visibleBand)) {
                     colors = colorizer.createColorModel(ImageUtilities.getBandType(sm), bands.size(), visibleBand);
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 1d5bd9e17e..d9b0fef705 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
@@ -753,7 +753,7 @@ public class ImageRenderer {
     @SuppressWarnings("UseOfObsoleteCollectionType")
     public RenderedImage createImage() {
         final Raster raster = createRaster();
-        final ColorModelBuilder colorizer = new ColorModelBuilder(colors, null);
+        final var colorizer = new ColorModelBuilder(colors, null, false);
         final ColorModel colors;
         final SampleModel sm = raster.getSampleModel();
         if (colorizer.initialize(sm, bands[visibleBand]) || colorizer.initialize(sm, visibleBand)) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 9f12e4c2df..07b4f7cd12 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -78,6 +78,7 @@ final class BandSelectImage extends SourceAlignedImage {
     private BandSelectImage(final RenderedImage source, final ColorModel cm, final int[] bands) {
         super(source, cm, source.getSampleModel().createSubsetSampleModel(bands));
         this.bands = bands;
+        ensureCompatible(cm);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
index 22a6a95c71..2076fe5329 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
@@ -131,6 +131,7 @@ class BandedSampleConverter extends ComputedImage {
         this.colorModel = colorModel;
         this.converters = converters;
         this.sampleDimensions = sampleDimensions;
+        ensureCompatible(colorModel);
         /*
          * Get an estimation of the resolution, arbitrarily looking in the middle of the range of values.
          * If the converters are linear (which is the most common case), the middle value does not matter
@@ -238,7 +239,7 @@ class BandedSampleConverter extends ComputedImage {
             if (sampleDimensions != null && visibleBand >= 0 && visibleBand < sampleDimensions.length) {
                 sd = sampleDimensions[visibleBand];
             }
-            final var builder = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
+            final var builder = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null, false);
             if (builder.initialize(source.getSampleModel(), sd) ||
                 builder.initialize(source.getColorModel()))
             {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
index 6d32896d96..e1f1d7a937 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -291,7 +291,7 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode
                     final List<SampleDimension> ranges = target.getRanges().orElse(null);
                     if (visibleBand < ranges.size()) {
                         final SampleModel model = target.getSampleModel();
-                        final var c = new ColorModelBuilder(colors, null);
+                        final var c = new ColorModelBuilder(colors, null, false);
                         if (c.initialize(model, ranges.get(visibleBand))) {
                             return Optional.ofNullable(c.createColorModel(model.getDataType(), model.getNumBands(), visibleBand));
                         }
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 ee080d49dd..c7c44fa529 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
@@ -287,7 +287,8 @@ final class Visualization extends ResampledImage {
             if (colorizer != null) {
                 colorModel = colorizer.apply(target).orElse(null);
             }
-            final ColorModel sourceCM = coloredSource.getColorModel();
+            final SampleModel sourceSM = coloredSource.getSampleModel();
+            final ColorModel  sourceCM = coloredSource.getColorModel();
             /*
              * Get a `ColorModelBuilder` which will compute the `ColorModel` of destination image.
              * There is different ways to setup the builder, depending on which `Colorizer` is used.
@@ -307,8 +308,8 @@ final class Visualization extends ResampledImage {
                  * Ranges of sample values were not specified explicitly. Instead, we will try to infer them
                  * in various ways: sample dimensions, scaled color model, or image statistics in last resort.
                  */
-                builder = new ColorModelBuilder(target.categoryColors, sourceCM);
-                initialized = builder.initialize(coloredSource.getSampleModel(), visibleSD);
+                builder = new ColorModelBuilder(target.categoryColors, sourceCM, true);
+                initialized = builder.initialize(sourceSM, visibleSD);
                 if (initialized) {
                     /*
                      * If we have been able to configure ColorModelBuilder using SampleDimension, apply an adjustment
@@ -328,10 +329,10 @@ final class Visualization extends ResampledImage {
                     if (!initialized) {
                         if (coloredSource instanceof RecoloredImage) {
                             final RecoloredImage colored = (RecoloredImage) coloredSource;
-                            builder.initialize(colored.minimum, colored.maximum);
+                            builder.initialize(colored.minimum, colored.maximum, sourceSM.getDataType());
                             initialized = true;
                         } else {
-                            initialized = builder.initialize(coloredSource.getSampleModel(), visibleBand);
+                            initialized = builder.initialize(sourceSM, visibleBand);
                         }
                     }
                 }
@@ -343,13 +344,13 @@ final class Visualization extends ResampledImage {
                  */
                 final DoubleUnaryOperator[] sampleFilters = SampleDimensions.toSampleFilters(visibleSD);
                 final Statistics statistics = processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND];
-                builder.initialize(statistics.minimum(), statistics.maximum());
+                builder.initialize(statistics.minimum(), statistics.maximum(), sourceSM.getDataType());
             }
             if (colorModel == null) {
-                colorModel = builder.compactColorModel(NUM_BANDS, VISIBLE_BAND);
+                colorModel = builder.createColorModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, VISIBLE_BAND);
             }
             converters = new MathTransform1D[] {
-                builder.getSampleToIndexValues()            // Must be after `compactColorModel(…)`.
+                builder.getSampleToIndexValues()            // Must be after `createColorModel(…)`.
             };
             if (shortcut) {
                 if (converters[0].isIdentity() && colorModel.equals(sourceCM)) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
index a4c9a2d541..4a7214fca0 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
@@ -51,7 +51,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * <ol>
  *   <li>Create a new {@link ColorModelBuilder} instance.</li>
  *   <li>Invoke one of {@code initialize(…)} methods.</li>
- *   <li>Invoke {@link #createColorModel(int, int, int)} or {@link #compactColorModel(int, int)}.</li>
+ *   <li>Invoke {@link #createColorModel(int, int, int)}.</li>
  *   <li>Invoke {@link #getSampleToIndexValues()} if this auxiliary information is useful.</li>
  *   <li>Discards {@code ColorModelBuilder}. Each instance shall be used only once.</li>
  * </ol>
@@ -99,11 +99,16 @@ public final class ColorModelBuilder {
     private static final int MAX_VALUE = 0xFF;
 
     /**
-     * The type resulting from sample values conversion applied by {@link #compactColorModel(int, int)}.
-     * Current value is {@link DataBuffer#TYPE_BYTE}.
+     * The type resulting from sample values conversion in compact mode.
+     * The value is {@link DataBuffer#TYPE_BYTE}.
      */
     public static final int TYPE_COMPACT = DataBuffer.TYPE_BYTE;
 
+    /**
+     * Whether to rescale the range of sample values to the {@link #TYPE_COMPACT} range.
+     */
+    private final boolean compact;
+
     /**
      * Applies a gray scale to quantitative category and transparent colors to qualitative categories.
      * This is a possible argument for the {@link #ColorModelBuilder(Function)} constructor.
@@ -177,6 +182,9 @@ public final class ColorModelBuilder {
      * and to grayscale colors otherwise, unless {@code inherited} is non-null.
      * Empty arrays of colors are interpreted as explicitly transparent.</p>
      *
+     * <p>This constructor creates a builder in compact mode, unless all specified ranges
+     * already fit in {@link #TYPE_COMPACT} range.</p>
+     *
      * @param  colors     the colors to use for each range of values in source image.
      * @param  inherited  the colors to use as fallback if some ranges have undefined colors, or {@code null}.
      *                    Should be non-null only for styling an exiting image before visualization.
@@ -185,6 +193,14 @@ public final class ColorModelBuilder {
         entries = ColorsForRange.list(colors, inherited);
         inheritedColors = inherited;
         this.colors = GRAYSCALE;
+        for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) {
+            final NumberRange<?> range = entry.getKey();
+            if (range.getMinDouble() < 0 || range.getMaxDouble() >= MAX_VALUE + 1) {
+                compact = true;
+                return;
+            }
+        }
+        compact = false;
     }
 
     /**
@@ -200,10 +216,12 @@ public final class ColorModelBuilder {
      *                    The function may return {@code null} for unrecognized categories.
      * @param  inherited  the colors to use as fallback for unrecognized categories, or {@code null}.
      *                    Should be non-null only for styling an exiting image before visualization.
+     * @param  compact    Whether to rescale the range of sample values to the {@link #TYPE_COMPACT} range.
      */
-    public ColorModelBuilder(final Function<Category,Color[]> colors, final ColorModel inherited) {
+    public ColorModelBuilder(final Function<Category,Color[]> colors, final ColorModel inherited, final boolean compact) {
         this.colors = (colors != null) ? colors : GRAYSCALE;
         inheritedColors = inherited;
+        this.compact = compact;
     }
 
     /**
@@ -220,6 +238,9 @@ public final class ColorModelBuilder {
 
     /**
      * Returns {@code true} if the given range is already the [0 … 255] range.
+     *
+     * @see #TYPE_COMPACT
+     * @see #compact()
      */
     private static boolean isAlreadyScaled(final NumberRange<?> range) {
         return range.getMinDouble(true) == 0 && range.getMaxDouble(true) == MAX_VALUE;
@@ -310,7 +331,7 @@ public final class ColorModelBuilder {
             final ColorSpace cs = source.getColorSpace();
             if (cs instanceof ScaledColorSpace) {
                 final ScaledColorSpace scs = (ScaledColorSpace) cs;
-                initialize(scs.offset, scs.maximum);
+                initialize(scs.offset, scs.maximum, source.getTransferType());
                 return true;
             }
             /*
@@ -343,29 +364,60 @@ public final class ColorModelBuilder {
      */
 
     /**
-     * Applies colors on the given range of values. The 0 index will be reserved for NaN value,
+     * Applies colors on the given range of values.
+     * In compact mode, 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 all other {@code initialize(…)}
      * methods failed or cannot be applied. This method assumes that no {@link Category} information
      * is available.</p>
      *
-     * @param  minimum  minimum value, inclusive.
-     * @param  maximum  maximum value, inclusive.
+     * @param  minimum   minimum value, inclusive.
+     * @param  maximum   maximum value, inclusive.
+     * @param  dataType  type of sample values.
      * @throws IllegalStateException if a sample dimension is already defined on this colorizer.
      */
-    public void initialize(final double minimum, final double maximum) {
+    public void initialize(final double minimum, final double maximum, final int dataType) {
         checkInitializationStatus(false);
         ArgumentChecks.ensureFinite("minimum", minimum);
         ArgumentChecks.ensureFinite("maximum", maximum);
+        if (ImageUtilities.isIntegerType(dataType)) {
+            defaultRange = NumberRange.create(Math.round(minimum), true, Math.round(maximum), true);
+        } else {
+            defaultRange = NumberRange.create(minimum, true, maximum, true);
+        }
+        applyDefaultRange();
+    }
+
+    /**
+     * Applies colors on the given range of values.
+     * This method does the same work than {@link #initialize(double, double, int)},
+     * but is preferred to the latter when the sample values are known to be integer values.
+     *
+     * @param  minimum  minimum value, inclusive.
+     * @param  maximum  maximum value, inclusive.
+     * @throws IllegalStateException if a sample dimension is already defined on this colorizer.
+     */
+    public void initialize(final long minimum, final long maximum) {
+        checkInitializationStatus(false);
         defaultRange = NumberRange.create(minimum, true, maximum, true);
-        target = new SampleDimension.Builder()
-                .setBackground(TRANSPARENT, 0)
-                .addQuantitative(COLOR_INDEX, NumberRange.create(1, true, MAX_VALUE, true), defaultRange)
-                .setName(VISUAL).build();
+        applyDefaultRange();
+    }
+
+    /**
+     * Initializes this builder with the {@link #defaultRange} value.
+     */
+    private void applyDefaultRange() {
+        final var builder = new SampleDimension.Builder().setName(VISUAL);
+        if (compact) {
+            var samples = NumberRange.create(1, true, MAX_VALUE, true);
+            builder.setBackground(TRANSPARENT, 0).addQuantitative(COLOR_INDEX, samples, defaultRange);
+        } else {
+            builder.addQuantitative(COLOR_INDEX, defaultRange, identity(), null);
+        }
+        target = builder.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.
          */
@@ -603,6 +655,13 @@ reuse:  if (source != null) {
      * This method builds up the color model from each set of colors associated to ranges in the given array.
      * Returned instances of {@link ColorModel} are shared among all callers in the running virtual machine.
      *
+     * <h4>Compact mode</h4>
+     * If the {@code compact} argument given to the constructor was {@code true},
+     * then the color model has colors interpolated in the [0 … 255] range of values.
+     * Conversions from range specified at construction time to the [0 … 255] range is
+     * given by {@link #getSampleToIndexValues()}. Images using this color model shall
+     * use a {@link DataBuffer} of type {@link #TYPE_COMPACT}.
+     *
      * @param  dataType     the color model type. One of {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
      *                      {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
      *                      or {@link DataBuffer#TYPE_DOUBLE}.
@@ -612,31 +671,17 @@ reuse:  if (source != null) {
      * @param  visibleBand  the band to be made visible (usually 0). All other bands, if any, will be ignored.
      * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges.
      */
-    public ColorModel createColorModel(final int dataType, final int numBands, final int visibleBand) {
+    public ColorModel createColorModel(int dataType, final int numBands, final int visibleBand) {
         checkInitializationStatus(true);
+        if (compact) {
+            compact();
+            dataType = TYPE_COMPACT;
+        }
         ArgumentChecks.ensureStrictlyPositive("numBands", numBands);
         ArgumentChecks.ensureBetween("visibleBand", 0, numBands - 1, visibleBand);
         return ColorModelFactory.createPiecewise(dataType, numBands, visibleBand, entries);
     }
 
-    /**
-     * Returns a color model with colors interpolated in the [0 … 255] range of values.
-     * Conversions from range specified at construction time to the [0 … 255] range is
-     * given by {@link #getSampleToIndexValues()}. Images using this color model shall
-     * use a {@link DataBuffer} of type {@link #TYPE_COMPACT}.
-     *
-     * @param  numBands     the number of bands for the color model (usually 1). The returned color model will render only
-     *                      the {@code visibleBand} and ignore the others, but the existence of all {@code numBands} will
-     *                      be at least tolerated. Supplemental bands, even invisible, are useful for processing.
-     * @param  visibleBand  the band to be made visible (usually 0). All other bands, if any, will be ignored.
-     * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges.
-     */
-    public ColorModel compactColorModel(final int numBands, final int visibleBand) {
-        checkInitializationStatus(true);
-        compact();
-        return createColorModel(TYPE_COMPACT, numBands, visibleBand);
-    }
-
     /**
      * Returns the conversion from sample values in the source image to sample values in the recolored image.
      *
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
index 012e1dcae6..78c59df847 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
@@ -17,7 +17,6 @@
 package org.apache.sis.internal.coverage.j2d;
 
 import java.util.List;
-import java.util.Collection;
 import java.util.AbstractMap.SimpleEntry;
 import java.awt.Color;
 import java.awt.image.DataBuffer;
@@ -43,7 +42,7 @@ import static org.junit.Assert.*;
  */
 public final class ColorModelBuilderTest extends TestCase {
     /**
-     * Tests the creation of an index color model using {@link ColorModelBuilder#ColorModelBuilder(Collection)}.
+     * Tests the creation of an index color model using explicit range of sample values.
      *
      * @throws TransformException if a sample value cannot be converted.
      */
@@ -85,8 +84,7 @@ public final class ColorModelBuilderTest extends TestCase {
     }
 
     /**
-     * Tests the creation of an index color model using {@link ColorModelBuilder#ColorModelBuilder(Function)}
-     * and an initialization with a {@link SampleDimension}.
+     * Tests the creation of an index color model using sample dimensions.
      *
      * @throws TransformException if a sample value cannot be converted.
      */
@@ -99,9 +97,9 @@ public final class ColorModelBuilderTest extends TestCase {
                 .addQualitative ("Error", MathFunctions.toNanFloat(3))
                 .setName("Temperature").build();
 
-        final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
+        final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null, true);
         assertTrue("initialize", colorizer.initialize(null, sd));
-        final IndexColorModel cm = (IndexColorModel) colorizer.compactColorModel(1, 0);     // Must be first.
+        final var cm = (IndexColorModel) colorizer.createColorModel(ColorModelBuilder.TYPE_COMPACT, 1, 0);      // Must be first.
         /*
          * Test conversion of a few sample values to packed values.
          */