You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/10/23 16:14:45 UTC
[sis] branch geoapi-4.0 updated: 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.
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 <ma...@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)