This is an automated email from the ASF dual-hosted git repository.

amanin pushed a commit to branch feat/resource-processor
in repository https://gitbox.apache.org/repos/asf/sis.git

commit a0a748dea2ca79335f6967f9e40bcf52deca5619
Author: Alexis Manin <alexis.ma...@geomatys.com>
AuthorDate: Mon Nov 28 17:59:18 2022 +0100

    feat(Feature): add a computed image for band aggregation
    
    First prototype with tests on aligned tiles.
    Miss color model.
    Lack some optimizations over banded images.
---
 .../org/apache/sis/image/BandAggregateImage.java   | 394 +++++++++++++++++++++
 .../java/org/apache/sis/image/ImageProcessor.java  |  31 ++
 .../apache/sis/image/BandAggregateImageTest.java   | 226 ++++++++++++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 4 files changed, 652 insertions(+)

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
new file mode 100644
index 0000000000..0177d90563
--- /dev/null
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@ -0,0 +1,394 @@
+package org.apache.sis.image;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.geom.Point2D;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.ColorModel;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.awt.image.WritableRaster;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import org.apache.sis.util.ArgumentChecks;
+
+import static java.lang.Math.multiplyExact;
+
+final class BandAggregateImage extends ComputedImage {
+
+    private final Specification spec;
+
+    private BandAggregateImage(Specification spec) {
+        super(spec.tileModel, 
spec.sources.stream().map(TileSource::getSource).toArray(RenderedImage[]::new));
+        this.spec = spec;
+    }
+
+    @Override
+    protected Raster computeTile(int tileX, int tileY, WritableRaster 
previous) throws Exception {
+        final Point tileLocation = tileToPixel(tileX, tileY);
+        return spec.strategy.aggregate(tileX, tileY, tileLocation, spec);
+    }
+
+    @Override
+    public ColorModel getColorModel() { return spec.aggregationColors; }
+
+    @Override
+    public int getWidth() {
+        return spec.pixelRegion.width;
+    }
+
+    @Override
+    public int getHeight() {
+        return spec.pixelRegion.height;
+    }
+
+    @Override
+    public int getMinX() { return spec.pixelRegion.x; }
+
+    @Override
+    public int getMinY() { return spec.pixelRegion.y; }
+
+    @Override
+    public int getMinTileX() { return spec.tileDisposition.x; }
+
+    @Override
+    public int getMinTileY() { return spec.tileDisposition.y; }
+
+    @Override
+    public int getNumXTiles() { return spec.tileDisposition.width; }
+
+    @Override
+    public int getNumYTiles() { return spec.tileDisposition.height; }
+
+    private Point tileToPixel(int tileX, int tileY) {
+        int pX = this.getMinX() + multiplyExact((tileX - getMinTileX()), 
getTileWidth());
+        int pY = this.getMinY() + multiplyExact((tileY - getMinTileY()), 
getTileHeight());
+        return new Point(pX, pY);
+    }
+
+    /*
+     * FACTORY METHODS
+     */
+
+    static RenderedImage aggregateBands(RenderedImage[] sources, int[][] 
bandsToPreserve) {
+        final ContextInformation info = parseAndValidateInput(sources, 
bandsToPreserve);
+        return tryTileOptimizedStrategy(info)
+                .rightOr(reason
+                        -> fallbackStrategy(info)
+                        .mapLeft(otherReason -> "Reasons: " + reason + ", " + 
otherReason)
+                )
+                .mapRight(BandAggregateImage::new)
+                .getRightOrThrow(IllegalArgumentException::new);
+    }
+
+    /*
+     * PRIVATE STATIC METHODS
+     */
+
+    /**
+     * Initial analysis of input images to aggregate. Note that this method 
aims to make source information more
+     * accessible and easy to use before further processing. It also try to 
detect incompatibilities early, to
+     * raise meaningful errors for users.
+     *
+     * Note: crunching data into a more dense/accessible shape aims to ease 
further analysis/optimisations. This should
+     * allow more lisible and less coupled code, to ease setup of strategies, 
readability and maintenance.
+     *
+     * @param sources images to aggregate, in order.
+     * @param bandsToPreserve Bands to use for each image, in order. Holds 
same contract as the {@link #aggregateBands(RenderedImage[], int[][]) factory 
method}.
+     * @return Parsed information about data sources.
+     * @throws IllegalArgumentException If we detect an incompatibility in 
source images that make them impossible to merge.
+     */
+    private static ContextInformation parseAndValidateInput(RenderedImage[] 
sources, int[][] bandsToPreserve) throws IllegalArgumentException {
+        if (bandsToPreserve != null && sources.length > 
bandsToPreserve.length) throw new IllegalArgumentException("More band 
selections than source images are provided.");
+        if (sources.length < 2) throw new IllegalArgumentException("At least 
two images are required for band aggregation. For band selection on a single 
image, please use dedicated utility");
+
+        List<Rectangle> domains = new ArrayList<>(sources.length);
+        List<SourceSelection> sourcesWithBands = new 
ArrayList<>(sources.length);
+        int commonDataType, minTileWidth, minTileWidthIdx, minTileHeight, 
minTileHeightIdx;
+        RenderedImage source = sources[0];
+        int[] bands = bandsToPreserve == null || bandsToPreserve.length < 1 ? 
null : bandsToPreserve[0];
+        SampleModel sourceSM = source.getSampleModel();
+        commonDataType = sourceSM.getDataType();
+        int numBands = validateAndCountBands(bands, sourceSM);
+        sourcesWithBands.add(new SourceSelection(source, bands));
+        minTileWidthIdx = minTileHeightIdx = 0;
+        minTileWidth = source.getTileWidth();
+        minTileHeight = source.getTileHeight();
+        domains.add(new Rectangle(source.getMinX(), source.getMinY(), 
source.getWidth(), source.getHeight()));
+        for (int i = 1 ; i < sources.length ; i++) {
+            source = sources[i];
+            sourceSM = source.getSampleModel();
+            int dataType = sourceSM.getDataType();
+            if (dataType != commonDataType) throw new 
IllegalArgumentException("Images to merge define different data types. This is 
not supported. Please align all images on a single data-type beforehand.");
+
+            bands = bandsToPreserve == null || bandsToPreserve.length <= i ? 
null : bandsToPreserve[i];
+            numBands += validateAndCountBands(bands, sourceSM);
+
+            sourcesWithBands.add(new SourceSelection(source, bands));
+
+            domains.add(new Rectangle(source.getMinX(), source.getMinY(), 
source.getWidth(), source.getHeight()));
+
+            if (minTileWidth  > source.getTileWidth()) {
+                minTileWidth = source.getTileWidth();
+                minTileWidthIdx  = i;
+            }
+            if (minTileHeight > source.getTileHeight()) {
+                minTileHeight = source.getTileHeight();
+                minTileHeightIdx = i;
+            }
+        }
+
+        // TODO: with current information, we should be able to adapt domain 
definition to a user configuration : intersection, union, strict.
+        // User could specify if he wants band aggregation image to cover the 
intersection or union of input images.
+        // "strict" mode would serve to prevent band aggregation on images 
using different domains (raise an error if domains are not the same).
+        // For now, we will use intersection.
+        final Rectangle intersection = domains.stream()
+                .reduce(Rectangle::intersection)
+                .filter(it -> !it.isEmpty())
+                .orElseThrow(() -> new IllegalArgumentException("source images 
do not intersect."));
+
+        return new ContextInformation(commonDataType, numBands, 
minTileWidthIdx, minTileHeightIdx, domains, intersection, sourcesWithBands);
+    }
+
+    private static int validateAndCountBands(int[] bandSelection, SampleModel 
model) {
+        final int numBands = model.getNumBands();
+        if (bandSelection == null || bandSelection.length < 1) return numBands;
+        for (int band : bandSelection) 
ArgumentChecks.ensureValidIndex(numBands, band);
+        return bandSelection.length;
+    }
+
+    private static Either<String, Specification> 
tryTileOptimizedStrategy(ContextInformation context) {
+        final RenderedImage tilingX = 
context.sources.get(context.minTileWidthIndex).image;
+        final int tileWidth = tilingX.getTileWidth();
+        final int minTileX = tilingX.getMinTileX();
+
+        final  RenderedImage tilingY = 
context.sources.get(context.minTileHeightIndex).image;
+        final int tileHeight = tilingY.getTileHeight();
+        final int minTileY = tilingY.getMinTileY();
+        for (final SourceSelection source : context.sources) {
+            final RenderedImage img = source.image;
+            if (img.getNumXTiles() > 1 && img.getTileWidth() % tileWidth != 0) 
{
+                return Either.left("no common integer multiplier between 
sources for tile width");
+            }
+            else if (img.getNumYTiles() > 1 && img.getTileHeight() % 
tileHeight != 0) {
+                return Either.left("no common integer multiplier between 
sources for tile height");
+            }
+
+            if ((img.getMinX() - tilingX.getMinX()) % tileWidth != 0) {
+                return Either.left("Tiles are not aligned on X axis");
+            }
+
+            if ((img.getMinY() - tilingX.getMinY()) % tileHeight != 0) {
+                return Either.left("Tiles are not aligned on Y axis");
+            }
+        }
+
+        // Ensure domain covers entire tiles.
+        final Rectangle pixelDomain = context.intersection;
+        assert pixelDomain.width  % tileWidth  == 0 : "Computed pixel domain 
does not align with tile borders";
+        assert pixelDomain.height % tileHeight == 0 : "Computed pixel domain 
does not align with tile borders";
+
+        TileAlignedSource[] preparedSources = new 
TileAlignedSource[context.sources.size()];
+        for (int i = 0 ; i < preparedSources.length ; i++) {
+            final SourceSelection source = context.sources.get(i);
+            preparedSources[i] = new TileAlignedSource(source.image, 
source.bands);
+        }
+
+        final SampleModel tileModel = new 
BandedSampleModel(context.commonDataType, tileWidth, tileHeight, 
context.outputBandNumber);
+
+        Rectangle tileDisposition = new Rectangle(minTileX, minTileY, 
pixelDomain.width / tileWidth, pixelDomain.height / tileHeight);
+        return Either.right(new 
Specification(Collections.unmodifiableList(Arrays.asList(preparedSources)), 
createColorModel(context), tileModel, pixelDomain, tileDisposition, new 
TileCopy()));
+    }
+
+    private static ColorModel createColorModel(ContextInformation context) {
+        return null; // TODO
+    }
+
+    private static Either<String, Specification> 
fallbackStrategy(ContextInformation info) {
+        return Either.left("No fallback strategy yet");
+    }
+
+    /**
+     * INTERNAL TYPES
+     */
+
+    private static final class SourceSelection {
+        final RenderedImage image;
+        final int[] bands;
+
+        private SourceSelection(RenderedImage image, int[] bands) {
+            this.image = image;
+            this.bands = bands == null ? null : bands.clone();
+        }
+    }
+
+    private static class ContextInformation {
+        /**
+         * DataBuffer type used by all sources without exception. If sources 
used different datatypes, this is -1.
+         */
+        final int commonDataType;
+        /**
+         * Number of bands resulting from the image merge.
+         */
+        final int outputBandNumber;
+
+        /**
+         * Index of source image providing minimal tile <em>width</em>
+         */
+        final int minTileWidthIndex;
+
+        /**
+         * Index of source image providing minimal tile <em>height</em>
+         */
+        final int minTileHeightIndex;
+
+        /**
+         * Commodity attribute: provides source images <em>pixel</em> 
boundaries, in order.
+         */
+        final List<Rectangle> sourcePxDomains;
+
+        /**
+         * Intersection of all {@link #sourcePxDomains source pixel domains}.
+         */
+        private final Rectangle intersection;
+
+        final List<SourceSelection> sources;
+
+        public ContextInformation(int commonDataType, int outputBandNumber, 
int minTileWidthIndex, int minTileHeightIndex, List<Rectangle> sourcePxDomains, 
Rectangle intersection, List<SourceSelection> sources) {
+            this.commonDataType = commonDataType;
+            this.outputBandNumber = outputBandNumber;
+            this.minTileWidthIndex = minTileWidthIndex;
+            this.minTileHeightIndex = minTileHeightIndex;
+            this.sourcePxDomains = Collections.unmodifiableList(new 
ArrayList<>(sourcePxDomains));
+            this.intersection = intersection;
+            this.sources = Collections.unmodifiableList(new 
ArrayList<>(sources));
+        }
+    }
+
+    static class Specification {
+        final List<TileSource> sources;
+        final ColorModel aggregationColors;
+        final SampleModel tileModel;
+
+        final Rectangle pixelRegion;
+        final Rectangle tileDisposition;
+        final BandAggregationStrategy strategy;
+
+        public Specification(List<TileSource> sources, ColorModel 
aggregationColors, SampleModel tileModel, Rectangle pixelRegion, Rectangle 
tileDisposition, BandAggregationStrategy strategy) {
+            this.sources = sources;
+            this.aggregationColors = aggregationColors;
+            this.tileModel = tileModel;
+            this.pixelRegion = pixelRegion;
+            this.tileDisposition = tileDisposition;
+            this.strategy = strategy;
+        }
+    }
+
+    /**
+     * For now, only available tile source is "aligned", i.e. sources 
providing tiles covering computed image tiles.
+     * In the future, another implementation capable of providing Raster built 
from multiple crossing tiles could be
+     * done, to extend band aggregation capabilities.
+     */
+    interface TileSource {
+        RenderedImage getSource();
+
+        /**
+         * Return a raster aligned with <em>parent computed tile</em> 
described by input offset.
+         * @param computedTileX X coordinate of the <em>parent computed 
tile</em>.
+         * @param computedTileY Y coordinate of the <em>parent computed 
tile</em>.
+         * @return A raster whose domain match output computed tile.
+         */
+        Raster getTile(int computedTileX, int computedTileY, Rectangle 
computedTileRegion);
+    }
+
+    /**
+     * WARNING: only work with sources whose tile size is a multiple of 
computed image tiles, i.e: All and any tiles in
+     * computed images MUST be contained in one and only one source tile.
+     */
+    private static class TileAlignedSource implements TileSource {
+
+        private final RenderedImage source;
+
+        /**
+         * Null if all bands in the image are selected. Otherwise,ordered (not 
necessarily sorted) indices of selected bands.
+         */
+        private final int[] selectedBands;
+
+        public TileAlignedSource(RenderedImage source, int[] selectedBands) {
+            this.source = source;
+            this.selectedBands = selectedBands;
+        }
+
+        @Override
+        public RenderedImage getSource() {
+            return source;
+        }
+
+        @Override
+        public Raster getTile(int computedTileX, int computedTileY, Rectangle 
computedTileRegion) {
+            final double tX = source.getMinTileX() + (computedTileRegion.x - 
source.getMinX()) / (double) source.getTileWidth();
+            final double tY = source.getMinTileY() + (computedTileRegion.y - 
source.getMinY()) / (double) source.getTileHeight();
+            assert regionsAligned(computedTileRegion, new Point2D.Double(tX, 
tY))
+                    : "Source tiles do not align with computed image tiles";
+            final Raster sourceTile = source.getTile((int) Math.floor(tX), 
(int)  Math.floor(tY));
+            return sourceTile.createChild(computedTileRegion.x, 
computedTileRegion.y, computedTileRegion.width, computedTileRegion.height, 
computedTileRegion.x, computedTileRegion.y, selectedBands);
+        }
+
+        private boolean regionsAligned(Rectangle computedTileRegion, Point2D 
origin) {
+            final double sourceX = origin.getX() * source.getTileWidth() + 
source.getTileGridXOffset();
+            final double sourceY = origin.getY() * source.getTileHeight() + 
source.getTileGridYOffset();
+            return Math.abs(sourceX - computedTileRegion.x) < 1 && 
Math.abs(sourceY - computedTileRegion.y) < 1;
+        }
+    }
+
+    @FunctionalInterface
+    interface BandAggregationStrategy {
+        Raster aggregate(int tileX, int tileY, Point tilePixelOrigin, 
BandAggregateImage.Specification spec);
+    }
+
+    private static class TileCopy implements BandAggregationStrategy {
+
+        @Override
+        public Raster aggregate(int tileX, int tileY, Point tilePixelOrigin, 
BandAggregateImage.Specification spec) {
+            final WritableRaster output = 
WritableRaster.createWritableRaster(spec.tileModel, tilePixelOrigin);
+            int i = 0;
+            for (TileSource source : spec.sources) {
+                Raster sourceRaster = source.getTile(tileX, tileY, new 
Rectangle(tilePixelOrigin, new Dimension(spec.tileModel.getWidth(), 
spec.tileModel.getHeight())));
+                final int sourceNumBands = sourceRaster.getNumBands();
+                final int[] targetBands = IntStream.range(i, i + 
sourceNumBands).toArray();
+                WritableRaster outputBandsOfInterest = 
output.createWritableChild(output.getMinX(), output.getMinY(), 
output.getWidth(), output.getHeight(), output.getMinX(), output.getMinY(), 
targetBands);
+                outputBandsOfInterest.setRect(sourceRaster);
+                i += sourceNumBands;
+            }
+
+            assert i == spec.tileModel.getNumBands() : "Mismatch: unexpected 
number of transferred bands";
+
+            return output;
+        }
+    }
+
+    private static class Either<L, R> {
+
+        private final L left;
+        private final R right;
+
+        Either(L left, R right) {
+            if (left == null && right == null || left != null && right != 
null) throw new IllegalArgumentException("Expect exactly one non-null branch");
+            this.left = left;
+            this.right = right;
+        }
+
+        <V> Either<L, V> mapRight(Function<? super R, ? extends V> 
transformRightToValue) { return left == null ? 
right(transformRightToValue.apply(right)) : (Either<L, V>) this; }
+        <V> Either<V, R> mapLeft(Function<? super L, ? extends V> 
transformLeftToValue) { return right == null ? 
left(transformLeftToValue.apply(left)) : (Either<V, R>) this; }
+        public Either<L, R> rightOr(Function<? super L, Either<L, R>> recover) 
{ return right == null ? recover.apply(left) : this; }
+        public <E extends Exception> R getRightOrThrow(Function<L, E> 
createError) throws E { if (right != null) return right; else throw 
createError.apply(left); }
+        static <L, R> Either<L, R> left(L leftValue) { return new 
Either<>(leftValue, null); }
+        static <L, R> Either<L, R> right(R rightValue) { return new 
Either<>(null, rightValue); }
+    }
+}
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 67396be6cc..27a3bbb6a0 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
@@ -1213,6 +1213,37 @@ public class ImageProcessor implements Cloneable {
         }
     }
 
+    /**
+     * Commodity method for {@link #aggregateBands(List, List)}. Calling it is 
equivalent to:
+     *
+     * {@code aggregateBands(Arrays.asList(sources), null);}
+     * @param sources images whose bands must be aggregated, in order. At 
least two images must be provided.
+     *
+     * @see #aggregateBands(List, List)
+     */
+    public RenderedImage aggregateBands(RenderedImage... sources) {
+        return aggregateBands(Arrays.asList(sources), null);
+    }
+
+    /**
+     *
+     * @param sources Rendered images to aggregate, in order.
+     * @param bandsToSelectPerSource Bands to select for each source image, in 
order.
+     *                               If null or empty, we assume that all 
bands of all images must be selected.
+     *                               Any null or empty item means that all 
bands of the respective source image must be preserved.
+     * @return A computed image whose bands are the bands of provided images, 
in order.
+     */
+
+    public RenderedImage aggregateBands(List<RenderedImage> sources, 
List<int[]> bandsToSelectPerSource) {
+        RenderedImage[] sourceArray = sources.toArray(new 
RenderedImage[sources.size()]);
+        int[][] bandSelection = bandsToSelectPerSource == null || 
bandsToSelectPerSource.isEmpty()
+                ? null
+                : bandsToSelectPerSource.stream()
+                    .map(it -> it == null ? null : it.clone())
+                    .toArray(int[][]::new);
+        return BandAggregateImage.aggregateBands(sourceArray, bandSelection);
+    }
+
     /**
      * Returns {@code true} if the given object is an image processor
      * of the same class with the same configuration.
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
new file mode 100644
index 0000000000..3807345951
--- /dev/null
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@ -0,0 +1,226 @@
+package org.apache.sis.image;
+
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRenderedImage;
+import java.util.Arrays;
+import java.util.function.IntBinaryOperator;
+import java.util.stream.IntStream;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class BandAggregateImageTest extends TestCase {
+
+    @Test
+    public void aggregateSingleBandImages() {
+        BufferedImage im1 = new BufferedImage(3, 3, 
BufferedImage.TYPE_BYTE_GRAY);
+        im1.getRaster().setSamples(0, 0, 3, 3, 0, IntStream.range(0, 
3*3).map(it -> 1).toArray());
+        BufferedImage im2 = new BufferedImage(3, 3, 
BufferedImage.TYPE_BYTE_GRAY);
+        im2.getRaster().setSamples(0, 0, 3, 3, 0, IntStream.range(0, 
3*3).map(it -> 2).toArray());
+
+        final RenderedImage result = processor().aggregateBands(im1, im2);
+        assertNotNull(result);
+
+        final Raster tile = result.getTile(0, 0);
+        assertEquals(2, tile.getNumBands());
+        assertEquals(new Rectangle(0, 0, 3, 3), tile.getBounds());
+        assertArrayEquals(
+                new int[] {
+                        1, 2,    1, 2,    1, 2,
+
+                        1, 2,    1, 2,    1, 2,
+
+                        1, 2,    1, 2,    1, 2
+                },
+                tile.getPixels(0, 0, 3, 3, (int[]) null)
+        );
+    }
+
+    @Test
+    public void aggregateSimilarTiledImages() {
+        aggregateSimilarTiledImages(true, true);
+        aggregateSimilarTiledImages(false, false);
+        aggregateSimilarTiledImages(true, false);
+        aggregateSimilarTiledImages(false, true);
+    }
+    
+    @Test
+    public void aggregateImagesUsingSameExtentButDifferentTileSizes() {
+        // Note: we use different tile indices to test robustness. The 
aggregation algorithm should rely on pixel coordinates for absolute positioning 
and synchronisation of image domains.
+        final TiledImageMock tiled2x2 = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 2, 2, 1, 2, true);
+        final TiledImageMock tiled4x1 = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 4, 1, 3, 4, true);
+        final TiledImageMock oneTile  = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 3, 1, 8, 4, 8, 4, 5, 6, true);
+        
+        init(tiled2x2, tiled4x1, oneTile);
+        
+        final RenderedImage result = processor().aggregateBands(tiled2x2, 
tiled4x1, oneTile);
+        assertNotNull(result);
+        assertArrayEquals(new int[] { 3, 1, 8, 4, 2, 1, 1, 4 }, new int[] {
+                result.getMinX(), result.getMinY(), result.getWidth(), 
result.getHeight(), result.getTileWidth(), result.getTileHeight(), 
result.getMinTileX(), result.getMinTileY()
+        });
+
+        final Raster raster = result.getData();
+        assertEquals(new Rectangle(3, 1, 8, 4), raster.getBounds());
+        assertArrayEquals(
+                new int[] {
+                    1100, 2100, 3100,  1101, 2101, 3101,    1200, 2102, 3102,  
1201, 2103, 3103,    1300, 2200, 3104,  1301, 2201, 3105,    1400, 2202, 3106,  
1401, 2203, 3107,
+                    1110, 2300, 3110,  1111, 2301, 3111,    1210, 2302, 3112,  
1211, 2303, 3113,    1310, 2400, 3114,  1311, 2401, 3115,    1410, 2402, 3116,  
1411, 2403, 3117,
+                    1500, 2500, 3120,  1501, 2501, 3121,    1600, 2502, 3122,  
1601, 2503, 3123,    1700, 2600, 3124,  1701, 2601, 3125,    1800, 2602, 3126,  
1801, 2603, 3127,
+                    1510, 2700, 3130,  1511, 2701, 3131,    1610, 2702, 3132,  
1611, 2703, 3133,    1710, 2800, 3134,  1711, 2801, 3135,    1810, 2802, 3136,  
1811, 2803, 3137,
+                },
+                raster.getPixels(3, 1, 8, 4, (int[]) null)
+        );
+    }
+
+    @Test
+    public void 
aggregateImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
+        // Tip: band number match image tile width. i.e:
+        // untiled image -> band 1
+        // tiled 2x2 -> bands 2 and 3
+        // tiled 4x4 -> bands 4 and 5
+        // tiled 6x6 -> band 6
+        final TiledImageMock untiled = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 0, 0, 16, 13, 16, 13, 0, 0, true);
+        final TiledImageMock tiled2x2 = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 2, 4, 2, 8, 10, 2, 2, 0, 0, true);
+        final TiledImageMock tiled4x4 = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 2, 4, 2, 8, 8, 4, 4, 0, 0, true);
+        final TiledImageMock tiled6x6 = new 
TiledImageMock(DataBuffer.TYPE_FLOAT, 1, 2, 0, 12, 6, 6, 6, 0, 0, true);
+
+        init(untiled, tiled2x2, tiled4x4, tiled6x6);
+
+        final RenderedImage result = processor().aggregateBands(untiled, 
tiled2x2, tiled4x4, tiled6x6);
+        assertNotNull(result);
+        assertArrayEquals(new int[] { 4, 2, 8, 4, 2, 2, 0, 0 }, new int[] {
+                result.getMinX(), result.getMinY(), result.getWidth(), 
result.getHeight(), result.getTileWidth(), result.getTileHeight(), 
result.getMinTileX(), result.getMinTileY()
+        });
+
+        final Raster raster = result.getData();
+        assertEquals(new Rectangle(4, 2, 8, 4), raster.getBounds());
+        assertArrayEquals(
+                new int[] {
+                    1124, 2100, 3100, 4100, 5100, 6122,  1125, 2101, 3101, 
4101, 5101, 6123,    1126, 2200, 3200, 4102, 5102, 6124,  1127, 2201, 3201, 
4103, 5103, 6125,    1128, 2300, 3300, 4200, 5200, 6220,  1129, 2301, 3301, 
4201, 5201, 6221,    1130, 2400, 3400, 4202, 5202, 6222,  1131, 2401, 3401, 
4203, 5203, 6223,
+                    1134, 2110, 3110, 4110, 5110, 6132,  1135, 2111, 3111, 
4111, 5111, 6133,    1136, 2210, 3210, 4112, 5112, 6134,  1137, 2211, 3211, 
4113, 5113, 6135,    1138, 2310, 3310, 4210, 5210, 6230,  1139, 2311, 3311, 
4211, 5211, 6231,    1140, 2410, 3410, 4212, 5212, 6232,  1141, 2411, 3411, 
4213, 5213, 6233,
+
+                    1144, 2500, 3500, 4120, 5120, 6142,  1145, 2501, 3501, 
4121, 5121, 6143,    1146, 2600, 3600, 4122, 5122, 6144,  1147, 2601, 3601, 
4123, 5123, 6145,    1148, 2700, 3700, 4220, 5220, 6240,  1149, 2701, 3701, 
4221, 5221, 6241,    1150, 2800, 3800, 4222, 5222, 6242,  1151, 2801, 3801, 
4223, 5223, 6243,
+                    1154, 2510, 3510, 4130, 5130, 6152,  1155, 2511, 3511, 
4131, 5131, 6153,    1156, 2610, 3610, 4132, 5132, 6154,  1157, 2611, 3611, 
4133, 5133, 6155,    1158, 2710, 3710, 4230, 5230, 6250,  1159, 2711, 3711, 
4231, 5231, 6251,    1160, 2810, 3810, 4232, 5232, 6252,  1161, 2811, 3811, 
4233, 5233, 6253,
+                },
+                raster.getPixels(4, 2, 8, 4, (int[]) null)
+        );
+    }
+
+    /*
+
+        @Test
+        public void validateColorModel() {
+            throw new UnsupportedOperationException("TODO");
+        }
+
+    */
+    private void aggregateSimilarTiledImages(Boolean firstBanded, Boolean 
secondBanded) {
+        final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_INT, 2, 
7, 7, 6, 9, 3, 3, 1, 2, firstBanded);
+        final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_INT, 2, 
7, 7, 6, 9, 3, 3, 3, 4, secondBanded);
+
+        init(im1, im2);
+
+        final ImageProcessor processor = processor();
+        RenderedImage result = processor.aggregateBands(im1, im2);
+        assertNotNull(result);
+        assertArrayEquals(new int[] { 7, 7, 6, 9, 3, 3, 1, 2 }, new int[] { 
result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), 
result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), 
result.getMinTileY()});
+        Raster raster = result.getData();
+        assertEquals(4, raster.getNumBands());
+        assertEquals(new Rectangle(7, 7, 6, 9), raster.getBounds());
+
+        assertArrayEquals(
+                new int[] {
+                    // Tile 1                                                  
                  Tile 2
+                    1100, 2100, 3100, 4100,  1101, 2101, 3101, 4101,  1102, 
2102, 3102, 4102,    1200, 2200, 3200, 4200,  1201, 2201, 3201, 4201,  1202, 
2202, 3202, 4202,
+                    1110, 2110, 3110, 4110,  1111, 2111, 3111, 4111,  1112, 
2112, 3112, 4112,    1210, 2210, 3210, 4210,  1211, 2211, 3211, 4211,  1212, 
2212, 3212, 4212,
+                    1120, 2120, 3120, 4120,  1121, 2121, 3121, 4121,  1122, 
2122, 3122, 4122,    1220, 2220, 3220, 4220,  1221, 2221, 3221, 4221,  1222, 
2222, 3222, 4222,
+                    // Tile 3                                                  
                  Tile 4
+                    1300, 2300, 3300, 4300,  1301, 2301, 3301, 4301,  1302, 
2302, 3302, 4302,    1400, 2400, 3400, 4400,  1401, 2401, 3401, 4401,  1402, 
2402, 3402, 4402,
+                    1310, 2310, 3310, 4310,  1311, 2311, 3311, 4311,  1312, 
2312, 3312, 4312,    1410, 2410, 3410, 4410,  1411, 2411, 3411, 4411,  1412, 
2412, 3412, 4412,
+                    1320, 2320, 3320, 4320,  1321, 2321, 3321, 4321,  1322, 
2322, 3322, 4322,    1420, 2420, 3420, 4420,  1421, 2421, 3421, 4421,  1422, 
2422, 3422, 4422,
+                    // Tile 5                                                  
                  Tile 6
+                    1500, 2500, 3500, 4500,  1501, 2501, 3501, 4501,  1502, 
2502, 3502, 4502,    1600, 2600, 3600, 4600,  1601, 2601, 3601, 4601,  1602, 
2602, 3602, 4602,
+                    1510, 2510, 3510, 4510,  1511, 2511, 3511, 4511,  1512, 
2512, 3512, 4512,    1610, 2610, 3610, 4610,  1611, 2611, 3611, 4611,  1612, 
2612, 3612, 4612,
+                    1520, 2520, 3520, 4520,  1521, 2521, 3521, 4521,  1522, 
2522, 3522, 4522,    1620, 2620, 3620, 4620,  1621, 2621, 3621, 4621,  1622, 
2622, 3622, 4622
+                },
+                raster.getPixels(7, 7, 6, 9, (int[]) null)
+        );
+
+        // Repeat the test with a custom band selection.
+        result = processor.aggregateBands(Arrays.asList(im1, im2, im1), 
Arrays.asList(null, new int[] { 1 }, new int[] { 0 }));
+        assertNotNull(result);
+        assertArrayEquals(new int[] { 7, 7, 6, 9, 3, 3, 1, 2 }, new int[] { 
result.getMinX(), result.getMinY(), result.getWidth(), result.getHeight(), 
result.getTileWidth(), result.getTileHeight(), result.getMinTileX(), 
result.getMinTileY()});
+        raster = result.getData();
+        assertEquals(4, raster.getNumBands());
+        assertEquals(new Rectangle(7, 7, 6, 9), raster.getBounds());
+
+        assertArrayEquals(
+                new int[] {
+                        // Tile 1                                              
                      Tile 2
+                        1100, 2100, 4100, 1100,  1101, 2101, 4101, 1101,  
1102, 2102, 4102, 1102,    1200, 2200, 4200, 1200,  1201, 2201, 4201, 1201,  
1202, 2202, 4202, 1202,
+                        1110, 2110, 4110, 1110,  1111, 2111, 4111, 1111,  
1112, 2112, 4112, 1112,    1210, 2210, 4210, 1210,  1211, 2211, 4211, 1211,  
1212, 2212, 4212, 1212,
+                        1120, 2120, 4120, 1120,  1121, 2121, 4121, 1121,  
1122, 2122, 4122, 1122,    1220, 2220, 4220, 1220,  1221, 2221, 4221, 1221,  
1222, 2222, 4222, 1222,
+                        // Tile 3                                              
                      Tile 4
+                        1300, 2300, 4300, 1300,  1301, 2301, 4301, 1301,  
1302, 2302, 4302, 1302,    1400, 2400, 4400, 1400,  1401, 2401, 4401, 1401,  
1402, 2402, 4402, 1402,
+                        1310, 2310, 4310, 1310,  1311, 2311, 4311, 1311,  
1312, 2312, 4312, 1312,    1410, 2410, 4410, 1410,  1411, 2411, 4411, 1411,  
1412, 2412, 4412, 1412,
+                        1320, 2320, 4320, 1320,  1321, 2321, 4321, 1321,  
1322, 2322, 4322, 1322,    1420, 2420, 4420, 1420,  1421, 2421, 4421, 1421,  
1422, 2422, 4422, 1422,
+                        // Tile 5                                              
                      Tile 6
+                        1500, 2500, 4500, 1500,  1501, 2501, 4501, 1501,  
1502, 2502, 4502, 1502,    1600, 2600, 4600, 1600,  1601, 2601, 4601, 1601,  
1602, 2602, 4602, 1602,
+                        1510, 2510, 4510, 1510,  1511, 2511, 4511, 1511,  
1512, 2512, 4512, 1512,    1610, 2610, 4610, 1610,  1611, 2611, 4611, 1611,  
1612, 2612, 4612, 1612,
+                        1520, 2520, 4520, 1520,  1521, 2521, 4521, 1521,  
1522, 2522, 4522, 1522,    1620, 2620, 4620, 1620,  1621, 2621, 4621, 1621,  
1622, 2622, 4622, 1622
+                },
+                raster.getPixels(7, 7, 6, 9, (int[]) null)
+        );
+    }
+
+    private ImageProcessor processor() { return new ImageProcessor(); }
+
+    /**
+     * Initialize all bands of all input images with a "BTYX" pattern where:
+     * <ol>
+     *     <li>
+     *         "B" is the band index over all encountered images.
+     *         It means that be start at 1 for the first band of first 
encountered image,
+     *         and then it is incremented for each band of each encountered 
image
+     *     </li>
+     *     <li>"TYX" is defined by {@link 
TiledImageMock#initializeAllTiles(int...)}</li>
+     * </ol>
+     */
+    private void init(TiledImageMock... images) {
+        int b = 1;
+        for (TiledImageMock image : images) {
+            int[] allBands = IntStream.range(0, 
image.getSampleModel().getNumBands()).toArray();
+            image.initializeAllTiles(allBands);
+            final int cursor = b;
+            updateValues(image, (band, sample) -> (cursor + band) * 1000 + 
sample);
+            b += allBands.length;
+        }
+    }
+
+    /**
+     * Change pixel values of input image with provided binary operator.
+     *
+     * @param target The image to update (mutated directly)
+     * @param valueUpdate Operator updating current sample value.
+     *                    It receives as input the current band index 
(0-based) and the value associated to this band on the current pixel (the 
current sample value).
+     *                    It must return the new value to associate to the 
pixel.
+     */
+    private void updateValues(WritableRenderedImage target, IntBinaryOperator 
valueUpdate) {
+        try (WritablePixelIterator it = new 
PixelIterator.Builder().createWritable(target)) {
+            int[] pixel = new int[it.getNumBands()];
+            while (it.next()) {
+                it.getPixel(pixel);
+                for (int i = 0; i < pixel.length; i++) pixel[i] = 
valueUpdate.applyAsInt(i, pixel[i]);
+                it.setPixel(pixel);
+            }
+        }
+    }
+}
+
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 78e4a38b3e..2f00f9990e 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
@@ -101,6 +101,7 @@ import org.junit.runners.Suite;
     org.apache.sis.image.ResamplingGridTest.class,
     org.apache.sis.image.ResampledImageTest.class,
     org.apache.sis.image.MaskedImageTest.class,
+    org.apache.sis.image.BandAggregateImageTest.class,
     org.apache.sis.image.BandedSampleConverterTest.class,
     org.apache.sis.image.ImageCombinerTest.class,
     org.apache.sis.image.ImageProcessorTest.class,


Reply via email to