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 4349cc81c84cc74694eff6a3d8f75b8c771429af Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Dec 21 15:21:39 2024 +0100 More accurate checks by `ImageOverlay` of which sources are worth to keep. This is using the `getValidArea()` method added in the previous commit. --- .../main/org/apache/sis/image/ImageOverlay.java | 113 ++++++++++++++++----- .../org/apache/sis/image/ImageOverlayTest.java | 2 + 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java index 2810bfd375..c7f13c83fe 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java @@ -20,6 +20,9 @@ import java.awt.Image; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; import java.awt.image.Raster; import java.awt.image.ColorModel; import java.awt.image.SampleModel; @@ -32,6 +35,7 @@ import java.util.function.BiConsumer; import javax.measure.Quantity; import javax.measure.UnconvertibleException; import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Disposable; import org.apache.sis.util.logging.Logging; import org.apache.sis.math.Statistics; import org.apache.sis.measure.Quantities; @@ -58,6 +62,19 @@ import org.apache.sis.coverage.privy.ImageUtilities; * @author Martin Desruisseaux (Geomatys) */ final class ImageOverlay extends MultiSourceImage { + /** + * The valid area. + * + * @see #getValidArea() + */ + private final Shape validArea; + + /** + * The contribution of each source as the valid area of a source minus all previous contributions. + * The length of this array is the number of sources, in same order. + */ + private final Area[] contributions; + /** * Creates a new image overlay or returns one of the given sources if equivalent. * All source images shall have the same pixels coordinate system and the same number of bands. @@ -77,16 +94,17 @@ final class ImageOverlay extends MultiSourceImage { static RenderedImage create(RenderedImage[] sources, Rectangle bounds, SampleModel sampleModel, ColorModel colorModel, final boolean autoTileSize, final boolean parallel) { + final Area aoi = (bounds != null) ? new Area(bounds) : null; + Area[] contributions = new Area[sources.length]; + final Area validArea = new Area(); /* * Filter the source images for keeping only the ones that intersect the bounds. * Check image compatibility (number of bands) and color model in the same loop. * If there is only one image left after filtering, it may be returned directly. */ int numBands=0, count=0; - final boolean computeUnion = (bounds == null); - final var sourceBounds = new Rectangle[sources.length]; sources = sources.clone(); -next: for (final RenderedImage source : sources) { + for (final RenderedImage source : sources) { final int n = ImageUtilities.getNumBands(source); if (n == 0) continue; // Skip null elements. if (n != numBands) { @@ -99,23 +117,15 @@ next: for (final RenderedImage source : sources) { * If the current source does not intersect the specified area of interest, or if a previous source * fully overlaps the current source, then the latter image will never be drawn and can be omitted. */ - Rectangle aoi = ImageUtilities.getBounds(source); - if (computeUnion) { - if (bounds == null) { - bounds = new Rectangle(aoi); - } else { - bounds.add(aoi); - } - } else { - aoi = aoi.intersection(bounds); - if (aoi.isEmpty()) continue; + final Area area = new Area(ImageUtilities.getValidArea(source)); + if (aoi != null) area.intersect(aoi); + if (area.isEmpty()) continue; // Source does not intersect the specified bounds. + validArea.add(area); + if (count != 0) { + area.subtract(contributions[count - 1]); + if (area.isEmpty()) continue; // The new source is fully masked by previous sources. } - for (int i=0; i<count; i++) { - if (sourceBounds[i].contains(aoi)) { - continue next; - } - } - sourceBounds[count] = aoi; + contributions[count] = area; sources[count++] = source; /* * The default sample model is selected after filtering because the choice of a sample model @@ -148,9 +158,15 @@ next: for (final RenderedImage source : sources) { return (colorModel != null) ? RecoloredImage.apply(main, colorModel) : main; } sources = ArraysExt.resize(sources, count); + contributions = ArraysExt.resize(contributions, count); /* + * Last area is now the union of the Area Of iInterest (AOI) of all source images. + * The size of the destination image is this valid area, unless specified otherwise. * If the tile size is not a divisor of the image size, try to find a better tile size. */ + if (bounds == null) { + bounds = validArea.getBounds(); + } if (autoTileSize) { var tileSize = new Dimension(sampleModel.getWidth(), sampleModel.getHeight()); if ((bounds.width % tileSize.width) != 0 || (bounds.height % tileSize.height) != 0) { @@ -160,17 +176,36 @@ next: for (final RenderedImage source : sources) { } var minTile = new Point(ImageUtilities.pixelToTileX(main, bounds.x), ImageUtilities.pixelToTileY(main, bounds.y)); - return ImageProcessor.unique(new ImageOverlay(sources, bounds, minTile, sampleModel, colorModel, parallel)); + return ImageProcessor.unique(new ImageOverlay( + sources, contributions, validArea, bounds, minTile, sampleModel, colorModel, parallel)); } /** * Creates a new image overlay. */ - private ImageOverlay(final RenderedImage[] sources, final Rectangle bounds, final Point minTile, - final SampleModel sampleModel, final ColorModel colorModel, + private ImageOverlay(final RenderedImage[] sources, final Area[] contributions, final Area validArea, final Rectangle bounds, + final Point minTile, final SampleModel sampleModel, final ColorModel colorModel, final boolean parallel) { super(sources, bounds, minTile, sampleModel, colorModel, parallel); + this.validArea = validArea.isRectangular() ? validArea.getBounds2D() : validArea; + this.contributions = contributions; + } + + /** + * Returns a shape containing all pixels that are valid in this image. + * + * @return the valid area of the source converted to the coordinate system of this resampled image. + */ + @Override + public Shape getValidArea() { + Shape domain = validArea; + 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; } /** @@ -356,15 +391,39 @@ next: for (final RenderedImage source : sources) { if (target == null) { target = createTile(tileX, tileY); } + final Rectangle aoi = target.getBounds(); final int n = getNumSources(); for (int i=n; --i >= 0;) { - final RenderedImage source = getSource(i); - final Rectangle bounds = getBounds(); - ImageUtilities.clipBounds(source, bounds); - if (!bounds.isEmpty()) { - copyData(bounds, source, target); + if (contributions[i].intersects(aoi)) { + final RenderedImage source = getSource(i); + final Rectangle bounds = getBounds(); + ImageUtilities.clipBounds(source, bounds); + if (!bounds.isEmpty()) { + copyData(bounds, source, target); + } } } return target; } + + /** + * Notifies the source images that tiles will be computed soon in the given region. + * This method forwards the notification to all images that are instances of {@link PlanarImage}. + */ + @Override + protected Disposable prefetch(final Rectangle tiles) { + final Rectangle aoi = ImageUtilities.tilesToPixels(this, tiles); + final int n = getNumSources(); + final var sources = new RenderedImage[n]; + int count = 0; + for (int i=0; i<n; i++) { + final RenderedImage source = getSource(i); + if (source instanceof PlanarImage) { + if (contributions[i].intersects(aoi)) { + sources[count++] = source; + } + } + } + return new MultiSourcePrefetch(ArraysExt.resize(sources, count), aoi).run(parallel); + } } diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java index 1a597f7c27..996e75f112 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java @@ -26,6 +26,7 @@ import java.awt.image.DataBuffer; import java.awt.image.RenderedImage; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; +import org.apache.sis.coverage.privy.ImageUtilities; // Test dependencies import org.junit.jupiter.api.Test; @@ -91,6 +92,7 @@ public final class ImageOverlayTest extends TestCase { assertEquals(5, image.getTileHeight()); assertEquals(1, image.getNumXTiles()); assertEquals(1, image.getNumYTiles()); + assertEquals(new Rectangle(7, 5), ImageUtilities.getValidArea(image).getBounds()); assertArrayEquals(new String[] {PlanarImage.SAMPLE_RESOLUTIONS_KEY}, image.getPropertyNames()); assertArrayEquals(new double[] {2, 5, 1}, (double[]) image.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY)); assertValuesEqual(image.getData(), 0, new int[][] {