sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/03: Add `BandedIterator`, a specialization of `PixelIterator` for the case of images using a `BandedSampleModel`. This is an attempt to resolve a performance bootleneck observed during `Isolines` calculations.
Date Tue, 29 Dec 2020 17:27:25 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 4f8f236a06443b71094c92a345653a7c9165061e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Dec 29 14:49:20 2020 +0100

    Add `BandedIterator`, a specialization of `PixelIterator` for the case of images using a `BandedSampleModel`.
    This is an attempt to resolve a performance bootleneck observed during `Isolines` calculations.
---
 .../org/apache/sis/gui/coverage/GridViewApp.java   |   3 +-
 .../java/org/apache/sis/image/BandedIterator.java  | 479 +++++++++++++++++++++
 .../java/org/apache/sis/image/LinearIterator.java  |   4 +-
 .../java/org/apache/sis/image/PixelIterator.java   | 271 +++++++++---
 .../org/apache/sis/image/StatisticsCalculator.java |   2 +-
 .../apache/sis/image/WritablePixelIterator.java    |  10 +-
 .../coverage/grid/ResampledGridCoverageTest.java   |   3 +-
 .../sis/coverage/grid/ReshapedImageTest.java       |   3 +-
 .../org/apache/sis/image/BandSelectImageTest.java  |   2 +-
 .../org/apache/sis/image/BandedIteratorTest.java   |  79 ++++
 .../sis/image/BandedSampleConverterTest.java       |   3 +-
 .../org/apache/sis/image/ImageCombinerTest.java    |   6 +-
 .../org/apache/sis/image/PixelIteratorTest.java    |  19 +-
 .../java/org/apache/sis/image/PlanarImageTest.java |   3 +-
 .../org/apache/sis/image/ResampledImageTest.java   |   3 +-
 .../apache/sis/image/StatisticsCalculatorTest.java |   3 +-
 .../java/org/apache/sis/image/TiledImageMock.java  |  10 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 18 files changed, 816 insertions(+), 88 deletions(-)

diff --git a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridViewApp.java b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridViewApp.java
index 19c2a82..c416872 100644
--- a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridViewApp.java
+++ b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/GridViewApp.java
@@ -84,7 +84,8 @@ public final strictfp class GridViewApp extends Application {
                 TILE_WIDTH,
                 TILE_HEIGHT,
                 3,                              // minTileX
-                -5);                            // minTileY
+                -5,                             // minTileY
+                false);
         image.validate();
         image.initializeAllTiles(0);
         image.failRandomly(new Random());
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedIterator.java
new file mode 100644
index 0000000..312ac7b
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedIterator.java
@@ -0,0 +1,479 @@
+/*
+ * 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.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.ComponentSampleModel;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+
+
+/**
+ * A pixel iterator reading values directly from a {@link DataBuffer} instead than using {@link Raster} API.
+ * This iterator has the same behavior than the default implementation and is provided only for performance reasons.
+ * It can bring performance benefits when reading values as {@code float} or {@code double} values, but the benefits
+ * are more dubious for {@code int} values because Java2D has optimizations for that specific type.
+ *
+ * <p>This class assumes a {@link BandedSampleModel} or other models having an equivalently simple mapping from pixel
+ * coordinates to indices in banks. For other kinds of sample model, the default implementation should be used.
+ * More specifically assumptions are!</p>
+ *
+ * <ul>
+ *   <li>One sample value per band, or (equivalently) only one band.</li>
+ *   <li>{@linkplain ComponentSampleModel#getPixelStride() Pixel stride} equals to 1.</li>
+ *   <li>{@linkplain ComponentSampleModel#getBankIndices() Bank indices} are the 0, 1, 2, … sequence.</li>
+ *   <li>{@linkplain ComponentSampleModel#getBandOffsets() Band offsets} are all zero.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class BandedIterator extends WritablePixelIterator {
+    /**
+     * The buffer from where to read data. This is the buffer backing {@link #currentRaster}.
+     * It is the lowest-level object we can use before the plain Java array. We do not fetch
+     * the Java array because doing so may cause Java2D to disable GPU accelerations.
+     */
+    private DataBuffer buffer;
+
+    /**
+     * The buffer where to write data, or {@code null} if none.
+     * May be the same instance than {@link #buffer}.
+     */
+    private DataBuffer destBuffer;
+
+    /**
+     * The translation from {@link SampleModel} coordinates to {@link Raster} coordinates.
+     */
+    private int sampleModelTranslateX, sampleModelTranslateY;
+
+    /**
+     * The translation from {@link Raster} <var>x</var> coordinates to {@link #buffer} indices.
+     * This is constant for a row and needs to be updated only when {@link #y} changed.
+     * Given buffer index computed by the following formula:
+     *
+     * <pre>index = (y - sampleModelTranslateY) * scanlineStride + (x - sampleModelTranslateX)</pre>
+     *
+     * Then {@code xToBuffer} is above value with <var>x</var> = 0. This value may be negative.
+     *
+     * @see #updateForRowChange()
+     */
+    private int xToBuffer;
+
+    /**
+     * Number of {@linkplain #buffer} elements between a given sample and the corresponding sample
+     * in the same column of the next row. This value shall be the same for all tiles.
+     */
+    private final int scanlineStride;
+
+    /**
+     * Creates an iterator for the given region in the given raster.
+     *
+     * @param  input    the raster which contains the sample values to read.
+     * @param  output   the raster where to write the sample values, or {@code null} for read-only iterator.
+     * @param  subArea  the raster region where to perform the iteration, or {@code null} for iterating over all the raster domain.
+     * @param  window   size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
+     * @param  scanlineStride  value of {@code getScanlineStride(input.getSampleModel()}. Shall be greater than zero.
+     */
+    BandedIterator(final Raster input, final WritableRaster output, final Rectangle subArea,
+                   final Dimension window, final int scanlineStride)
+    {
+        super(input, output, subArea, window);
+        this.scanlineStride = scanlineStride;
+        acquiredTile(input);
+        updateForRowChange();
+    }
+
+    /**
+     * Creates an iterator for the given region in the given image.
+     *
+     * @param  input    the image which contains the sample values to read.
+     * @param  output   the image where to write the sample values, or {@code null} for read-only iterator.
+     * @param  subArea  the image region where to perform the iteration, or {@code null} for iterating over all the image domain.
+     * @param  window   size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
+     * @param  scanlineStride  value of {@code getScanlineStride(input.getSampleModel()}. Shall be greater than zero.
+     */
+    BandedIterator(final RenderedImage input, final WritableRenderedImage output, final Rectangle subArea,
+                   final Dimension window, final int scanlineStride)
+    {
+        super(input, output, subArea, window);
+        this.scanlineStride = scanlineStride;
+        // `acquiredTile(…)` will be invoked later by `fetchTile()`.
+    }
+
+    /**
+     * Recomputes {@link #xToBuffer} for the new {@link #y} value. This method shall be invoked
+     * when the iterator moved to new row, or when the iterator fetched a new tile but only after
+     * the (x,y) coordinates have been updated.
+     */
+    private void updateForRowChange() {
+        xToBuffer = (y - sampleModelTranslateY) * scanlineStride - sampleModelTranslateX;
+    }
+
+    /**
+     * Restores the iterator to the start position.
+     */
+    @Override
+    public void rewind() {
+        super.rewind();
+        updateForRowChange();
+    }
+
+    /**
+     * Moves the pixel iterator to the given column (x) and row (y) indices.
+     *
+     * @throws IndexOutOfBoundsException if the given indices are outside the iteration domain.
+     */
+    @Override
+    public void moveTo(final int px, final int py) {
+        if (py == y && px >= currentLowerX() && px < currentUpperX()) {
+            x = px;
+        } else {
+            super.moveTo(px, py);
+            updateForRowChange();
+        }
+    }
+
+    /**
+     * Moves the iterator to the next pixel. This implementation is a copy of {@link PixelIterator#next()}
+     * with only a call to {@link #updateForRowChange()} added when the {@link #y} value changed.
+     *
+     * @return {@code true} if the current pixel is valid, or {@code false} if there is no more pixels.
+     */
+    @Override
+    public boolean next() {
+        if (++x >= currentUpperX()) {
+            if (++y >= currentUpperY()) {
+                releaseTile();
+                if (++tileX >= tileUpperX) {
+                    if (++tileY >= tileUpperY) {
+                        endOfIteration();
+                        return false;
+                    }
+                    tileX = tileLowerX;
+                }
+                y = fetchTile();
+                updateForRowChange();
+            } else {
+                xToBuffer += scanlineStride;
+            }
+            x = currentLowerX();
+        }
+        return true;
+    }
+
+    /**
+     * Invoked when the iterator fetched a new tile. This method updates {@code BandedIterator} fields
+     * with raster properties, except {@link #xToBuffer} which is not updated here because {@link #y}
+     * is not yet updated to its new value. {@code BandedIterator} must override all methods invoking
+     * {@link #fetchTile()} ane ensure that {@link #updateForRowChange()} is invoked after (x,y) have
+     * been updated.
+     */
+    @Override
+    final void acquiredTile(Raster tile) {
+        assert PixelIterator.Builder.getScanlineStride(tile.getSampleModel()) == scanlineStride;
+        sampleModelTranslateY = tile.getSampleModelTranslateY();
+        sampleModelTranslateX = tile.getSampleModelTranslateX();
+        buffer                = tile.getDataBuffer();
+        tile = destination();
+        if (tile != null) {
+            destBuffer = tile.getDataBuffer();
+            assert PixelIterator.Builder.getScanlineStride(tile.getSampleModel()) == scanlineStride &&
+                   tile.getSampleModelTranslateX() == sampleModelTranslateX &&
+                   tile.getSampleModelTranslateY() == sampleModelTranslateY;
+        }
+    }
+
+    /**
+     * Releases the buffer acquired by this iterator, if any.
+     * This is safety for avoiding accidental usage of wrong buffer.
+     * Also avoid to retain large array if the tile is garbage collected.
+     */
+    @Override
+    final void releaseTile() {
+        if (image != null) {
+            buffer = null;
+            destBuffer = null;
+        }
+        super.releaseTile();
+    }
+
+    /** Returns the sample value in the specified band of current pixel. */
+    @Override public int    getSample      (final int band) {return buffer.getElem      (band, x + xToBuffer);}
+    @Override public float  getSampleFloat (final int band) {return buffer.getElemFloat (band, x + xToBuffer);}
+    @Override public double getSampleDouble(final int band) {return buffer.getElemDouble(band, x + xToBuffer);}
+    @Override public void   setSample(int band, int    value)  {destBuffer.setElem      (band, x + xToBuffer, value);}
+    @Override public void   setSample(int band, float  value)  {destBuffer.setElemFloat (band, x + xToBuffer, value);}
+    @Override public void   setSample(int band, double value)  {destBuffer.setElemDouble(band, x + xToBuffer, value);}
+
+    /**
+     * Returns the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public int[] getPixel​(int[] dest) {
+        if (dest == null) {
+            dest = new int[numBands];
+        }
+        /*
+         * `getElement(index)` is synonymous to `getElement(0, index)` but possibly slightly faster
+         * since it is implemented with a single array access instead of 3. After that, the loop is
+         * not executed at all in the common case of an image with a single band.
+         */
+        final int index = x + xToBuffer;
+        dest[0] = buffer.getElem(index);
+        for (int i=1; i<numBands; i++) {
+            dest[i] = buffer.getElem(i, index);
+        }
+        return dest;
+    }
+
+    /**
+     * Returns the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public float[] getPixel​(float[] dest) {
+        if (dest == null) {
+            dest = new float[numBands];
+        }
+        final int index = x + xToBuffer;
+        dest[0] = buffer.getElemFloat(index);           // See comment in `getPixel(int[])`.
+        for (int i=1; i<numBands; i++) {
+            dest[i] = buffer.getElemFloat(i, index);
+        }
+        return dest;
+    }
+
+    /**
+     * Returns the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public double[] getPixel​(double[] dest) {
+        if (dest == null) {
+            dest = new double[numBands];
+        }
+        final int index = x + xToBuffer;
+        dest[0] = buffer.getElemDouble(index);          // See comment in `getPixel(int[])`.
+        for (int i=1; i<numBands; i++) {
+            dest[i] = buffer.getElemDouble(i, index);
+        }
+        return dest;
+    }
+
+    /**
+     * Sets the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public void setPixel​(final int[] values) {
+        final int index = x + xToBuffer;
+        destBuffer.setElem(index, values[0]);           // See comment in `getPixel(int[])`.
+        for (int i=1; i<numBands; i++) {
+            destBuffer.setElem(i, index, values[i]);
+        }
+    }
+
+    /**
+     * Sets the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public void setPixel​(final float[] values) {
+        final int index = x + xToBuffer;
+        destBuffer.setElemFloat(index, values[0]);      // See comment in `getPixel(int[])`.
+        for (int i=1; i<numBands; i++) {
+            destBuffer.setElemFloat(i, index, values[i]);
+        }
+    }
+
+    /**
+     * Sets the sample values of current pixel for all bands. If the iterator is not in a valid position
+     * as documented in parent class, then this method behavior is undetermined: It may either throw an
+     * {@link ArrayIndexOutOfBoundsException} or return a random value.
+     */
+    @Override
+    public void setPixel​(final double[] values) {
+        final int index = x + xToBuffer;
+        destBuffer.setElemDouble(index, values[0]);     // See comment in `getPixel(int[])`.
+        for (int i=1; i<numBands; i++) {
+            destBuffer.setElemDouble(i, index, values[i]);
+        }
+    }
+
+    /**
+     * Creates a window for floating point values using the given arrays.
+     */
+    @Override Window<FloatBuffer>  createWindow( float[] data,  float[] transfer) {return new  FloatWindow(data, transfer);}
+    @Override Window<DoubleBuffer> createWindow(double[] data, double[] transfer) {return new DoubleWindow(data, transfer);}
+
+    /**
+     * {@link Window} implementation backed by an array of {@code float[]}.
+     * This is a copy of {@link org.apache.sis.image.PixelIterator.FloatWindow}
+     * except in {@code getPixels(…)} implementation.
+     */
+    private final class FloatWindow extends Window<FloatBuffer> {
+        /**
+         * Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
+         * Those arrays are overwritten when {@link #update()} is invoked.
+         */
+        private final float[] data, transfer;
+
+        /**
+         * Creates a new window which will store the sample values in the given {@code data} array.
+         */
+        FloatWindow(final float[] data, final float[] transfer) {
+            super(FloatBuffer.wrap(data).asReadOnlyBuffer());
+            this.data = data;
+            this.transfer = transfer;
+        }
+
+        /**
+         * Returns the iterator that created this window.
+         */
+        @Override
+        final PixelIterator owner() {
+            return BandedIterator.this;
+        }
+
+        /**
+         * Performs the transfer between the underlying raster and this window.
+         */
+        @Override
+        Object getPixels(final Raster raster, int subX, int subY, final int subWidth, int subHeight, final int mode) {
+            if (mode != TRANSFER_FROM_OTHER) {
+                assert subX == x && subY == y;          // Constraint documented in parent class.
+                final DataBuffer source = buffer;
+                final float[]    target = (mode == DIRECT) ? data : transfer;
+                final int        toNext = scanlineStride - subWidth;
+                final int        numBds = numBands;
+                int srcOff = subX + xToBuffer;
+                int tgtOff = 0;
+                do {
+                    int c = subWidth;
+                    do {
+                        target[tgtOff++] = source.getElemFloat(srcOff);
+                        for (int b=1; b<numBds; b++) {
+                            target[tgtOff++] = source.getElemFloat(b, srcOff);
+                        }
+                        srcOff++;
+                    } while (--c != 0);
+                    srcOff += toNext;
+                } while (--subHeight != 0);
+                return target;
+            }
+            // Fallback for all cases that we can not handle with above loop.
+            return raster.getPixels(subX, subY, subWidth, subHeight, transfer);
+        }
+
+        /**
+         * Updates this window with the sample values in the region starting at current iterator position.
+         * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
+         */
+        @Override
+        public void update() {
+            values.clear();
+            fetchValues(this, data);
+        }
+    }
+
+    /**
+     * {@link Window} implementation backed by an array of {@code double[]}.
+     * This is a copy of {@link org.apache.sis.image.PixelIterator.DoubleWindow}
+     * except in {@code getPixels(…)} implementation.
+     */
+    private final class DoubleWindow extends Window<DoubleBuffer> {
+        /**
+         * Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
+         * Those arrays are overwritten when {@link #update()} is invoked.
+         */
+        private final double[] data, transfer;
+
+        /**
+         * Creates a new window which will store the sample values in the given {@code data} array.
+         */
+        DoubleWindow(final double[] data, final double[] transfer) {
+            super(DoubleBuffer.wrap(data).asReadOnlyBuffer());
+            this.data = data;
+            this.transfer = transfer;
+        }
+
+        /**
+         * Returns the iterator that created this window.
+         */
+        @Override
+        final PixelIterator owner() {
+            return BandedIterator.this;
+        }
+
+        /**
+         * Performs the transfer between the underlying raster and this window.
+         */
+        @Override
+        Object getPixels(final Raster raster, int subX, int subY, final int subWidth, int subHeight, final int mode) {
+            if (mode != TRANSFER_FROM_OTHER) {
+                assert subX == x && subY == y;          // Constraint documented in parent class.
+                final DataBuffer source = buffer;
+                final double[]   target = (mode == DIRECT) ? data : transfer;
+                final int        toNext = scanlineStride - subWidth;
+                final int        numBds = numBands;
+                int srcOff = subX + xToBuffer;
+                int tgtOff = 0;
+                do {
+                    int c = subWidth;
+                    do {
+                        target[tgtOff++] = source.getElemDouble(srcOff);
+                        for (int b=1; b<numBds; b++) {
+                            target[tgtOff++] = source.getElemDouble(b, srcOff);
+                        }
+                        srcOff++;
+                    } while (--c != 0);
+                    srcOff += toNext;
+                } while (--subHeight != 0);
+                return target;
+            }
+            // Fallback for all cases that we can not handle with above loop.
+            return raster.getPixels(subX, subY, subWidth, subHeight, transfer);
+        }
+
+        /**
+         * Updates this window with the sample values in the region starting at current iterator position.
+         * This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
+         */
+        @Override
+        public void update() {
+            values.clear();
+            fetchValues(this, data);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/LinearIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/LinearIterator.java
index e5486ae..e1d4fdd 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/LinearIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/LinearIterator.java
@@ -90,13 +90,13 @@ final class LinearIterator extends WritablePixelIterator {
      */
     @Override
     public boolean next() {
-        if (++x >= currentUpperX) {                 // Move to next column, potentially on a different tile.
+        if (++x >= currentUpperX()) {               // Move to next column, potentially on a different tile.
             if (x < upperX) {
                 releaseTile();                      // Must be invoked before `tileX` change.
                 tileX++;
             } else {
                 x = lowerX;                         // Beginning of next row.
-                if (++y >= currentUpperY) {         // Move to next line.
+                if (++y >= currentUpperY()) {       // Move to next line.
                     releaseTile();                  // Must be invoked before `tileY` change.
                     if (++tileY >= tileUpperY) {
                         endOfIteration();
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index 1fff354..de88021 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -25,7 +25,6 @@ import java.nio.DoubleBuffer;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
-import java.awt.Shape;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
@@ -33,11 +32,15 @@ import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import java.awt.image.SampleModel;
+import java.awt.image.ComponentSampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
 import java.awt.image.RasterFormatException;
 import java.util.NoSuchElementException;
 import org.opengis.coverage.grid.SequenceType;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.feature.Resources;
@@ -103,7 +106,7 @@ public class PixelIterator {
      * Number of bands in all tiles in the {@linkplain #image}.
      * The {@link #currentRaster} shall always have this number of bands.
      */
-    private final int numBands;
+    final int numBands;
 
     /**
      * The domain, in pixel coordinates, of the region traversed by this pixel iterator.
@@ -150,10 +153,27 @@ public class PixelIterator {
     int x, y;
 
     /**
-     * Bounds of the region traversed by the iterator in current raster.
+     * Bounds of the region traversed by the iterator in {@linkplain #currentRaster current raster}.
      * When iteration reaches the upper coordinates, the iterator needs to move to next tile.
+     * This the raster bounds clipped to the area of interest.
+     *
+     * @see #currentUpperX()
+     * @see #currentUpperY()
+     */
+    private int currentLowerX, currentUpperX, currentUpperY;
+
+    /**
+     * Maximal {@linkplain #x} and {@linkplain #y} coordinates (exclusive) that {@link Window} can use for
+     * fetching values in current tile. If some (x,y) coordinates inside the window are equal or greater,
+     * then the window will need to fetch some values on neighbor tiles (i.e. the window is overlapping
+     * two or more tiles).
+     *
+     * <p>This is initialized by {@link #fetchTile()} to the same values than {@link #currentUpperX} and
+     * {@link #currentUpperY} but without clipping to the area of interest. We want to keep the flexibility
+     * to overwrite with other coordinate system in future versions, if useful for {@link Window} performance.
+     * Consequently hose values should not be used in other context than {@link #fetchValues(Window, Object)}.</p>
      */
-    int currentLowerX, currentUpperX, currentUpperY;
+    private int windowLimitX, windowLimitY;
 
     /**
      * Creates an iterator for the given region in the given raster.
@@ -186,7 +206,9 @@ public class PixelIterator {
         currentLowerX   = lowerX;
         currentUpperX   = upperX;
         currentUpperY   = upperY;
-        x               = Math.decrementExact(lowerX);          // Set to the position before first pixel.
+        windowLimitX    = Math.addExact(tileGridXOffset, tileWidth);    // Initialized here because `fetchTile()` will not be invoked.
+        windowLimitY    = Math.addExact(tileGridYOffset, tileHeight);
+        x               = Math.decrementExact(lowerX);                  // Set to the position before first pixel.
         y               = lowerY;
     }
 
@@ -361,6 +383,36 @@ public class PixelIterator {
         }
 
         /**
+         * If the given sample model is compatible with {@link BandedIterator}, returns the model scanline stride.
+         * Otherwise returns 0. A {@code BandedIterator} can be used only if the returned value is greater than 0.
+         */
+        static int getScanlineStride(final SampleModel sm) {
+            if (sm instanceof ComponentSampleModel) {
+                final ComponentSampleModel csm = (ComponentSampleModel) sm;
+                if (csm.getPixelStride() == 1) {
+                    for (final int offset : csm.getBandOffsets()) {
+                        if (offset != 0) return 0;
+                    }
+                    if (ArraysExt.isRange(0, csm.getBankIndices())) {
+                        return csm.getScanlineStride();
+                    }
+                }
+            } else if (sm instanceof SinglePixelPackedSampleModel) {
+                final SinglePixelPackedSampleModel csm = (SinglePixelPackedSampleModel) sm;
+                final int[] offsets = csm.getBitOffsets();
+                if (offsets.length == 1 && offsets[0] == 0) {
+                    return csm.getScanlineStride();
+                }
+            } else if (sm instanceof MultiPixelPackedSampleModel) {
+                final MultiPixelPackedSampleModel csm = (MultiPixelPackedSampleModel) sm;
+                if (csm.getDataBitOffset() == 0 && csm.getPixelBitStride() == DataBuffer.getDataTypeSize(csm.getDataType())) {
+                    return csm.getScanlineStride();
+                }
+            }
+            return 0;
+        }
+
+        /**
          * Creates a read-only iterator for the given raster.
          *
          * @param  data  the raster which contains the sample values on which to iterate.
@@ -368,13 +420,19 @@ public class PixelIterator {
          */
         public PixelIterator create(final Raster data) {
             ArgumentChecks.ensureNonNull("data", data);
-            if (order == SequenceType.LINEAR) {
-                return new LinearIterator(data, null, subArea, window);
-            } else if (order != null) {
+            if (order != null && order != SequenceType.LINEAR) {
                 throw new IllegalStateException(Errors.format(Errors.Keys.UnsupportedType_1, order));
             }
-            // TODO: check here for cases that we can optimize (after we ported corresponding implementations).
-            return new PixelIterator(data, subArea, window);
+            /*
+             * No need to instantiate `LinearIterator` because the default iterator
+             * has the same iteration order when there is only one tile.
+             */
+            final int scanlineStride = getScanlineStride(data.getSampleModel());
+            if (scanlineStride > 0) {
+                return new BandedIterator(data, null, subArea, window, scanlineStride);
+            } else {
+                return new PixelIterator(data, subArea, window);
+            }
         }
 
         /**
@@ -403,8 +461,12 @@ public class PixelIterator {
             } else if (order != null) {
                 throw new IllegalStateException(Errors.format(Errors.Keys.UnsupportedType_1, order));
             }
-            // TODO: check here for cases that we can optimize (after we ported corresponding implementations).
-            return new PixelIterator(data, subArea, window);
+            final int scanlineStride = getScanlineStride(data.getSampleModel());
+            if (scanlineStride > 0) {
+                return new BandedIterator(data, null, subArea, window, scanlineStride);
+            } else {
+                return new PixelIterator(data, subArea, window);
+            }
         }
 
         /**
@@ -434,6 +496,7 @@ public class PixelIterator {
 
         /**
          * Creates an iterator which will read and write in two different rasters.
+         * The two rasters must use the same sample model and have the same bounds.
          *
          * @param  input    the raster which contains the sample values to read.
          * @param  output   the raster where to write the sample values. Can be the same than {@code input}.
@@ -442,17 +505,24 @@ public class PixelIterator {
         public WritablePixelIterator createWritable(final Raster input, final WritableRaster output) {
             ArgumentChecks.ensureNonNull("input",  input);
             ArgumentChecks.ensureNonNull("output", output);
-            if (order == SequenceType.LINEAR) {
-                return new LinearIterator(input, output, subArea, window);
-            } else if (order != null) {
+            if (order != null && order != SequenceType.LINEAR) {
                 throw new IllegalStateException(Errors.format(Errors.Keys.UnsupportedType_1, order));
             }
-            // TODO: check here for cases that we can optimize (after we ported corresponding implementations).
-            return new WritablePixelIterator(input, output, subArea, window);
+            /*
+             * No need to instantiate `LinearIterator` because the default iterator
+             * has the same iteration order when there is only one tile.
+             */
+            final int scanlineStride = getScanlineStride(input.getSampleModel());
+            if (scanlineStride > 0) {
+                return new BandedIterator(input, output, subArea, window, scanlineStride);
+            } else {
+                return new WritablePixelIterator(input, output, subArea, window);
+            }
         }
 
         /**
          * Creates an iterator which will read and write in two different images.
+         * The two images must use the same sample model and have the same bounds.
          *
          * @param  input    the image which contains the sample values to read.
          * @param  output   the image where to write the sample values. Can be the same than {@code input}.
@@ -467,8 +537,12 @@ public class PixelIterator {
             } else if (order != null) {
                 throw new IllegalStateException(Errors.format(Errors.Keys.UnsupportedType_1, order));
             }
-            // TODO: check here for cases that we can optimize (after we ported corresponding implementations).
-            return new WritablePixelIterator(input, output, subArea, window);
+            final int scanlineStride = getScanlineStride(input.getSampleModel());
+            if (scanlineStride > 0) {
+                return new BandedIterator(input, output, subArea, window, scanlineStride);
+            } else {
+                return new WritablePixelIterator(input, output, subArea, window);
+            }
         }
     }
 
@@ -582,7 +656,7 @@ public class PixelIterator {
      * @return order in which pixels are traversed.
      */
     public Optional<SequenceType> getIterationOrder() {
-        if (image == null || (tileUpperX - tileLowerX) <=1 && (tileUpperY - tileLowerY) <= 1) {
+        if (image == null || (tileUpperX - tileLowerX) <= 1 && (tileUpperY - tileLowerY) <= 1) {
             return Optional.of(SequenceType.LINEAR);
         } else {
             return Optional.empty();                // Undefined order.
@@ -630,18 +704,6 @@ public class PixelIterator {
     }
 
     /**
-     * Returns {@code true} if current iterator position is inside the given shape.
-     * Current version does not verify if iteration started or finished
-     * (this method is non-public for that reason).
-     *
-     * @param  domain  the shape for which to test inclusion.
-     * @return whether current iterator position is inside the given shape.
-     */
-    final boolean isInside(final Shape domain) {
-        return domain.contains(x, y);
-    }
-
-    /**
      * Moves the pixel iterator to the given column (x) and row (y) indices. After this method invocation,
      * the iterator state is as if the {@link #next()} method has been invoked just before to reach the
      * specified position.
@@ -713,28 +775,58 @@ public class PixelIterator {
     }
 
     /**
+     * Returns the lower limit of the region traversed by the iterator in current raster.
+     */
+    final int currentLowerX() {
+        return currentLowerX;
+    }
+
+    /**
+     * Returns the upper limit of the region traversed by the iterator in current raster.
+     * When iteration reaches this limit, the iterator needs to move to next row or next tile.
+     */
+    final int currentUpperX() {
+        return currentUpperX;
+    }
+
+    /**
+     * Returns the upper limit of the region traversed by the iterator in current raster.
+     * When iteration reaches this limit, the iterator needs to move to next tile.
+     */
+    final int currentUpperY() {
+        return currentUpperY;
+    }
+
+    /**
      * Fetches from the image a tile for the current {@link #tileX} and {@link #tileY} coordinates.
      * All fields prefixed by {@code current} are updated by this method. The caller is responsible
      * for updating the {@link #x} and {@link #y} fields.
      *
-     * <p>Note that there is no {@code currentLowerY} field in this {@code PixelIterator} class.
+     * <p>Note 1: {@link #releaseTile()} is always invoked before this method.
+     * Consequently {@link #currentRaster} is already {@code null}.</p>
+     *
+     * <p>Note 2: there is no {@code currentLowerY} field in this {@code PixelIterator} class.
      * Instead, that value is returned by this method.</p>
      *
      * @return the {@link #y} value of the first row of new tile.
      */
     final int fetchTile() {
-        currentRaster = fetchWritableTile();
-        if (currentRaster == null) {
-            currentRaster = image.getTile(tileX, tileY);
+        Raster tile = fetchWritableTile();
+        if (tile == null) {
+            tile = image.getTile(tileX, tileY);
         }
-        final int minX = currentRaster.getMinX();
-        final int minY = currentRaster.getMinY();
-        currentLowerX  = Math.max(lowerX, minX);
-        currentUpperX  = Math.min(upperX, minX + tileWidth);
-        currentUpperY  = Math.min(upperY, minY + tileHeight);
-        if (currentRaster.getNumBands() != numBands) {
+        if (tile.getNumBands() != numBands || tile.getWidth() != tileWidth || tile.getHeight() != tileHeight) {
             throw new RasterFormatException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
         }
+        final int minX = tile.getMinX();
+        final int minY = tile.getMinY();
+        windowLimitX   = Math.addExact(minX, tileWidth);
+        windowLimitY   = Math.addExact(minY, tileHeight);
+        currentUpperX  = Math.min(upperX, windowLimitX);
+        currentUpperY  = Math.min(upperY, windowLimitY);
+        currentLowerX  = Math.max(lowerX, minX);
+        currentRaster  = tile;
+        acquiredTile(tile);
         return Math.max(lowerY, minY);
     }
 
@@ -752,15 +844,31 @@ public class PixelIterator {
     }
 
     /**
+     * Invoked when the iterator fetched a new tile. This is a hook for subclasses.
+     * The default implementation does nothing. This is overridden when a subclass
+     * needs to store additional raster properties, for example its buffer for more
+     * direct access to sample values.
+     *
+     * @param  tile  the new tile from which to read sample values.
+     */
+    void acquiredTile(Raster tile) {
+    }
+
+    /**
      * Releases the tiles acquired by this iterator, if any.
-     * This method does nothing if the iterator is read-only.
      */
     void releaseTile() {
+        if (image != null) {
+            currentRaster = null;
+        }
     }
 
     /**
      * Invoked when a call to {@link #next()} moved to the end of iteration. This method sets fields to values
      * that will allow {@link #moveTo(int,int)} and {@link #next()} to detect that we already finished iteration.
+     *
+     * <p>Note: {@link #releaseTile()} is always invoked before this method.
+     * Consequently {@link #currentRaster} is already {@code null}.</p>
      */
     final void endOfIteration() {
         /*
@@ -890,7 +998,7 @@ public class PixelIterator {
      * in the image. By contrast this {@code getDataElements​(…)} method may return an array of length 1 with
      * all sample values packed as a single ARGB value.</div>
      *
-     * Data elements are useful for copying efficiently values in another image using the same sample model,
+     * Data elements are useful for copying values in another image using the same sample model,
      * or for getting colors with a call to {@link java.awt.image.ColorModel#getRGB(Object)}.
      *
      * @param  dest  a pre-allocated array where to store the data elements, or {@code null} if none.
@@ -964,14 +1072,24 @@ public class PixelIterator {
         final int transferLength = length - numBands * Math.min(windowWidth, windowHeight);
         // `transfer` will always have at least one row or one column less than `data`.
         switch (type.dataBufferType) {
-            case DataBuffer.TYPE_INT:    return (Window<T>) new IntWindow   (new int   [length], new int   [transferLength]);
-            case DataBuffer.TYPE_FLOAT:  return (Window<T>) new FloatWindow (new float [length], new float [transferLength]);
-            case DataBuffer.TYPE_DOUBLE: return (Window<T>) new DoubleWindow(new double[length], new double[transferLength]);
+            case DataBuffer.TYPE_INT:    return (Window<T>) new IntWindow(new int   [length], new int   [transferLength]);
+            case DataBuffer.TYPE_FLOAT:  return (Window<T>)  createWindow(new float [length], new float [transferLength]);
+            case DataBuffer.TYPE_DOUBLE: return (Window<T>)  createWindow(new double[length], new double[transferLength]);
             default: throw new AssertionError(type);  // Should never happen unless we updated TransferType and forgot to update this method.
         }
     }
 
     /**
+     * Creates a window for floating point values using the given arrays. This is a hook for allowing subclasses
+     * to specify alternative implementations. We provide hooks only for floating point types, not for integers,
+     * because the {@code int} type is already optimized by Java2D with specialized {@code Raster.getPixels(…)}
+     * method implementations. By contract the {@code float} and {@code double} types in Java2D use generic and
+     * slower code path.
+     */
+    Window<FloatBuffer>  createWindow( float[] data,  float[] transfer) {return new  FloatWindow(data, transfer);}
+    Window<DoubleBuffer> createWindow(double[] data, double[] transfer) {return new DoubleWindow(data, transfer);}
+
+    /**
      * Contains the sample values in a moving window over the image. Windows are created by calls to
      * {@link PixelIterator#createWindow(TransferType)} and sample values are stored in {@link Buffer}s.
      * The buffer content is replaced ever time {@link #update()} is invoked.
@@ -986,6 +1104,17 @@ public class PixelIterator {
      */
     public abstract static class Window<T extends Buffer> {
         /**
+         * Enumeration values for the last argument in {@link #getPixels(Raster, int, int, int, int, int)}.
+         * <ul>
+         *   <li>{@code DIRECT}: store sample values directly in the final destination array.</li>
+         *   <li>{@code TRANSFER}: store sample values in a temporary buffer (copied to destination by caller).</li>
+         *   <li>{@code TRANSFER_FROM_OTHER}: same as {@code TRANSFER}, but also notify that the given raster is not
+         *       {@link PixelIterator#currentRaster}.</li>
+         * </ul>
+         */
+        static final int DIRECT = 0, TRANSFER = 1, TRANSFER_FROM_OTHER = 2;
+
+        /**
          * A buffer containing all sample values fetched by the last call to {@link #update()}. The buffer
          * capacity is <var>(number of bands)</var> × <var>(window width)</var> × <var>(window height)</var>.
          * Values are always stored with band index varying fastest, then column index, then row index.
@@ -1040,16 +1169,22 @@ public class PixelIterator {
          * per array element. Subclasses should delegate to one of the {@code Raster#getPixels(…)} methods
          * depending on the buffer data type.
          *
+         * <h4>Constraints</h4>
+         * If {@code mode} == {@link #DIRECT} or {@link #TRANSFER}, then {@code subX}={@link #x} and
+         * {@code subY}={@link #y}. This constraint allows subclasses to use cached values for current position.
+         * Otherwise ({@code mode} == {@link #TRANSFER_FROM_OTHER}), {@code subX} and {@code subY} can be anything.
+         *
+         * <p>{@code subWidth} and {@code subHeight} shall always be greater than zero.</p>
+         *
          * @param  raster     the raster from which to get the pixel values.
          * @param  subX       the X coordinate of the upper-left pixel location.
          * @param  subY       the Y coordinate of the upper-left pixel location.
          * @param  subWidth   width of the pixel rectangle.
          * @param  subHeight  height of the pixel rectangle.
-         * @param  direct     {@code true} for storing directly in the final array,
-         *                    or {@code false} for using the transfer array.
+         * @param  mode       one of {@link #DIRECT}, {@link #TRANSFER} or {@link #TRANSFER_FROM_OTHER}.
          * @return the array in which sample values have been stored.
          */
-        abstract Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct);
+        abstract Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, int mode);
     }
 
     /**
@@ -1092,8 +1227,8 @@ public class PixelIterator {
          * Performs the transfer between the underlying raster and this window.
          */
         @Override
-        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
-            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
+        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, int mode) {
+            return raster.getPixels(subX, subY, subWidth, subHeight, (mode == DIRECT) ? data : transfer);
         }
 
         /**
@@ -1103,7 +1238,7 @@ public class PixelIterator {
         @Override
         public void update() {
             values.clear();
-            PixelIterator.this.update(this, data);
+            fetchValues(this, data);
         }
     }
 
@@ -1138,8 +1273,8 @@ public class PixelIterator {
          * Performs the transfer between the underlying raster and this window.
          */
         @Override
-        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
-            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
+        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, int mode) {
+            return raster.getPixels(subX, subY, subWidth, subHeight, (mode == DIRECT) ? data : transfer);
         }
 
         /**
@@ -1149,7 +1284,7 @@ public class PixelIterator {
         @Override
         public void update() {
             values.clear();
-            PixelIterator.this.update(this, data);
+            fetchValues(this, data);
         }
     }
 
@@ -1184,8 +1319,8 @@ public class PixelIterator {
          * Performs the transfer between the underlying raster and this window.
          */
         @Override
-        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
-            return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
+        Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, int mode) {
+            return raster.getPixels(subX, subY, subWidth, subHeight, (mode == DIRECT) ? data : transfer);
         }
 
         /**
@@ -1195,7 +1330,7 @@ public class PixelIterator {
         @Override
         public void update() {
             values.clear();
-            PixelIterator.this.update(this, data);
+            fetchValues(this, data);
         }
     }
 
@@ -1206,10 +1341,9 @@ public class PixelIterator {
      * @param  data    the array of primitive type where sample values are stored.
      */
     @SuppressWarnings("SuspiciousSystemArraycopy")
-    final void update(final Window<?> window, final Object data) {
-        Raster  raster    = currentRaster;
-        int     subEndX   = (raster.getMinX() - x) + raster.getWidth();
-        int     subEndY   = (raster.getMinY() - y) + raster.getHeight();
+    final void fetchValues(final Window<?> window, final Object data) {
+        int     subEndX   = windowLimitX - x;
+        int     subEndY   = windowLimitY - y;
         int     subWidth  = Math.min(windowWidth,  subEndX);
         int     subHeight = Math.min(windowHeight, subEndY);
         boolean fullWidth = (subWidth == windowWidth);
@@ -1219,7 +1353,7 @@ public class PixelIterator {
              * This is the vast majority of cases, so we perform this check soon before
              * to compute more internal variables.
              */
-            final Object transfer = window.getPixels(raster, x, y, subWidth, subHeight, true);
+            final Object transfer = window.getPixels(currentRaster, x, y, subWidth, subHeight, Window.DIRECT);
             assert transfer == data;
             return;
         }
@@ -1227,6 +1361,8 @@ public class PixelIterator {
          * At this point, we determined that the window is overlapping two or more tiles.
          * We will need more variables for iterating over the tiles around `currentRaster`.
          */
+        Raster raster    = currentRaster;
+        int mode         = Window.TRANSFER;
         int destOffset   = 0;                       // Index in `window` array where to copy the sample values.
         int subX         = 0;                       // Upper-left corner of a sub-window inside the window.
         int subY         = 0;
@@ -1236,7 +1372,7 @@ public class PixelIterator {
         final int rewind = subEndX;
         for (;;) {
             if (subWidth > 0 && subHeight > 0) {
-                final Object transfer = window.getPixels(raster, x + subX, y + subY, subWidth, subHeight, false);
+                final Object transfer = window.getPixels(raster, x + subX, y + subY, subWidth, subHeight, mode);
                 if (fullWidth) {
                     System.arraycopy(transfer, 0, data, destOffset, stride * subHeight);
                 } else {
@@ -1267,6 +1403,7 @@ public class PixelIterator {
                 subEndX  = rewind;
                 subX     = 0;                               // Move x position back to the window left border.
             }
+            mode       = Window.TRANSFER_FROM_OTHER;
             raster     = image.getTile(tileSubX, tileSubY);
             destOffset = (subY * windowWidth + subX) * numBands;
             subWidth   = Math.min(windowWidth,  subEndX) - subX;
@@ -1284,15 +1421,13 @@ public class PixelIterator {
         if (image == null) {
             tileX = 0;
             tileY = 0;
-            currentUpperX = upperX;
-            currentUpperY = upperY;
         } else {
             tileX = tileLowerX - 1;     // Note: no need for decrementExact(…) because already checked by constructor.
             tileY = tileLowerY;
+            currentLowerX = lowerX;
             currentUpperX = lowerX;     // Really `lower`, so the position is the tile before the first tile.
             currentUpperY = lowerY;
         }
-        currentLowerX = lowerX;
         x = lowerX - 1;                 // Set to the position before first pixel.
         y = lowerY;
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
index b0af678..75bbd36 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
@@ -85,7 +85,7 @@ final class StatisticsCalculator extends AnnotatedImage {
     private void compute(final Statistics[] accumulator, final PixelIterator it) {
         double[] samples = null;
         while (it.next()) {
-            if (areaOfInterest == null || it.isInside(areaOfInterest)) {
+            if (areaOfInterest == null || areaOfInterest.contains(it.x, it.y)) {
                 samples = it.getPixel(samples);                 // Get values in all bands.
                 for (int i=0; i<accumulator.length; i++) {
                     accumulator[i].accept(samples[i]);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/WritablePixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/WritablePixelIterator.java
index 1dc30e9..6ee7598 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/WritablePixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/WritablePixelIterator.java
@@ -266,6 +266,13 @@ public class WritablePixelIterator extends PixelIterator implements Closeable {
     }
 
     /**
+     * The current raster destination raster, or {@code null} if none.
+     */
+    final WritableRaster destination() {
+        return destRaster;
+    }
+
+    /**
      * Invoked by {@link #fetchTile()} when iteration switch to a new tile.
      *
      * @return if the new writable tile can also be used for reading, that tile. Otherwise {@code null}.
@@ -284,7 +291,8 @@ public class WritablePixelIterator extends PixelIterator implements Closeable {
      * This method does nothing if the iterator is read-only.
      */
     @Override
-    final void releaseTile() {
+    void releaseTile() {
+        super.releaseTile();
         if (destination != null && destRaster != null) {
             destRaster = null;
             destination.releaseWritableTile(tileX, tileY);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
index ee67304..17d8c1d 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
@@ -98,7 +98,8 @@ public final strictfp class ResampledGridCoverageTest extends TestCase {
                 width, height,                  // Image size
                 width, height,                  // Tile size
                 random.nextInt(32) - 10,        // minTileX
-                random.nextInt(32) - 10);       // minTileY
+                random.nextInt(32) - 10,        // minTileY
+                random.nextBoolean());          // Banded or interleaved sample model
         image.validate();
         image.initializeAllTiles(0);
         final int x = random.nextInt(32) - 10;
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ReshapedImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ReshapedImageTest.java
index 316e855..b155e6c 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ReshapedImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ReshapedImageTest.java
@@ -154,7 +154,8 @@ public final strictfp class ReshapedImageTest extends TestCase {
         width     = numXTiles * TILE_WIDTH;
         height    = numYTiles * TILE_HEIGHT;
         final TiledImageMock data = new TiledImageMock(DataBuffer.TYPE_USHORT, 1, dataMinX, dataMinY,
-                                        width, height, TILE_WIDTH, TILE_HEIGHT, minTileX, minTileY);
+                                        width, height, TILE_WIDTH, TILE_HEIGHT, minTileX, minTileY,
+                                        random.nextBoolean());  // Banded or interleaved sample model
         data.validate();
         data.initializeAllTiles(0);
         /*
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
index 4575edc..ca9b0ac 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
@@ -67,7 +67,7 @@ public final strictfp class BandSelectImageTest extends TestCase {
      * @param  icm          {@code true} for using index color model, or {@code false} for scaled color model.
      */
     private void createImage(final int numBands, final int checkedBand, final boolean icm) {
-        image = new TiledImageMock(DataBuffer.TYPE_BYTE, numBands, 0, 0, WIDTH, HEIGHT, WIDTH, HEIGHT, 0, 0);
+        image = new TiledImageMock(DataBuffer.TYPE_BYTE, numBands, 0, 0, WIDTH, HEIGHT, WIDTH, HEIGHT, 0, 0, false);
         image.initializeAllTiles(checkedBand);
         final Random random = new Random();
         for (int i=0; i<numBands; i++) {
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandedIteratorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandedIteratorTest.java
new file mode 100644
index 0000000..3221552
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandedIteratorTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.Dimension;
+import java.awt.Rectangle;
+import java.awt.image.DataBuffer;
+import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
+import org.opengis.coverage.grid.SequenceType;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link BandedIterator} on floating point values.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ */
+public final strictfp class BandedIteratorTest extends PixelIteratorTest {
+    /**
+     * Creates a new test case.
+     */
+    public BandedIteratorTest() {
+        super(DataBuffer.TYPE_FLOAT);
+        useBandedSampleModel = true;
+    }
+
+    /**
+     * Creates a {@code PixelIterator} for a sub-area of given raster.
+     */
+    @Override
+    void createPixelIterator(final WritableRaster raster, final Rectangle subArea) {
+        final int scanlineStride = PixelIterator.Builder.getScanlineStride(raster.getSampleModel());
+        assertTrue(scanlineStride >= raster.getWidth());
+        iterator = new BandedIterator(raster, isWritable ? raster : null, subArea, null, scanlineStride);
+        assertEquals("getIterationOrder()", SequenceType.LINEAR, iterator.getIterationOrder().get());
+        assertEquals("isWritable", isWritable, iterator.isWritable());
+    }
+
+    /**
+     * Creates a {@code PixelIterator} for a sub-area of given image.
+     */
+    @Override
+    void createPixelIterator(final WritableRenderedImage image, final Rectangle subArea) {
+        final int scanlineStride = PixelIterator.Builder.getScanlineStride(image.getSampleModel());
+        assertTrue(scanlineStride >= image.getTileWidth());
+        iterator = new BandedIterator(image, isWritable ? image : null, subArea, null, scanlineStride);
+        assertEquals("isWritable", isWritable, iterator.isWritable());
+    }
+
+    /**
+     * Creates a {@code PixelIterator} for a window in the given image.
+     * The iterator shall be assigned to the {@link #iterator} field.
+     */
+    @Override
+    void createWindowIterator(final WritableRenderedImage image, final Dimension window) {
+        final int scanlineStride = PixelIterator.Builder.getScanlineStride(image.getSampleModel());
+        assertTrue(scanlineStride >= image.getTileWidth());
+        iterator = new BandedIterator(image, isWritable ? image : null, null, window, scanlineStride);
+        assertEquals("isWritable", isWritable, iterator.isWritable());
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
index df355f9..3b7727a 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
@@ -61,7 +61,8 @@ public final strictfp class BandedSampleConverterTest extends ImageTestCase {
                 TILE_WIDTH,
                 TILE_HEIGHT,
                 random.nextInt(20) - 10,        // minTileX
-                random.nextInt(20) - 10);       // minTileY
+                random.nextInt(20) - 10,        // minTileY
+                random.nextBoolean());          // Banded or interleaved sample model
         source.validate();
         source.initializeAllTiles(0);
         image = BandedSampleConverter.create(source, ImageLayout.DEFAULT, null,
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java
index 710e7ea..e1f320c 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java
@@ -54,7 +54,8 @@ public final strictfp class ImageCombinerTest extends ImageTestCase {
                  3,  4,                         // minX, minY
                 12,  8,                         // width, height
                  4,  4,                         // tileWidth, tileHeight
-                -2,  3);                        // minTileX, minTileY
+                -2,  3,                         // minTileX, minTileY
+                false);
         /*
          * An image intersecting the destination, with a small part outside.
          * Intentionally use a different data type and different tile layout.
@@ -64,7 +65,8 @@ public final strictfp class ImageCombinerTest extends ImageTestCase {
                  5,  3,                         // minX, minY
                  9,  6,                         // width, height
                  3,  2,                         // tileWidth, tileHeight
-                 5,  9);                        // minTileX, minTileY
+                 5,  9,                         // minTileX, minTileY
+                false);
 
         source.validate();
         source.initializeAllTiles(0);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
index 373e020..bf67736 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
@@ -24,6 +24,7 @@ import java.awt.image.DataBuffer;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.PixelInterleavedSampleModel;
 import java.awt.image.Raster;
+import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import java.nio.FloatBuffer;
@@ -115,6 +116,11 @@ public strictfp class PixelIteratorTest extends TestCase {
     boolean isWritable;
 
     /**
+     * {@code true} for using {@link BandedSampleModel} instead of {@link PixelInterleavedSampleModel}.
+     */
+    boolean useBandedSampleModel;
+
+    /**
      * Creates a new test case for the given data type.
      *
      * @param  dataType  the raster or image data type as one of the {@link DataBuffer} constants.
@@ -157,8 +163,14 @@ public strictfp class PixelIteratorTest extends TestCase {
             subMaxY = StrictMath.min(ymax, subArea.y + subArea.height);
         }
         expected = new float[StrictMath.max(subMaxX - subMinX, 0) * StrictMath.max(subMaxY - subMinY, 0) * numBands];
-        final WritableRaster raster = Raster.createWritableRaster(new PixelInterleavedSampleModel(dataType,
-                width, height, numBands, width * numBands, ArraysExt.range(0, numBands)), new Point(xmin, ymin));
+        final SampleModel sm;
+        if (useBandedSampleModel) {
+            sm = new BandedSampleModel(dataType, width, height, numBands);
+        } else {
+            sm = new PixelInterleavedSampleModel(dataType, width, height, numBands,
+                        width * numBands, ArraysExt.range(0, numBands));
+        }
+        final WritableRaster raster = Raster.createWritableRaster(sm, new Point(xmin, ymin));
         /*
          * At this point, all data structures have been created an initialized to zero sample values.
          * Now fill the data structures with arbitrary values.
@@ -212,7 +224,8 @@ public strictfp class PixelIteratorTest extends TestCase {
             subMaxY = StrictMath.min(ymax, subArea.y + subArea.height);
         }
         expected = new float[StrictMath.max(subMaxX - subMinX, 0) * StrictMath.max(subMaxY - subMinY, 0) * numBands];
-        final TiledImageMock image = new TiledImageMock(dataType, numBands, xmin, ymin, width, height, tileWidth, tileHeight, minTileX, minTileY);
+        final TiledImageMock image = new TiledImageMock(dataType, numBands, xmin, ymin, width, height,
+                                        tileWidth, tileHeight, minTileX, minTileY, useBandedSampleModel);
         image.validate();
         /*
          * At this point, all data structures have been created an initialized to zero sample values.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/PlanarImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/PlanarImageTest.java
index 3f82965..112f3e6 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/PlanarImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/PlanarImageTest.java
@@ -56,7 +56,8 @@ public final strictfp class PlanarImageTest extends TestCase {
                 TILE_WIDTH,
                 TILE_HEIGHT,
                 random.nextInt(20) - 10,        // minTileX
-                random.nextInt(20) - 10);       // minTileY
+                random.nextInt(20) - 10,        // minTileY
+                random.nextBoolean());          // Banded or interleaved sample model
         image.validate();
         image.initializeAllTiles(0);
         return image;
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
index 9502cbc..083387d 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
@@ -92,7 +92,8 @@ public final strictfp class ResampledImageTest extends TestCase {
                 tileWidth,
                 tileHeight,
                 random.nextInt(32) - 10,        // minTileX
-                random.nextInt(32) - 10);       // minTileY
+                random.nextInt(32) - 10,        // minTileY
+                random.nextBoolean());          // Banded or interleaved sample model
         image.validate();
         image.initializeAllTiles(0);
         image.setRandomValues(1, random, 1024);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index a0110e4..20c2c9b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -69,7 +69,8 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
                 TILE_WIDTH,
                 TILE_HEIGHT,
                 -3,                             // minTileX
-                +2);                            // minTileY
+                +2,                             // minTileY
+                false);
         image.initializeAllTiles(0);
         image.setRandomValues(1, new Random(), 1000);
         image.validate();
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
index 334656e..6dbc034 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
@@ -18,6 +18,7 @@ package org.apache.sis.image;
 
 import java.awt.Point;
 import java.awt.image.ColorModel;
+import java.awt.image.BandedSampleModel;
 import java.awt.image.ImagingOpException;
 import java.awt.image.PixelInterleavedSampleModel;
 import java.awt.image.Raster;
@@ -127,12 +128,14 @@ public final strictfp class TiledImageMock extends PlanarImage implements Writab
      * @param tileHeight   number of pixels along Y axis in a single tile of the image.
      * @param minTileX     minimum tile index in the X direction.
      * @param minTileY     minimum tile index in the Y direction.
+     * @param banded       whether to use {@link BandedSampleModel} instead of {@link PixelInterleavedSampleModel}.
      */
     public TiledImageMock(final int dataType,  final int numBands,
                           final int minX,      final int minY,
                           final int width,     final int height,
                           final int tileWidth, final int tileHeight,
-                          final int minTileX,  final int minTileY)
+                          final int minTileX,  final int minTileY,
+                          final boolean banded)
     {
         this.minX        = minX;
         this.minY        = minY;
@@ -145,8 +148,9 @@ public final strictfp class TiledImageMock extends PlanarImage implements Writab
         this.numXTiles   = Numerics.ceilDiv(width,  tileWidth);
         this.numYTiles   = Numerics.ceilDiv(height, tileHeight);
         this.tiles       = new WritableRaster[numXTiles * numYTiles];
-        this.sampleModel = new PixelInterleavedSampleModel(dataType, tileWidth, tileHeight,
-                                numBands, tileWidth * numBands, ArraysExt.range(0, numBands));
+        this.sampleModel = banded ? new BandedSampleModel(dataType, tileWidth, tileHeight, numBands) :
+                          new PixelInterleavedSampleModel(dataType, tileWidth, tileHeight, numBands,
+                                 StrictMath.multiplyExact(numBands, tileWidth), ArraysExt.range(0, numBands));
     }
 
     /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index d2a500d..095cf62 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -85,6 +85,7 @@ import org.junit.runners.Suite;
     org.apache.sis.image.ComputedImageTest.class,
     org.apache.sis.image.PixelIteratorTest.class,
     org.apache.sis.image.LinearIteratorTest.class,
+    org.apache.sis.image.BandedIteratorTest.class,
     org.apache.sis.image.StatisticsCalculatorTest.class,
     org.apache.sis.image.BandSelectImageTest.class,
     org.apache.sis.image.InterpolationTest.class,


Mime
View raw message