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 <[email protected]> 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,
