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 5f36de44f40febbc4c8f4b08a92427f63d49fc9a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Apr 10 18:33:18 2023 +0200 "Band select" on a band aggregation should be able to return the original component. Aggregation of aggregations should use a flattened list or source images. --- .../org/apache/sis/image/BandAggregateImage.java | 90 ++++++++-- .../java/org/apache/sis/image/BandSelectImage.java | 47 +++-- .../java/org/apache/sis/image/ImageProcessor.java | 2 +- .../org/apache/sis/image/MultiSourceImage.java | 4 +- .../org/apache/sis/image/MultiSourceLayout.java | 4 +- .../sis/internal/coverage/MultiSourceArgument.java | 190 ++++++++++++++------- .../apache/sis/image/BandAggregateImageTest.java | 48 +++++- .../org/apache/sis/image/BandSelectImageTest.java | 14 ++ .../main/java/org/apache/sis/util/ArraysExt.java | 2 +- 9 files changed, 301 insertions(+), 100 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 2e672a1bb1..f1d06ca4b4 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 @@ -25,6 +25,7 @@ import java.awt.image.WritableRaster; import java.awt.image.WritableRenderedImage; import org.apache.sis.util.ArraysExt; import org.apache.sis.internal.coverage.j2d.ImageUtilities; +import org.apache.sis.internal.coverage.MultiSourceArgument; /** @@ -51,20 +52,92 @@ class BandAggregateImage extends MultiSourceImage { */ private final boolean allowSharing; + /* + * The method declaration order below is a little bit unusual, + * but it follows an execution order. + */ + + /** + * Returns potentially deeper sources than the user-supplied image. + * This method unwraps {@link BandSelectImage} for making possible to detect that two + * consecutive images are actually the same image, with only different bands selected. + * + * @param unwrapper a handler where to supply the result of an aggregate decomposition. + */ + static void unwrap(final MultiSourceArgument<RenderedImage>.Unwrapper unwrapper) { + RenderedImage source = unwrapper.source; + int[] bands = unwrapper.bands; + while (source instanceof ImageAdapter) { + source = ((ImageAdapter) source).source; + } + if (source instanceof BandSelectImage) { + final var select = (BandSelectImage) source; + bands = select.getSourceBands(bands); + source = select.getSource(); + } + if (source instanceof BandAggregateImage) { + ((BandAggregateImage) source).subset(bands, null, unwrapper); + } else if (source != unwrapper.source) { + unwrapper.apply(new RenderedImage[] {source}, new int[][] {bands}); + } + } + + /** + * Decomposes this aggregate for the specified subset of bands. + * The result can be used either for creating a new aggregate, + * or consumed by {@code unwrapper} for flattening an aggregation. + * + * <p>This is a kind of constructor, but for an image derived from this instance. + * The returned image may be one of the source images for simplifying the result.</p> + * + * @param bands the bands to keep. + * @param colors the colors to apply, or {@code null} if unspecified. + * @param unwrapper where to provide decomposition result, or {@code null} for creating the image immediately. + * @return an image with a subset of the bands of this image, or {@code null} if {@code unwrapper} was non-null. + */ + final RenderedImage subset(final int[] bands, final ColorModel colors, + final MultiSourceArgument<RenderedImage>.Unwrapper unwrapper) + { + final RenderedImage[] sources = new RenderedImage[bands.length]; + final int[][] bandsPerSource = new int[bands.length][]; + int lower=0, upper=0, sourceIndex = -1; + RenderedImage source = null; + for (int i=0; i<bands.length; i++) { + final int band = bands[i]; + if (band < lower) { + lower = upper = 0; + sourceIndex = -1; + } + while (band >= upper) { + source = getSource(++sourceIndex); + lower = upper; + upper += ImageUtilities.getNumBands(source); + } + sources[i] = source; + bandsPerSource[i] = new int[] {band - lower}; + } + if (unwrapper != null) { + unwrapper.apply(sources, bandsPerSource); + return null; + } + return create(sources, bandsPerSource, (colors != null) ? Colorizer.forInstance(colors) : null, false, allowSharing, parallel); + } + /** * Creates a new aggregation of bands. * * @param sources images to combine, in order. * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements. * @param colorizer provider of color model to use for this image, or {@code null} for automatic. + * @param forceColors whether to force application of {@code colorizer} when a source image is returned. * @param allowSharing whether to allow the sharing of data buffers (instead of copying) if possible. * @param parallel whether parallel computation is allowed. * @throws IllegalArgumentException if there is an incompatibility between some source images * or if some band indices are duplicated or outside their range of validity. * @return the band aggregate image. */ - static RenderedImage create(final RenderedImage[] sources, final int[][] bandsPerSource, - final Colorizer colorizer, final boolean allowSharing, final boolean parallel) + static RenderedImage create(final RenderedImage[] sources, final int[][] bandsPerSource, final Colorizer colorizer, + final boolean forceColors, final boolean allowSharing, final boolean parallel) { final var layout = MultiSourceLayout.create(sources, bandsPerSource, allowSharing); final BandAggregateImage image; @@ -74,16 +147,13 @@ class BandAggregateImage extends MultiSourceImage { image = new BandAggregateImage(layout, colorizer, allowSharing, parallel); } if (image.getNumSources() == 1) { - final RenderedImage c = image.getSource(); - if (image.colorModel == null) { - return c; - } - final ColorModel cm = c.getColorModel(); - if (cm == null || image.colorModel.equals(cm)) { - return c; + RenderedImage source = image.getSource(); + if ((forceColors && colorizer != null)) { + source = RecoloredImage.applySameColors(source, image); } + return source; } - return image; + return ImageProcessor.unique(image); } /** 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 f8fb8bc318..7dfd7c1279 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 @@ -85,32 +85,28 @@ class BandSelectImage extends SourceAlignedImage { } /** - * 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. + * Returns the indices of bands in the source image for the given bands in this image. + * A reference to the given array will be returned if the band indices are the same. * - * @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. + * @param bands the band to select in this image. + * @return the bands to select in source image. + * + * @see #getSource() */ - 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(); + final int[] getSourceBands(final int[] subset) { + final int[] select = new int[subset.length]; + for (int i=0; i<subset.length; i++) { + select[i] = bands[subset[i]]; } - return image; + return Arrays.equals(subset, select) ? subset : select; } /** * 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. Shall be a clone of user-specified argument - * because it may be modified in-place. + * @param bands the bands to select. Not cloned in order to share common arrays when possible. + * If that array instance was user-supplied, then it should be cloned by caller. */ static RenderedImage create(RenderedImage source, int... bands) { final int numBands = ImageUtilities.getNumBands(source); @@ -118,8 +114,23 @@ class BandSelectImage extends SourceAlignedImage { return source; } ArgumentChecks.ensureNonEmptyBounded("bands", false, 0, numBands - 1, bands); - source = unwrap(source, bands); final ColorModel cm = ColorModelFactory.createSubset(source.getColorModel(), bands); + /* + * Since this operation applies its own ColorModel anyway, skip operation that was doing nothing else + * than changing the color model. Operations adding properties such as stastics are kept because this + * class can inherit some of them (see `REDUCED_PROPERTIES`). + */ + if (source instanceof RecoloredImage) { + source = ((RecoloredImage) source).source; + } + if (source instanceof BandSelectImage) { + final var select = (BandSelectImage) source; + bands = select.getSourceBands(bands); + source = select.getSource(); + } + if (source instanceof BandAggregateImage) { + return ((BandAggregateImage) source).subset(bands, cm, null); + } /* * If the image is an instance of `BufferedImage`, create the subset immediately * (reminder: this operation will not copy pixel data). It allows us to return a 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 eebd30af21..ad9b1fee78 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 @@ -1004,7 +1004,7 @@ public class ImageProcessor implements Cloneable { colorizer = this.colorizer; parallel = executionMode != Mode.SEQUENTIAL; } - return unique(BandAggregateImage.create(sources, bandsPerSource, colorizer, true, parallel)); + return BandAggregateImage.create(sources, bandsPerSource, colorizer, true, true, parallel); } /** 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 743e1580aa..9fd47dc9c1 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 @@ -44,7 +44,7 @@ abstract class MultiSourceImage extends WritableComputedImage { * * @see #getColorModel() */ - protected final ColorModel colorModel; + private final ColorModel colorModel; /** * Domain of pixel coordinates. All images shall share the same pixel coordinate space, @@ -63,7 +63,7 @@ abstract class MultiSourceImage extends WritableComputedImage { /** * Whether parallel computation is allowed. */ - private final boolean parallel; + final boolean parallel; /** * Creates a new multi-sources image. 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 f649382dfe..7ce6ba198b 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 @@ -129,7 +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.unwrap(BandAggregateImage::unwrap); aggregate.validate(ImageUtilities::getNumBands); sources = aggregate.sources(); @@ -242,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.clone()); + source = BandSelectImage.create(source, bands); } 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 be9fa9b6fb..6685ed04a8 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 @@ -21,8 +21,8 @@ import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; +import java.util.function.Consumer; 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; @@ -41,8 +41,8 @@ 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> * - * <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 + * <p>This class can optionally verify if some sources are themselves aggregated images or coverages. + * This is done by an {@link #unwrap(Consumer)}, which should be invoked in order to get a flattened * view of nested aggregations.</p> * * @author Martin Desruisseaux (Geomatys) @@ -54,18 +54,19 @@ import org.apache.sis.util.ComparisonMode; */ public final class MultiSourceArgument<S> { /** - * The sources of sample dimensions with empty sources removed. - * After a {@code validate(…)} method has been invoked, this array become a - * (potentially modified) copy of the array argument given to the constructor. + * The user-specified sources, usually grid coverages or rendered images. + * This is initially a copy of the array specified at construction time. + * This array is modified in-place by {@code validate(…)} methods for + * removing empty sources and flattening nested aggregations. */ private S[] sources; /** - * Indices of selected sample dimensions for each source. - * After a {@code validate(…)} method has been invoked, this array become a - * (potentially modified) copy of the array argument given to the constructor, - * with the same length than {@link #sources} and all elements themselves copied. + * Indices of selected bands or sample dimensions for each source. + * The length of this array must be always equal to the {@link #sources} array length. * The array is non-null but may contain {@code null} elements for meaning "all bands". + * This array is modified in-place by {@code validate(…)} methods for removing empty + * elements and flattening nested aggregations. */ private int[][] bandsPerSource; @@ -75,11 +76,9 @@ 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. + * A method which may decompose a source in a sequence of deeper sources associated with their bands to select. */ - private BiFunction<S,int[],S> unwrapper; + private Consumer<Unwrapper> unwrapper; /** * Union of all selected bands in all specified sources, or {@code null} if not applicable. @@ -105,15 +104,36 @@ public final class MultiSourceArgument<S> { /** * Prepares an argument validator for the given sources and bands arguments. - * One of the {@code validate(…)} method should be invoked after this constructor. + * The optional {@code bandsPerSource} argument specifies the bands to select in each source images. + * That array can be {@code null} for selecting all bands in all source images, + * or may contain {@code null} elements for selecting all bands of the corresponding image. + * An empty array element (i.e. zero band to select) discards the corresponding source image. + * + * <p>One of the {@code validate(…)} method shall be invoked after this constructor.</p> * * @param sources the sources from which to get the sample dimensions. * @param bandsPerSource sample dimensions for each source. May contain {@code null} elements. */ - public MultiSourceArgument(final S[] sources, final int[][] bandsPerSource) { - this.sources = sources; + public MultiSourceArgument(S[] sources, int[][] bandsPerSource) { + /* + * Ensure that both arrays are non-null and have the same length. + * Copy those arrays because their content will be overwritten. + */ + ArgumentChecks.ensureNonEmpty("sources", sources); + final int n = sources.length; + if (bandsPerSource != null) { + if (bandsPerSource.length > n) { + throw new IllegalArgumentException(Errors.format( + Errors.Keys.TooManyCollectionElements_3, + "bandsPerSource", bandsPerSource.length, n)); + } + bandsPerSource = Arrays.copyOf(bandsPerSource, n); + } else { + bandsPerSource = new int[n][]; + } + this.sources = sources.clone(); this.bandsPerSource = bandsPerSource; - sourceOfGridToCRS = -1; + sourceOfGridToCRS = -1; } /** @@ -127,18 +147,93 @@ public final class MultiSourceArgument<S> { } /** - * 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. + * Specifies a method which, given a source, may decompose that source + * in a sequence of deeper sources associated with their bands to select. + * The consumer will be invoked for all sources specified to the constructor. + * If a source can be decomposed, then the specified consumer should invoke + * {@code apply(…)} on the given {@code Unwrapper} instance. * - * @param filter the function to invoke for getting the source of an image or coverage. + * @param filter the method to invoke for getting the sources of an image or coverage. */ - public void unwrap(final BiFunction<S,int[],S> filter) { + public void unwrap(final Consumer<Unwrapper> filter) { if (validated) throw new IllegalStateException(); unwrapper = filter; } + /** + * Asks to the {@linkplain #unwrapper} if the given source can be decomposed into deeper sources. + * + * @param index index of {@code source} in the {@link #sources} array. + * @param source the source to potentially unwrap. + * @param bands the bands to use in the source. Shall not be {@code null}. + * @return whether the source has been decomposed. + */ + private boolean unwrap(int index, S source, int[] bands) { + if (unwrapper == null) { + return false; + } + final Unwrapper handler = new Unwrapper(index, source, bands); + unwrapper.accept(handler); + return handler.done; + } + + /** + * Replace a user-supplied source by a deeper source with the bands to select. + * This is used for getting a flattened view of nested aggregations. + */ + public final class Unwrapper { + /** + * Index of {@link #source} in the {@link #sources} array. + */ + private final int index; + + /** + * The source to potentially unwrap. + */ + public final S source; + + /** + * The bands to use in the source (never {@code null}). + * This array shall not modified because it may be a reference to an internal array. + */ + public final int[] bands; + + /** + * Whether the source has been decomposed in deeper sources. + */ + private boolean done; + + /** + * Creates a new instance to be submitted to user-supplied {@link #unwrapper}. + */ + private Unwrapper(final int index, final S source, final int[] bands) { + this.index = index; + this.source = source; + this.bands = bands; + } + + /** + * Notifies the enclosing {@code MultiSourceArgument} that the {@linkplain #source} + * shall be replaced by deeper sources. The {@code componentBands} array specifies + * the bands to use for each source and shall take in account the {@link #bands} subset. + * + * @param components the deeper sources to use in replacement to {@link #source}. + * @param componentBands the bands to use in replacement for {@link #bands}. + */ + public void apply(final S[] components, final int[][] componentBands) { + final int n = components.length; + if (componentBands.length != n) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths)); + } + if (done) throw new IllegalStateException(); + sources = ArraysExt.insert(sources, index+1, n-1); + bandsPerSource = ArraysExt.insert(bandsPerSource, index+1, n-1); + System.arraycopy(components, 0, sources, index, n); + System.arraycopy(componentBands, 0, bandsPerSource, index, n); + done = true; + } + } + /** * Clones and validates the arguments given to the constructor. * @@ -174,47 +269,27 @@ public final class MultiSourceArgument<S> { * @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) { + final HashMap<Integer,int[]> pool = identityAsNull ? null : new HashMap<>(); + int filteredCount = 0; /* - * Ensure that both arrays are non-null and have the same length. - * Copy those arrays as their content may be overwritten. - */ - ArgumentChecks.ensureNonEmpty("sources", sources); - final int sourceCount = sources.length; - if (bandsPerSource != null) { - if (bandsPerSource.length > sourceCount) { - throw new IllegalArgumentException(Errors.format( - Errors.Keys.TooManyCollectionElements_3, - "bandsPerSource", bandsPerSource.length, sourceCount)); - } - bandsPerSource = Arrays.copyOf(bandsPerSource, sourceCount); - } else { - bandsPerSource = new int[sourceCount][]; - } - sources = sources.clone(); - /* - * Compute the number of sources and the total number of bands. * This loop ensures that all band indices are in their ranges of validity * with no duplicated value, then stores a copy of the band indices or null. * If an empty array of bands is specified, then the source is omitted. */ - final HashMap<Integer,int[]> pool = identityAsNull ? null : new HashMap<>(); - int filteredCount = 0; - for (int i=0; i<sourceCount; i++) { - int[] selected = bandsPerSource[i]; - if (selected != null && selected.length == 0) { - // Note that the source is allowed to be null in this particular case. - continue; - } - 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. - */ +next: for (int i=0; i<sources.length; i++) { // `sources.length` may change during the loop. + S source; + int[] selected; List<SampleDimension> sourceBands; int numSourceBands; RangeArgument range; do { + selected = bandsPerSource[i]; + if (selected != null && selected.length == 0) { + // Note that the source is allowed to be null in this particular case. + continue next; + } + source = sources[i]; + ArgumentChecks.ensureNonNullElement("sources", i, source); if (getter != null) { sourceBands = getter.apply(source); numSourceBands = sourceBands.size(); @@ -228,9 +303,10 @@ public final class MultiSourceArgument<S> { * 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))); + } while (unwrap(i, source, selected)); /* * Store now the sample dimensions before the `selected` array get modified. + * Should be done only after `RangeArgument.validate(…)` has been successful. */ if (ranges != null) { for (int b : selected) { diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java index ea7641b54d..144510cee8 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java @@ -70,6 +70,13 @@ public final class BandAggregateImageTest extends TestCase { allowSharing = true; // This is the default mode of `ImageProcessor`. } + /** + * Creates the band aggregate instance to test using current value of {@link #sourceImages}. + */ + private RenderedImage createBandAggregate() { + return BandAggregateImage.create(sourceImages, null, null, false, allowSharing, false); + } + /** * Tests the aggregation of two untiled images with forced copy of sample values. * This is the simplest case in this test class. @@ -95,7 +102,7 @@ public final class BandAggregateImageTest extends TestCase { im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray()); sourceImages = new RenderedImage[] {im1, im2}; - final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false); + final RenderedImage result = createBandAggregate(); assertNotNull(result); assertEquals(0, result.getMinTileX()); assertEquals(0, result.getMinTileY()); @@ -186,9 +193,8 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 1, 2, firstBanded); final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded); initializeAllTiles(im1, im2); - sourceImages = new RenderedImage[] {im1, im2}; - RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false); + RenderedImage result = createBandAggregate(); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); @@ -255,7 +261,7 @@ public final class BandAggregateImageTest extends TestCase { new int[] {1}, // Take second band of image 1. null, // Take all bands of image 2. new int[] {0} // Take first band of image 1. - }, null, allowSharing, false); + }, null, false, allowSharing, false); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); @@ -314,9 +320,8 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 4, 1, 3, 4, true); final TiledImageMock oneTile = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true); initializeAllTiles(tiled2x2, tiled4x1, oneTile); - sourceImages = new RenderedImage[] {tiled2x2, tiled4x1, oneTile}; - final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false); + final RenderedImage result = createBandAggregate(); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); @@ -382,9 +387,8 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2, 8, 8, 4, 4, 0, 0, true); final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12, 6, 6, 6, 0, 0, true); initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6); - sourceImages = new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6}; - RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, prefetch); + RenderedImage result = BandAggregateImage.create(sourceImages, null, null, false, allowSharing, prefetch); assertNotNull(result); assertEquals(4, result.getMinX()); assertEquals(2, result.getMinY()); @@ -418,6 +422,31 @@ public final class BandAggregateImageTest extends TestCase { } } + /** + * Tests aggregation of aggregated images. The result should be a flattened view. + * Opportunistically tests a "band select" operation after the aggregation. + */ + @Test + public void testNestedAggregation() { + final int minX = 7; + final int minY = -5; + final int width = 6; + final int height = 4; + final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 3, minX, minY, width, height, 3, 2, 1, 2, true); + final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 1, minX, minY, width, height, 3, 2, 3, 4, true); + final TiledImageMock im3 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 2, 2, 1, true); + initializeAllTiles(im1, im2, im3); + + RenderedImage result; + result = BandAggregateImage.create(new RenderedImage[] {im2, im3}, null, null, false, allowSharing, false); + result = BandAggregateImage.create(new RenderedImage[] {im1, result}, null, null, false, allowSharing, false); + assertArrayEquals(sourceImages, ((BandAggregateImage) result).getSourceArray()); + + assertSame(im1, BandSelectImage.create(result, 0, 1, 2)); + assertSame(im2, BandSelectImage.create(result, 3)); + assertSame(im3, BandSelectImage.create(result, 4, 5)); + } + /** * Initializes all bands of all input images to testing values. * The testing values are defined by a "BTYX" pattern where: @@ -429,7 +458,8 @@ public final class BandAggregateImageTest extends TestCase { * <li><var>X</var> is the <var>x</var> coordinate (column 0-based index) of the sample value relative to current tile.</li> * </ol> */ - private static void initializeAllTiles(final TiledImageMock... images) { + private void initializeAllTiles(final TiledImageMock... images) { + sourceImages = images; int band = 0; for (final TiledImageMock image : images) { final int numBands = image.getSampleModel().getNumBands(); 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 2a1d0be7ae..60db8f4b7d 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 @@ -231,4 +231,18 @@ public final class BandSelectImageTest extends TestCase { writable.setData(data); assertValuesEqual(writable.getData(), 0, expectedSampleValues()); } + + /** + * Tests a band select on an image which is already a band select. + * The nested operations should be simplified to a single band select operation. + */ + @Test + public void testNestedBandSelect() { + createImage(3, 2, true); + final ImageProcessor processor = new ImageProcessor(); + RenderedImage test = processor.selectBands(image, 1, 2); + test = processor.selectBands(test, 1); + assertSame(image, ((BandSelectImage) test).getSource()); + assertValuesEqual(test.getData(), 0, expectedSampleValues()); + } } diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java index 4c072de282..ae973f5f6e 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java @@ -644,7 +644,7 @@ public final class ArraysExt extends Static { if (length == 0) { return array; // May be null } - ArgumentChecks.ensureNonNull ("array", array); + ArgumentChecks.ensureNonNull("array", array); final int arrayLength = Array.getLength(array); ArgumentChecks.ensureBetween("first", 0, arrayLength, first); ArgumentChecks.ensurePositive("length", length);