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