This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new ed74a09dc9 When doing an aggregation of "band select", verify if the operations cancel each other. ed74a09dc9 is described below commit ed74a09dc99377677e4a1b0d6c3ccb4f0e29e991 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Apr 9 22:54:58 2023 +0200 When doing an aggregation of "band select", verify if the operations cancel each other. --- .../org/apache/sis/image/BandAggregateImage.java | 22 ++-- .../java/org/apache/sis/image/BandSelectImage.java | 28 +++++- .../java/org/apache/sis/image/ComputedImage.java | 18 ++++ .../java/org/apache/sis/image/ImageProcessor.java | 4 +- .../org/apache/sis/image/MultiSourceImage.java | 19 +--- .../org/apache/sis/image/MultiSourceLayout.java | 5 +- .../sis/internal/coverage/MultiSourceArgument.java | 111 +++++++++++++-------- 7 files changed, 136 insertions(+), 71 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java index 7ffbd60648..2e672a1bb1 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java @@ -73,8 +73,8 @@ class BandAggregateImage extends MultiSourceImage { } else { image = new BandAggregateImage(layout, colorizer, allowSharing, parallel); } - if (image.filteredSources.length == 1) { - final RenderedImage c = image.filteredSources[0]; + if (image.getNumSources() == 1) { + final RenderedImage c = image.getSource(); if (image.colorModel == null) { return c; } @@ -92,8 +92,8 @@ class BandAggregateImage extends MultiSourceImage { * @param layout pixel and tile coordinate spaces of this image, together with sample model. * @param colorizer provider of color model to use for this image, or {@code null} for automatic. */ - BandAggregateImage(final MultiSourceLayout layout, final Colorizer colorizer, - final boolean allowSharing, final boolean parallel) + private BandAggregateImage(final MultiSourceLayout layout, final Colorizer colorizer, + final boolean allowSharing, final boolean parallel) { super(layout, colorizer, parallel); this.allowSharing = allowSharing; @@ -120,7 +120,7 @@ class BandAggregateImage extends MultiSourceImage { if (allowSharing) { final BandSharing sharing = BandSharing.create((BandedSampleModel) sampleModel); if (sharing != null) { - tile = shared = sharing.createRaster(tileToPixel(tileX, tileY), filteredSources); + tile = shared = sharing.createRaster(tileToPixel(tileX, tileY), getSourceArray()); } } /* @@ -131,8 +131,9 @@ class BandAggregateImage extends MultiSourceImage { tile = createTile(tileX, tileY); } int band = 0; - for (int i=0; i < filteredSources.length; i++) { - final RenderedImage source = filteredSources[i]; + final int n = getNumSources(); + for (int i=0; i<n; i++) { + final RenderedImage source = getSource(i); final int numBands = ImageUtilities.getNumBands(source); if (shared == null || shared.needCopy(i)) { final Rectangle aoi = tile.getBounds(); @@ -172,7 +173,7 @@ class BandAggregateImage extends MultiSourceImage { public WritableRaster getWritableTile(final int tileX, final int tileY) { final WritableRaster tile = (WritableRaster) getTile(tileX, tileY); if (tile instanceof BandSharedRaster) { - ((BandSharedRaster) tile).acquireWritableTiles(filteredSources); + ((BandSharedRaster) tile).acquireWritableTiles(getSourceArray()); } try { markTileWritable(tileX, tileY, true); @@ -210,8 +211,9 @@ class BandAggregateImage extends MultiSourceImage { public void setData(final Raster tile) { final BandSharedRaster shared = (tile instanceof BandSharedRaster) ? (BandSharedRaster) tile : null; int band = 0; - for (int i=0; i < filteredSources.length; i++) { - final var target = (WritableRenderedImage) filteredSources[i]; + final int n = getNumSources(); + for (int i=0; i<n; i++) { + final var target = (WritableRenderedImage) getSource(i); final int numBands = ImageUtilities.getNumBands(target); if (shared == null || shared.needCopy(i)) { final Rectangle aoi = tile.getBounds(); diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java index 60fbb426ec..f8fb8bc318 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java @@ -84,19 +84,41 @@ class BandSelectImage extends SourceAlignedImage { ensureCompatible(cm); } + /** + * If the given image is already a band select operation, returns the original source + * and updates the band indices. If there is no replacement, then the {@code image} + * argument is returned as-is and the {@code bands} array shall be unmodified. + * + * @param image the image to check. + * @param bands the band to select in the specified source. + * Will be updated in-place if the source is replaced. + * @return the source of the image, or {@code image} if no replacement. + */ + static RenderedImage unwrap(final RenderedImage image, final int[] bands) { + if (image instanceof BandSelectImage) { + final var select = (BandSelectImage) image; + for (int i=0; i<bands.length; i++) { + bands[i] = select.bands[bands[i]]; + } + return select.getSource(); + } + return image; + } + /** * Creates a new "band select" operation for the given source. * * @param source the image in which to select bands. - * @param bands the bands to select. Should be a clone of user-specified argument - * for protection against user changes in the given array. + * @param bands the bands to select. Shall be a clone of user-specified argument + * because it may be modified in-place. */ - static RenderedImage create(final RenderedImage source, final int... bands) { + static RenderedImage create(RenderedImage source, int... bands) { final int numBands = ImageUtilities.getNumBands(source); if (bands.length == numBands && ArraysExt.isRange(0, bands)) { return source; } ArgumentChecks.ensureNonEmptyBounded("bands", false, 0, numBands - 1, bands); + source = unwrap(source, bands); final ColorModel cm = ColorModelFactory.createSubset(source.getColorModel(), bands); /* * If the image is an instance of `BufferedImage`, create the subset immediately diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java index c991a18cb4..792bb06d08 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java @@ -337,6 +337,24 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { return sources[0]; } + /** + * Returns the sources as an array. + * A future version may remove this method if Java allows unmodifiable arrays. + */ + final RenderedImage[] getSourceArray() { + return sources.clone(); + } + + /** + * Returns the number of sources, or 0 if none or unknown. + * This is the size of the vector returned by {@link #getSources()}. + * + * @return number of sources, or 0 if none or unknown. + */ + final int getNumSources() { + return (sources != null) ? sources.length : 0; + } + /** * Returns the source at the given index. * diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java index 34d0e60ca3..eebd30af21 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java @@ -1245,7 +1245,7 @@ public class ImageProcessor implements Cloneable { if (source == null || source instanceof BufferedImage || source instanceof TiledImage) { return source; } - while (source instanceof PrefetchedImage) { + if (source instanceof PrefetchedImage) { source = ((PrefetchedImage) source).source; } final boolean parallel; @@ -1254,7 +1254,7 @@ public class ImageProcessor implements Cloneable { parallel = parallel(source); errorListener = errorHandler; } - final PrefetchedImage image = new PrefetchedImage(source, areaOfInterest, errorListener, parallel); + final var image = new PrefetchedImage(source, areaOfInterest, errorListener, parallel); return image.isEmpty() ? source : image; } diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java index ed67b5446c..743e1580aa 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java @@ -17,11 +17,9 @@ package org.apache.sis.image; import java.awt.Point; -import java.util.Arrays; import java.util.Objects; import java.awt.Rectangle; import java.awt.image.ColorModel; -import java.awt.image.RenderedImage; import java.awt.image.WritableRenderedImage; import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.util.Disposable; @@ -41,13 +39,6 @@ import org.apache.sis.util.Disposable; * @since 1.4 */ abstract class MultiSourceImage extends WritableComputedImage { - /** - * The source images, potentially with a preprocessing applied. - * Those sources may be different than {@link #getSources()} for example with the - * application of a "band select" operation for retaining only the bands needed. - */ - protected final RenderedImage[] filteredSources; - /** * Color model of this image. * @@ -82,7 +73,7 @@ abstract class MultiSourceImage extends WritableComputedImage { * @param parallel whether parallel computation is allowed. */ MultiSourceImage(final MultiSourceLayout layout, final Colorizer colorizer, final boolean parallel) { - super(layout.sampleModel, layout.sources); + super(layout.sampleModel, layout.filteredSources); final Rectangle r = layout.domain; minX = r.x; minY = r.y; @@ -90,7 +81,6 @@ abstract class MultiSourceImage extends WritableComputedImage { height = r.height; minTileX = layout.minTileX; minTileY = layout.minTileY; - filteredSources = layout.filteredSources; colorModel = layout.createColorModel(colorizer); ensureCompatible(colorModel); this.parallel = parallel; @@ -129,7 +119,7 @@ abstract class MultiSourceImage extends WritableComputedImage { * tile indices for each source because the tile numbering may not be the same. */ final Rectangle aoi = ImageUtilities.tilesToPixels(this, tiles); - return new MultiSourcePrefetch(filteredSources, aoi).run(parallel); + return new MultiSourcePrefetch(getSourceArray(), aoi).run(parallel); } /** @@ -137,7 +127,7 @@ abstract class MultiSourceImage extends WritableComputedImage { */ @Override public int hashCode() { - return hashCodeBase() + 37 * (Arrays.hashCode(filteredSources) + 31 * Objects.hashCode(colorModel)); + return hashCodeBase() + 37 * Objects.hashCode(colorModel); } /** @@ -151,8 +141,7 @@ abstract class MultiSourceImage extends WritableComputedImage { minTileX == other.minTileX && minTileY == other.minTileY && getBounds().equals(other.getBounds()) && - Objects.equals(colorModel, other.colorModel) && - Arrays.equals(filteredSources, other.filteredSources); + Objects.equals(colorModel, other.colorModel); } return false; } diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java index afa30b76e5..f649382dfe 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java @@ -59,7 +59,7 @@ final class MultiSourceLayout extends ImageLayout { * The source images. This is a copy of the user-specified array, * except that images associated to an empty set of bands are discarded. */ - final RenderedImage[] sources; + private final RenderedImage[] sources; /** * The source images with only the user-specified bands. @@ -129,6 +129,7 @@ final class MultiSourceLayout extends ImageLayout { static MultiSourceLayout create(RenderedImage[] sources, int[][] bandsPerSource, boolean allowSharing) { final var aggregate = new MultiSourceArgument<RenderedImage>(sources, bandsPerSource); aggregate.identityAsNull(); + aggregate.unwrap(BandSelectImage::unwrap); aggregate.validate(ImageUtilities::getNumBands); sources = aggregate.sources(); @@ -241,7 +242,7 @@ final class MultiSourceLayout extends ImageLayout { RenderedImage source = sources[i]; final int[] bands = bandsPerSource[i]; if (bands != null) { - source = BandSelectImage.create(source, bands); + source = BandSelectImage.create(source, bands.clone()); } filteredSources[i] = source; } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java index 27d82af545..be9fa9b6fb 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; import java.util.function.Function; +import java.util.function.BiFunction; import java.util.function.ToIntFunction; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridGeometry; @@ -40,8 +41,9 @@ import org.apache.sis.util.ComparisonMode; * <p>Instances of this class should be short-lived. * They are used only the time needed for constructing an image or coverage operation.</p> * - * @todo Verify if a source is itself an aggregated image or coverage, - * and provide a way to get a flattened view of such nested aggregations. + * <p>This class can optionally verify if a source is itself an aggregated image or coverage. + * This is done by an "unwrapper", which should be specified in order to provide a flattened + * view of nested aggregations.</p> * * @author Martin Desruisseaux (Geomatys) * @version 1.4 @@ -72,6 +74,13 @@ public final class MultiSourceArgument<S> { */ private boolean identityAsNull; + /** + * A function which, given an (image, bands) pair, may return the source of the image. + * If the source is returned, then the bands array is updated with the indices in that + * source. + */ + private BiFunction<S,int[],S> unwrapper; + /** * Union of all selected bands in all specified sources, or {@code null} if not applicable. */ @@ -117,6 +126,19 @@ public final class MultiSourceArgument<S> { identityAsNull = true; } + /** + * Specifies a function which, given an (image, bands) pair, may return the source of the image. + * If the source is returned, then the bands array is updated with the indices in that source. + * The function shall modify the given {@code int[]} in-place and return the new source, + * or return the {@code S} value unchanged if no unwrapping has been done. + * + * @param filter the function to invoke for getting the source of an image or coverage. + */ + public void unwrap(final BiFunction<S,int[],S> filter) { + if (validated) throw new IllegalStateException(); + unwrapper = filter; + } + /** * Clones and validates the arguments given to the constructor. * @@ -147,8 +169,8 @@ public final class MultiSourceArgument<S> { * * <p>Exactly one of {@code getter} or {@code count} arguments shall be non-null.</p> * - * @param getter method to invoke for getting the list of sample dimensions. - * @param counter method to invoke for counting the number of bands in a source. + * @param getter method to invoke for getting the list of sample dimensions. + * @param counter method to invoke for counting the number of bands in a source. * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. */ private void validate(final Function<S, List<SampleDimension>> getter, final ToIntFunction<S> counter) { @@ -183,56 +205,67 @@ public final class MultiSourceArgument<S> { // Note that the source is allowed to be null in this particular case. continue; } - final S source = sources[i]; - sources[filteredCount] = source; + S source = sources[i]; ArgumentChecks.ensureNonNullElement("sources", i, source); /* * Get the number of bands, or optionally the bands themselves. * This information is required before to validate arguments. */ - final List<SampleDimension> bands; - final int n; - if (getter != null) { - bands = getter.apply(source); - n = bands.size(); - } else { - bands = null; - n = counter.applyAsInt(source); + List<SampleDimension> sourceBands; + int numSourceBands; + RangeArgument range; + do { + if (getter != null) { + sourceBands = getter.apply(source); + numSourceBands = sourceBands.size(); + } else { + sourceBands = null; + numSourceBands = counter.applyAsInt(source); + } + range = RangeArgument.validate(numSourceBands, selected, null); + selected = range.getSelectedBands(); + /* + * Verify if the source is a nested aggregation, in order to get a flattened view. + * This replacement must be done before the optimization for consecutive images. + */ + } while (unwrapper != null && source != (source = unwrapper.apply(source, selected))); + /* + * Store now the sample dimensions before the `selected` array get modified. + */ + if (ranges != null) { + for (int b : selected) { + ranges.add(sourceBands.get(b)); + } } /* - * If the next source is the same than the source in current iteration, merge the bands together. + * If the source in current iteration is the same than the previous source, merge the bands together. * The `BandAggregateGridResource.read(…)` implementation relies on that optimization. */ - final int next = i+1; - if (next < sourceCount && sources[next] == source) { - final int[] nextBands = bandsPerSource[next]; - ArgumentChecks.ensureNonNullElement("bandsPerSource", i, selected); - ArgumentChecks.ensureNonNullElement("bandsPerSource", next, nextBands); - final int[] merged = Arrays.copyOf(selected, selected.length + nextBands.length); - System.arraycopy(nextBands, 0, merged, selected.length, nextBands.length); - bandsPerSource[next] = merged; - bandsPerSource[i] = ArraysExt.EMPTY_INT; - continue; + if (filteredCount > 0 && sources[filteredCount-1] == source) { + final int[] previous = bandsPerSource[--filteredCount]; + ArgumentChecks.ensureNonNullElement("bandsPerSource", filteredCount, previous); + ArgumentChecks.ensureNonNullElement("bandsPerSource", filteredCount+1, selected); + numBands -= previous.length; // Rollback the value added in previous iteration. + + final int[] merged = Arrays.copyOf(previous, previous.length + selected.length); + System.arraycopy(selected, 0, merged, previous.length, selected.length); + range = RangeArgument.validate(numSourceBands, merged, null); + selected = range.getSelectedBands(); } /* - * Validate the `bandsPerSource` argument given at construction time. - * Then store a copy of that argument. + * Store a copy of the `bandsPerSource` argument given at construction time. + * Its validation has been done by `RangeArgument.validate(…)` above calls. */ - final var range = RangeArgument.validate(n, selected, null); if (range.isIdentity()) { - selected = (pool != null) ? pool.computeIfAbsent(n, (k) -> ArraysExt.range(0, k)) : null; - if (ranges != null) { - ranges.addAll(bands); - } - } else { - selected = range.getSelectedBands(); - if (ranges != null) { - for (int b : selected) { - ranges.add(bands.get(b)); - } + if (pool != null) { + int[] previous = pool.putIfAbsent(numSourceBands, selected); + if (previous != null) selected = previous; + } else { + selected = null; } } - bandsPerSource[filteredCount++] = selected; + bandsPerSource[filteredCount] = selected; + sources[filteredCount++] = source; numBands += range.getNumBands(); } sources = ArraysExt.resize(sources, filteredCount);