Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save petebankhead/486690d13912f6a95bb0489458df959e to your computer and use it in GitHub Desktop.
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.
/**
* 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