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/03 15:28:54 UTC

[sis] 02/02: `BandAggregateImage` should share references to data arrays when possible. It avoids copying the sample values.

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 03a6a48e266ed8bea63814567801ce20a5055a95
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Apr 3 17:09:48 2023 +0200

    `BandAggregateImage` should share references to data arrays when possible.
    It avoids copying the sample values.
---
 .../org/apache/sis/image/BandAggregateImage.java   | 312 +++++++++++++++++++--
 .../apache/sis/image/BandedSampleConverter.java    |   2 +-
 .../org/apache/sis/image/CombinedImageLayout.java  |  74 +++--
 .../java/org/apache/sis/image/ComputedImage.java   |  15 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   2 +-
 .../java/org/apache/sis/image/PlanarImage.java     |   2 +-
 .../java/org/apache/sis/image/Visualization.java   |   2 +-
 .../sis/internal/coverage/j2d/ImageLayout.java     |  14 +-
 .../sis/internal/coverage/j2d/RasterFactory.java   |  30 +-
 .../apache/sis/image/BandAggregateImageTest.java   | 114 +++++++-
 .../org/apache/sis/image/ImageProcessorTest.java   |  31 ++
 11 files changed, 539 insertions(+), 59 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
index fd7aaa5758..e1e437b86f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@ -20,11 +20,19 @@ import java.util.Arrays;
 import java.util.Objects;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.ComponentSampleModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferShort;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.DataBufferInt;
+import java.awt.image.DataBufferFloat;
+import java.awt.image.DataBufferDouble;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.Workaround;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
 
@@ -70,21 +78,28 @@ final class BandAggregateImage extends ComputedImage {
      */
     private final int minTileX, minTileY;
 
+    /**
+     * Whether all sources have tiles at the same locations and use the same scanline stride.
+     * In such case, it is possible to share references to data arrays without copying them.
+     */
+    private final boolean allowSharing;
+
     /**
      * Creates a new aggregation of bands.
-     * This static method is a workaround for RFE #4093999
-     * ("Relax constraint on placement of this()/super() call in constructors").
      *
-     * @param  sources          images to combine, in order.
-     * @param  bandsPerSource   bands to use for each source image, in order. May contain {@code null} elements.
-     * @param  colorizer        provider of color model to use for this image, or {@code null} for automatic.
+     * @param  sources         images to combine, in order.
+     * @param  bandsPerSource  bands to use for each source image, in order. May contain {@code null} elements.
+     * @param  colorizer       provider of color model to use for this image, or {@code null} for automatic.
+     * @param  allowSharing    whether to allow the sharing of data buffers (instead of copying) if possible.
      * @throws IllegalArgumentException if there is an incompatibility between some source images
      *         or if some band indices are duplicated or outside their range of validity.
      * @return the band aggregate image.
      */
-    @Workaround(library="JDK", version="1.8")
-    static RenderedImage create(RenderedImage[] sources, int[][] bandsPerSource, Colorizer colorizer) {
-        var image = new BandAggregateImage(CombinedImageLayout.create(sources, bandsPerSource), colorizer);
+    static RenderedImage create(final RenderedImage[] sources, final int[][] bandsPerSource,
+                                final Colorizer colorizer, final boolean allowSharing)
+    {
+        final var layout = CombinedImageLayout.create(sources, bandsPerSource, allowSharing);
+        final var image  = new BandAggregateImage(layout, colorizer);
         if (image.filteredSources.length == 1) {
             final RenderedImage c = image.filteredSources[0];
             if (image.colorModel == null) {
@@ -95,7 +110,7 @@ final class BandAggregateImage extends ComputedImage {
                 return c;
             }
         }
-        return ImageProcessor.unique(image);
+        return image;
     }
 
     /**
@@ -113,6 +128,7 @@ final class BandAggregateImage extends ComputedImage {
         height          = r.height;
         minTileX        = layout.minTileX;
         minTileY        = layout.minTileY;
+        allowSharing    = layout.allowSharing;
         filteredSources = layout.getFilteredSources();
         colorModel      = layout.createColorModel(colorizer);
         ensureCompatible(colorModel);
@@ -128,16 +144,33 @@ final class BandAggregateImage extends ComputedImage {
     @Override public int        getMinTileY()   {return minTileY;}
 
     /**
-     * Creates a raster sharing containing a copy of the selected bands in source images.
+     * Creates a raster containing the selected bands of source images.
      *
-     * @param  tileX     the column index of the tile to compute.
-     * @param  tileY     the row index of the tile to compute.
-     * @param  previous  the previous tile, reused if non-null.
-     *
-     * @todo Share data arrays instead of copying when possible.
+     * @param  tileX  the column index of the tile to compute.
+     * @param  tileY  the row index of the tile to compute.
+     * @param  tile   the previous tile, reused if non-null.
      */
     @Override
     protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) {
+        /*
+         * If we are allowed to share the data arrays, try that first.
+         */
+        if (allowSharing) {
+            final Sharing sharing = Sharing.create(sampleModel.getDataType(), sampleModel.getNumBands());
+            if (sharing != null) {
+                final DataBuffer buffer = sharing.createDataBuffer(
+                        Math.multiplyFull(tileX - minTileX, getTileWidth())  + minX,
+                        Math.multiplyFull(tileY - minTileY, getTileHeight()) + minY,
+                        filteredSources);
+                if (buffer != null) {
+                    return Raster.createRaster(sampleModel, buffer, computeTileLocation(tileX, tileY));
+                }
+            }
+        }
+        /*
+         * Fallback when the data arrays can not be shared.
+         * This code copies all sample values in new arrays.
+         */
         if (tile == null) {
             tile = createTile(tileX, tileY);
         }
@@ -155,6 +188,253 @@ final class BandAggregateImage extends ComputedImage {
         return tile;
     }
 
+    /**
+     * A builder of data buffers sharing arrays of source images.
+     * There is a subclass for each supported data type.
+     */
+    private abstract static class Sharing {
+        /**
+         * The offsets of the first valid element into each bank array.
+         * Will be computed with the assumption that all offsets are zero
+         * in the target {@link java.awt.image.BandedSampleModel}.
+         */
+        protected final int[] offsets;
+
+        /**
+         * For subclass constructors.
+         */
+        protected Sharing(final int numBands) {
+            offsets = new int[numBands];
+        }
+
+        /**
+         * Creates a new builder.
+         *
+         * @param  dataType  the data type as one of {@link DataBuffer} constants.
+         * @param  numBands  number of banks of the data buffer to create.
+         * @return the data buffer, or {@code null} if the dat type is not recognized.
+         */
+        static Sharing create(final int dataType, final int numBands) {
+            switch (dataType) {
+                case DataBuffer.TYPE_BYTE:   return new Bytes   (numBands);
+                case DataBuffer.TYPE_SHORT:  return new Shorts  (numBands);
+                case DataBuffer.TYPE_USHORT: return new UShorts (numBands);
+                case DataBuffer.TYPE_INT:    return new Integers(numBands);
+                case DataBuffer.TYPE_FLOAT:  return new Floats  (numBands);
+                case DataBuffer.TYPE_DOUBLE: return new Doubles (numBands);
+            }
+            return null;
+        }
+
+        /**
+         * Creates a data buffer sharing the arrays of all given sources, in order.
+         * This method assumes a target {@link java.awt.image.BandedSampleModel} where
+         * all band offsets are zero and where bank indices define an identity mapping.
+         *
+         * @param  x        <var>x</var> pixel coordinate of the tile.
+         * @param  y        <var>y</var> pixel coordinate of the tile.
+         * @param  sources  the sources for which to aggregate all bands.
+         * @return a data buffer containing the aggregation of all bands, or {@code null} if it can not be created.
+         */
+        final DataBuffer createDataBuffer(final long x, final long y, final RenderedImage[] sources) {
+            int band = 0;
+            int size = Integer.MAX_VALUE;
+            for (final RenderedImage source : sources) {
+                final int tileWidth  = source.getTileWidth();
+                final int tileHeight = source.getTileHeight();
+                long tileX = x - source.getTileGridXOffset();
+                long tileY = y - source.getTileGridYOffset();
+                if (((tileX % tileWidth) | (tileY % tileHeight)) != 0) {
+                    return null;    // Source tile not aligned on target tile.
+                }
+                tileX /= tileWidth;
+                tileY /= tileHeight;
+                final Raster raster = source.getTile(Math.toIntExact(tileX), Math.toIntExact(tileY));
+                final SampleModel c = raster.getSampleModel();
+                if (!(c instanceof ComponentSampleModel)) {
+                    return null;    // Should never happen if `BandAggregateImage.allowSharing` is true.
+                }
+                final var   sm       = (ComponentSampleModel) c;
+                final var   buffer   = raster.getDataBuffer();
+                final int[] offsets1 = buffer.getOffsets();
+                final int[] offsets2 = sm.getBandOffsets();
+                final int[] indices  = sm.getBankIndices();
+                for (int i=0; i<indices.length; i++) {
+                    final int b = indices[i];
+                    takeReference(buffer, b, band);
+                    offsets[band] = offsets1[b] + offsets2[i];      // Assume zero offset in target `BandedSampleModel`.
+                    band++;
+                }
+                size = Math.min(size, buffer.getSize());
+            }
+            final DataBuffer buffer = build(size);
+            assert buffer.getNumBanks() == band;
+            return buffer;
+        }
+
+        /**
+         * Takes a reference to an array in the given data buffer.
+         *
+         * @param source  the data buffer from which to take a reference to an array.
+         * @param src     bank index of the reference to take.
+         * @param dst     band index where to store the reference.
+         */
+        abstract void takeReference(DataBuffer source, int src, int dst);
+
+        /**
+         * Builds the data buffer after all references have been taken.
+         * The data buffer shall specify {@link #offsets} to the buffer constructor.
+         *
+         * @param  size  number of elements in the data buffer.
+         * @return the new data buffer.
+         */
+        abstract DataBuffer build(int size);
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_BYTE}.
+     */
+    private static final class Bytes extends Sharing {
+        /** The shared arrays. */
+        private final byte[][] data;
+
+        /** Creates a new builder. */
+        Bytes(final int numBands) {
+            super(numBands);
+            data = new byte[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferByte) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferByte(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_SHORT}.
+     */
+    private static final class Shorts extends Sharing {
+        /** The shared arrays. */
+        private final short[][] data;
+
+        /** Creates a new builder. */
+        Shorts(final int numBands) {
+            super(numBands);
+            data = new short[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferShort) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferShort(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_USHORT}.
+     */
+    private static final class UShorts extends Sharing {
+        /** The shared arrays. */
+        private final short[][] data;
+
+        /** Creates a new builder. */
+        UShorts(final int numBands) {
+            super(numBands);
+            data = new short[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferUShort) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferUShort(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_INT}.
+     */
+    private static final class Integers extends Sharing {
+        /** The shared arrays. */
+        private final int[][] data;
+
+        /** Creates a new builder. */
+        Integers(final int numBands) {
+            super(numBands);
+            data = new int[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferInt) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferInt(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_FLOAT}.
+     */
+    private static final class Floats extends Sharing {
+        /** The shared arrays. */
+        private final float[][] data;
+
+        /** Creates a new builder. */
+        Floats(final int numBands) {
+            super(numBands);
+            data = new float[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferFloat) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferFloat(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_DOUBLE}.
+     */
+    private static final class Doubles extends Sharing {
+        /** The shared arrays. */
+        private final double[][] data;
+
+        /** Creates a new builder. */
+        Doubles(final int numBands) {
+            super(numBands);
+            data = new double[numBands][];
+        }
+
+        /** Takes a reference to an array in the given data buffer. */
+        @Override void takeReference(DataBuffer buffer, int src, int dst) {
+            data[dst] = ((DataBufferDouble) buffer).getData(src);
+        }
+
+        /** Builds the data buffer after all references have been taken. */
+        @Override DataBuffer build(int size) {
+            return new DataBufferDouble(data, size, offsets);
+        }
+    }
+
     /**
      * Returns a hash code value for this image.
      */
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 3ba4de609c..22a6a95c71 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
@@ -220,7 +220,7 @@ class BandedSampleConverter extends ComputedImage {
             source = ((RecoloredImage) source).source;
         }
         final int numBands = converters.length;
-        final BandedSampleModel sampleModel = layout.createBandedSampleModel(targetType, numBands, source, null);
+        final BandedSampleModel sampleModel = layout.createBandedSampleModel(targetType, numBands, source, null, 0);
         final SampleDimension[] sampleDimensions = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get();
         final int visibleBand = ImageUtilities.getVisibleBand(source);
         ColorModel colorModel = ColorModelBuilder.NULL_COLOR_MODEL;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
index 2ab7c0ef27..0edd294a15 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
@@ -24,6 +24,8 @@ import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.ComponentSampleModel;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.collection.FrequencySortedSet;
 import org.apache.sis.internal.feature.Resources;
@@ -68,8 +70,11 @@ final class CombinedImageLayout extends ImageLayout {
 
     /**
      * The sample model of the combined image.
+     * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zero and
+     * all {@linkplain BandedSampleModel#getBankIndices() bank indices} are identity mapping.
+     * This simplicity is needed by current implementation of {@link BandAggregateImage}.
      */
-    final SampleModel sampleModel;
+    final BandedSampleModel sampleModel;
 
     /**
      * The domain of pixel coordinates in the combined image. All sources images are assumed to use
@@ -96,6 +101,12 @@ final class CombinedImageLayout extends ImageLayout {
      */
     private final boolean exactTileSize;
 
+    /**
+     * Whether all sources have tiles at the same locations and use the same scanline stride.
+     * In such case, it is possible to share references to data arrays without copying them.
+     */
+    final boolean allowSharing;
+
     /**
      * Computes the layout of an image combining all the specified source images.
      * The optional {@code bandsPerSource} argument specifies the bands to select in each source images.
@@ -109,41 +120,62 @@ final class CombinedImageLayout extends ImageLayout {
      *
      * @param  sources         images to combine, in order.
      * @param  bandsPerSource  bands to use for each source image, in order. May contain {@code null} elements.
+     * @param  allowSharing    whether to allow the sharing of data buffers (instead of copying) if possible.
      * @throws IllegalArgumentException if there is an incompatibility between some source images
      *         or if some band indices are duplicated or outside their range of validity.
      */
     @Workaround(library="JDK", version="1.8")
-    static CombinedImageLayout create(RenderedImage[] sources, int[][] bandsPerSource) {
+    static CombinedImageLayout create(RenderedImage[] sources, int[][] bandsPerSource, boolean allowSharing) {
         final var aggregate = new MultiSourcesArgument<RenderedImage>(sources, bandsPerSource);
         aggregate.identityAsNull();
         aggregate.validate(ImageUtilities::getNumBands);
 
         sources            = aggregate.sources();
         bandsPerSource     = aggregate.bandsPerSource();
-        Rectangle domain   = null;
+        Rectangle domain   = null;          // Nullity check used for telling when the first image is processed.
+        int scanlineStride = 0;
+        int tileWidth      = 0;
+        int tileHeight     = 0;
+        int tileAlignX     = 0;
+        int tileAlignY     = 0;
         int commonDataType = DataBuffer.TYPE_UNDEFINED;
         for (final RenderedImage source : sources) {
             /*
-             * Ensure that all images use the same data type.
-             * Get the domain of the combined image to create.
-             *
-             * TODO: current implementation computes the intersection of all sources.
-             * But a future version should allow users to specify if they want intersection,
-             * union or strict mode instead. A "strict" mode would prevent the combination of
-             * images using different domains (i.e. raise an error if domains are not the same).
+             * Ensure that all images use the same data type. This is mandatory.
+             * If in addition all images use the same pixel and scanline stride,
+             * we may be able to share their buffers instead of copying values.
              */
-            final int dataType = source.getSampleModel().getDataType();
+            final SampleModel sm = source.getSampleModel();
+            if (allowSharing && (allowSharing = (sm instanceof ComponentSampleModel))) {
+                final ComponentSampleModel csm = (ComponentSampleModel) sm;
+                if (allowSharing = (csm.getPixelStride() == 1)) {
+                    allowSharing &= scanlineStride == (scanlineStride = csm.getScanlineStride());
+                    allowSharing &= tileWidth      == (tileWidth      = source.getTileWidth());
+                    allowSharing &= tileHeight     == (tileHeight     = source.getTileHeight());
+                    allowSharing &= tileAlignX     == (tileAlignX     = Math.floorMod(source.getTileGridXOffset(), tileWidth));
+                    allowSharing &= tileAlignY     == (tileAlignY     = Math.floorMod(source.getTileGridYOffset(), tileHeight));
+                    allowSharing |= (domain == null);
+                }
+            }
+            final int dataType = sm.getDataType();
             if (domain == null) {
                 domain = ImageUtilities.getBounds(source);
                 commonDataType = dataType;
             } else {
+                if (dataType != commonDataType) {
+                    throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedDataType));
+                }
+                /*
+                 * Get the domain of the combined image to create.
+                 * TODO: current implementation computes the intersection of all sources.
+                 * But a future version should allow users to specify if they want intersection,
+                 * union or strict mode instead. A "strict" mode would prevent the combination of
+                 * images using different domains (i.e. raise an error if domains are not the same).
+                 */
                 ImageUtilities.clipBounds(source, domain);
                 if (domain.isEmpty()) {
                     throw new DisjointExtentException(Resources.format(Resources.Keys.SourceImagesDoNotIntersect));
                 }
-                if (dataType != commonDataType) {
-                    throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedDataType));
-                }
             }
         }
         if (domain == null) {
@@ -153,7 +185,7 @@ final class CombinedImageLayout extends ImageLayout {
         /*
          * Tile size is chosen after the domain has been computed, because we prefer a tile size which
          * is a divisor of the combined image size. Tile sizes of existing source images are preferred,
-         * especially when the tiles are aligned, for increasing the chances that computation a tile of
+         * especially when the tiles are aligned, for increasing the chances that computing a tile of
          * the combined image causes the computation of a single tile of each source image.
          */
         long cx, cy;        // A combination of tile size with alignment on the tile matrix grid.
@@ -168,10 +200,11 @@ final class CombinedImageLayout extends ImageLayout {
         }
         final var preferredTileSize = new Dimension((int) cx, (int) cy);
         final boolean exactTileSize = ((cx | cy) >>> Integer.SIZE) == 0;
+        allowSharing &= exactTileSize;
         return new CombinedImageLayout(sources, bandsPerSource, domain, preferredTileSize, exactTileSize,
                 chooseMinTile(tileGridXOffset, domain.x, preferredTileSize.width),
                 chooseMinTile(tileGridYOffset, domain.y, preferredTileSize.height),
-                commonDataType, aggregate.numBands());
+                commonDataType, aggregate.numBands(), allowSharing ? scanlineStride : 0);
     }
 
     /**
@@ -182,11 +215,13 @@ final class CombinedImageLayout extends ImageLayout {
      * @param  domain             bounds of the image to create.
      * @param  preferredTileSize  the preferred tile size.
      * @param  commonDataType     data type of the combined image.
+     * @param  scanlineStride     common scanline stride if data buffers will be shared, or 0 if no sharing.
      * @param  numBands           number of bands of the image to create.
      */
     private CombinedImageLayout(final RenderedImage[] sources, final int[][] bandsPerSource,
             final Rectangle domain, final Dimension preferredTileSize, final boolean exactTileSize,
-            final int minTileX, final int minTileY, final int commonDataType, final int numBands)
+            final int minTileX, final int minTileY, final int commonDataType, final int numBands,
+            final int scanlineStride)
     {
         super(preferredTileSize, false);
         this.exactTileSize  = exactTileSize;
@@ -195,7 +230,8 @@ final class CombinedImageLayout extends ImageLayout {
         this.domain         = domain;
         this.minTileX       = minTileX;
         this.minTileY       = minTileY;
-        this.sampleModel    = createBandedSampleModel(commonDataType, numBands, null, domain);
+        this.allowSharing   = (scanlineStride > 0);
+        this.sampleModel    = createBandedSampleModel(commonDataType, numBands, null, domain, scanlineStride);
         // Sample model must be last (all other fields must be initialized before).
     }
 
@@ -218,7 +254,7 @@ final class CombinedImageLayout extends ImageLayout {
      */
     private static long chooseTileSize(final long current, final int tileSize, final int imageSize, final int offset) {
         if ((imageSize % tileSize) == 0) {
-            long c = Math.abs(offset % tileSize);       // How close the grid are aligned (ideal would be zero).
+            long c = Math.floorMod(offset, tileSize);   // How close the grid are aligned (ideal would be zero).
             c <<= Integer.SIZE;                         // Pack grid offset in higher bits.
             c |= tileSize;                              // Pack tile size in lower bits.
             /*
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 859cb95883..a9033a25de 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -260,7 +260,7 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     }
 
     /**
-     * Ensures that a user-supplied color model is compatible.
+     * Ensures that a user-supplied color model is compatible with the sample model.
      * This is a helper method for argument validation in sub-classes constructors.
      *
      * @param  colors  the color model to validate. Can be {@code null}.
@@ -586,10 +586,21 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
      * @return initially empty tile for the given indices (cannot be null).
      */
     protected WritableRaster createTile(final int tileX, final int tileY) {
+        return WritableRaster.createWritableRaster(getSampleModel(), computeTileLocation(tileX, tileY));
+    }
+
+    /**
+     * Returns the location of the tile to create for the given tile indices.
+     *
+     * @param  tileX  the column index of the tile to create.
+     * @param  tileY  the row index of the tile to create.
+     * @return location of the tile to create.
+     */
+    final Point computeTileLocation(final int tileX, final int tileY) {
         // A temporary `int` overflow may occur before the final addition.
         final int x = Math.toIntExact((((long) tileX) - getMinTileX()) * getTileWidth()  + getMinX());
         final int y = Math.toIntExact((((long) tileY) - getMinTileY()) * getTileHeight() + getMinY());
-        return WritableRaster.createWritableRaster(getSampleModel(), new Point(x,y));
+        return new Point(x,y);
     }
 
     /**
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 1e71d8569d..656b267a52 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
@@ -987,7 +987,7 @@ public class ImageProcessor implements Cloneable {
         synchronized (this) {
             colorizer = this.colorizer;
         }
-        return BandAggregateImage.create(sources, bandsPerSource, colorizer);
+        return unique(BandAggregateImage.create(sources, bandsPerSource, colorizer, true));
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index de86487842..0141edf318 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -541,8 +541,8 @@ public abstract class PlanarImage implements RenderedImage {
      */
     static String verifyCompatibility(final SampleModel sm, final ColorModel cm) {
         if (cm == null || cm.isCompatibleSampleModel(sm))  return null;
-        if (cm.getNumComponents() != sm.getNumBands())     return "numComponents";
         if (cm.getTransferType()  != sm.getTransferType()) return "transferType";
+        if (cm.getNumComponents() != sm.getNumBands())     return "numComponents";
         return "";
     }
 
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 a741724ca9..ee080d49dd 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
@@ -282,7 +282,7 @@ final class Visualization extends ResampledImage {
              * The sample model is a mandatory argument before we invoke user-supplied colorizer,
              * which must be done before to build the color model.
              */
-            sampleModel = layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, source, bounds);
+            sampleModel = layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, source, bounds, 0);
             final Target target = new Target(sampleModel, VISIBLE_BAND, visibleSD != null);
             if (colorizer != null) {
                 colorModel = colorizer.apply(target).orElse(null);
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 a985bc2251..3783305f0a 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
@@ -27,6 +27,7 @@ import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.image.ComputedImage;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.util.Strings;
@@ -324,17 +325,26 @@ public class ImageLayout {
      * This method uses the {@linkplain #suggestTileSize(RenderedImage, Rectangle, boolean)
      * suggested tile size} for the given image and bounds.
      *
+     * <p>This method constructs the simplest possible banded sample model:
+     * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zero and
+     * all {@linkplain BandedSampleModel#getBankIndices() bank indices} are identity mapping.</p>
+     *
      * @param  dataType  desired data type as a {@link java.awt.image.DataBuffer} constant.
      * @param  numBands  desired number of bands.
      * @param  image     the image which will be the source of the image for which a sample model is created.
      * @param  bounds    the bounds of the image to create, or {@code null} if same as {@code image}.
+     * @param  scanlineStride  the line stride of the of the image data, or ≤ 0 for automatic.
      * @return a banded sample model of the given type with the given number of bands.
      */
     public BandedSampleModel createBandedSampleModel(final int dataType, final int numBands,
-            final RenderedImage image, final Rectangle bounds)
+            final RenderedImage image, final Rectangle bounds, int scanlineStride)
     {
         final Dimension tileSize = suggestTileSize(image, bounds, isBoundsAdjustmentAllowed);
-        return RasterFactory.unique(new BandedSampleModel(dataType, tileSize.width, tileSize.height, numBands));
+        if (scanlineStride <= 0) {
+            scanlineStride = tileSize.width;
+        }
+        return RasterFactory.unique(new BandedSampleModel(dataType, tileSize.width, tileSize.height,
+                                    scanlineStride, ArraysExt.range(0, numBands), new int[numBands]));
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index 083501ea6e..c8a31f3b97 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -53,7 +53,7 @@ import org.apache.sis.util.collection.WeakHashSet;
  * creating {@link BufferedImage} since that kind of images wraps a single raster.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   1.0
  */
 public final class RasterFactory extends Static {
@@ -223,6 +223,34 @@ public final class RasterFactory extends Static {
         }
     }
 
+    /**
+     * Creates a NIO buffer wrapping an existing Java2D buffer.
+     * The buffer will be a view over the valid portion of the data array.
+     * The buffer position is zero. The capacity and limit are the number of valid elements.
+     *
+     * @param  data  the Java2D buffer to wrap.
+     * @param  bank  bank index of the data array to wrap.
+     * @return buffer wrapping the data array of the specified bank.
+     */
+    public static Buffer createBuffer(final DataBuffer data, final int bank) {
+        Buffer buffer;
+        switch (data.getDataType()) {
+            case DataBuffer.TYPE_BYTE:   buffer = ByteBuffer  .wrap(((DataBufferByte)   data).getData(bank)); break;
+            case DataBuffer.TYPE_USHORT: buffer = ShortBuffer .wrap(((DataBufferUShort) data).getData(bank)); break;
+            case DataBuffer.TYPE_SHORT:  buffer = ShortBuffer .wrap(((DataBufferShort)  data).getData(bank)); break;
+            case DataBuffer.TYPE_INT:    buffer = IntBuffer   .wrap(((DataBufferInt)    data).getData(bank)); break;
+            case DataBuffer.TYPE_FLOAT:  buffer = FloatBuffer .wrap(((DataBufferFloat)  data).getData(bank)); break;
+            case DataBuffer.TYPE_DOUBLE: buffer = DoubleBuffer.wrap(((DataBufferDouble) data).getData(bank)); break;
+            default: throw new AssertionError();
+        }
+        final int lower = data.getOffsets()[bank];
+        final int upper = lower + data.getSize();
+        if (lower != 0 || upper != buffer.capacity()) {
+            buffer.position(lower).limit(upper).slice();        // TODO: use slice(lower, length) with JDK13.
+        }
+        return buffer;
+    }
+
     /**
      * Wraps the backing arrays of given NIO buffers into Java2D buffers.
      * This method wraps the underlying array of primitive types; data are not copied.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
index 2de3cd9135..0cb29939fa 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@ -16,12 +16,16 @@
  */
 package org.apache.sis.image;
 
+import java.util.HashSet;
+import java.util.stream.IntStream;
+import java.util.function.Consumer;
 import java.awt.Rectangle;
+import java.awt.image.BandedSampleModel;
 import java.awt.image.BufferedImage;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
-import java.util.stream.IntStream;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.test.DependsOnMethod;
@@ -40,22 +44,34 @@ import static org.junit.Assert.*;
  */
 public final class BandAggregateImageTest extends TestCase {
     /**
-     * The image processor to use for testing band aggregations.
+     * Whether to allow the sharing of data arrays.
+     * If {@code false}, tests will force copies.
      */
-    private final ImageProcessor processor;
+    private boolean allowSharing;
 
     /**
      * Creates a new test case.
      */
     public BandAggregateImageTest() {
-        processor = new ImageProcessor();
+        allowSharing = true;            // This is the default mode of `ImageProcessor`.
     }
 
     /**
-     * Tests the aggregation of two untiled images having the same bounds and only one band.
+     * Tests the aggregation of two untiled images with forced copy of sample values.
      * This is the simplest case in this test class.
      */
     @Test
+    public void copyUntiledImages() {
+        allowSharing = false;
+        aggregateUntiledImages();
+    }
+
+    /**
+     * Tests the aggregation of two untiled images having the same bounds and only one band.
+     * Sample values should not be copied unless forced to.
+     */
+    @Test
+    @DependsOnMethod("copyUntiledImages")
     public void aggregateUntiledImages() {
         final int width  = 3;
         final int height = 4;
@@ -64,7 +80,7 @@ public final class BandAggregateImageTest extends TestCase {
         im1.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s + 1).toArray());
         im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray());
 
-        final RenderedImage result = processor.aggregateBands(im1, im2);
+        final RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(0, result.getMinTileX());
         assertEquals(0, result.getMinTileY());
@@ -83,19 +99,23 @@ public final class BandAggregateImageTest extends TestCase {
             },
             tile.getPixels(0, 0, width, height, (int[]) null)
         );
+        verifySharing(result, allowSharing);
     }
 
     /**
      * Tests the aggregation of two tiled images having the same tile matrix.
      * The same test is executed many times with different but equivalent classes of sample models.
+     * Bands may be copied or references, depending on the sample models.
      */
     @Test
     @DependsOnMethod("aggregateUntiledImages")
     public void aggregateSimilarlyTiledImages() {
-        aggregateSimilarlyTiledImages(true,  true);
-        aggregateSimilarlyTiledImages(false, false);
-        aggregateSimilarlyTiledImages(true,  false);
-        aggregateSimilarlyTiledImages(false, true);
+        do {
+            aggregateSimilarlyTiledImages(true,  true);
+            aggregateSimilarlyTiledImages(false, false);
+            aggregateSimilarlyTiledImages(true,  false);
+            aggregateSimilarlyTiledImages(false, true);
+        } while ((allowSharing = !allowSharing) == false);      // Loop executed exactly twice.
     }
 
     /**
@@ -114,7 +134,7 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded);
         initializeAllTiles(im1, im2);
 
-        RenderedImage result = processor.aggregateBands(im1, im2);
+        RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -148,15 +168,16 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(minX, minY, width, height, (int[]) null)
         );
+        verifySharing(result, allowSharing && allowSharing(im1, im2));
         /*
          * Repeat the test with a custom band selection.
          * One of the source images is used twice, but with a different selection of bands.
          */
-        result = processor.aggregateBands(new RenderedImage[] {im1, im2, im1}, new int[][] {
+        result = BandAggregateImage.create(new RenderedImage[] {im1, im2, im1}, new int[][] {
             new int[] {1},      // Take second band of image 1.
             null,               // Take all bands of image 2.
             new int[] {0}       // Take first band of image 1.
-        });
+        }, null, allowSharing);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -189,10 +210,15 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(minX, minY, width, height, (int[]) null)
         );
+        /*
+         * Do not invoke `verifySharing(result)` because this test
+         * references the same `DataBuffer` more than once.
+         */
     }
 
     /**
      * Tests the aggregation of three tiled images having different tile matrices.
+     * A copy of sample values can not be avoided in this case.
      */
     @Test
     @DependsOnMethod("aggregateSimilarlyTiledImages")
@@ -211,7 +237,9 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock oneTile  = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true);
         initializeAllTiles(tiled2x2, tiled4x1, oneTile);
 
-        final RenderedImage result = processor.aggregateBands(tiled2x2, tiled4x1, oneTile);
+        final RenderedImage result = BandAggregateImage.create(
+                new RenderedImage[] {tiled2x2, tiled4x1, oneTile}, null, null, allowSharing);
+
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -236,10 +264,12 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(minX, minY, width, height, (int[]) null)
         );
+        verifySharing(result, false);
     }
 
     /**
      * Tests the aggregation of three tiled images having different extents and different tile matrices.
+     * A copy of sample values can not be avoided in this case.
      */
     @Test
     @DependsOnMethod("aggregateImagesUsingSameExtentButDifferentTileSizes")
@@ -258,7 +288,9 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12,  6,  6,  6, 0, 0, true);
         initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6);
 
-        final RenderedImage result = processor.aggregateBands(untiled, tiled2x2, tiled4x4, tiled6x6);
+        final RenderedImage result = BandAggregateImage.create(
+                new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6}, null, null, allowSharing);
+
         assertNotNull(result);
         assertEquals(4, result.getMinX());
         assertEquals(2, result.getMinY());
@@ -284,6 +316,7 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(4, 2, 8, 4, (int[]) null)
         );
+        verifySharing(result, false);
     }
 
     /**
@@ -305,4 +338,55 @@ public final class BandAggregateImageTest extends TestCase {
             band += numBands;
         }
     }
+
+    /**
+     * Returns {@code true} if the sample model used by the given sources makes possible to share
+     * the internal data arrays. This method should be invoked for {@link TiledImageMock} having
+     * more than 1 band, because their sample model is selected randomly.
+     */
+    private static boolean allowSharing(final RenderedImage... sources) {
+        for (final RenderedImage source : sources) {
+            if (!(source.getSampleModel() instanceof BandedSampleModel)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Verifies if the given image reuses the data arrays of all its source.
+     *
+     * @param  result   the result of band aggregation.
+     * @param  sharing  whether the caller expects the result to share data arrays.
+     */
+    private static void verifySharing(final RenderedImage result, final boolean sharing) {
+        final var arrays = new HashSet<Object>();
+        for (final RenderedImage source : result.getSources()) {
+            forAllDataArrays(source, (data) -> assertTrue("Found two references to the same array.", arrays.add(data)));
+        }
+        final String message = sharing
+                ? "Expected the target image to reference an existing array."
+                : "Expected only copies, no references to existing arrays.";
+        forAllDataArrays(result, (data) -> assertEquals(message, sharing, arrays.remove(data)));
+        assertEquals("Expected sharing of either all arrays or none of them.", sharing, arrays.isEmpty());
+    }
+
+    /**
+     * Performs the given action on data arrays for all bands of all tiles of the given image.
+     *
+     * @param  source  the image for which to get data arrays.
+     * @param  action  the action to execute for each data arrays.
+     */
+    private static void forAllDataArrays(final RenderedImage source, final Consumer<Object> action) {
+        for (int x = source.getNumXTiles(); --x >= 0;) {
+            final int tileX = source.getMinTileX() + x;
+            for (int y = source.getNumYTiles(); --y >= 0;) {
+                final int tileY = source.getMinTileY() + y;
+                final DataBuffer buffer = source.getTile(tileX, tileY).getDataBuffer();
+                for (int b = buffer.getNumBanks(); --b >= 0;) {
+                    action.accept(RasterFactory.createBuffer(buffer, b).array());
+                }
+            }
+        }
+    }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
index cf5c1a23bb..92b865cf01 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
@@ -17,7 +17,10 @@
 package org.apache.sis.image;
 
 import java.util.Map;
+import java.util.stream.IntStream;
 import java.awt.Shape;
+import java.awt.Rectangle;
+import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import org.apache.sis.internal.processing.isoline.IsolinesTest;
@@ -51,6 +54,34 @@ public final class ImageProcessorTest extends TestCase {
         processor = new ImageProcessor();
     }
 
+    /**
+     * Tests {@link ImageProcessor#aggregateBands(RenderedImage...)}.
+     *
+     * @see BandAggregateImageTest
+     */
+    @Test
+    public void testBandAggregate() {
+        final int width  = 3;
+        final int height = 4;
+        final BufferedImage im1 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+        final BufferedImage im2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+        im1.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s +  10).toArray());
+        im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s + 100).toArray());
+
+        final Raster data = processor.aggregateBands(im1, im2).getData();
+        assertEquals(new Rectangle(0, 0, width, height), data.getBounds());
+        assertEquals(2, data.getNumBands());
+        assertArrayEquals(
+            new int[] {
+                10, 100,  11, 101,  12, 102,
+                13, 103,  14, 104,  15, 105,
+                16, 106,  17, 107,  18, 108,
+                19, 109,  20, 110,  21, 111
+            },
+            data.getPixels(0, 0, width, height, (int[]) null)
+        );
+    }
+
     /**
      * Tests {@link ImageProcessor#addUserProperties(RenderedImage, Map)}.
      */