Last active
May 11, 2023 16:52
-
-
Save petebankhead/486690d13912f6a95bb0489458df959e to your computer and use it in GitHub Desktop.
Interactively create a convex or concave hull annotation from the selected objects in QuPath v0.4.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Create a convex or concave hull annotation from the selected objects. | |
* | |
* The calculations are performed using Java Topology Suite's 'ConcaveHullOfPolygons' class - | |
* see https://locationtech.github.io/jts/javadoc/org/locationtech/jts/algorithm/hull/ConcaveHullOfPolygons.html | |
* | |
* This was written for QuPath v0.4.3. | |
* If it's useful enough, a similar feature might be added to QuPath directly in the future. | |
* | |
* @author Pete Bankhead | |
*/ | |
import javafx.application.Platform | |
import javafx.geometry.Insets | |
import javafx.scene.Scene | |
import javafx.scene.control.Button | |
import javafx.scene.control.CheckBox | |
import javafx.scene.control.ComboBox | |
import javafx.scene.control.Label | |
import javafx.scene.control.Tooltip | |
import javafx.scene.layout.GridPane | |
import javafx.stage.Stage | |
import org.locationtech.jts.algorithm.hull.ConcaveHullOfPolygons | |
import qupath.lib.gui.QuPathGUI | |
import qupath.lib.gui.dialogs.Dialogs | |
import qupath.lib.gui.tools.GuiTools | |
import qupath.lib.gui.tools.PaneTools | |
import qupath.lib.objects.PathObject | |
import qupath.lib.objects.PathObjectTools | |
import qupath.lib.objects.PathObjects | |
import qupath.lib.objects.classes.PathClass | |
import qupath.lib.roi.GeometryTools | |
Platform.runLater { buildStage().show()} | |
Stage buildStage() { | |
def qupath = QuPathGUI.getInstance() | |
def pane = new GridPane() | |
def combo = new ComboBox<PathClass>(qupath.getAvailablePathClasses()) | |
combo.getSelectionModel().selectFirst() | |
combo.setTooltip(new Tooltip("Classification for the new annotation to create")) | |
def labelCombo = new Label("Classification") | |
labelCombo.setLabelFor(combo) | |
Set<PathObject> lastParentObjects = new HashSet<>() | |
PathObject lastAnnotation = null | |
int row = 0 | |
pane.add(labelCombo, 0, row, 1, 1) | |
pane.add(combo, 1, row, 1, 1) | |
def spinner = GuiTools.createDynamicStepSpinner(0, 1, 1.0, 0.05, 1) | |
def labelSpinner = new Label("Length ratio") | |
spinner.setTooltip(new Tooltip("Length ratio to compute a (possibly) concave hull.\n" + | |
"Use 0 for the original objects, or 1 to generate a convex hull (if 'tight polygon' is false).")) | |
row++; | |
pane.add(labelSpinner, 0, row, 1, 1) | |
pane.add(spinner, 1, row, 1, 1) | |
def cbHoles = new CheckBox("Allow holes") | |
cbHoles.setTooltip(new Tooltip("Retain holes in polygons (if present) rather than filling them in")) | |
row++; | |
pane.add(cbHoles, 0, row, 2, 1) | |
def cbTight = new CheckBox("Tight polygon") | |
cbTight.setSelected(true) | |
cbTight.setTooltip(new Tooltip("Create a tight polygon that more closely follows the shape of the objects\n" + | |
"rather than a convex hull that may extend beyond the objects")) | |
row++; | |
pane.add(cbTight, 0, row, 2, 1) | |
def btnCreate = new Button("Create") | |
row++; | |
pane.add(btnCreate, 0, row, 2, 1) | |
btnCreate.setOnAction {e -> | |
def hierarchy = qupath.getViewer()?.getHierarchy() | |
def objects = hierarchy == null ? [] : hierarchy.getSelectionModel().getSelectedObjects() | |
if (objects.isEmpty()) { | |
lastParentObjects.clear() | |
lastAnnotation = null | |
Dialogs.showErrorMessage("Create Hull", | |
"Please select some objects to compute a concave or convex hull!") | |
return | |
} | |
def selected = new HashSet<>(objects) | |
if (lastParentObjects.equals(selected) && | |
lastAnnotation != null && | |
PathObjectTools.hierarchyContainsObject(hierarchy, lastAnnotation)) { | |
hierarchy.removeObject(lastAnnotation, true) | |
} | |
def annotation = createAnnotation(selected, spinner.getValue(), combo.getValue(), | |
cbHoles.isSelected(), cbTight.isSelected()) | |
if (annotation != null && hierarchy != null) { | |
hierarchy.addObject(annotation) | |
} | |
lastAnnotation = annotation | |
lastParentObjects = selected | |
} | |
pane.setHgap(5) | |
pane.setVgap(5) | |
pane.setPadding(new Insets(5)) | |
PaneTools.setToExpandGridPaneWidth(combo, btnCreate, spinner, cbTight, cbHoles) | |
def scene = new Scene(pane) | |
def stage = new Stage() | |
stage.setTitle("Create surrounding annotation") | |
stage.setScene(scene) | |
stage.initOwner(qupath.getStage()) | |
return stage | |
} | |
def createAnnotation(Collection<? extends PathObject> objects, Double lengthRatio, PathClass pathClass, | |
Boolean allowHoles, | |
Boolean tight) { | |
if (pathClass == null) { | |
println "WARN: Please select a classification for the output annotation" | |
return | |
} | |
def geometries = objects.collect { p -> p.getROI().getGeometry() } | |
def geometry = GeometryTools.union(geometries) | |
def hull = new ConcaveHullOfPolygons(geometry) | |
hull.setMaximumEdgeLengthRatio(lengthRatio) | |
hull.setHolesAllowed(allowHoles) | |
hull.setTight(tight) | |
def output = hull.getHull() | |
if (!allowHoles) | |
output = GeometryTools.fillHoles(output) | |
def annotation = PathObjects.createAnnotationObject( | |
GeometryTools.geometryToROI(output, objects.iterator().next().getROI().getImagePlane()), | |
PathClass.NULL_CLASS == pathClass ? null : pathClass | |
) | |
return annotation | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment