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,