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));