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 2021/11/03 11:50:45 UTC

[sis] 02/03: Replace `Visualization.create(…)` method by a builder. It simplifies some code and made easier to fix a bug related to mismatched tile layout in the `converters == null` optimization case.

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 381fe9d9cade15ffdbc74511d78d18b698b106d5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 3 11:36:23 2021 +0100

    Replace `Visualization.create(…)` method by a builder.
    It simplifies some code and made easier to fix a bug related to mismatched tile layout in the `converters == null` optimization case.
---
 .../java/org/apache/sis/image/ImageProcessor.java  |  51 +--
 .../java/org/apache/sis/image/Visualization.java   | 420 +++++++++++++--------
 .../sis/internal/coverage/j2d/ImageLayout.java     |  40 ++
 3 files changed, 321 insertions(+), 190 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 08866cb..866e754 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
@@ -983,11 +983,7 @@ public class ImageProcessor implements Cloneable {
     public RenderedImage visualize(final RenderedImage source, final Map<NumberRange<?>,Color[]> colors) {
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("colors", colors);
-        try {
-            return Visualization.create(this, null, source, null, null, colors.entrySet());
-        } catch (IllegalStateException | NoninvertibleTransformException e) {
-            throw new IllegalArgumentException(Resources.format(Resources.Keys.UnconvertibleSampleValues), e);
-        }
+        return visualize(new Visualization.Builder(source, colors.entrySet()));
     }
 
     /**
@@ -1024,11 +1020,7 @@ public class ImageProcessor implements Cloneable {
      */
     public RenderedImage visualize(final RenderedImage source, final List<SampleDimension> ranges) {
         ArgumentChecks.ensureNonNull("source", source);
-        try {
-            return Visualization.create(this, null, source, null, ranges, null);
-        } catch (IllegalStateException | NoninvertibleTransformException e) {
-            throw new IllegalArgumentException(Resources.format(Resources.Keys.UnconvertibleSampleValues), e);
-        }
+        return visualize(new Visualization.Builder(null, source, null, ranges));
     }
 
     /**
@@ -1076,38 +1068,25 @@ public class ImageProcessor implements Cloneable {
         ArgumentChecks.ensureNonNull("bounds",   bounds);
         ArgumentChecks.ensureNonNull("toSource", toSource);
         ensureNonEmpty(bounds);
-        try {
-            return Visualization.create(this, bounds, source, toSource, ranges, null);
-        } catch (IllegalStateException | NoninvertibleTransformException e) {
-            throw new IllegalArgumentException(Resources.format(Resources.Keys.UnconvertibleSampleValues), e);
-        }
+        return visualize(new Visualization.Builder(bounds, source, toSource, ranges));
     }
 
     /**
-     * Callback method for {@link Visualization}.
-     *
-     * @param  source      image to be resampled and converted.
-     * @param  toSource    conversion of pixel coordinates of this image to pixel coordinates of {@code source} image.
-     * @param  converters  transfer functions to apply on each band of the source image. This array is not cloned.
-     * @param  bounds      domain of pixel coordinates of this image, or {@code null} if same as {@code source} image.
-     * @param  colorModel  color model of the image to create.
+     * Finishes builder configuration and creates the {@link Visualization} image.
      */
-    final RenderedImage resampleAndConvert(final RenderedImage source, final MathTransform toSource,
-            final MathTransform1D[] converters, final Rectangle bounds, final ColorModel colorModel)
-    {
-        final ImageLayout   layout;
-        final Interpolation interpolation;
-        final Number[]      fillValues;
-        final Quantity<?>[] positionalAccuracyHints;
-        ensureNonEmpty(bounds);
+    private RenderedImage visualize(final Visualization.Builder builder) {
         synchronized (this) {
-            layout                  = this.layout;
-            interpolation           = this.interpolation;
-            fillValues              = this.fillValues;
-            positionalAccuracyHints = this.positionalAccuracyHints;
+            builder.layout                  = layout;
+            builder.interpolation           = interpolation;
+            builder.categoryColors          = colors;
+            builder.fillValues              = fillValues;
+            builder.positionalAccuracyHints = positionalAccuracyHints;
+        }
+        try {
+            return builder.create(this);
+        } catch (IllegalStateException | NoninvertibleTransformException e) {
+            throw new IllegalArgumentException(Resources.format(Resources.Keys.UnconvertibleSampleValues), e);
         }
-        return unique(new Visualization(source, layout, bounds, toSource, toSource.isIdentity(),
-                      interpolation, converters, fillValues, colorModel, positionalAccuracyHints));
     }
 
     /**
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 807f3ba..785056c 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
@@ -19,17 +19,20 @@ package org.apache.sis.image;
 import java.util.Map;
 import java.util.List;
 import java.util.Collection;
+import java.util.function.Function;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.IndexColorModel;
+import java.awt.image.SampleModel;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import java.awt.image.RenderedImage;
 import java.awt.image.DataBuffer;
 import java.nio.DoubleBuffer;
 import javax.measure.Quantity;
+import org.apache.sis.coverage.Category;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
@@ -50,65 +53,252 @@ import org.apache.sis.util.collection.BackingStoreException;
  * Image generated for visualization purposes only (not to be used for computation purposes).
  * This class merges {@link ResampledImage}, {@link BandedSampleConverter} and {@link RecoloredImage} operations
  * in a single operation for efficiency. This merge avoids creating intermediate tiles of {@code float} values.
- * By writing directly {@code byte} values, we save memory and CPU since {@link WritableRaster#setPixel(int, int, int[])}
- * has more efficient implementations for integers.
+ * By writing directly {@code byte} values, we save memory and CPU because
+ * {@link WritableRaster#setPixel(int, int, int[])} has more efficient implementations for integers.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
 final class Visualization extends ResampledImage {
     /**
-     * Transfer functions to apply on each band of the source image, or {@code null} if those conversions are done
-     * by {@link InterpConvert}. Non-null array is used for allowing {@link #computeTile(int, int, WritableRaster)}
-     * to use a shortcut avoiding {@link ResampledImage} cost. Outputs should be values in the [0 … 255] range;
-     * values outside that ranges will be clamped.
+     * Builds an image where all sample values are indices of colors in an {@link IndexColorModel}.
+     * If the given image stores sample values as unsigned bytes or short integers, then those values
+     * are used as-is (they are not copied or converted). Otherwise {@link Visualization} will convert
+     * sample values to unsigned bytes in order to enable the use of {@link IndexColorModel}.
+     *
+     * <p>This builder accepts two kinds of input:</p>
+     * <ul>
+     *   <li>Non-null {@code sourceBands} and {@link ImageProcessor#getCategoryColors()}.</li>
+     *   <li>Non-null {@code rangesAndColors}.</li>
+     * </ul>
+     *
+     * The resulting image is suitable for visualization purposes but should not be used for computation purposes.
+     * There is no guarantees about the number of bands in returned image and the formulas used for converting
+     * floating point values to integer values.
+     *
+     * <h2>Resampling</h2>
+     * {@link Visualization} can optionally be combined with a {@link ResampledImage} operation.
+     * This can be done by providing a non-null value to the {@code toSource} argument.
+     *
+     * @see ImageProcessor#visualize(RenderedImage, Map)
      */
-    private final MathTransform1D[] converters;
+    static final class Builder {
+        /** Number of bands of the image to create. */
+        private static final int NUM_BANDS = 1;
 
-    /**
-     * The color model for the expected range of values. Typically an {@link IndexColorModel} for byte values.
-     * May be {@code null} if the color model is unknown.
-     */
-    private final ColorModel colorModel;
+        /** Band to make visible. */
+        private static final int VISIBLE_BAND = 0;
 
-    /**
-     * Creates a new image which will resample and convert values of the given image.
-     * See parent class for more details on arguments.
-     *
-     * @param source         image to be resampled and converted.
-     * @param layout         computer of tile size.
-     * @param bounds         domain of pixel coordinates of this image, or {@code null} if same as {@code source} image.
-     * @param toSource       conversion of pixel coordinates of this image to pixel coordinates of {@code source} image.
-     * @param isIdentity     value of {@code toSource.isIdentity()}.
-     * @param interpolation  object to use for performing interpolations.
-     * @param converters     transfer functions to apply on each band of the source image. This array is not cloned.
-     * @param fillValues     values to use for pixels in this image that can not be mapped to pixels in source image.
-     * @param colorModel     color model of this image.
-     * @param accuracy       values of {@value #POSITIONAL_ACCURACY_KEY} property, or {@code null} if none.
-     */
-    Visualization(final RenderedImage source,         final ImageLayout layout,  final Rectangle bounds,
-                  final MathTransform toSource,       final boolean isIdentity,  final Interpolation interpolation,
-                  final MathTransform1D[] converters, final Number[] fillValues, final ColorModel colorModel,
-                  final Quantity<?>[] accuracy)
-    {
-        super(source,
-              layout.createBandedSampleModel(Colorizer.TYPE_COMPACT, converters.length, source, bounds),
-              layout.getMinTile(), (bounds != null) ? bounds : ImageUtilities.getBounds(source),
-              toSource,
-              isIdentity ? Interpolation.NEAREST : combine(interpolation, converters),
-              fillValues,
-              accuracy);
-
-        this.colorModel = colorModel;
-        this.converters = isIdentity ? converters : null;
+        ////  ┌─────────────────────────────────────┐
+        ////  │ Arguments given by user             │
+        ////  └─────────────────────────────────────┘
+
+        /** Pixel coordinates of the visualization image, or {@code null} if same as {@link #source} image. */
+        private Rectangle bounds;
+
+        /** Image to be resampled and converted. */
+        private RenderedImage source;
+
+        /** Conversion from pixel coordinates of visualization image to pixel coordinates of {@link #source} image. */
+        private MathTransform toSource;
+
+        /** Description of {@link #source} bands, or {@code null} if none. */
+        private List<SampleDimension> sourceBands;
+
+        /** Colors to apply for range of sample values in source image, or {@code null} if none. */
+        private Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors;
+
+        ////  ┌─────────────────────────────────────┐
+        ////  │ Given by ImageProcesor.configure(…) │
+        ////  └─────────────────────────────────────┘
+
+        /** Computer of tile size. */
+        ImageLayout layout;
+
+        /** Object to use for performing interpolations. */
+        Interpolation interpolation;
+
+        /** The colors to use for given categories of sample values, or {@code null} is unspecified. */
+        Function<Category,Color[]> categoryColors;
+
+        /** Values to use for pixels in this image that can not be mapped to pixels in source image. */
+        Number[] fillValues;
+
+        /** Values of {@value #POSITIONAL_ACCURACY_KEY} property, or {@code null} if none. */
+        Quantity<?>[] positionalAccuracyHints;
+
+        ////  ┌─────────────────────────────────────┐
+        ////  │ Computed by `create(…)`             │
+        ////  └─────────────────────────────────────┘
+
+        /** Transfer functions to apply on each band of the source image. */
+        private MathTransform1D[] converters;
+
+        /** Sample model of {@link Visualization} image. */
+        private SampleModel sampleModel;
+
+        /** Color model of {@link Visualization} image. */
+        private ColorModel colorModel;
+
+        /**
+         * Creates a builder for a visualization image with colors inferred from sample dimensions.
+         *
+         * @param bounds       desired domain of pixel coordinates, or {@code null} if same as {@code source} image.
+         * @param source       the image for which to replace the color model.
+         * @param toSource     pixel coordinates conversion to {@code source} image, or {@code null} if none.
+         * @param sourceBands  description of {@code source} bands.
+         */
+        Builder(final Rectangle bounds, final RenderedImage source, final MathTransform toSource,
+                final List<SampleDimension> sourceBands)
+        {
+            this.bounds      = bounds;
+            this.source      = source;
+            this.toSource    = toSource;
+            this.sourceBands = sourceBands;
+        }
+
+        /**
+         * Creates a builder for a visualization image with colors specified for range of values.
+         * Current version assumes that target image bounds are the same than source image bounds
+         * and that there is no change of pixel coordinates, but this is not a real restriction.
+         * The {@code bounds} and {@code toSource} arguments could be added back in the future if useful.
+         *
+         * @param source           the image for which to replace the color model.
+         * @param rangesAndColors  range of sample values in source image associated to colors to apply.
+         */
+        Builder(final RenderedImage source, final Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors) {
+            this.source          = source;
+            this.rangesAndColors = rangesAndColors;
+        }
+
+        /**
+         * Returns an image where all sample values are indices of colors in an {@link IndexColorModel}.
+         * If the source image stores sample values as unsigned bytes or short integers, then those values
+         * are used as-is (they are not copied or converted). Otherwise this operation will convert sample
+         * values to unsigned bytes in order to enable the use of {@link IndexColorModel}.
+         *
+         * <p>The resulting image is suitable for visualization but should not be used for computational purposes.
+         * There is no guarantees about the number of bands in returned image and the formulas used for converting
+         * floating point values to integer values.</p>
+         *
+         * <h4>Resampling</h4>
+         * This operation can optionally be combined with a {@link ResampledImage} operation.
+         * This can be done by providing a non-null value to the {@link #toSource} field.
+         *
+         * @param  processor  the processor invoking this constructor.
+         * @return resampled and recolored image for visualization purposes only.
+         * @throws NoninvertibleTransformException if sample values in source image
+         *         can not be converted to sample values in the recolored image.
+         */
+        RenderedImage create(final ImageProcessor processor) throws NoninvertibleTransformException {
+            final int visibleBand = ImageUtilities.getVisibleBand(source);
+            if (visibleBand < 0) {
+                // This restriction may be relaxed in a future version if we implement conversion to RGB images.
+                throw new IllegalArgumentException(Resources.format(Resources.Keys.OperationRequiresSingleBand));
+            }
+            /*
+             * Get a `Colorizer` which will compute the `ColorModel` of destination image.
+             * There is different ways to create colorizer, depending on which arguments
+             * were supplied by user. In precedence order:
+             *
+             *    - rangesAndColor  : Collection<Map.Entry<NumberRange<?>,Color[]>>
+             *    - sourceBands     : List<SampleDimension>
+             *    - statistics
+             */
+            boolean initialized;
+            final Colorizer colorizer;
+            if (rangesAndColors != null) {
+                colorizer = new Colorizer(rangesAndColors);
+                initialized = true;
+            } else {
+                /*
+                 * Ranges of sample values were not specified explicitly. Instead we will try to infer them
+                 * in various ways: sample dimensions, scaled color model, statistics in last resort.
+                 */
+                colorizer = new Colorizer(categoryColors);
+                initialized = (sourceBands != null) && colorizer.initialize(sourceBands.get(visibleBand));
+                if (initialized) {
+                    /*
+                     * If we have been able to configure Colorizer using the SampleModel, 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.
+                     */
+                    colorizer.rescaleMainRange(source.getColorModel());
+                } else {
+                    /*
+                     * If we have not been able to use the SampleDimension, try to use the ColorModel or SampleModel.
+                     * 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);
+                }
+            }
+            source = BandSelectImage.create(source, new int[] {visibleBand});               // Make single-banded.
+            if (!initialized) {
+                /*
+                 * If none of above Colorizer configurations worked, use statistics in last resort. We do that
+                 * after we reduced the image to a single band, in order to reduce the amount of calculations.
+                 */
+                final Statistics statistics = processor.valueOfStatistics(source, null)[VISIBLE_BAND];
+                colorizer.initialize(statistics.minimum(), statistics.maximum());
+            }
+            /*
+             * At this point we finished to configure the colorizer; we are ready to build the `ColorModel`.
+             * If the source image uses unsigned integer types and there is no resampling operation, we can
+             * update the color model without changing sample values. This is much cheaper and as accurate.
+             */
+            final int dataType = source.getSampleModel().getDataType();
+            if (dataType == DataBuffer.TYPE_BYTE || dataType == DataBuffer.TYPE_USHORT) {
+                if (toSource != null && !toSource.isIdentity()) {
+                    source = processor.resample(source, bounds, toSource);
+                }
+                return RecoloredImage.create(source, colorizer.createColorModel(dataType, NUM_BANDS, VISIBLE_BAND));
+            }
+            /*
+             * If we reach this point, sample values need to be converted 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;
+            }
+            colorModel = colorizer.compactColorModel(NUM_BANDS, VISIBLE_BAND);
+            converters = new MathTransform1D[] {
+                colorizer.getSampleToIndexValues()          // Must be after `compactColorModel(…)`.
+            };
+            /*
+             * If there is no conversion of pixel coordinates, there is no need for interpolations.
+             * In such case the `Visualization.computeTile(…)` implementation takes a shortcut which
+             * requires the tile layout of destination image to be the same as source image.
+             */
+            if (toSource == null) {
+                toSource = MathTransforms.identity(BIDIMENSIONAL);
+            }
+            if (toSource.isIdentity() && (bounds == null || ImageUtilities.getBounds(source).contains(bounds))) {
+                layout        = ImageLayout.fixedSize(source);
+                interpolation = Interpolation.NEAREST;
+            } else {
+                interpolation = combine(interpolation, converters);
+                converters    = null;
+            }
+            /*
+             * Final image creation after the tile layout has been chosen.
+             */
+            sampleModel = layout.createBandedSampleModel(Colorizer.TYPE_COMPACT, NUM_BANDS, source, bounds);
+            if (bounds == null) {
+                bounds = ImageUtilities.getBounds(source);
+            }
+            return ImageProcessor.unique(new Visualization(this));
+        }
     }
 
     /**
      * Combines the given interpolation method with the given sample conversion.
      */
-    static Interpolation combine(final Interpolation interpolation, final MathTransform1D[] converters) {
+    private static Interpolation combine(final Interpolation interpolation, final MathTransform1D[] converters) {
         final MathTransform converter = CompoundTransform.create(converters);
         if (converter.isIdentity()) {
             return interpolation;
@@ -203,121 +393,43 @@ final class Visualization extends ResampledImage {
     }
 
     /**
-     * Returns {@code true} if this image can not have mask.
+     * Transfer functions to apply on each band of the source image, or {@code null} if those conversions are done
+     * by {@link InterpConvert}. Non-null array is used for allowing {@link #computeTile(int, int, WritableRaster)}
+     * to use a shortcut avoiding {@link ResampledImage} cost. Outputs should be values in the [0 … 255] range;
+     * values outside that ranges will be clamped.
      */
-    @Override
-    final boolean hasNoMask() {
-        return !(interpolation instanceof InterpConvert) && super.hasNoMask();
+    private final MathTransform1D[] converters;
+
+    /**
+     * The color model for the expected range of values. Typically an {@link IndexColorModel} for byte values.
+     * May be {@code null} if the color model is unknown.
+     */
+    private final ColorModel colorModel;
+
+    /**
+     * Creates a new image which will resample and convert values of the given image.
+     * See parent class for more details about arguments.
+     */
+    private Visualization(final Builder builder) {
+        super(builder.source,
+              builder.sampleModel,
+              builder.layout.getMinTile(),
+              builder.bounds,
+              builder.toSource,
+              builder.interpolation,
+              builder.fillValues,
+              builder.positionalAccuracyHints);
+
+        this.colorModel = builder.colorModel;
+        this.converters = builder.converters;
     }
 
     /**
-     * Returns an image where all sample values are indices of colors in an {@link IndexColorModel}.
-     * If the given image stores sample values as unsigned bytes or short integers, then those values
-     * are used as-is (they are not copied or converted). Otherwise this operation will convert sample
-     * values to unsigned bytes in order to enable the use of {@link IndexColorModel}.
-     *
-     * <p>This method accepts two kinds of input. Use only one of the followings:</p>
-     * <ul>
-     *   <li>Non-null {@code sourceBands} and {@link ImageProcessor#getCategoryColors()}.</li>
-     *   <li>Non-null {@code rangesAndColors}.</li>
-     * </ul>
-     *
-     * The resulting image is suitable for visualization purposes but should not be used for computation purposes.
-     * There is no guarantees about the number of bands in returned image and the formulas used for converting
-     * floating point values to integer values.
-     *
-     * <h4>Resampling</h4>
-     * This operation can optionally be combined with a {@link ResampledImage} operation.
-     * This can be done by providing a non-null value to the {@code toSource} argument.
-     *
-     * @param  processor        the processor invoking this method.
-     * @param  bounds           desired domain of pixel coordinates, or {@code null} if same as {@code source} image.
-     * @param  source           the image for which to replace the color model.
-     * @param  toSource         pixel coordinates conversion to {@code source} image, or {@code null} if none.
-     * @param  sourceBands      description of {@code source} bands, or {@code null} if none.
-     * @param  rangesAndColors  range of sample values in source image associated to colors to apply,
-     *                          or {@code null} for using {@code sourceBands} instead.
-     * @return resampled and recolored image for visualization purposes only.
-     * @throws NoninvertibleTransformException if sample values in source image can not be converted
-     *         to sample values in the recolored image.
-     *
-     * @see ImageProcessor#visualize(RenderedImage, Map)
+     * Returns {@code true} if this image can not have mask.
      */
-    static RenderedImage create(final ImageProcessor processor, final Rectangle bounds,
-                                RenderedImage source, MathTransform toSource,
-                                final List<SampleDimension> sourceBands,
-                                final Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors)
-            throws NoninvertibleTransformException
-    {
-        final int visibleBand = ImageUtilities.getVisibleBand(source);
-        if (visibleBand < 0) {
-            // This restriction may be relaxed in a future version if we implement conversion to RGB images.
-            throw new IllegalArgumentException(Resources.format(Resources.Keys.OperationRequiresSingleBand));
-        }
-        boolean initialized;
-        final Colorizer colorizer;
-        if (rangesAndColors != null) {
-            colorizer = new Colorizer(rangesAndColors);
-            initialized = true;
-        } else {
-            /*
-             * Ranges of sample values were not specified explicitly. Instead we will try to infer them
-             * in various ways: sample dimensions, scaled color model, statistics in last resort.
-             */
-            colorizer = new Colorizer(processor.getCategoryColors());
-            initialized = (sourceBands != null) && colorizer.initialize(sourceBands.get(visibleBand));
-            if (initialized) {
-                /*
-                 * If we have been able to configure Colorizer using the SampleModel, 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.
-                 */
-                colorizer.rescaleMainRange(source.getColorModel());
-            } else {
-                /*
-                 * If we have not been able to use the SampleDimension, try to use the ColorModel or SampleModel.
-                 * 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);
-            }
-        }
-        source = BandSelectImage.create(source, new int[] {visibleBand});               // Make single-banded.
-        if (!initialized) {
-            /*
-             * If none of above Colorizer configurations worked, use statistics in last resort. We do that
-             * after we reduced the image to a single band, in order to reduce the amount of calculations.
-             */
-            final Statistics statistics = processor.valueOfStatistics(source, null)[0];
-            colorizer.initialize(statistics.minimum(), statistics.maximum());
-        }
-        /*
-         * If the source image uses unsigned integer types and there is no resampling operation, we can
-         * update the color model without changing sample values. This is much cheaper and as accurate.
-         */
-        final int dataType = source.getSampleModel().getDataType();
-        if (dataType == DataBuffer.TYPE_BYTE || dataType == DataBuffer.TYPE_USHORT) {
-            if (toSource != null && !toSource.isIdentity()) {
-                source = processor.resample(source, bounds, toSource);
-            }
-            return RecoloredImage.create(source, colorizer.createColorModel(dataType, 1, 0));
-        }
-        /*
-         * If we reach this point, sample values need to be converted 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;
-        }
-        if (toSource == null) {
-            toSource = MathTransforms.identity(BIDIMENSIONAL);
-        }
-        final ColorModel      colorModel = colorizer.compactColorModel(1, 0);           // Must be first.
-        final MathTransform1D converter  = colorizer.getSampleToIndexValues();
-        return processor.resampleAndConvert(source, toSource,
-                new MathTransform1D[] {converter}, bounds, colorModel);
+    @Override
+    final boolean hasNoMask() {
+        return !(interpolation instanceof InterpConvert) && super.hasNoMask();
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
index 243fd3b..9bc28c9 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@ -99,6 +99,46 @@ public class ImageLayout {
     }
 
     /**
+     * Creates a new layout with exactly the tile size of given image.
+     *
+     * @param  source  image from which to take tile size and indices.
+     * @return layout giving exactly the tile size and indices of given image.
+     */
+    public static ImageLayout fixedSize(final RenderedImage source) {
+        return new FixedSize(source);
+    }
+
+    /**
+     * Override preferred tile size with a fixed size.
+     */
+    private static final class FixedSize extends ImageLayout {
+        /** Indices of the first tile. */
+        private final int xmin, ymin;
+
+        /** Creates a new layout with exactly the tile size of given image. */
+        FixedSize(final RenderedImage source) {
+            super(new Dimension(source.getTileWidth(), source.getTileHeight()), false);
+            xmin = source.getMinTileX();
+            ymin = source.getMinTileY();
+        }
+
+        /** Returns the fixed tile size. All parameters are ignored. */
+        @Override public Dimension suggestTileSize(int imageWidth, int imageHeight, boolean allowPartialTiles) {
+            return getPreferredTileSize();
+        }
+
+        /** Returns the fixed tile size. All parameters are ignored. */
+        @Override public Dimension suggestTileSize(RenderedImage image, Rectangle bounds, boolean allowPartialTiles) {
+            return getPreferredTileSize();
+        }
+
+        /** Returns indices of the first tile. */
+        @Override public Point getMinTile() {
+            return new Point(xmin, ymin);
+        }
+    }
+
+    /**
      * Returns the preferred tile size. This is the dimension values specified at construction time.
      *
      * @return the preferred tile size.