Skip to content

Commit

Permalink
Merge pull request #22 from alanocallaghan/tiler
Browse files Browse the repository at this point in the history
Add tiler
  • Loading branch information
petebankhead authored Aug 21, 2023
2 parents 0c5b414 + d2e7d93 commit 63a8da0
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 26 deletions.
6 changes: 1 addition & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@ plugins {
id 'org.bytedeco.gradle-javacpp-platform'
}

// TODO: Change the module name
ext.moduleName = 'io.github.qupath.extension.wsinfer'

// TODO: Define the extension version & provide a short description
version = "0.1.0-SNAPSHOT"
description = 'An extension to run WSInfer in QuPath'

// TODO: Specify the QuPath version, compatible with the extension.
// The default 'gradle.ext.qupathVersion' reads this from settings.gradle.
ext.qupathVersion = gradle.ext.qupathVersion

// TODO: Specify the Java version compatible with the extension
// Generally 11 for QuPath v0.4.3, but will be 17 for QuPath v0.5.0
ext.qupathJavaVersion = 11

Expand Down Expand Up @@ -147,4 +143,4 @@ repositories {
url "https://maven.scijava.org/content/repositories/snapshots"
}

}
}
267 changes: 267 additions & 0 deletions src/main/java/qupath/ext/wsinfer/Tiler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/**
* Copyright 2023 University of Edinburgh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package qupath.ext.wsinfer;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.objects.PathObject;
import qupath.lib.objects.PathObjects;
import qupath.lib.objects.PathTileObject;
import qupath.lib.regions.ImagePlane;
import qupath.lib.roi.GeometryTools;
import qupath.lib.roi.interfaces.ROI;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* A class used to split {@link ROI} or {@link Geometry} objects into
* rectangular tiles. Useful for parallel processing.
*/
public class Tiler {
private int tileWidth;
private int tileHeight;
private boolean trimToParent = true;
private boolean symmetric = true;
private boolean filterByCentroid = true;

/**
* Create a Tiler object.
* @param tileWidth the width in pixels.
* @param tileHeight the height in pixels.
*/
public Tiler(int tileWidth, int tileHeight) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
}

/**
*
* @param tileWidth tile width in pixels.
* @param tileHeight tile height in pixels.
* @param trimToParent controls whether tiles should be trimmed to fit
* within the parent object.
* @param symmetric controls whether the Tiler should aim to split the
* parent object symmetrically. If false, it will
* begin at the top left of the parent.
* @param filterByCentroid controls whether tiles whose centroid is outwith
* the parent object will be removed from the
* output.
*/
public Tiler(int tileWidth, int tileHeight,
boolean trimToParent, boolean symmetric,
boolean filterByCentroid) {
this(tileWidth, tileHeight);
this.trimToParent = trimToParent;
this.symmetric = symmetric;
this.filterByCentroid = filterByCentroid;
}

/**
* Get the width of output tiles
* @return the width in pixels
*/
public int getTileWidth() {
return tileWidth;
}

/**
* Change the width of output tiles
* @param tileWidth the new width in pixels
*/
public void setTileWidth(int tileWidth) {
this.tileWidth = tileWidth;
}

/**
* Change the height of output tiles
* @return the height in pixels
*/
public int getTileHeight() {
return tileHeight;
}

/**
* Change the height of output tiles
* @param tileHeight the new height in pixels
*/
public void setTileHeight(int tileHeight) {
this.tileHeight = tileHeight;
}

/**
* Check if the tiler is set to trim output to the input parent.
* @return whether the tiler is set to trim output to the parent object
*/
public boolean isTrimToParent() {
return trimToParent;
}

/**
* Set whether the tiler is set to trim output to the input parent.
* @param trimToParent the new setting
*/
public void setTrimToParent(boolean trimToParent) {
this.trimToParent = trimToParent;
}

/**
* Check if the tiler will try to tile symmetrically, or will start
* directly from the top-left of the parent.
* @return The current setting
*/
public boolean isSymmetric() {
return symmetric;
}

/**
* Set if the tiler will try to tile symmetrically, or will start
* directly from the top-left of the parent.
* @param symmetric The new setting
*/
public void setSymmetric(boolean symmetric) {
this.symmetric = symmetric;
}

/**
* Check if the tiler will filter the output based on whether the centroid
* of tiles lies within the parent
* @return The current setting
*/
public boolean isFilterByCentroid() {
return filterByCentroid;
}

/**
* Set if the tiler will filter the output based on whether the centroid
* of tiles lies within the parent
* @param filterByCentroid the new setting
*/
public void setFilterByCentroid(boolean filterByCentroid) {
this.filterByCentroid = filterByCentroid;
}

/**
* Create a list of {@link Geometry} tiles from the input. These may
* not all be rectangular based on the settings used.
* @param parent the object that will be split into tiles.
* @return a list of tiles
*/
public List<Geometry> createGeometries(Geometry parent) {
if (parent == null) {
return new ArrayList<>();
}
Geometry boundingBox = parent.isRectangle() ? parent : parent.getEnvelope();
Coordinate[] coordinates = boundingBox.getCoordinates(); // (minx miny, minx maxy, maxx maxy, maxx miny, minx miny).
double xStart = coordinates[0].x;
double yStart = coordinates[0].y;
double xEnd = coordinates[2].x;
double yEnd = coordinates[2].y;

double bBoxWidth = xEnd - xStart;
double bBoxHeight = yEnd - yStart;

if (symmetric) {
xStart += calculateOffset(tileWidth, bBoxWidth);
yStart += calculateOffset(tileHeight, bBoxHeight);
}
List<Geometry> tiles = new ArrayList<>();
for (int x = (int) xStart; x < xEnd; x += tileWidth) {
for (int y = (int) yStart; y < yEnd; y += tileHeight) {
Geometry tile = GeometryTools.createRectangle(x, y, tileWidth, tileHeight);
// straightforward case 1:
// if there's no intersection, we're in the bounding box but not
// the parent
if (!parent.intersects(tile)) {
continue;
}
// straightforward case 2:
// tile is cleanly within roi
if (parent.contains(tile)) {
tiles.add(tile);
continue;
}

// trimming:
if (trimToParent) {
// trim the tile to fit the parent
tile = tile.intersection(parent);
tiles.add(tile);
} else if (!filterByCentroid | parent.contains(tile.getCentroid())) {
// If we aren't trimming based on centroids,
// or it'd be included anyway
tiles.add(tile);
}
}
}
return tiles;
}

/**
* Create a list of {@link ROI} tiles from the input. These may
* not all be rectangular based on the settings used.
* @param parent the object that will be split into tiles.
* @return a list of tiles
*/
public List<ROI> createROIs(ROI parent) {
return createGeometries(parent.getGeometry()).stream()
.map(g -> GeometryTools.geometryToROI(g, parent.getImagePlane()))
.collect(Collectors.toList());
}

/**
* Create a list of {@link PathObject} tiles from the input. These may
* not all be rectangular based on the settings used.
* @param parent the object that will be split into tiles.
* @param creator a function used to create the desired type
* of {@link PathObject}
* @return a list of tiles
*/
public List<PathObject> createObjects(ROI parent, Function<ROI, PathObject> creator) {
return createROIs(parent).stream().map(creator).collect(Collectors.toList());
}

/**
* Create a list of {@link PathTileObject} tiles from the input. These may
* not all be rectangular based on the settings used.
* @param parent the object that will be split into tiles.
* @return a list of tiles
*/
public List<PathObject> createTiles(ROI parent) {
return createObjects(parent, PathObjects::createTileObject);
}

/**
* Create a list of {@link PathAnnotationObject} tiles from the input. These may
* not all be rectangular based on the settings used.
* @param parent the object that will be split into tiles.
* @return a list of tiles
*/
public List<PathObject> createAnnotations(ROI parent) {
return createObjects(parent, PathObjects::createAnnotationObject);
}

private static double calculateOffset(final int tileDim, final double parentDim) {
double mod = parentDim % tileDim;
if (mod == 0) {
return 0;
}
return mod / 2;
}
}
58 changes: 38 additions & 20 deletions src/main/java/qupath/ext/wsinfer/WSInfer.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@
import qupath.lib.gui.dialogs.Dialogs;
import qupath.lib.images.ImageData;
import qupath.lib.images.servers.ImageServer;
import qupath.lib.images.servers.PixelCalibration;
import qupath.lib.objects.PathObject;
import qupath.lib.objects.PathObjects;
import qupath.lib.objects.classes.PathClass;
import qupath.lib.roi.GeometryTools;
import qupath.lib.scripting.QP;

import java.awt.image.BufferedImage;
Expand Down Expand Up @@ -370,31 +373,46 @@ private static List<PathObject> getTilesForInference(ImageData<BufferedImage> im
throw new IllegalArgumentException(resources.getString("No tiles or annotations selected!"));
}

// create tiles from the selected annotations
var annotationSet = new LinkedHashSet<>(selectedAnnotations); // We want this later
Map<String, Object> pluginArgs = new LinkedHashMap<>();
if (imageData.getServer().getPixelCalibration().hasPixelSizeMicrons()) {
pluginArgs.put("tileSizeMicrons", config.getPatchSizePixels() * config.getSpacingMicronPerPixel());
double tileWidth, tileHeight;
PixelCalibration cal = imageData.getServer().getPixelCalibration();
if (cal.hasPixelSizeMicrons()) {
double tileSizeMicrons = config.getPatchSizePixels() * config.getSpacingMicronPerPixel();
tileWidth = (int)(tileSizeMicrons / cal.getPixelWidthMicrons() + .5);
tileHeight = (int)(tileSizeMicrons / cal.getPixelHeightMicrons() + .5);
} else {
logger.warn("Pixel calibration not available, so using pixels instead of microns");
pluginArgs.put("tileSizePixels", config.getPatchSizePixels());
tileWidth = (int)(config.getPatchSizePixels() + .5);
tileHeight = tileWidth;
}
pluginArgs.put("trimToROI", false);
pluginArgs.put("makeAnnotations", false);
pluginArgs.put("removeParentAnnotation", false);
try {
QP.runPlugin("qupath.lib.algorithms.TilerPlugin", imageData, pluginArgs);
// We want our new tiles to be selected... but we also want to ensure that any tile object
// has a selected annotation as a parent (in case there were other tiles already)
return imageData.getHierarchy().getTileObjects()
.stream()
.filter(t -> annotationSet.contains(t.getParent()))
.collect(Collectors.toList());
} catch (InterruptedException e) {
logger.warn("Tiling interrupted", e);
return new ArrayList<>();
var tiler = new Tiler(
(int)tileWidth,
(int)tileHeight);
tiler.setTrimToParent(false);
tiler.setFilterByCentroid(true);
tiler.setSymmetric(true);

for (var annotation: selectedAnnotations) {
var tiles = tiler.createTiles(annotation.getROI());

// add tiles to the hierarchy
annotation.clearChildObjects();
for (int i = 0; i < tiles.size(); i++) {
var tile = tiles.get(i);
tile.setName("Tile " + i);
annotation.addChildObject(tile);
}
annotation.setLocked(true);
imageData.getHierarchy().fireHierarchyChangedEvent(annotation);
}
}

// We want our new tiles to be selected... but we also want to ensure that any tile object
// has a selected annotation as a parent (in case there were other tiles already)
return imageData.getHierarchy().getTileObjects()
.stream()
.filter(t -> annotationSet.contains(t.getParent()))
.collect(Collectors.toList());

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* Class representing the configuration of a WSInfer model.
* <p>
* This is a Java representation of the JSON configuration file,
* and stored the key information needed to run the model (preprocessing, resolution, output classes).
* and stores the key information needed to run the model (preprocessing, resolution, output classes).
* <p>
* See https://github.com/SBU-BMI/wsinfer-zoo/blob/main/wsinfer_zoo/schemas/model-config.schema.json
*/
Expand Down

0 comments on commit 63a8da0

Please sign in to comment.