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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 6fb53af Compute only the tiles that intersect the bounding box of the mask. It saves memory and computation when a tile does not intersect the clip. 6fb53af is described below commit 6fb53af600bbab808fde0f1330c12328e5c42676 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Oct 23 15:32:41 2021 +0200 Compute only the tiles that intersect the bounding box of the mask. It saves memory and computation when a tile does not intersect the clip. --- .../java/org/apache/sis/image/MaskedImage.java | 203 ++++++++++++++++----- .../java/org/apache/sis/image/package-info.java | 7 + 2 files changed, 165 insertions(+), 45 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java index 1266a54..9ac7dab 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/MaskedImage.java @@ -19,6 +19,7 @@ package org.apache.sis.image; import java.util.Objects; import java.nio.ByteBuffer; import java.nio.LongBuffer; +import java.awt.Rectangle; import java.awt.Point; import java.awt.Shape; import java.awt.Color; @@ -33,6 +34,7 @@ import java.awt.image.MultiPixelPackedSampleModel; import java.lang.ref.SoftReference; import java.nio.ByteOrder; import org.apache.sis.internal.coverage.j2d.FillValues; +import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.coverage.j2d.TilePlaceholder; import static org.apache.sis.internal.util.Numerics.ceilDiv; @@ -60,12 +62,23 @@ final class MaskedImage extends SourceAlignedImage { private final boolean maskInside; /** + * Bounds of the {@linkplain #clip} in pixels coordinates and in tile coordinates. + * The later provides a fast way to determine if a tile intersects the mask. + * The bounds are computed together when first needed. + * + * @see #getMaskTiles() + */ + private transient volatile Rectangle maskBounds, maskTiles; + + /** * The clip after rasterization. Each element contains 8 pixel values. * Index of pixel value at coordinate (x,y) can be obtained as below: * * {@preformat java - * int element = mask[y*scanlineStride + x/Byte.SIZE]; - * int shift = (Byte.SIZE-1) - (x & (Byte.SIZE-1)); + * int xm = x - maskBounds.x; + * int xy = y - maskBounds.y; + * int element = mask[ym*scanlineStride + xm/Byte.SIZE]; + * int shift = (Byte.SIZE-1) - (xm & (Byte.SIZE-1)); * int pixel = (element >>> shift) & 1; * } * @@ -124,37 +137,68 @@ final class MaskedImage extends SourceAlignedImage { } /** + * Returns the bounds of the {@linkplain #clip} in tile coordinates. + * It provides a fast way to determine if a tile intersects the mask. + */ + private Rectangle getMaskTiles() { + Rectangle bt = maskTiles; + if (bt == null) { + synchronized (this) { + bt = maskTiles; + if (bt == null) { + final RenderedImage source = getSource(); + final Rectangle bp = clip.getBounds(); + ImageUtilities.clipBounds(source, bp); + bt = new Rectangle(); + if (!bp.isEmpty()) { + final int xmax = ImageUtilities.pixelToTileX(source, bp.x + bp.width - 1) + 1; + final int ymax = ImageUtilities.pixelToTileY(source, bp.y + bp.height - 1) + 1; + bt.width = xmax - (bt.x = ImageUtilities.pixelToTileX(source, bp.x)); + bt.height = ymax - (bt.y = ImageUtilities.pixelToTileY(source, bp.y)); + } + maskBounds = bp; + maskTiles = bt; + } + } + } + return bt; + } + + /** * Returns pixel values of the mask in a multi pixel packed array. * After conversion to {@link LongBuffer}, index of pixel value at * coordinate (x,y) can be obtained as below: * * {@preformat java - * int element = mask[y*scanlineStride + x/Long.SIZE]; - * int shift = (Long.SIZE-1) - (x & (Long.SIZE-1)); + * int xm = x - maskBounds.x; + * int xy = y - maskBounds.y; + * int element = mask[ym*scanlineStride + xm/Long.SIZE]; + * int shift = (Long.SIZE-1) - (xm & (Long.SIZE-1)); * int pixel = (element >>> shift) & 1; * } + * + * <h4>Pre-conditions</h4> + * The {@link #getMaskTiles()} method must have been invoked at least once before this method. */ private synchronized ByteBuffer getMask() { ByteBuffer mask; if (maskRef == null || (mask = maskRef.get()) == null) { - /* - * Create a 1-bit image with an `IndexColorModel` with two colors: {0, 0, 0} and {255, 255, 255}. - * Java2D has specialized code for TYPE_BYTE_BINARY; we reproduce something equivalent but we the - * array size rounded to an integer multiple of {@code long} size. - */ - final int width = getWidth(); - final int height = getHeight(); - int size = ceilDiv(width, Byte.SIZE) * height; + final Rectangle maskBounds = this.maskBounds; + int size = ceilDiv(maskBounds.width, Byte.SIZE) * maskBounds.height; final int r = size & (Long.BYTES - 1); if (r != 0) size += Long.BYTES - r; // Round to a multiple of 8 bytes. final DataBufferByte buffer = new DataBufferByte(size); - + /* + * Create a 1-bit image with an `IndexColorModel` with two colors: {0, 0, 0} and {255, 255, 255}. + * Java2D has specialized code for TYPE_BYTE_BINARY; we reproduce something equivalent but with + * the array size rounded to an integer multiple of {@code long} size. + */ final byte[] gray = {0, -1}; final IndexColorModel cm = new IndexColorModel(1, gray.length, gray, gray, gray); - final WritableRaster raster = Raster.createPackedRaster(buffer, width, height, 1, null); + final WritableRaster raster = Raster.createPackedRaster(buffer, maskBounds.width, maskBounds.height, 1, null); final Graphics2D g = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null).createGraphics(); try { - g.translate(-getMinX(), -getMinY()); + g.translate(-maskBounds.x, -maskBounds.y); g.setColor(Color.WHITE); g.fill(clip); } finally { @@ -191,17 +235,33 @@ final class MaskedImage extends SourceAlignedImage { */ @Override protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) { - final Raster source = getSource().getTile(tileX, tileY); + /* + * Before to compute the tile, check if the tile is outside the mask. + * If this is the case, we can return a tile (source or empty) as-is. + */ + final RenderedImage source = getSource(); + final int xmin = ImageUtilities.tileToPixelX(source, tileX); + final int ymin = ImageUtilities.tileToPixelY(source, tileY); + if (!getMaskTiles().contains(tileX, tileY)) { + if (maskInside) { + return source.getTile(tileX, tileY); + } else { + return createEmptyTile(xmin, ymin); + } + } + /* + * Tile may intersect the mask. Computation is necessary, but we may discover at + * the end of this method that the result is still an empty tile or source tile. + */ + final Rectangle maskBounds = this.maskBounds; final LongBuffer mask = getMask().asLongBuffer(); - final int xImage = getMinX(); - final int yImage = getMinY(); - final int xmin = source.getMinX(); - final int ymin = source.getMinY(); - final int xmax = Math.min(xmin + source.getWidth(), xImage + getWidth()); // Exclusive. - final int ymax = Math.min(ymin + source.getHeight(), yImage + getHeight()); - final int imax = xmax - xImage - 1; // Inclusive. - final int xoff = xmin - xImage; - final int stride = maskScanlineStride; // Must be after call to `getMask()`. + final int xStart = Math.max(xmin, maskBounds.x); + final int yStart = Math.max(ymin, maskBounds.y); + final int xEnd = Math.min(xmin + source.getTileWidth(), maskBounds.x + maskBounds.width); + final int yEnd = Math.min(ymin + source.getTileHeight(), maskBounds.y + maskBounds.height); + final int imax = xEnd - maskBounds.x; // Maximum x index in mask, exclusive. + final int xoff = xStart - maskBounds.x; + Raster data = null; Object transfer = null; int transferSize = 0; long present = -1; // Bits will be set to 0 if some pixels are masked. @@ -212,22 +272,22 @@ final class MaskedImage extends SourceAlignedImage { * to copy. It allows us to use the Java2D API for transferring blocks of data, which is more efficient * than looping over individual pixels. */ - for (int y=ymin; y<ymax; y++) { - int index = (y - yImage) * stride; // Index in unit of bits for now (converted later). + for (int y=yStart; y<yEnd; y++) { + int index = (y - maskBounds.y) * maskScanlineStride; // Index in unit of bits for now (converted later). final int emax = (index + imax) / Long.SIZE; // Last index in unit of long elements, inclusive. final int shift = (index += xoff) & (Long.SIZE-1); // First bit to read in the long, 0 = highest bit. index /= Long.SIZE; // Convert from bit (pixel) index to long[] index. /* - * We want a value such as `base + index*Long.SIZE + lower` is equal to `xmin` - * when all variables point to the first pixel in the current row of the tile: + * We want a value such as `base + index*Long.SIZE + lower` is equal to `xStart` + * when all variables point to the first potentially masked pixel of the tile: * * - `index` has not yet been incremented * - `lower = shift` (number of leading zeros) * * `remaining` is the number of bits to use in the last element (at index = emax). */ - final int base = xmin - (index*Long.SIZE + shift); - final int remaining = xmax - (base + emax*Long.SIZE); + final int base = xStart - (index*Long.SIZE + shift); + final int remaining = xEnd - (base + emax*Long.SIZE); assert remaining >= 0 && remaining < Long.SIZE : remaining; /* * Read the bit mask for the first pixels (up to 64) of current row. Some leading bits of @@ -258,9 +318,14 @@ final class MaskedImage extends SourceAlignedImage { final int count = upper - lower; assert count > 0 && count <= Long.SIZE : count; if (count > transferSize) { - if (transferSize == 0) { - // First time that we copy pixels. - boolean clean = needCreate(tile, source); + if (data == null) { + /* + * First time that we copy pixels. Get the rasters only at this point. + * This delay allows to avoid computing the source tile when fully masked. + */ + data = source.getTile(tileX, tileY); + assert data.getMinX() == xmin && data.getMinY() == ymin; + boolean clean = needCreate(tile, data); if (clean) { tile = createTile(tileX, tileY); clean = fillValues.isFullyZero; @@ -272,7 +337,7 @@ final class MaskedImage extends SourceAlignedImage { transferSize = count; transfer = null; } - transfer = source.getDataElements(x, y, count, 1, transfer); + transfer = data.getDataElements(x, y, count, 1, transfer); tile.setDataElements(x, y, count, 1, transfer); element &= (1L << (Long.SIZE - upper)) - 1; } @@ -299,22 +364,70 @@ final class MaskedImage extends SourceAlignedImage { } } /* - * The tile has been created only if at least one pixel needs to be copied from the source tile. - * If the tile is still null at this point, it means that it is fully empty. + * The tile is fetched only if at least one pixel needs to be copied from the source tile. + * If the source tile is still null at this point, it means that target tile is fully empty. + * Note that the target tile may be non-null because it was an argument to this method. */ - if (tile == null) { - TilePlaceholder p = emptyTiles; - if (p == null) { - // Not a problem if invoked concurrently by two threads. - emptyTiles = p = TilePlaceholder.filled(sampleModel, fillValues); - } - return p.create(new Point(source.getMinX(), source.getMinY())); + if (data == null) { + return createEmptyTile(xmin, ymin); } /* * If no bit from the `present` mask have been cleared, then it means that all pixels * have been copied. In such case the source tile can be returned directly. */ - return (present == -1) ? source : tile; + if (present == -1) { + return data; + } + /* + * The tile is partially masked. If the tile is not fully included in `maskBounds`, + * there is some pixels that we need to copy here. + */ + if (maskInside) { + final int width = tile.getWidth(); + int height = tile.getHeight(); + final int xmax = xmin + width; + final int ymax = ymin + height; + height -= (yStart - ymin) + (ymax - yEnd); +complete: for (int border = 0; ; border++) { + final int start, span; + switch (border) { + case 0: span = yStart - (start = ymin); break; // Top (horizontal, lower y) + case 1: span = ymax - (start = yEnd); break; // Bottom (horizontal, upper y) + case 2: span = xStart - (start = xmin); break; // Left (vertical, lower x) + case 3: span = xmax - (start = xEnd); break; // Right (vertical, upper x) + default: break complete; + } + final boolean horizontal = (border & 2) == 0; + final int area = span * (horizontal ? width : height); + if (area > 0) { + if (area > transferSize) { + transferSize = area; + transfer = null; + } + if (horizontal) { + transfer = data.getDataElements(xmin, start, width, span, transfer); + tile.setDataElements(xmin, start, width, span, transfer); + } else { + transfer = data.getDataElements(start, yStart, span, height, transfer); + tile.setDataElements(start, yStart, span, height, transfer); + } + } + } + } + return tile; + } + + /** + * Returns an empty tile starting at the given pixel coordinates. Each empty tile will share its + * data buffer with other empty tiles, including from other images using the same sample model. + */ + private Raster createEmptyTile(final int xmin, final int ymin) { + TilePlaceholder p = emptyTiles; + if (p == null) { + // Not a problem if invoked concurrently by two threads. + emptyTiles = p = TilePlaceholder.filled(sampleModel, fillValues); + } + return p.create(new Point(xmin, ymin)); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/image/package-info.java index a2ecb26..0862082 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/package-info.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/package-info.java @@ -32,6 +32,13 @@ * {@linkplain org.apache.sis.metadata.iso.spatial.DefaultGeoreferenceable georeferenceable} * <cite>grid coverages</cite>. * + * <h2>Usage note</h2> + * Some images are writable. But modifying pixel values should be done by invoking the {@code getWritableTile(…)} + * and {@code releaseWritableTile(…)} methods of {@link java.awt.image.WritableRenderedImage} interface. + * Do not cast directly a {@link java.awt.image.Raster} to {@link java.awt.image.WritableRaster} + * even when the cast is safe, because some raster data may be shared by many tiles having identical content. + * Furthermore changes in pixel values may be lost if {@code releaseWritableTile(…)} is not invoked. + * * @author Rémi Maréchal (Geomatys) * @author Martin Desruisseaux (Geomatys) * @author Johann Sorel (Geomatys)