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/05 18:25:29 UTC

[sis] branch geoapi-4.0 updated: Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images. Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support. `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays.

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 297e7a67fe Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images. Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support. `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays.
297e7a67fe is described below

commit 297e7a67feb23ed45d356270e455139ff15d9f17
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Apr 5 09:53:38 2023 +0200

    Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images.
    Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support.
    `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays.
---
 .../org/apache/sis/image/BandAggregateImage.java   | 354 ++++++-------------
 .../org/apache/sis/image/BandSharedRaster.java     | 181 ++++++++++
 .../java/org/apache/sis/image/BandSharing.java     | 382 +++++++++++++++++++++
 .../apache/sis/image/BandedSampleConverter.java    |  83 +----
 .../org/apache/sis/image/CombinedImageLayout.java  |  49 +--
 .../java/org/apache/sis/image/ComputedImage.java   |  35 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   5 +
 .../apache/sis/image/WritableComputedImage.java    | 177 ++++++++++
 .../apache/sis/image/BandAggregateImageTest.java   | 222 ++++++++----
 9 files changed, 1048 insertions(+), 440 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 e1e437b86f..46c0a540cf 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,18 +20,11 @@ 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.BandedSampleModel;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
@@ -50,12 +43,12 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities;
  *
  * @since 1.4
  */
-final class BandAggregateImage extends ComputedImage {
+class BandAggregateImage extends WritableComputedImage {
     /**
      * The source images with only the bands to aggregate, in order.
      * Those images are views; the band sample values are not copied.
      */
-    private final RenderedImage[] filteredSources;
+    protected final RenderedImage[] filteredSources;
 
     /**
      * Color model of the aggregated image.
@@ -79,8 +72,11 @@ 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.
+     * Whether the sharing of data arrays is allowed.
+     * When a source tile has the same bounds and scanline stride than the target tile,
+     * it is possible to share references to data arrays without copying the pixels.
+     * This sharing is decided automatically on a source-by-source basis.
+     * This flag allows to disable completely the sharing for all sources.
      */
     private final boolean allowSharing;
 
@@ -99,7 +95,12 @@ final class BandAggregateImage extends ComputedImage {
                                 final Colorizer colorizer, final boolean allowSharing)
     {
         final var layout = CombinedImageLayout.create(sources, bandsPerSource, allowSharing);
-        final var image  = new BandAggregateImage(layout, colorizer);
+        final BandAggregateImage image;
+        if (layout.isWritable()) {
+            image = new Writable(layout, colorizer, allowSharing);
+        } else {
+            image = new BandAggregateImage(layout, colorizer, allowSharing);
+        }
         if (image.filteredSources.length == 1) {
             final RenderedImage c = image.filteredSources[0];
             if (image.colorModel == null) {
@@ -119,8 +120,9 @@ final class BandAggregateImage extends ComputedImage {
      * @param  layout     pixel and tile coordinate spaces of this image, together with sample model.
      * @param  colorizer  provider of color model to use for this image, or {@code null} for automatic.
      */
-    private BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer) {
+    BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) {
         super(layout.sampleModel, layout.sources);
+        this.allowSharing = allowSharing;
         final Rectangle r = layout.domain;
         minX            = r.x;
         minY            = r.y;
@@ -128,8 +130,7 @@ final class BandAggregateImage extends ComputedImage {
         height          = r.height;
         minTileX        = layout.minTileX;
         minTileY        = layout.minTileY;
-        allowSharing    = layout.allowSharing;
-        filteredSources = layout.getFilteredSources();
+        filteredSources = layout.filteredSources;
         colorModel      = layout.createColorModel(colorizer);
         ensureCompatible(colorModel);
     }
@@ -152,19 +153,21 @@ final class BandAggregateImage extends ComputedImage {
      */
     @Override
     protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) {
+        if (tile instanceof BandSharedRaster) {
+            tile = null;        // Do not take the risk of writing in source images.
+        }
         /*
          * If we are allowed to share the data arrays, try that first.
+         * The cast to `BandedSampleModel` is safe because this is the
+         * type given by `CombinedImageLayout` in the constructor.
          */
+        BandSharedRaster shared = null;
         if (allowSharing) {
-            final Sharing sharing = Sharing.create(sampleModel.getDataType(), sampleModel.getNumBands());
+            final BandSharing sharing = BandSharing.create((BandedSampleModel) sampleModel);
             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));
-                }
+                final long x = Math.multiplyFull(tileX - minTileX, getTileWidth())  + minX;
+                final long y = Math.multiplyFull(tileY - minTileY, getTileHeight()) + minY;
+                tile = shared = sharing.createRaster(x, y, filteredSources);
             }
         }
         /*
@@ -175,263 +178,116 @@ final class BandAggregateImage extends ComputedImage {
             tile = createTile(tileX, tileY);
         }
         int band = 0;
-        for (final RenderedImage source : filteredSources) {
-            final Rectangle aoi = tile.getBounds();
-            ImageUtilities.clipBounds(source, aoi);
+        for (int i=0; i < filteredSources.length; i++) {
+            final RenderedImage source = filteredSources[i];
             final int numBands = ImageUtilities.getNumBands(source);
-            final int[] bands = ArraysExt.range(band, band + numBands);
-            var target = tile.createWritableChild(aoi.x, aoi.y, aoi.width, aoi.height,
-                                                  aoi.x, aoi.y, bands);
+            if (shared == null || shared.needCopy(i)) {
+                final Rectangle aoi = tile.getBounds();
+                ImageUtilities.clipBounds(source, aoi);
+                if (!aoi.isEmpty()) {
+                    final int[] bands = ArraysExt.range(band, band + numBands);
+                    var target = tile.createWritableChild(aoi.x, aoi.y, aoi.width, aoi.height,
+                                                          aoi.x, aoi.y, bands);
+                    copyData(aoi, source, target);
+                }
+            }
             band += numBands;
-            copyData(aoi, source, target);
         }
         return tile;
     }
 
     /**
-     * A builder of data buffers sharing arrays of source images.
-     * There is a subclass for each supported data type.
+     * A {@code BandAggregateImage} where all sources are writable rendered images.
      */
-    private abstract static class Sharing {
+    private static final class Writable extends BandAggregateImage implements WritableRenderedImage {
         /**
-         * 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}.
+         * Creates a new writable rendered image.
+         *
+         * @param  layout     pixel and tile coordinate spaces of this image, together with sample model.
+         * @param  colorizer  provider of color model to use for this image, or {@code null} for automatic.
          */
-        protected final int[] offsets;
+        Writable(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) {
+            super(layout, colorizer, allowSharing);
+        }
 
         /**
-         * For subclass constructors.
+         * Checks out a tile for writing.
          */
-        protected Sharing(final int numBands) {
-            offsets = new int[numBands];
+        @Override
+        public WritableRaster getWritableTile(final int tileX, final int tileY) {
+            final WritableRaster tile = (WritableRaster) getTile(tileX, tileY);
+            if (tile instanceof BandSharedRaster) {
+                ((BandSharedRaster) tile).acquireWritableTiles(filteredSources);
+            }
+            try {
+                markTileWritable(tileX, tileY, true);
+            } catch (RuntimeException e) {
+                if (tile instanceof BandSharedRaster) {
+                    ((BandSharedRaster) tile).releaseWritableTiles(e);
+                }
+                throw e;
+            }
+            return tile;
         }
 
         /**
-         * 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.
+         * Relinquishes the right to write to a tile.
          */
-        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);
+        @Override
+        public void releaseWritableTile(final int tileX, final int tileY) {
+            if (markTileWritable(tileX, tileY, false)) {
+                final Raster tile = getTile(tileX, tileY);
+                if (tile instanceof BandSharedRaster) {
+                    ((BandSharedRaster) tile).releaseWritableTiles(null);
+                }
+                setData(tile);
             }
-            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.
+         * Sets a region of the image to the contents of the given raster.
+         * The raster is assumed to be in the same coordinate space as this image.
+         * The operation is clipped to the bounds of this image.
          *
-         * @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.
+         * @param  tile  the values to write in this image.
          */
-        final DataBuffer createDataBuffer(final long x, final long y, final RenderedImage[] sources) {
+        @Override
+        public void setData(final Raster tile) {
+            final BandSharedRaster shared = (tile instanceof BandSharedRaster) ? (BandSharedRaster) tile : null;
             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.
+            for (int i=0; i < filteredSources.length; i++) {
+                final var target = (WritableRenderedImage) filteredSources[i];
+                final int numBands = ImageUtilities.getNumBands(target);
+                if (shared == null || shared.needCopy(i)) {
+                    final Rectangle aoi = tile.getBounds();
+                    ImageUtilities.clipBounds(target, aoi);
+                    if (!aoi.isEmpty()) {
+                        final int[] bands = ArraysExt.range(band, band + numBands);
+                        var source = tile.createChild(aoi.x, aoi.y, aoi.width, aoi.height,
+                                                      aoi.x, aoi.y, bands);
+                        target.setData(source);
+                    }
                 }
-                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());
+                band += numBands;
             }
-            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.
+         * Restores the identity behavior for writable image,
+         * because it may have listeners attached to this specific instance.
          */
-        abstract void takeReference(DataBuffer source, int src, int dst);
+        @Override
+        public int hashCode() {
+            return System.identityHashCode(this);
+        }
 
         /**
-         * 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.
+         * Restores the identity behavior for writable image,
+         * because it may have listeners attached to this specific instance.
          */
-        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);
+        @Override
+        public boolean equals(final Object object) {
+            return object == this;
         }
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java
new file mode 100644
index 0000000000..cb477b2084
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.awt.Point;
+import java.awt.image.Raster;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+import java.awt.image.RasterFormatException;
+
+
+/**
+ * A raster where some or all bands are shared with other rasters.
+ * This implementation is restricted to {@link BandedSampleModel}.
+ *
+ * <h2>Performance note</h2>
+ * The standard Java library has many specialized implementations for different sample models.
+ * By using our own implementation, we block ourselves from using those specialized subclasses.
+ * However as of OpenJDK 19, all those specialized subclasses are for sample models other than
+ * {@link BandedSampleModel}. Consequently we do not expect a big difference in this case.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+final class BandSharedRaster extends WritableRaster {
+    /**
+     * The sources of this raster for which bands are shared.
+     * A null element means that the pixels of the corresponding source needs to be copied.
+     * The non-null elements are not directly used but kept for avoiding garbage collection.
+     * Because this {@code BandSharedRaster} keep references to {@code sources} data arrays,
+     * garbage collection of source {@link Raster} instances will not free a lot of memory.
+     * Quite the opposite, it would consume more memory if a source raster needs to be recomputed.
+     */
+    private final Raster[] parents;
+
+    /**
+     * Sources for which a writable raster has been acquired.
+     * The length is always the total number of sources, but elements in this array may be null.
+     * Non-null elements exist only if this tile has been acquired for write operations by a call
+     * to {@link WritableRenderedImage#getWritableTile(int, int)}.
+     */
+    private final WritableRenderedImage[] writableSources;
+
+    /**
+     * Indices of tiles in source images.
+     * Values at even indices are <var>x</var> tile coordinates and
+     * values at odd  indices are <var>y</var> tile coordinates.
+     * Values may be invalid when the corresponding {@code parents} element is null.
+     */
+    private final int[] sourceTileIndices;
+
+    /**
+     * Creates a new raster.
+     *
+     * @param srcCount  total number of source images.
+     * @param parents   the sources of this raster for which bands are shared.
+     * @param model     the sample model that specifies the layout.
+     * @param buffer    the buffer that contains the image data.
+     * @param location  the coordinate of upper-left corner.
+     */
+    BandSharedRaster(final int[] sourceTileIndices, final Raster[] parents,
+            final SampleModel model, final DataBuffer buffer, final Point location)
+    {
+        super(model, buffer, location);
+        writableSources = new WritableRenderedImage[sourceTileIndices.length >>> 1];
+        this.sourceTileIndices = sourceTileIndices;
+        this.parents = parents;
+        int numBands = 0;
+        for (final Raster source : parents) {
+            if (source != null) {
+                final int n = source.getNumBands();
+                if (n > numBands) {
+                    numBands = n;
+                    parent = source;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if pixel values for the given source index needs to be copied.
+     * It may happen because {@code BandSharedRaster} does not necessarily share the data arrays
+     * of all sources. We may have a mix of shared sources and sources that need to be copied.
+     *
+     * @param  i  index of a source image.
+     * @return whether pixel values for the specified source needs to be copied.
+     */
+    final boolean needCopy(final int i) {
+        return parents[i] == null;
+    }
+
+    /**
+     * Notifies all shared sources that the tile is about to be written.
+     *
+     * @param  sources  all sources of the band aggregate image.
+     */
+    final synchronized void acquireWritableTiles(final RenderedImage[] sources) {
+        final var pending = new WritableRenderedImage[sources.length];
+        try {
+            for (int i=0; i < sources.length; i++) {
+                final Raster parent = parents[i];
+                if (parent != null && writableSources[i] == null) {
+                    final int n = i << 1;
+                    final WritableRenderedImage target = (WritableRenderedImage) sources[i];
+                    final WritableRaster tile = target.getWritableTile(sourceTileIndices[n], sourceTileIndices[n+1]);
+                    pending[i] = target;
+                    if (parent != tile) {       // Quick test for the most common case.
+                        if (parent.getDataBuffer() != tile.getDataBuffer() ||
+                           !parent.getSampleModel().equals(tile.getSampleModel()))
+                        {
+                            throw new RasterFormatException("DataBuffer replacement not yet supported.");
+                        }
+                    }
+                }
+            }
+        } catch (RuntimeException error) {
+            releaseWritableTiles(pending, error);       // Rollback the tile acquisitions.
+        }
+        /*
+         * Save the writable status only after we know that the operation is successful.
+         * We want a "all or nothing" behavior: after we acquired all tiles and the method
+         * returns successfully, or we acquired none of them and the method throws an exception.
+         */
+        for (int i=0; i < pending.length; i++) {
+            final WritableRenderedImage target = pending[i];
+            if (target != null) writableSources[i] = target;
+        }
+    }
+
+    /**
+     * Release all tiles which were acquired for write operations.
+     *
+     * @param  error  the exception to throw after this method completed, or {@code null} if none.
+     */
+    final synchronized void releaseWritableTiles(RuntimeException error) {
+        releaseWritableTiles(writableSources, error);
+    }
+
+    /**
+     * Release all non-null tiles in the specified array.
+     * Released tiles are set to null.
+     *
+     * @param  sources  the band aggregate image sources for which to release writable tiles.
+     * @param  error    the exception to throw after this method completed, or {@code null} if none.
+     */
+    private void releaseWritableTiles(final WritableRenderedImage[] sources, RuntimeException error) {
+        for (int i=0; i < sources.length; i++) {
+            final WritableRenderedImage source = sources[i];
+            if (source != null) try {
+                sources[i] = null;
+                final int n = i << 1;
+                source.releaseWritableTile(sourceTileIndices[n], sourceTileIndices[n+1]);
+            } catch (RuntimeException e) {
+                if (error == null) error = e;
+                else error.addSuppressed(e);
+            }
+        }
+        if (error != null) {
+            throw error;
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java
new file mode 100644
index 0000000000..29ffb0fba5
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java
@@ -0,0 +1,382 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.awt.Point;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+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 org.apache.sis.internal.coverage.j2d.ImageUtilities;
+
+
+/**
+ * A builder of data buffers sharing arrays of source images.
+ * There is a subclass for each supported data type.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+abstract class BandSharing {
+    /**
+     * The offsets of the first valid element into each bank array.
+     * Will be computed with the assumption that all offsets are zeros
+     * in the target {@link BandedSampleModel}.
+     */
+    protected final int[] offsets;
+
+    /**
+     * The sample model of the raster to create.
+     * All band offsets shall be zeros and bank indices shall define an identity mapping.
+     */
+    private final BandedSampleModel target;
+
+    /**
+     * The sources of the tile for which bands can be shared.
+     * The length of this array is the number of source images.
+     * Some elements may be {@code null} if we cannot share data arrays
+     * of the corresponding source and instead need to copy pixel values.
+     *
+     * @see BandSharedRaster#parents
+     */
+    private Raster[] parents;
+
+    /**
+     * Indices of tiles in source images.
+     * Values at even indices are <var>x</var> tile coordinates and
+     * values at odd  indices are <var>y</var> tile coordinates.
+     * Values may be invalid when the corresponding {@code parents} element is null.
+     */
+    private int[] sourceTileIndices;
+
+    /**
+     * For subclass constructors.
+     */
+    protected BandSharing(final BandedSampleModel target) {
+        this.target = target;
+        offsets = new int[target.getNumBands()];
+    }
+
+    /**
+     * Creates a new builder.
+     *
+     * @param  target  the sample model of the tile to create.
+     * @return the data buffer, or {@code null} if the data type is not recognized.
+     */
+    static BandSharing create(final BandedSampleModel target) {
+        switch (target.getDataType()) {
+            case DataBuffer.TYPE_BYTE:   return new Bytes   (target);
+            case DataBuffer.TYPE_SHORT:  return new Shorts  (target);
+            case DataBuffer.TYPE_USHORT: return new UShorts (target);
+            case DataBuffer.TYPE_INT:    return new Integers(target);
+            case DataBuffer.TYPE_FLOAT:  return new Floats  (target);
+            case DataBuffer.TYPE_DOUBLE: return new Doubles (target);
+        }
+        return null;
+    }
+
+    /**
+     * Prepares sharing the arrays of the given sources when possible.
+     * This method does not allocate new {@link DataBuffer} banks.
+     *
+     * @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 data buffer size, or 0 if there is nothing to share.
+     */
+    private int prepare(final long x, final long y, final RenderedImage[] sources) {
+        final int tileWidth      = target.getWidth();
+        final int tileHeight     = target.getHeight();
+        final int scanlineStride = target.getScanlineStride();
+        int size = scanlineStride * tileHeight;     // Size of the data buffer to create.
+        int band = 0;                               // Band of the target image.
+        boolean sharing = false;
+        parents = new Raster[sources.length];
+        sourceTileIndices = new int[sources.length * 2];
+        for (int si=0; si < sources.length; si++) {
+            final RenderedImage source = sources[si];
+            if (source.getTileWidth()  == tileWidth &&
+                source.getTileHeight() == tileHeight)
+            {
+                long tileX = x - source.getTileGridXOffset();
+                long tileY = y - source.getTileGridYOffset();
+                if (((tileX % tileWidth) | (tileY % tileHeight)) == 0) {
+                    tileX /= tileWidth;
+                    tileY /= tileHeight;
+                    final int tx = Math.toIntExact(tileX);
+                    final int ty = Math.toIntExact(tileY);
+                    final int n  = si << 1;
+                    sourceTileIndices[n  ] = tx;
+                    sourceTileIndices[n+1] = ty;
+                    final Raster raster = source.getTile(tx, ty);
+                    final SampleModel c = raster.getSampleModel();
+                    if (c instanceof ComponentSampleModel) {
+                        final var sm = (ComponentSampleModel) c;
+                        if (sm.getPixelStride()    == 1 &&
+                            sm.getScanlineStride() == scanlineStride)
+                        {
+                            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.max(size, buffer.getSize());
+                            parents[si] = raster;
+                            sharing = true;
+                            continue;
+                        }
+                    }
+                }
+            }
+            /*
+             * If we reach this point, it was not possible to share the data arrays of a source.
+             * We will need to copy the pixels. New arrays will be allocated for holding the copy.
+             */
+            band += ImageUtilities.getNumBands(source);
+        }
+        if (band != offsets.length) {   // No `assert` keyword because it is okay to let this check be unconditional.
+            throw new AssertionError();
+        }
+        return sharing ? size : 0;
+    }
+
+    /**
+     * Creates a raster sharing the arrays of given sources when possible.
+     * This method assumes a target {@link BandedSampleModel} where all band offsets.
+     *
+     * @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 raster containing the aggregation of all bands, or {@code null} if the is nothing to share.
+     */
+    final BandSharedRaster createRaster(final long x, final long y, final RenderedImage[] sources) {
+        final int size = prepare(x, y, sources);
+        if (size == 0) {
+            return null;
+        }
+        final DataBuffer buffer = allocate(size);
+        final var location = new Point(Math.toIntExact(x), Math.toIntExact(y));
+        return new BandSharedRaster(sourceTileIndices, parents, target, buffer, location);
+    }
+
+    /**
+     * 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);
+
+    /**
+     * Allocates banks for all bands that are not shared, then builds the data buffer.
+     * Subclasses shall specify the {@link #offsets} array to the buffer constructor.
+     *
+     * @param  size  number of elements in the data buffer.
+     * @return the new data buffer.
+     */
+    abstract DataBuffer allocate(int size);
+
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_BYTE}.
+     */
+    private static final class Bytes extends BandSharing {
+        /** The shared arrays. */
+        private final byte[][] data;
+
+        /** Creates a new builder. */
+        Bytes(final BandedSampleModel target) {
+            super(target);
+            data = new byte[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new byte[size];
+                }
+            }
+            return new DataBufferByte(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_SHORT}.
+     */
+    private static final class Shorts extends BandSharing {
+        /** The shared arrays. */
+        private final short[][] data;
+
+        /** Creates a new builder. */
+        Shorts(final BandedSampleModel target) {
+            super(target);
+            data = new short[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new short[size];
+                }
+            }
+            return new DataBufferShort(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_USHORT}.
+     */
+    private static final class UShorts extends BandSharing {
+        /** The shared arrays. */
+        private final short[][] data;
+
+        /** Creates a new builder. */
+        UShorts(final BandedSampleModel target) {
+            super(target);
+            data = new short[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new short[size];
+                }
+            }
+            return new DataBufferUShort(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_INT}.
+     */
+    private static final class Integers extends BandSharing {
+        /** The shared arrays. */
+        private final int[][] data;
+
+        /** Creates a new builder. */
+        Integers(final BandedSampleModel target) {
+            super(target);
+            data = new int[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new int[size];
+                }
+            }
+            return new DataBufferInt(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_FLOAT}.
+     */
+    private static final class Floats extends BandSharing {
+        /** The shared arrays. */
+        private final float[][] data;
+
+        /** Creates a new builder. */
+        Floats(final BandedSampleModel target) {
+            super(target);
+            data = new float[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new float[size];
+                }
+            }
+            return new DataBufferFloat(data, size, offsets);
+        }
+    }
+
+    /**
+     * A builder of data buffer of {@link DataBuffer#TYPE_DOUBLE}.
+     */
+    private static final class Doubles extends BandSharing {
+        /** The shared arrays. */
+        private final double[][] data;
+
+        /** Creates a new builder. */
+        Doubles(final BandedSampleModel target) {
+            super(target);
+            data = new double[offsets.length][];
+        }
+
+        /** 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 allocate(int size) {
+            for (int i=0; i<data.length; i++) {
+                if (data[i] == null) {
+                    data[i] = new double[size];
+                }
+            }
+            return new DataBufferDouble(data, size, offsets);
+        }
+    }
+}
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 2076fe5329..e1075cac30 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
@@ -27,7 +27,6 @@ import java.awt.image.BandedSampleModel;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
-import java.awt.image.TileObserver;
 import java.lang.reflect.Array;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
@@ -36,7 +35,6 @@ import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
-import org.apache.sis.internal.coverage.j2d.WriteSupport;
 import org.apache.sis.internal.coverage.SampleDimensions;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.Numbers;
@@ -72,7 +70,7 @@ import static org.apache.sis.internal.coverage.j2d.ImageUtilities.LOGGER;
  * @version 1.4
  * @since   1.1
  */
-class BandedSampleConverter extends ComputedImage {
+class BandedSampleConverter extends WritableComputedImage {
     /*
      * Do not extend `SourceAlignedImage` because we want to inherit the `getNumTiles()`
      * and `getTileGridOffset()` methods defined by `PlanarImage`.
@@ -429,17 +427,6 @@ class BandedSampleConverter extends ComputedImage {
          */
         private final MathTransform1D[] inverses;
 
-        /**
-         * The observers, or {@code null} if none. This is a copy-on-write array:
-         * values are never modified after construction (new arrays are created).
-         *
-         * This field is declared volatile because it is read without synchronization by
-         * {@link #markTileWritable(int, int, boolean)}. Since this is a copy-on-write array,
-         * it is okay to omit synchronization for that method but we still need the memory effect.
-         */
-        @SuppressWarnings("VolatileArrayField")
-        private volatile TileObserver[] observers;
-
         /**
          * Creates a new writable image which will compute values using the given converters.
          */
@@ -452,74 +439,6 @@ class BandedSampleConverter extends ComputedImage {
             this.inverses = inverses;
         }
 
-        /**
-         * Adds an observer to be notified when a tile is checked out for writing.
-         * If the observer is already present, it will receive multiple notifications.
-         *
-         * @param  observer  the observer to notify.
-         */
-        @Override
-        public synchronized void addTileObserver(final TileObserver observer) {
-            observers = WriteSupport.addTileObserver(observers, observer);
-        }
-
-        /**
-         * Removes an observer from the list of observers notified when a tile is checked out for writing.
-         * If the observer was not registered, nothing happens. If the observer was registered for multiple
-         * notifications, it will now be registered for one fewer.
-         *
-         * @param  observer  the observer to stop notifying.
-         */
-        @Override
-        public synchronized void removeTileObserver(final TileObserver observer) {
-            observers = WriteSupport.removeTileObserver(observers, observer);
-        }
-
-        /**
-         * Sets or clears whether a tile is checked out for writing and notifies the listener if needed.
-         *
-         * @param  tileX    the <var>x</var> index of the tile to acquire or release.
-         * @param  tileY    the <var>y</var> index of the tile to acquire or release.
-         * @param  writing  {@code true} for acquiring the tile, or {@code false} for releasing it.
-         */
-        @Override
-        protected boolean markTileWritable(final int tileX, final int tileY, final boolean writing) {
-            final boolean notify = super.markTileWritable(tileX, tileY, writing);
-            if (notify) {
-                WriteSupport.fireTileUpdate(observers, this, tileX, tileY, writing);
-            }
-            return notify;
-        }
-
-        /**
-         * Checks out a tile for writing.
-         *
-         * @param  tileX  the <var>x</var> index of the tile.
-         * @param  tileY  the <var>y</var> index of the tile.
-         * @return the specified tile as a writable tile.
-         */
-        @Override
-        public WritableRaster getWritableTile(final int tileX, final int tileY) {
-            final WritableRaster tile = (WritableRaster) getTile(tileX, tileY);
-            markTileWritable(tileX, tileY, true);
-            return tile;
-        }
-
-        /**
-         * Relinquishes the right to write to a tile. If the tile goes from having one writer to
-         * having no writers, the values are inverse converted and written in the original image.
-         * If the caller continues to write to the tile, the results are undefined.
-         *
-         * @param  tileX  the <var>x</var> index of the tile.
-         * @param  tileY  the <var>y</var> index of the tile.
-         */
-        @Override
-        public void releaseWritableTile(final int tileX, final int tileY) {
-            if (markTileWritable(tileX, tileY, false)) {
-                setData(getTile(tileX, tileY));
-            }
-        }
-
         /**
          * Sets a region of the image to the contents of the given raster.
          * The raster is assumed to be in the same coordinate space as this image.
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 0edd294a15..7f283984fe 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
@@ -26,6 +26,7 @@ import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.ComponentSampleModel;
+import java.awt.image.WritableRenderedImage;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.collection.FrequencySortedSet;
 import org.apache.sis.internal.feature.Resources;
@@ -60,6 +61,12 @@ final class CombinedImageLayout extends ImageLayout {
      */
     final RenderedImage[] sources;
 
+    /**
+     * The source images with only the user-specified bands.
+     * Those images are views, the pixels are not copied.
+     */
+    final RenderedImage[] filteredSources;
+
     /**
      * Ordered (not necessarily sorted) indices of bands to select in each source image.
      * The length of this array is always equal to the length of the {@link #sources} array.
@@ -70,7 +77,7 @@ final class CombinedImageLayout extends ImageLayout {
 
     /**
      * The sample model of the combined image.
-     * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zero and
+     * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zeros and
      * all {@linkplain BandedSampleModel#getBankIndices() bank indices} are identity mapping.
      * This simplicity is needed by current implementation of {@link BandAggregateImage}.
      */
@@ -101,12 +108,6 @@ 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.
@@ -230,9 +231,20 @@ final class CombinedImageLayout extends ImageLayout {
         this.domain         = domain;
         this.minTileX       = minTileX;
         this.minTileY       = minTileY;
-        this.allowSharing   = (scanlineStride > 0);
         this.sampleModel    = createBandedSampleModel(commonDataType, numBands, null, domain, scanlineStride);
-        // Sample model must be last (all other fields must be initialized before).
+        /*
+         * Note: above call to `createBandedSampleModel(…)` must be last,
+         * except for `filteredSources` which is not needed by that method.
+         */
+        filteredSources = new RenderedImage[sources.length];
+        for (int i=0; i<filteredSources.length; i++) {
+            RenderedImage source = sources[i];
+            final int[] bands = bandsPerSource[i];
+            if (bands != null) {
+                source = BandSelectImage.create(source, bands);
+            }
+            filteredSources[i] = source;
+        }
     }
 
     /**
@@ -327,22 +339,17 @@ final class CombinedImageLayout extends ImageLayout {
     }
 
     /**
-     * Returns the source images with only the user-specified bands.
-     * The returned images are views; the bands are not copied.
+     * Returns {@code true} if all filtered sources are writable.
      *
-     * @return the source images with only user-supplied bands.
+     * @return whether a destination using all filtered sources could be writable.
      */
-    final RenderedImage[] getFilteredSources() {
-        final RenderedImage[] images = new RenderedImage[sources.length];
-        for (int i=0; i<images.length; i++) {
-            RenderedImage source = sources[i];
-            final int[] bands = bandsPerSource[i];
-            if (bands != null) {
-                source = BandSelectImage.create(source, bands);
+    final boolean isWritable() {
+        for (final RenderedImage source : filteredSources) {
+            if (!(source instanceof WritableRenderedImage)) {
+                return false;
             }
-            images[i] = source;
         }
-        return images;
+        return true;
     }
 
     /**
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 a9033a25de..126fe58caf 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
@@ -183,14 +183,14 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
      * If this field is set to a non-null value, then this assignment should be done
      * soon after construction time before any tile computation started.
      *
-     * <div class="note"><b>Note on interaction with tile cache</b><br>
+     * <h4>Note on interaction with tile cache</h4>
      * The use of a destination image may produce unexpected result if {@link #computeTile(int, int, WritableRaster)}
      * is invoked two times or more for the same destination tile. It may look like a problem because computed tiles
      * can be discarded and recomputed at any time. However, this problem should not happen because tiles computed by
      * this {@code ComputedImage} will not be discarded as long as {@code destination} has a reference to that tile.
      * If a {@code ComputedImage} tile has been discarded, then it implies that the corresponding {@code destination}
      * tile has been discarded as well, in which case the tile computation will restart from scratch; it will not be
-     * a recomputation of only this {@code ComputedImage} on top of an old {@code destination} tile.</div>
+     * a recomputation of only this {@code ComputedImage} on top of an old {@code destination} tile.
      *
      * @see #setDestination(WritableRenderedImage)
      */
@@ -203,10 +203,12 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
      * and the {@linkplain SampleModel#getHeight() sample model height}
      * determines this {@linkplain #getTileHeight() image tile height}.
      *
-     * <div class="note"><b>Design note:</b>
+     * <h4>Design note:</h4>
      * {@code ComputedImage} requires the sample model to have exactly the desired tile size
      * otherwise tiles created by {@link #createTile(int, int)} will consume more memory
-     * than needed.</div>
+     * than needed.
+     *
+     * @see #getSampleModel()
      */
     protected final SampleModel sampleModel;
 
@@ -406,10 +408,10 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     /**
      * Returns the width of tiles in this image. The default implementation returns {@link SampleModel#getWidth()}.
      *
-     * <div class="note"><b>Note:</b>
-     * a raster can have a smaller width than its sample model, for example when a raster is a view over a subregion
+     * <h4>Note</h4>
+     * A raster can have a smaller width than its sample model, for example when a raster is a view over a subregion
      * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class,
-     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.</div>
+     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.
      *
      * @return the width of this image in pixels.
      */
@@ -421,10 +423,10 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     /**
      * Returns the height of tiles in this image. The default implementation returns {@link SampleModel#getHeight()}.
      *
-     * <div class="note"><b>Note:</b>
-     * a raster can have a smaller height than its sample model, for example when a raster is a view over a subregion
+     * <h4>Note</h4>
+     * A raster can have a smaller height than its sample model, for example when a raster is a view over a subregion
      * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class,
-     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.</div>
+     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.
      *
      * @return the height of this image in pixels.
      */
@@ -586,21 +588,10 @@ 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 new Point(x,y);
+        return WritableRaster.createWritableRaster(sampleModel, 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 656b267a52..fab9c30fe6 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
@@ -922,6 +922,11 @@ public class ImageProcessor implements Cloneable {
      * contain values from the pixels at the same coordinates in all source images.
      * The result image will be bounded by the intersection of all source images.
      *
+     * <p>If all source images are {@link WritableRenderedImage} instances,
+     * then the returned image will also be a {@link WritableRenderedImage}.
+     * In such case values written in the returned image will be copied back
+     * to the source images.</p>
+     *
      * <h4>Restrictions</h4>
      * All images shall use the same {@linkplain SampleModel#getDataType() data type},
      * and all source images shall intersect each other with a non-empty intersection area.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java
new file mode 100644
index 0000000000..adcfd1b6fc
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.awt.image.SampleModel;
+import java.awt.image.TileObserver;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+import org.apache.sis.internal.coverage.j2d.WriteSupport;
+
+
+/**
+ * Parent classes for computed images that are <em>potentially</em> writable.
+ * This class implements some {@link WritableRenderedImage} methods such as
+ * the methods for adding or removing tile listeners. However this class does
+ * <em>not</em> implement the {@link WritableRenderedImage} interface itself.
+ * It is up to subclasses to implement that interface explicitly
+ * when they have determined that the image is effectively writable.
+ *
+ * <h2>Usage pattern</h2>
+ * Create a package-private read-only image by extending this class as if
+ * {@link ComputedImage} was extended directly. Ignore all public methods
+ * defined in this class. Do not make the class public for preventing users
+ * users to access those public methods.
+ *
+ * <p>Create a package-private writable image as a subclass of above read-only image.
+ * Override {@link #setData(Raster)}, {@link #equals(Object)} and {@link #hashCode()}.
+ * The latter two methods need to be overridden for restoring the identity behavior
+ * for writable image, because it may have listeners attached to this specific instance.
+ * Example:</p>
+ *
+ * {@snippet lang="java" :
+ * class MyOperation extends WritableComputedImage {
+ *
+ *     // Constructors omitted for brevity.
+ *
+ *     static final class Writable extends MyOperation implements WritableRenderedImage {
+ *         @Override
+ *         public void setData(Raster data) {
+ *             // Write data back to original images here.
+ *         }
+ *
+ *         @Override
+ *         public boolean equals(final Object object) {
+ *             return object == this;
+ *         }
+ *
+ *         @Override
+ *         public int hashCode() {
+ *             return System.identityHashCode(this);
+ *         }
+ *     }
+ * }
+ * }
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+abstract class WritableComputedImage extends ComputedImage {
+    /**
+     * The observers, or {@code null} if none. This is a copy-on-write array:
+     * values are never modified after construction (new arrays are created).
+     *
+     * This field is declared volatile because it is read without synchronization by
+     * {@link #markTileWritable(int, int, boolean)}. Since this is a copy-on-write array,
+     * it is okay to omit synchronization for that method but we still need the memory effect.
+     */
+    @SuppressWarnings("VolatileArrayField")
+    private volatile TileObserver[] observers;
+
+    /**
+     * Creates an initially empty image with the given sample model.
+     * The source images are not necessarily {@link WritableRenderedImage}
+     * because this {@code WritableComputedImage} instance may be effectively read-only.
+     * However if this {@code WritableComputedImage} instance is effectively writable,
+     * then the given sources should be writable too.
+     *
+     * @param  sampleModel  the sample model shared by all tiles in this image.
+     * @param  sources      sources of this image (may be an empty array), or a null array if unknown.
+     */
+    protected WritableComputedImage(SampleModel sampleModel, RenderedImage... sources) {
+        super(sampleModel, sources);
+    }
+
+    /**
+     * Adds an observer to be notified when a tile is checked out for writing.
+     * If the observer is already present, it will receive multiple notifications.
+     *
+     * @param  observer  the observer to notify.
+     */
+    public synchronized void addTileObserver(final TileObserver observer) {
+        observers = WriteSupport.addTileObserver(observers, observer);
+    }
+
+    /**
+     * Removes an observer from the list of observers notified when a tile is checked out for writing.
+     * If the observer was not registered, nothing happens. If the observer was registered for multiple
+     * notifications, it will now be registered for one fewer.
+     *
+     * @param  observer  the observer to stop notifying.
+     */
+    public synchronized void removeTileObserver(final TileObserver observer) {
+        observers = WriteSupport.removeTileObserver(observers, observer);
+    }
+
+    /**
+     * Sets or clears whether a tile is checked out for writing and notifies the listener if needed.
+     *
+     * @param  tileX    the <var>x</var> index of the tile to acquire or release.
+     * @param  tileY    the <var>y</var> index of the tile to acquire or release.
+     * @param  writing  {@code true} for acquiring the tile, or {@code false} for releasing it.
+     */
+    @Override
+    protected boolean markTileWritable(final int tileX, final int tileY, final boolean writing) {
+        final boolean notify = super.markTileWritable(tileX, tileY, writing);
+        if (notify && this instanceof WritableRenderedImage) {
+            WriteSupport.fireTileUpdate(observers, (WritableRenderedImage) this, tileX, tileY, writing);
+        }
+        return notify;
+    }
+
+    /**
+     * Checks out a tile for writing.
+     *
+     * @param  tileX  the <var>x</var> index of the tile.
+     * @param  tileY  the <var>y</var> index of the tile.
+     * @return the specified tile as a writable tile.
+     */
+    public WritableRaster getWritableTile(final int tileX, final int tileY) {
+        final WritableRaster tile = (WritableRaster) getTile(tileX, tileY);
+        markTileWritable(tileX, tileY, true);
+        return tile;
+    }
+
+    /**
+     * Relinquishes the right to write to a tile.
+     * If the tile goes from having one writer to having no writers,
+     * then the values are written to the original images by a call to {@link #setData(Raster)}.
+     * If the caller continues to write to the tile, the results are undefined.
+     *
+     * @param  tileX  the <var>x</var> index of the tile.
+     * @param  tileY  the <var>y</var> index of the tile.
+     */
+    public void releaseWritableTile(final int tileX, final int tileY) {
+        if (markTileWritable(tileX, tileY, false)) {
+            setData(getTile(tileX, tileY));
+        }
+    }
+
+    /**
+     * Sets a region of the image to the contents of the given raster.
+     * The raster is assumed to be in the same coordinate space as this image.
+     * The operation is clipped to the bounds of this image.
+     *
+     * @param  data  the values to write in this image.
+     */
+    protected void setData(final Raster data) {
+        throw new UnsupportedOperationException();
+    }
+}
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 0cb29939fa..d3ec94a516 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,15 +16,19 @@
  */
 package org.apache.sis.image;
 
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.stream.IntStream;
-import java.util.function.Consumer;
+import java.util.function.ObjIntConsumer;
 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.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.RasterFactory;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.test.TestCase;
@@ -43,6 +47,16 @@ import static org.junit.Assert.*;
  * @since   1.4
  */
 public final class BandAggregateImageTest extends TestCase {
+    /**
+     * Whether to test write operations.
+     */
+    private static final boolean WRITABLE = true;
+
+    /**
+     * Source images used for building the band aggregate image.
+     */
+    private RenderedImage[] sourceImages;
+
     /**
      * Whether to allow the sharing of data arrays.
      * If {@code false}, tests will force copies.
@@ -61,9 +75,9 @@ public final class BandAggregateImageTest extends TestCase {
      * This is the simplest case in this test class.
      */
     @Test
-    public void copyUntiledImages() {
+    public void testForcedCopy() {
         allowSharing = false;
-        aggregateUntiledImages();
+        testUntiledImages();
     }
 
     /**
@@ -71,16 +85,17 @@ public final class BandAggregateImageTest extends TestCase {
      * Sample values should not be copied unless forced to.
      */
     @Test
-    @DependsOnMethod("copyUntiledImages")
-    public void aggregateUntiledImages() {
+    @DependsOnMethod("testForcedCopy")
+    public void testUntiledImages() {
         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 + 1).toArray());
         im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray());
+        sourceImages = new RenderedImage[] {im1, im2};
 
-        final RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing);
+        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(0, result.getMinTileX());
         assertEquals(0, result.getMinTileY());
@@ -99,33 +114,71 @@ public final class BandAggregateImageTest extends TestCase {
             },
             tile.getPixels(0, 0, width, height, (int[]) null)
         );
-        verifySharing(result, allowSharing);
+        verifySharing(result, allowSharing, allowSharing);
+        /*
+         * Try writing two values, then check again.
+         */
+        if (WRITABLE) {
+            final int tileX = 0;
+            final int tileY = 0;
+            final WritableRenderedImage writable = (WritableRenderedImage) result;
+            final WritableRaster target = writable.getWritableTile(tileX, tileY);
+            assertSame(tile, target);
+            target.setPixel(2, 1, new int[] {100, 80});
+            target.setPixel(1, 3, new int[] { 60, 40});
+            writable.releaseWritableTile(tileX, tileY);
+            assertSame(target, result.getTile(tileX, tileY));
+            assertArrayEquals(
+                new int[] {
+                     1,  0,     2,  2,     3,  4,
+                     4,  6,     5,  8,   100, 80,
+                     7, 12,     8, 14,     9, 16,
+                    10, 18,    60, 40,    12, 22
+                },
+                tile.getPixels(0, 0, width, height, (int[]) null)
+            );
+            assertEquals(100, im1.getRaster().getSample(2, 1, 0));
+            assertEquals( 80, im2.getRaster().getSample(2, 1, 0));
+            assertEquals( 60, im1.getRaster().getSample(1, 3, 0));
+            assertEquals( 40, im2.getRaster().getSample(1, 3, 0));
+        }
     }
 
     /**
      * 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.
+     * Bands may be copied or referenced, depending on the sample models.
      */
     @Test
-    @DependsOnMethod("aggregateUntiledImages")
-    public void aggregateSimilarlyTiledImages() {
+    @DependsOnMethod("testUntiledImages")
+    public void testSimilarlyTiledImages() {
         do {
-            aggregateSimilarlyTiledImages(true,  true);
-            aggregateSimilarlyTiledImages(false, false);
-            aggregateSimilarlyTiledImages(true,  false);
-            aggregateSimilarlyTiledImages(false, true);
+            testSimilarlyTiledImages(true,  true,  false);
+            testSimilarlyTiledImages(false, false, false);
+            testSimilarlyTiledImages(true,  false, false);
+            testSimilarlyTiledImages(false, true,  false);
         } while ((allowSharing = !allowSharing) == false);      // Loop executed exactly twice.
     }
 
+    /**
+     * Tests write operations in the aggregation of two tiled images having the same tile matrix.
+     */
+    @Test
+    @DependsOnMethod("testSimilarlyTiledImages")
+    public void testWriteOperation() {
+        testSimilarlyTiledImages(true, true, WRITABLE);
+        // Other modes are not supported by `TiledImageMock`.
+    }
+
     /**
      * Implementation of {@link #aggregateSimilarlyTiledImages()} with sample model classes
      * specified by the boolean arguments.
      *
      * @param firstBanded   whether to use {@code BandedSampleModel} for the first image.
      * @param secondBanded  whether to use {@code BandedSampleModel} for the second image.
+     * @param testWrite     whether to test write operation.
      */
-    private void aggregateSimilarlyTiledImages(final boolean firstBanded, final boolean secondBanded) {
+    private void testSimilarlyTiledImages(final boolean firstBanded, final boolean secondBanded, final boolean testWrite) {
         final int minX   =  7;
         final int minY   = -5;
         final int width  =  6;
@@ -133,8 +186,9 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 1, 2, firstBanded);
         final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded);
         initializeAllTiles(im1, im2);
+        sourceImages = new RenderedImage[] {im1, im2};
 
-        RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing);
+        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -147,33 +201,57 @@ public final class BandAggregateImageTest extends TestCase {
         assertEquals(2,      result.getNumXTiles());
         assertEquals(3,      result.getNumYTiles());
         assertEquals(4,      result.getSampleModel().getNumBands());
-
+        final int[] expected = {
+            // Tile 1                                                                    Tile 2
+            1100, 2100, 3100, 4100,  1101, 2101, 3101, 4101,  1102, 2102, 3102, 4102,    1200, 2200, 3200, 4200,  1201, 2201, 3201, 4201,  1202, 2202, 3202, 4202,
+            1110, 2110, 3110, 4110,  1111, 2111, 3111, 4111,  1112, 2112, 3112, 4112,    1210, 2210, 3210, 4210,  1211, 2211, 3211, 4211,  1212, 2212, 3212, 4212,
+            1120, 2120, 3120, 4120,  1121, 2121, 3121, 4121,  1122, 2122, 3122, 4122,    1220, 2220, 3220, 4220,  1221, 2221, 3221, 4221,  1222, 2222, 3222, 4222,
+            // Tile 3                                                                    Tile 4
+            1300, 2300, 3300, 4300,  1301, 2301, 3301, 4301,  1302, 2302, 3302, 4302,    1400, 2400, 3400, 4400,  1401, 2401, 3401, 4401,  1402, 2402, 3402, 4402,
+            1310, 2310, 3310, 4310,  1311, 2311, 3311, 4311,  1312, 2312, 3312, 4312,    1410, 2410, 3410, 4410,  1411, 2411, 3411, 4411,  1412, 2412, 3412, 4412,
+            1320, 2320, 3320, 4320,  1321, 2321, 3321, 4321,  1322, 2322, 3322, 4322,    1420, 2420, 3420, 4420,  1421, 2421, 3421, 4421,  1422, 2422, 3422, 4422,
+            // Tile 5                                                                    Tile 6
+            1500, 2500, 3500, 4500,  1501, 2501, 3501, 4501,  1502, 2502, 3502, 4502,    1600, 2600, 3600, 4600,  1601, 2601, 3601, 4601,  1602, 2602, 3602, 4602,
+            1510, 2510, 3510, 4510,  1511, 2511, 3511, 4511,  1512, 2512, 3512, 4512,    1610, 2610, 3610, 4610,  1611, 2611, 3611, 4611,  1612, 2612, 3612, 4612,
+            1520, 2520, 3520, 4520,  1521, 2521, 3521, 4521,  1522, 2522, 3522, 4522,    1620, 2620, 3620, 4620,  1621, 2621, 3621, 4621,  1622, 2622, 3622, 4622
+        };
         Raster raster = result.getData();
         assertEquals(4, raster.getNumBands());
         assertEquals(new Rectangle(minX, minY, width, height), raster.getBounds());
-        assertArrayEquals(
-            new int[] {
-                // Tile 1                                                                    Tile 2
-                1100, 2100, 3100, 4100,  1101, 2101, 3101, 4101,  1102, 2102, 3102, 4102,    1200, 2200, 3200, 4200,  1201, 2201, 3201, 4201,  1202, 2202, 3202, 4202,
-                1110, 2110, 3110, 4110,  1111, 2111, 3111, 4111,  1112, 2112, 3112, 4112,    1210, 2210, 3210, 4210,  1211, 2211, 3211, 4211,  1212, 2212, 3212, 4212,
-                1120, 2120, 3120, 4120,  1121, 2121, 3121, 4121,  1122, 2122, 3122, 4122,    1220, 2220, 3220, 4220,  1221, 2221, 3221, 4221,  1222, 2222, 3222, 4222,
-                // Tile 3                                                                    Tile 4
-                1300, 2300, 3300, 4300,  1301, 2301, 3301, 4301,  1302, 2302, 3302, 4302,    1400, 2400, 3400, 4400,  1401, 2401, 3401, 4401,  1402, 2402, 3402, 4402,
-                1310, 2310, 3310, 4310,  1311, 2311, 3311, 4311,  1312, 2312, 3312, 4312,    1410, 2410, 3410, 4410,  1411, 2411, 3411, 4411,  1412, 2412, 3412, 4412,
-                1320, 2320, 3320, 4320,  1321, 2321, 3321, 4321,  1322, 2322, 3322, 4322,    1420, 2420, 3420, 4420,  1421, 2421, 3421, 4421,  1422, 2422, 3422, 4422,
-                // Tile 5                                                                    Tile 6
-                1500, 2500, 3500, 4500,  1501, 2501, 3501, 4501,  1502, 2502, 3502, 4502,    1600, 2600, 3600, 4600,  1601, 2601, 3601, 4601,  1602, 2602, 3602, 4602,
-                1510, 2510, 3510, 4510,  1511, 2511, 3511, 4511,  1512, 2512, 3512, 4512,    1610, 2610, 3610, 4610,  1611, 2611, 3611, 4611,  1612, 2612, 3612, 4612,
-                1520, 2520, 3520, 4520,  1521, 2521, 3521, 4521,  1522, 2522, 3522, 4522,    1620, 2620, 3620, 4620,  1621, 2621, 3621, 4621,  1622, 2622, 3622, 4622
-            },
-            raster.getPixels(minX, minY, width, height, (int[]) null)
-        );
-        verifySharing(result, allowSharing && allowSharing(im1, im2));
+        assertArrayEquals(expected, raster.getPixels(minX, minY, width, height, (int[]) null));
+        verifySharing(result, allowSharing(4, im1, im2));
+        /*
+         * Try writing two values, then check again.
+         * The modified tile is labeled "Tile 4" above.
+         */
+        if (testWrite) {
+            final int tileX = 2;        // minTileX = 1
+            final int tileY = 3;        // minTileY = 2
+            final WritableRenderedImage writable = (WritableRenderedImage) result;
+            final WritableRaster target = writable.getWritableTile(tileX, tileY);
+            target.setPixel(10, -2, new int[] {100,  80,  20,  30});        // Upper left corner of tile 4
+            target.setPixel(12, -1, new int[] {200, 240, 260, 250});
+            writable.releaseWritableTile(tileX, tileY);
+            assertEquals(1400, expected[ 84]);              // For verifying that we are at the correct location.
+            assertEquals(1412, expected[116]);
+            expected[ 84] = 100;
+            expected[ 85] =  80;
+            expected[ 86] =  20;
+            expected[ 87] =  30;
+            expected[116] = 200;
+            expected[117] = 240;
+            expected[118] = 260;
+            expected[119] = 250;
+            assertSame(target, result.getTile(tileX, tileY));
+            assertArrayEquals(expected, result.getData().getPixels(minX, minY, width, height, (int[]) null));
+            return;             // Can not continue the tests because the source images have been modified.
+        }
         /*
          * Repeat the test with a custom band selection.
          * One of the source images is used twice, but with a different selection of bands.
          */
-        result = BandAggregateImage.create(new RenderedImage[] {im1, im2, im1}, new int[][] {
+        sourceImages = new RenderedImage[] {im1, im2, im1};
+        result = BandAggregateImage.create(sourceImages, 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.
@@ -221,8 +299,8 @@ public final class BandAggregateImageTest extends TestCase {
      * A copy of sample values can not be avoided in this case.
      */
     @Test
-    @DependsOnMethod("aggregateSimilarlyTiledImages")
-    public void aggregateImagesUsingSameExtentButDifferentTileSizes() {
+    @DependsOnMethod("testSimilarlyTiledImages")
+    public void testImagesUsingSameExtentButDifferentTileSizes() {
         final int minX   = 3;
         final int minY   = 1;
         final int width  = 8;
@@ -236,10 +314,9 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 4, 1, 3, 4, true);
         final TiledImageMock oneTile  = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true);
         initializeAllTiles(tiled2x2, tiled4x1, oneTile);
+        sourceImages = new RenderedImage[] {tiled2x2, tiled4x1, oneTile};
 
-        final RenderedImage result = BandAggregateImage.create(
-                new RenderedImage[] {tiled2x2, tiled4x1, oneTile}, null, null, allowSharing);
-
+        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -264,21 +341,21 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(minX, minY, width, height, (int[]) null)
         );
-        verifySharing(result, false);
+        verifySharing(result, false, false, 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.
+     * A copy of sample values can not be avoided in this case, except on the second image.
      */
     @Test
-    @DependsOnMethod("aggregateImagesUsingSameExtentButDifferentTileSizes")
-    public void aggregateImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
+    @DependsOnMethod("testImagesUsingSameExtentButDifferentTileSizes")
+    public void testImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
         /*
          * Tip: band number match image tile width. i.e:
          *
          *   untiled    →  band 1
-         *   tiled 2x2  →  bands 2 and 3
+         *   tiled 2x2  →  bands 2 and 3        — reference to data arrays can be shared.
          *   tiled 4x4  →  bands 4 and 5
          *   tiled 6x6  →  band 6
          */
@@ -287,10 +364,9 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2,  8,  8,  4,  4, 0, 0, true);
         final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12,  6,  6,  6, 0, 0, true);
         initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6);
+        sourceImages = new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6};
 
-        final RenderedImage result = BandAggregateImage.create(
-                new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6}, null, null, allowSharing);
-
+        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
         assertNotNull(result);
         assertEquals(4, result.getMinX());
         assertEquals(2, result.getMinY());
@@ -316,7 +392,7 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(4, 2, 8, 4, (int[]) null)
         );
-        verifySharing(result, false);
+        verifySharing(result, false, allowSharing, true, false, false, false);
     }
 
     /**
@@ -344,31 +420,45 @@ public final class BandAggregateImageTest extends TestCase {
      * 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;
+    private boolean[] allowSharing(final int numBands, final RenderedImage... sources) {
+        final boolean[] sharingPerBand = new boolean[numBands];
+        if (allowSharing) {
+            int lower = 0;
+            for (final RenderedImage source : sources) {
+                final int upper = lower + ImageUtilities.getNumBands(source);
+                if (source.getSampleModel() instanceof BandedSampleModel) {
+                    Arrays.fill(sharingPerBand, lower, upper, true);
+                }
+                lower = upper;
             }
+            assertEquals(numBands, lower);
         }
-        return true;
+        return sharingPerBand;
     }
 
     /**
      * 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.
+     * @param  sharingPerBand  whether the caller expects the result to share data arrays. One value per band.
      */
-    private static void verifySharing(final RenderedImage result, final boolean sharing) {
+    private static void verifySharing(final RenderedImage result, final boolean... sharingPerBand) {
+        assertEquals(ImageUtilities.getNumBands(result), sharingPerBand.length);
         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)));
+            forAllDataArrays(source, (data, band) -> assertTrue("Found two references to the same array.", arrays.add(data)));
+        }
+        forAllDataArrays(result, (data, band) -> {
+            final boolean sharing = sharingPerBand[band];
+            assertEquals(sharing ? "Expected the target image to reference an existing array."
+                                 : "Expected only copies, no references to existing arrays.",
+                         sharing, arrays.remove(data));
+        });
+        boolean sharing = true;
+        for (int i=0; i < sharingPerBand.length; i++) {
+            sharing &= sharingPerBand[i];
         }
-        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());
+        assertEquals(sharing, arrays.isEmpty());
     }
 
     /**
@@ -377,14 +467,14 @@ public final class BandAggregateImageTest extends TestCase {
      * @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) {
+    private static void forAllDataArrays(final RenderedImage source, final ObjIntConsumer<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());
+                for (int band = buffer.getNumBanks(); --band >= 0;) {
+                    action.accept(RasterFactory.createBuffer(buffer, band).array(), band);
                 }
             }
         }