You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by am...@apache.org on 2023/02/15 17:30:15 UTC
[sis] 02/04: feat(Feature): Add a GridCoverageResource for band aggregation
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 c5b2f14be3b78275cc816f194c4d7085ea5a2480
Author: Alexis Manin <al...@geomatys.com>
AuthorDate: Mon Dec 5 13:15:27 2022 +0100
feat(Feature): Add a GridCoverageResource for band aggregation
---
.../sis/storage/BandAggregateGridResource.java | 186 +++++++++++++++++++++
.../sis/storage/MultiSourceGridResource.java | 48 ++++++
.../org/apache/sis/storage/ResourceProcessor.java | 19 +++
.../apache/sis/storage/ResourceProcessorTest.java | 115 ++++++++++++-
4 files changed, 367 insertions(+), 1 deletion(-)
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/BandAggregateGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/BandAggregateGridResource.java
new file mode 100644
index 0000000000..0493fe0703
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/BandAggregateGridResource.java
@@ -0,0 +1,186 @@
+package org.apache.sis.storage;
+
+import java.awt.image.ColorModel;
+import java.awt.image.RenderedImage;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.util.ComparisonMode;
+import org.opengis.coverage.CannotEvaluateException;
+import org.opengis.util.GenericName;
+
+/**
+ * Merge homogeneous {@link GridCoverageResource grid resources} by "stacking" their bands.
+ *
+ * <h3>Limitation</h3>
+ * For now, only datasets with <em>strictly</em> the same {@link GridCoverageResource#getGridGeometry() domain} can be merged.
+ *
+ * @see ImageProcessor#aggregateBands(List, List, ColorModel)
+ */
+class BandAggregateGridResource extends MultiSourceGridResource {
+ private final List<BandSelection> sources;
+ private final GridGeometry domain;
+ private final ColorModel userColors;
+
+ BandAggregateGridResource(GenericName name, List<BandSelection> sources, ColorModel userColors) throws DataStoreException {
+ super(name);
+ this.sources = sources;
+ this.domain = verifyDomainEquality(sources);
+ this.userColors = userColors;
+ }
+
+ @Override
+ List<GridCoverageResource> sources() {
+ return sources.stream().map(it -> it.data).collect(Collectors.toList());
+ }
+
+ @Override
+ public GridGeometry getGridGeometry() { return domain; }
+
+ @Override
+ public List<SampleDimension> getSampleDimensions() throws DataStoreException {
+ return sources.stream()
+ .flatMap(BandSelection::selectSampleDimensions)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException {
+ if (domain == null) domain = getGridGeometry();
+ else domain = getGridGeometry().derive().subgrid(domain).build();
+
+ final List<BandSelection> selection = select(ranges);
+ assert !selection.isEmpty();
+
+ final BandSelection firstSelection = selection.get(0);
+ final GridCoverage first = firstSelection.data.read(domain, firstSelection.selectedBands);
+ if (selection.size() == 1) return first;
+
+ List<GridCoverage> readData = new ArrayList<>(selection.size());
+ readData.add(first);
+ for (int i = 1 ; i < selection.size() ; i++) {
+ final BandSelection source = selection.get(i);
+ final GridCoverage data = source.data.read(domain, source.selectedBands);
+ if (!data.getGridGeometry().equals(first.getGridGeometry(), ComparisonMode.IGNORE_METADATA)) {
+ throw new UnsupportedOperationException("Band aggregation require all source datasets to provide the same domain");
+ }
+ readData.add(data);
+ }
+
+ final List<SampleDimension> outputSamples = readData.stream().flatMap(it -> it.getSampleDimensions().stream()).collect(Collectors.toList());
+ return new BandAggregateGridCoverage(domain, outputSamples, readData);
+ }
+
+ private GridGeometry verifyDomainEquality(List<BandSelection> sources) throws DataStoreException {
+ final GridGeometry first = sources.get(0).data.getGridGeometry();
+ for (int i = 1 ; i < sources.size() ; i++) {
+ final GridGeometry other = sources.get(i).data.getGridGeometry();
+ // TODO: rather than equality, we should check "alignment". It means that the coverage cells should be spatially aligned,
+ // but we should not require their grid extent to use the same offsets.
+ if (!first.equals(other, ComparisonMode.IGNORE_METADATA)) {
+ throw new IllegalArgumentException("Band merge only allow aligned datasets to be merged. Please resample your resources on a common grid beforehand");
+ }
+ }
+
+ return first;
+ }
+
+ private List<BandSelection> select(int... bands) throws DataStoreException {
+ if (bands == null || bands.length < 1) return sources;
+
+ class BandToData {
+ final int band; final GridCoverageResource source;
+
+ BandToData(int band, GridCoverageResource source) {
+ this.band = band;
+ this.source = source;
+ }
+ }
+
+ List<BandToData> perBandIndex = new ArrayList<>();
+ for (BandSelection source : sources) {
+ final int[] sourceBands = source.selectedBands == null || source.selectedBands.length < 1
+ ? IntStream.range(0, source.data.getSampleDimensions().size()).toArray()
+ : source.selectedBands;
+ for (int i : sourceBands) perBandIndex.add(new BandToData(i, source.data));
+ }
+
+ List<BandSelection> consolidated = new ArrayList<>(bands.length);
+ int previousIdx = 0;
+ BandToData previous = perBandIndex.get(bands[0]);
+ // Commodity: to avoid manipulating too many cursors, but also to avoid too many transformations,
+ // We use an array with a bigger size than needed to contain temporary source band indices.
+ // Its indices match target selected band numbers.
+ // Its content is the associated source band number for this target band.
+ int[] sourceSelectedBands = new int[bands.length];
+ sourceSelectedBands[0] = previous.band;
+ for (int i = 1 ; i < bands.length ; i++) {
+ int band = bands[i];
+ BandToData current = perBandIndex.get(band);
+ if (current.source != perBandIndex.get(bands[previousIdx]).source) {
+ final int[] sourceBands = Arrays.copyOfRange(sourceSelectedBands, previousIdx, i);
+ consolidated.add(new BandSelection(previous.source, sourceBands));
+ previous = current;
+ previousIdx = i;
+ }
+ sourceSelectedBands[i] = current.band;
+ }
+
+ consolidated.add(new BandSelection(previous.source, Arrays.copyOfRange(sourceSelectedBands, previousIdx, sourceSelectedBands.length)));
+
+ return consolidated;
+ }
+
+ private class BandAggregateGridCoverage extends GridCoverage {
+
+ private final List<GridCoverage> sources;
+
+ protected BandAggregateGridCoverage(GridGeometry domain, List<? extends SampleDimension> ranges, List<GridCoverage> sources) {
+ super(domain, ranges);
+ this.sources = sources;
+ assert sources != null && sources.size() > 1;
+ }
+
+
+ @Override
+ public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
+ final List<RenderedImage> sourceImages = sources.stream()
+ .map(it -> it.render(sliceExtent))
+ .collect(Collectors.toList());
+ // TODO: parent resource should keep a reference to the resource processor that created it.
+ // Then, we should retrieve the embedded image processor and use it, instead of using a fresh image processor.
+ // However, that require an API change somewhere, and I do not know where yet.
+ return new ImageProcessor().aggregateBands(sourceImages, null, userColors);
+ }
+ }
+
+ static class BandSelection {
+ final GridCoverageResource data;
+ final int[] selectedBands;
+ final List<SampleDimension> samples;
+
+ BandSelection(GridCoverageResource data, int[] selectedBands) throws DataStoreException {
+ this.data = data;
+ this.selectedBands = selectedBands;
+ this.samples = data.getSampleDimensions();
+ if (selectedBands != null) {
+ for (int band : selectedBands) {
+ if (band >= samples.size()) throw new IllegalArgumentException("Provided band selection is invalid. Input data provide only "+samples.size()+" bands, but band "+band+" was requested");
+ }
+ }
+ }
+
+ Stream<SampleDimension> selectSampleDimensions() {
+ if (selectedBands == null || selectedBands.length < 1) return samples.stream();
+ return Arrays.stream(selectedBands).mapToObj(samples::get);
+ }
+ }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/MultiSourceGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/MultiSourceGridResource.java
new file mode 100644
index 0000000000..5a52764c89
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/MultiSourceGridResource.java
@@ -0,0 +1,48 @@
+package org.apache.sis.storage;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.storage.event.StoreEvent;
+import org.apache.sis.storage.event.StoreListener;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.GenericName;
+
+abstract class MultiSourceGridResource implements GridCoverageResource {
+
+ private final GenericName name;
+
+ /**
+ *
+ * @param name Optional. The {@link #getIdentifier() identifier} of this resource.
+ */
+ MultiSourceGridResource(GenericName name) {
+ this.name = name;
+ }
+
+ abstract List<GridCoverageResource> sources();
+ @Override
+ public Optional<Envelope> getEnvelope() { return Optional.empty(); }
+
+ @Override
+ public Optional<GenericName> getIdentifier() { return Optional.ofNullable(name); }
+
+ @Override
+ public Metadata getMetadata() throws DataStoreException {
+ MetadataBuilder builder = new MetadataBuilder();
+ builder.addSpatialRepresentation(null, getGridGeometry(), false);
+ for (GridCoverageResource source : sources()) {
+ // TODO: not sure it is the right thing to do. I'm a little afraid of the performance impact.
+ builder.addSource(source.getMetadata());
+ }
+
+ return builder.buildAndFreeze();
+ }
+
+ @Override
+ public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {}
+
+ @Override
+ public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) {}
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java
index 3a0b745988..bce74739e5 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java
@@ -18,6 +18,9 @@ package org.apache.sis.storage;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.logging.Level;
@@ -46,6 +49,7 @@ import org.opengis.util.FactoryException;
import org.opengis.util.GenericName;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+import static org.apache.sis.util.ArgumentChecks.ensureValidIndex;
/**
* A predefined set of operations on resources as convenience methods.
@@ -152,6 +156,21 @@ public class ResourceProcessor implements Cloneable {
return new ResampledGridCoverageResource(source, reprojected, targetName, processor);
}
+ public GridCoverageResource aggregateBands(GridCoverageResource... bands) throws DataStoreException {
+ return aggregateBands(null, Arrays.asList(bands), null, null);
+ }
+
+ public GridCoverageResource aggregateBands(GenericName name, List<GridCoverageResource> resources, List<int[]> bandSelections, ColorModel userColors) throws DataStoreException {
+ ensureNonNull("resources", resources);
+ if (bandSelections != null) ensureValidIndex(resources.size(), bandSelections.size() - 1);
+ List<BandAggregateGridResource.BandSelection> selections = new ArrayList<>(resources.size());
+ for (int i = 0 ; i < resources.size() ; i++) {
+ int[] bands = (bandSelections == null || bandSelections.size() <= i) ? null : bandSelections.get(i);
+ selections.add(new BandAggregateGridResource.BandSelection(resources.get(i), bands));
+ }
+ return new BandAggregateGridResource(name, selections, userColors);
+ }
+
private static Optional<GeographicBoundingBox> searchGeographicExtent(GridCoverageResource source) throws DataStoreException {
final Optional<GeographicBoundingBox> bbox = source.getMetadata().getIdentificationInfo().stream()
.flatMap(it -> it.getExtents().stream())
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java
index 0f0211418e..59b4a4ef9d 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java
@@ -3,7 +3,11 @@ package org.apache.sis.storage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.RenderedImage;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.BufferedGridCoverage;
import org.apache.sis.coverage.grid.GridCoverage;
@@ -27,6 +31,7 @@ import org.opengis.util.LocalName;
import static org.apache.sis.referencing.operation.transform.MathTransforms.identity;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class ResourceProcessorTest extends TestCase {
@@ -76,6 +81,114 @@ public class ResourceProcessorTest extends TestCase {
}, values);
}
+ @Test
+ public void aggregateBandsFromSingleBandSources() throws Exception {
+ GridCoverageResource first = singleValuePerBand(1);
+ GridCoverageResource second = singleValuePerBand(2);
+
+ final GridCoverageResource aggregation = nearestInterpol().aggregateBands(first, second);
+ final RenderedImage rendering = aggregation.read(null).render(null);
+ assertNotNull(rendering);
+ assertArrayEquals(
+ new int[] {
+ 1, 2, 1, 2,
+ 1, 2, 1, 2
+ },
+ rendering.getData().getPixels(0, 0, 2, 2, (int[]) null)
+ );
+
+ assertArrayEquals(
+ new int[] {
+ 1, 1,
+ 1, 1
+ },
+ aggregation.read(null, 0).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null)
+ );
+
+
+ assertArrayEquals(
+ new int[] {
+ 2, 2,
+ 2, 2
+ },
+ aggregation.read(null, 1).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null)
+ );
+ }
+
+ @Test
+ public void aggregateBandsFromMultiBandSources() throws Exception {
+ GridCoverageResource firstAndSecondBands = singleValuePerBand(1, 2);
+ GridCoverageResource thirdAndFourthBands = singleValuePerBand(3, 4);
+ GridCoverageResource fifthAndSixthBands = singleValuePerBand(5, 6);
+
+ GridCoverageResource aggregation = nearestInterpol().aggregateBands(firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands);
+ aggregation.getIdentifier().ifPresent(name -> fail("No name provided at creation, but one was returned: "+name));
+ int[] values = aggregation.read(null).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(
+ new int[] {
+ 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6,
+ 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6,
+ },
+ values
+ );
+
+ values = aggregation.read(null, 1, 2, 4, 5).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(
+ new int[] {
+ 2, 3, 5, 6, 2, 3, 5, 6,
+ 2, 3, 5, 6, 2, 3, 5, 6
+ },
+ values
+ );
+
+ values = aggregation.read(null, 3, 4).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(
+ new int[] {
+ 4, 5, 4, 5,
+ 4, 5, 4, 5
+ },
+ values
+ );
+
+ final LocalName testName = Names.createLocalName(null, null, "test-name");
+ aggregation = nearestInterpol().aggregateBands(testName, Arrays.asList(firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands), Arrays.asList(null, new int[] { 0, 1 }, new int[] { 1 }), null);
+
+ assertEquals(testName, aggregation.getIdentifier().orElse(null));
+
+ values = aggregation.read(null).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(
+ new int[] {
+ 1, 2, 3, 4, 6, 1, 2, 3, 4, 6,
+ 1, 2, 3, 4, 6, 1, 2, 3, 4, 6
+ },
+ values
+ );
+
+ values = aggregation.read(null, 2, 4).render(null).getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(
+ new int[] {
+ 3, 6, 3, 6,
+ 3, 6, 3, 6
+ },
+ values
+ );
+ }
+
+ private static GridCoverageResource singleValuePerBand(int... bandValues) {
+ GridGeometry domain = new GridGeometry(new GridExtent(2, 2), PixelInCell.CELL_CENTER, identity(2), HardCodedCRS.WGS84);
+ final List<SampleDimension> samples = IntStream.of(bandValues)
+ .mapToObj(b -> new SampleDimension.Builder()
+ .setBackground(-1)
+ .addQuantitative("band-value", b, b + 1, 1, 0, Units.UNITY)
+ .build()
+ )
+ .collect(Collectors.toList());
+
+ DataBuffer values = new DataBufferInt(IntStream.range(0, 4).flatMap(it -> Arrays.stream(bandValues)).toArray(), 4 * bandValues.length);
+ return new MemoryGridResource(null, new BufferedGridCoverage(domain, samples, values));
+ }
+
+
/**
* Create a trivial 2D grid coverage of dimension 2x2. It uses an identity transform for grid to space conversion,
* and a common WGS84 coordinate reference system, with longitude first.
@@ -84,7 +197,7 @@ public class ResourceProcessorTest extends TestCase {
GridGeometry domain = new GridGeometry(new GridExtent(2, 2), PixelInCell.CELL_CENTER, identity(2), HardCodedCRS.WGS84);
SampleDimension band = new SampleDimension.Builder()
.setBackground(0)
- .addQuantitative("1-based row-major order pixel number", 1, 4, 1, 0, Units.UNITY)
+ .addQuantitative("1-based row-major order pixel number", 1, 5, 1, 0, Units.UNITY)
.build();
DataBuffer values = new DataBufferInt(new int[] {1, 2, 3, 4}, 4);
return new MemoryGridResource(null, new BufferedGridCoverage(domain, Collections.singletonList(band), values));