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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 076c21a2c1a901bc5602bf96a7769b246a9612ca
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Dec 21 14:33:39 2024 +0100

    Add a `PlanarImage.getValidArea()` method.
    `ResampledImage` computes this information by reprojecting the valid area 
of the source.
---
 .../apache/sis/coverage/privy/ImageUtilities.java  | 17 ++++++
 .../main/org/apache/sis/image/PlanarImage.java     | 22 +++++++-
 .../main/org/apache/sis/image/ResampledImage.java  | 61 ++++++++++++++++++++--
 .../org/apache/sis/image/SourceAlignedImage.java   | 10 ++++
 .../org/apache/sis/image/ResampledImageTest.java   | 21 ++++++--
 5 files changed, 120 insertions(+), 11 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
index 123bfeff39..fc75ec618a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/ImageUtilities.java
@@ -18,6 +18,7 @@ package org.apache.sis.coverage.privy;
 
 import java.util.Arrays;
 import java.util.logging.Logger;
+import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.color.ColorSpace;
 import java.awt.geom.AffineTransform;
@@ -36,6 +37,7 @@ import static java.lang.Math.floorDiv;
 import static java.lang.Math.toIntExact;
 import static java.lang.Math.multiplyFull;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.image.PlanarImage;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Static;
@@ -63,6 +65,21 @@ public final class ImageUtilities extends Static {
     private ImageUtilities() {
     }
 
+    /**
+     * Returns a shape containing all pixels that are valid in this image.
+     * The returned shape may conservatively contain more than the minimal set 
of valid pixels.
+     *
+     * @param  image  the image for which to get the valid area.
+     * @return a shape (not necessarily the smallest) containing all pixels 
that are valid.
+     */
+    public static Shape getValidArea(final RenderedImage image) {
+        if (image instanceof PlanarImage) {
+            return ((PlanarImage) image).getValidArea();
+        } else {
+            return getBounds(image);
+        }
+    }
+
     /**
      * Returns the bounds of the given image as a new rectangle.
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
index 6fc40e2987..05583a59a2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image;
 
 import java.awt.Image;
+import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.IndexColorModel;
@@ -106,7 +107,7 @@ import org.apache.sis.pending.jdk.JDK18;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   1.1
  */
 public abstract class PlanarImage implements RenderedImage {
@@ -291,6 +292,25 @@ public abstract class PlanarImage implements RenderedImage 
{
         return null;
     }
 
+    /**
+     * Returns a shape containing all pixels that are valid in this image.
+     * The returned shape may conservatively contain more than the minimal set 
of valid pixels.
+     * It should be relatively quick to compute. In particular, invoking this 
method should not
+     * cause the calculation of tiles (e.g. for searching NaN sample values).
+     * The shape should be fully contained inside the image {@linkplain 
#getBounds() bounds}.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation returns {@link #getBounds()}.
+     *
+     * @return a shape containing all pixels that are valid. Not necessarily 
the smallest shape
+     *         containing those pixels, but shall be fully contained inside 
the image bounds.
+     *
+     * @since 1.5
+     */
+    public Shape getValidArea() {
+        return getBounds();
+    }
+
     /**
      * Returns the image location (<var>x</var>, <var>y</var>) and image size 
(<var>width</var>, <var>height</var>).
      * This is a convenience method encapsulating the results of 4 method 
calls in a single object.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
index fe9393d9ea..8885502bc2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
@@ -19,9 +19,11 @@ package org.apache.sis.image;
 import java.util.Objects;
 import java.lang.ref.Reference;
 import java.nio.DoubleBuffer;
+import java.awt.Shape;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
+import java.awt.geom.Area;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.ColorModel;
 import java.awt.image.Raster;
@@ -32,10 +34,12 @@ import java.awt.image.SampleModel;
 import javax.measure.Quantity;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform2D;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.coverage.privy.ImageUtilities;
 import org.apache.sis.coverage.privy.FillValues;
 import org.apache.sis.feature.internal.Resources;
@@ -70,7 +74,7 @@ import static 
org.apache.sis.coverage.privy.ImageUtilities.LOGGER;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see Interpolation
  * @see java.awt.image.AffineTransformOp
@@ -172,6 +176,13 @@ public class ResampledImage extends ComputedImage {
      */
     private Reference<ComputedImage> mask;
 
+    /**
+     * The valid area, computed when first requested.
+     *
+     * @see #getValidArea()
+     */
+    private Shape validArea;
+
     /**
      * Creates a new image which will resample the given image. The resampling 
operation is defined
      * by a potentially non-linear transform from <em>this</em> image to the 
specified <em>source</em> image.
@@ -212,7 +223,7 @@ public class ResampledImage extends ComputedImage {
             final Number[] fillValues, final Quantity<?>[] accuracy)
     {
         super(sampleModel, source);
-        if (source.getWidth() <= 0 || source.getHeight() <= 0) {
+        if ((source.getWidth() | source.getHeight()) <= 0) {
             throw new 
IllegalArgumentException(Resources.format(Resources.Keys.EmptyImage));
         }
         ArgumentChecks.ensureNonNull("interpolation", interpolation);
@@ -260,13 +271,16 @@ public class ResampledImage extends ComputedImage {
         final double[] offset = new double[numDim];
         offset[0] = interpolationSupportOffset(s.width);
         offset[1] = interpolationSupportOffset(s.height);
+
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         MathTransform toSourceSupport = MathTransforms.concatenate(toSource, 
MathTransforms.translation(offset));
         /*
          * If the desired accuracy is large enough, try using a grid of 
precomputed values for faster operations.
          * This is optional; it is okay to abandon the grid if we cannot 
compute it.
          */
-        Boolean          canUseGrid     = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         Quantity<Length> linearAccuracy = null;
+        Boolean          canUseGrid     = null;
         if (accuracy != null) {
             for (final Quantity<?> hint : accuracy) {
                 if (hint != null) {
@@ -554,6 +568,43 @@ public class ResampledImage extends ComputedImage {
         return ArraysExt.resize(names, n);
     }
 
+    /**
+     * Returns a shape containing all pixels that are valid in this image.
+     * This method returns the valid area of the source image transformed
+     * by the inverse of {@link #toSource}, mapping pixel corners.
+     *
+     * @return the valid area of the source converted to the coordinate system 
of this resampled image.
+     *
+     * @since 1.5
+     */
+    @Override
+    public synchronized Shape getValidArea() {
+        Shape domain = validArea;
+        if (domain == null) try {
+            final var ts = new TransformSeparator(toSource);
+            ts.addSourceDimensionRange(0, BIDIMENSIONAL);
+            ts.addTargetDimensionRange(0, BIDIMENSIONAL);
+            MathTransform mt = ts.separate();
+            MathTransform centerToCorner = 
MathTransforms.uniformTranslation(BIDIMENSIONAL, -0.5);
+            mt = MathTransforms.concatenate(centerToCorner, mt);
+            mt = MathTransforms.concatenate(centerToCorner, mt.inverse());
+            domain = ImageUtilities.getValidArea(getSource());
+            domain = 
MathTransforms.bidimensional(mt).createTransformedShape(domain);
+            final Area area = new Area(domain);
+            area.intersect(new Area(getBounds()));
+            validArea = domain = area.isRectangular() ? area.getBounds2D() : 
area;
+        } catch (FactoryException | TransformException e) {
+            recoverableException("getValidArea", e);
+            validArea = domain = getBounds();
+        }
+        if (domain instanceof Area) {
+            domain = (Area) ((Area) domain).clone();    // Cloning an Area is 
cheap.
+        } else if (domain instanceof Rectangle2D) {
+            domain = (Rectangle2D) ((Rectangle2D) domain).clone();
+        }
+        return domain;
+    }
+
     /**
      * Returns the minimum tile index in the <var>x</var> direction.
      * This is often 0.
@@ -882,7 +933,7 @@ public class ResampledImage extends ComputedImage {
         if (source instanceof PlanarImage) try {
             final Dimension s = interpolation.getSupportSize();
             Rectangle pixels = ImageUtilities.tilesToPixels(this, tiles);
-            final Rectangle2D bounds = new Rectangle2D.Double(
+            final var bounds = new Rectangle2D.Double(
                     pixels.x      -  0.5  *  s.width,
                     pixels.y      -  0.5  *  s.height,
                     pixels.width  + (double) s.width,
@@ -908,7 +959,7 @@ public class ResampledImage extends ComputedImage {
     @Override
     public boolean equals(final Object object) {
         if (equalsBase(object)) {
-            final ResampledImage other = (ResampledImage) object;
+            final var other = (ResampledImage) object;
             return minX     == other.minX &&
                    minY     == other.minY &&
                    width    == other.width &&
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/SourceAlignedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/SourceAlignedImage.java
index b789ee896a..c10e792fba 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/SourceAlignedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/SourceAlignedImage.java
@@ -18,6 +18,7 @@ package org.apache.sis.image;
 
 import java.util.Set;
 import java.util.Objects;
+import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
@@ -25,6 +26,7 @@ import java.awt.image.RenderedImage;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Disposable;
 import org.apache.sis.util.Workaround;
+import org.apache.sis.coverage.privy.ImageUtilities;
 
 
 /**
@@ -169,6 +171,14 @@ abstract class SourceAlignedImage extends ComputedImage {
         return names;
     }
 
+    /**
+     * Delegates to source image if possible.
+     */
+    @Override
+    public Shape getValidArea() {
+        return ImageUtilities.getValidArea(getSource());
+    }
+
     /**
      * Delegates to source image.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
index 9754dd5818..56c8db06eb 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
@@ -115,8 +115,8 @@ public final class ResampledImageTest extends TestCase {
      * @param  minY  minimal Y coordinate to give to the resampled image.
      */
     private void createScaledByTwo(final int minX, final int minY) {
-        final Rectangle bounds = new Rectangle(minX, minY, source.getWidth() * 
2, source.getHeight() * 2);
-        final AffineTransform tr = 
AffineTransform.getTranslateInstance(source.getMinX(), source.getMinY());
+        final var bounds = new Rectangle(minX, minY, source.getWidth() * 2, 
source.getHeight() * 2);
+        final var tr = AffineTransform.getTranslateInstance(source.getMinX(), 
source.getMinY());
         tr.scale(0.5, 0.5);
         tr.translate(-bounds.x, -bounds.y);
         resample(bounds, tr);
@@ -127,7 +127,7 @@ public final class ResampledImageTest extends TestCase {
      * The interpolation result will be stored in {@link #target}.
      */
     private void resample(final Rectangle bounds, final AffineTransform tr) {
-        final ImageProcessor processor = new ImageProcessor();
+        final var processor = new ImageProcessor();
         processor.setInterpolation(interpolation);
         target = (ResampledImage) processor.resample(source, bounds, new 
AffineTransform2D(tr));
         try {
@@ -205,6 +205,13 @@ public final class ResampledImageTest extends TestCase {
         }
     }
 
+    /**
+     * Verifies the valid area of an image which is expected to have a 
rectangular result.
+     */
+    private void verifyRectangularResult() {
+        assertEquals(target.getBounds(), target.getValidArea().getBounds(), 
"validArea");
+    }
+
     /**
      * Tests {@link Interpolation#NEAREST} on floating point values.
      */
@@ -214,6 +221,7 @@ public final class ResampledImageTest extends TestCase {
         interpolation = Interpolation.NEAREST;
         createScaledByTwo(-30, 12);
         verifyAtIntegerPositions();
+        verifyRectangularResult();
     }
 
     /**
@@ -225,6 +233,7 @@ public final class ResampledImageTest extends TestCase {
         interpolation = Interpolation.NEAREST;
         createScaledByTwo(18, 20);
         verifyAtIntegerPositions();
+        verifyRectangularResult();
     }
 
     /**
@@ -237,6 +246,7 @@ public final class ResampledImageTest extends TestCase {
         createScaledByTwo(-40, 50);
         verifyAtIntegerPositions();
         verifyAtMiddlePositions(1E-12);
+        verifyRectangularResult();
     }
 
     /**
@@ -249,6 +259,7 @@ public final class ResampledImageTest extends TestCase {
         createScaledByTwo(40, -50);
         verifyAtIntegerPositions();
         verifyAtMiddlePositions(0.5);
+        verifyRectangularResult();
     }
 
     /**
@@ -287,7 +298,7 @@ public final class ResampledImageTest extends TestCase {
         } catch (NoninvertibleTransformException e) {
             throw new AssertionError(e);
         }
-        final Rectangle bounds = new Rectangle(9, 9);
+        final var bounds = new Rectangle(9, 9);
         target = new ResampledImage(source,
                 ImageLayout.DEFAULT.createCompatibleSampleModel(source, 
bounds),
                 null, bounds, toSource, interpolation, null, null);
@@ -343,7 +354,7 @@ public final class ResampledImageTest extends TestCase {
      */
     @Test
     public void testMultiBands() {
-        final BufferedImage image = new BufferedImage(6, 3, 
BufferedImage.TYPE_INT_ARGB);
+        final var image = new BufferedImage(6, 3, BufferedImage.TYPE_INT_ARGB);
         final Graphics2D g = image.createGraphics();
         g.setColor(Color.ORANGE);
         g.fillRect(0, 0, image.getWidth(), image.getHeight());

Reply via email to