Search in sources :

Example 6 with ImmutableDimension

use of qupath.lib.geom.ImmutableDimension in project qupath by qupath.

the class RoiTools method computeTiledROIs.

/**
 * Create a collection of tiled ROIs corresponding to a specified parentROI if it is larger than sizeMax, with optional overlaps.
 * <p>
 * The purpose of this is to create useful tiles whenever the exact tile size may not be essential, and overlaps may be required.
 * Tiles at the parentROI boundary will be trimmed to fit inside. If the parentROI is smaller, it is returned as is.
 *
 * @param parentROI main ROI to be tiled
 * @param sizePreferred the preferred size; in general tiles should have this size
 * @param sizeMax the maximum allowed size; occasionally it is more efficient to have a tile larger than the preferred size towards a ROI boundary to avoid creating very small tiles unnecessarily
 * @param fixedSize if true, the tile size is enforced so that complete tiles have the same size
 * @param overlap optional requested overlap between tiles
 * @return
 *
 * @see #makeTiles(ROI, int, int, boolean)
 */
public static Collection<? extends ROI> computeTiledROIs(ROI parentROI, ImmutableDimension sizePreferred, ImmutableDimension sizeMax, boolean fixedSize, int overlap) {
    ROI pathArea = parentROI != null && parentROI.isArea() ? parentROI : null;
    Rectangle2D bounds = AwtTools.getBounds2D(parentROI);
    if (pathArea == null || (bounds.getWidth() <= sizeMax.width && bounds.getHeight() <= sizeMax.height)) {
        return Collections.singletonList(parentROI);
    }
    Geometry geometry = pathArea.getGeometry();
    PreparedGeometry prepared = null;
    double xMin = bounds.getMinX();
    double yMin = bounds.getMinY();
    int nx = (int) Math.ceil(bounds.getWidth() / sizePreferred.width);
    int ny = (int) Math.ceil(bounds.getHeight() / sizePreferred.height);
    double w = fixedSize ? sizePreferred.width : (int) Math.ceil(bounds.getWidth() / nx);
    double h = fixedSize ? sizePreferred.height : (int) Math.ceil(bounds.getHeight() / ny);
    // Center the tiles
    xMin = (int) (bounds.getCenterX() - (nx * w * .5));
    yMin = (int) (bounds.getCenterY() - (ny * h * .5));
    // This can be very slow if we have an extremely large number of vertices/tiles.
    // For that reason, we try to split initially by either rows or columns if needed.
    boolean byRow = false;
    boolean byColumn = false;
    Map<Integer, Geometry> rowParents = null;
    Map<Integer, Geometry> columnParents = null;
    var envelope = geometry.getEnvelopeInternal();
    if (ny > 1 && nx > 1 && geometry.getNumPoints() > 1000) {
        // If we have a lot of points, create a prepared geometry so we can check covers/intersects quickly;
        // (for a regular geometry, it would be faster to just compute an intersection and see if it's empty)
        prepared = PreparedGeometryFactory.prepare(geometry);
        var prepared2 = prepared;
        var empty = geometry.getFactory().createEmpty(2);
        byRow = nx > ny;
        byColumn = !byRow;
        double yMin2 = yMin;
        double xMin2 = xMin;
        // Compute intersection by row so that later intersections are simplified
        if (byRow) {
            rowParents = IntStream.range(0, ny).parallel().mapToObj(yi -> yi).collect(Collectors.toMap(yi -> yi, yi -> {
                double y = yMin2 + yi * h - overlap;
                var row = GeometryTools.createRectangle(envelope.getMinX(), y, envelope.getMaxX(), h + overlap * 2);
                if (!prepared2.intersects(row))
                    return empty;
                else if (prepared2.covers(row))
                    return row;
                var temp = intersect(geometry, row);
                return temp == null ? geometry : temp;
            }));
        }
        if (byColumn) {
            columnParents = IntStream.range(0, nx).parallel().mapToObj(xi -> xi).collect(Collectors.toMap(xi -> xi, xi -> {
                double x = xMin2 + xi * w - overlap;
                var col = GeometryTools.createRectangle(x, envelope.getMinY(), w + overlap * 2, envelope.getMaxX());
                if (!prepared2.intersects(col))
                    return empty;
                else if (prepared2.covers(col))
                    return col;
                var temp = intersect(geometry, col);
                return temp == null ? geometry : temp;
            }));
        }
    }
    // Geometry local is the one we're working with for the current row or column
    // (often it's the same as the full ROI)
    Geometry geometryLocal = geometry;
    // Generate all the rectangles as geometries
    Map<Geometry, Geometry> tileGeometries = new LinkedHashMap<>();
    for (int yi = 0; yi < ny; yi++) {
        double y = yMin + yi * h - overlap;
        if (rowParents != null)
            geometryLocal = rowParents.getOrDefault(y, geometry);
        for (int xi = 0; xi < nx; xi++) {
            double x = xMin + xi * w - overlap;
            if (columnParents != null)
                geometryLocal = columnParents.getOrDefault(x, geometry);
            if (geometryLocal.isEmpty())
                continue;
            // Create the tile
            var rect = GeometryTools.createRectangle(x, y, w + overlap * 2, h + overlap * 2);
            // Use a prepared geometry if we have one to check covers/intersects & save some effort
            if (prepared != null) {
                if (!prepared.intersects(rect)) {
                    continue;
                } else if (prepared.covers(rect)) {
                    tileGeometries.put(rect, rect);
                    continue;
                }
            }
            // Checking geometryLocal.intersects(rect) first is actually much slower!
            // So add everything and filter out empty tiles later.
            tileGeometries.put(rect, geometryLocal);
        }
    }
    // Compute intersections & map to ROIs
    var plane = parentROI.getImagePlane();
    var tileROIs = tileGeometries.entrySet().parallelStream().map(entry -> intersect(entry.getKey(), entry.getValue())).filter(g -> g != null).map(g -> GeometryTools.geometryToROI(g, plane)).collect(Collectors.toList());
    // If there was an exception, the tile will be null
    if (tileROIs.size() < tileGeometries.size()) {
        logger.warn("Tiles lost during tiling: {}", tileGeometries.size() - tileROIs.size());
        logger.warn("You may be able to avoid tiling errors by calling 'Simplify shape' on any complex annotations first.");
    }
    // Remove any empty/non-area tiles
    return tileROIs.stream().filter(t -> !t.isEmpty() && t.isArea()).collect(Collectors.toList());
}
Also used : IntStream(java.util.stream.IntStream) Rectangle(java.awt.Rectangle) Area(java.awt.geom.Area) Rectangle2D(java.awt.geom.Rectangle2D) LoggerFactory(org.slf4j.LoggerFactory) HashMap(java.util.HashMap) PathIterator(java.awt.geom.PathIterator) ArrayList(java.util.ArrayList) LinkedHashMap(java.util.LinkedHashMap) Point2(qupath.lib.geom.Point2) Ellipse2D(java.awt.geom.Ellipse2D) ImageRegion(qupath.lib.regions.ImageRegion) Map(java.util.Map) PreparedGeometry(org.locationtech.jts.geom.prep.PreparedGeometry) AffineTransformation(org.locationtech.jts.geom.util.AffineTransformation) Shape(java.awt.Shape) Line2D(java.awt.geom.Line2D) Logger(org.slf4j.Logger) Collection(java.util.Collection) AwtTools(qupath.lib.awt.common.AwtTools) AffineTransform(java.awt.geom.AffineTransform) Collectors(java.util.stream.Collectors) Path2D(java.awt.geom.Path2D) ROI(qupath.lib.roi.interfaces.ROI) List(java.util.List) ImagePlane(qupath.lib.regions.ImagePlane) Geometry(org.locationtech.jts.geom.Geometry) ImmutableDimension(qupath.lib.geom.ImmutableDimension) Collections(java.util.Collections) PreparedGeometryFactory(org.locationtech.jts.geom.prep.PreparedGeometryFactory) Rectangle2D(java.awt.geom.Rectangle2D) ROI(qupath.lib.roi.interfaces.ROI) LinkedHashMap(java.util.LinkedHashMap) PreparedGeometry(org.locationtech.jts.geom.prep.PreparedGeometry) Geometry(org.locationtech.jts.geom.Geometry) PreparedGeometry(org.locationtech.jts.geom.prep.PreparedGeometry)

Aggregations

ImmutableDimension (qupath.lib.geom.ImmutableDimension)6 ROI (qupath.lib.roi.interfaces.ROI)6 BufferedImage (java.awt.image.BufferedImage)4 SimpleModifiableImage (qupath.lib.analysis.images.SimpleModifiableImage)4 MeasurementList (qupath.lib.measurements.MeasurementList)4 RegionRequest (qupath.lib.regions.RegionRequest)4 ArrayList (java.util.ArrayList)3 LinkedHashMap (java.util.LinkedHashMap)2 List (java.util.List)2 PixelCalibration (qupath.lib.images.servers.PixelCalibration)2 PathCellObject (qupath.lib.objects.PathCellObject)2 ParameterList (qupath.lib.plugins.parameters.ParameterList)2 Rectangle (java.awt.Rectangle)1 Shape (java.awt.Shape)1 AffineTransform (java.awt.geom.AffineTransform)1 Area (java.awt.geom.Area)1 Ellipse2D (java.awt.geom.Ellipse2D)1 Line2D (java.awt.geom.Line2D)1 Path2D (java.awt.geom.Path2D)1 PathIterator (java.awt.geom.PathIterator)1