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:14 UTC

[sis] 01/04: feat(Feature): allow user to override output color model 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 17a4412ed745359710c89121ace77e9c7f9a2100
Author: Alexis Manin <al...@geomatys.com>
AuthorDate: Mon Dec 5 10:20:10 2022 +0100

    feat(Feature): allow user to override output color model for band aggregation
    
    Also, add a fallback strategy to guess a color model if user has not provided any.
---
 .../org/apache/sis/image/BandAggregateImage.java   | 65 ++++++++++++++++++----
 .../java/org/apache/sis/image/ImageProcessor.java  | 16 ++++--
 .../apache/sis/image/BandAggregateImageTest.java   |  2 +-
 3 files changed, 66 insertions(+), 17 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
index 0177d90563..924fae4a6f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@ -6,6 +6,7 @@ import java.awt.Rectangle;
 import java.awt.geom.Point2D;
 import java.awt.image.BandedSampleModel;
 import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
@@ -16,6 +17,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
 import java.util.stream.IntStream;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.util.ArgumentChecks;
 
 import static java.lang.Math.multiplyExact;
@@ -76,8 +78,8 @@ final class BandAggregateImage extends ComputedImage {
      * FACTORY METHODS
      */
 
-    static RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsToPreserve) {
-        final ContextInformation info = parseAndValidateInput(sources, bandsToPreserve);
+    static RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsToPreserve, ColorModel userColorModel) {
+        final ContextInformation info = parseAndValidateInput(sources, bandsToPreserve, userColorModel);
         return tryTileOptimizedStrategy(info)
                 .rightOr(reason
                         -> fallbackStrategy(info)
@@ -95,16 +97,17 @@ final class BandAggregateImage extends ComputedImage {
      * 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.
-     *
+     * <p>
      * 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}.
+     * @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[][], ColorModel) factory method}.
+     * @param userColorModel
      * @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 {
+    private static ContextInformation parseAndValidateInput(RenderedImage[] sources, int[][] bandsToPreserve, ColorModel userColorModel) 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");
 
@@ -153,7 +156,7 @@ final class BandAggregateImage extends ComputedImage {
                 .filter(it -> !it.isEmpty())
                 .orElseThrow(() -> new IllegalArgumentException("source images do not intersect."));
 
-        return new ContextInformation(commonDataType, numBands, minTileWidthIdx, minTileHeightIdx, domains, intersection, sourcesWithBands);
+        return new ContextInformation(commonDataType, numBands, minTileWidthIdx, minTileHeightIdx, domains, intersection, sourcesWithBands, userColorModel);
     }
 
     private static int validateAndCountBands(int[] bandSelection, SampleModel model) {
@@ -203,11 +206,50 @@ final class BandAggregateImage extends ComputedImage {
         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()));
+        ColorModel outColorModel = context.userColorModel;
+        if (outColorModel == null) outColorModel = createColorModel(context);
+        else if (!context.userColorModel.isCompatibleSampleModel(tileModel)) {
+            throw new IllegalArgumentException("User color model is not compatible with band aggregation sample model. Please provide a banded color model.");
+        }
+
+        return Either.right(new Specification(Collections.unmodifiableList(Arrays.asList(preparedSources)), outColorModel, tileModel, pixelDomain, tileDisposition, new TileCopy()));
     }
 
+    /**
+     * Approximate guess of the output color model:
+     * <ol>
+     *     <li>
+     *         If aggregation result is 3 or 4 bands, and data type is byte or short, we create a RGB color model.
+     *         If there's 4 bands, an RGBA color model is defined.
+     *     </li>
+     *     <li>Otherwise, if the first image is already single banded, we return directly its color model (if non null)</li>
+     *     <li>As a last resort, a greyscale color model is made, that try to "guess" value range from the data-type.</li>
+     * </ol>
+     */
     private static ColorModel createColorModel(ContextInformation context) {
-        return null; // TODO
+        if (context.outputBandNumber == 3 || context.outputBandNumber == 4) {
+            switch (context.commonDataType) {
+                case DataBuffer.TYPE_BYTE:
+                case DataBuffer.TYPE_SHORT:
+                    return ColorModelFactory.createRGB(context.commonDataType * Byte.SIZE, false, context.outputBandNumber == 4);
+            }
+        }
+
+        final SourceSelection first = context.sources.get(0);
+        if (first.image.getSampleModel().getNumBands() == 1 && first.image.getColorModel() != null) {
+            return first.image.getColorModel();
+        }
+
+        final double vmin, vmax;
+        switch (context.commonDataType) {
+            case DataBuffer.TYPE_BYTE:   vmin = 0               ; vmax = 255               ; break;
+            case DataBuffer.TYPE_SHORT:  vmin = Short.MIN_VALUE ; vmax = Short.MAX_VALUE   ; break;
+            case DataBuffer.TYPE_USHORT: vmin = 0               ; vmax = 65535             ; break;
+            case DataBuffer.TYPE_INT:    vmin = 0               ; vmax = Integer.MAX_VALUE ; break;
+            default:                     vmin = 0.0             ; vmax = 1.0;
+        }
+
+        return ColorModelFactory.createGrayScale(context.commonDataType, 1, 0, vmin, vmax);
     }
 
     private static Either<String, Specification> fallbackStrategy(ContextInformation info) {
@@ -260,7 +302,9 @@ final class BandAggregateImage extends ComputedImage {
 
         final List<SourceSelection> sources;
 
-        public ContextInformation(int commonDataType, int outputBandNumber, int minTileWidthIndex, int minTileHeightIndex, List<Rectangle> sourcePxDomains, Rectangle intersection, List<SourceSelection> sources) {
+        final ColorModel userColorModel;
+
+        public ContextInformation(int commonDataType, int outputBandNumber, int minTileWidthIndex, int minTileHeightIndex, List<Rectangle> sourcePxDomains, Rectangle intersection, List<SourceSelection> sources, ColorModel userColorModel) {
             this.commonDataType = commonDataType;
             this.outputBandNumber = outputBandNumber;
             this.minTileWidthIndex = minTileWidthIndex;
@@ -268,6 +312,7 @@ final class BandAggregateImage extends ComputedImage {
             this.sourcePxDomains = Collections.unmodifiableList(new ArrayList<>(sourcePxDomains));
             this.intersection = intersection;
             this.sources = Collections.unmodifiableList(new ArrayList<>(sources));
+            this.userColorModel = userColorModel;
         }
     }
 
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 27a3bbb6a0..1c00c78bfd 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
@@ -1214,15 +1214,15 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
-     * Commodity method for {@link #aggregateBands(List, List)}. Calling it is equivalent to:
+     * Commodity method for {@link #aggregateBands(List, List, ColorModel)}. Calling it is equivalent to:
      *
-     * {@code aggregateBands(Arrays.asList(sources), null);}
+     * {@code aggregateBands(Arrays.asList(sources), null, null);}
      * @param sources images whose bands must be aggregated, in order. At least two images must be provided.
      *
-     * @see #aggregateBands(List, List)
+     * @see #aggregateBands(List, List, ColorModel)
      */
     public RenderedImage aggregateBands(RenderedImage... sources) {
-        return aggregateBands(Arrays.asList(sources), null);
+        return aggregateBands(Arrays.asList(sources), null, null);
     }
 
     /**
@@ -1231,17 +1231,21 @@ public class ImageProcessor implements Cloneable {
      * @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.
+     * @param userColorModel Optional. The color model to apply on output image.
+     *                       If null, an approximate color model will be inferred using output number of bands and sample data type.
+     *                       There's no guarantee about the output color model, but it will not be null,
+     *                       and might be RGB or grey scale.
      * @return A computed image whose bands are the bands of provided images, in order.
      */
 
-    public RenderedImage aggregateBands(List<RenderedImage> sources, List<int[]> bandsToSelectPerSource) {
+    public RenderedImage aggregateBands(List<RenderedImage> sources, List<int[]> bandsToSelectPerSource, ColorModel userColorModel) {
         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);
+        return BandAggregateImage.aggregateBands(sourceArray, bandSelection, userColorModel);
     }
 
     /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
index 3807345951..868113a33d 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@ -154,7 +154,7 @@ public class BandAggregateImageTest extends TestCase {
         );
 
         // 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 }));
+        result = processor.aggregateBands(Arrays.asList(im1, im2, im1), Arrays.asList(null, new int[] { 1 }, new int[] { 0 }), null);
         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();