Skip to content

Instantly share code, notes, and snippets.

@Svidro
Last active July 17, 2023 02:16
Show Gist options
  • Save Svidro/ffb6951e70187de5eb007290f61aea4a to your computer and use it in GitHub Desktop.
Save Svidro/ffb6951e70187de5eb007290f61aea4a to your computer and use it in GitHub Desktop.
Creating objects in QuPath
Collections of scripts harvested mainly from Pete, but also picked up from the forums
TOC
Annotation subtraction example.groovy - An example of creating and subtracing ROIs through scripting
Create annotation of fixed size.groovy - see https://petebankhead.github.io/qupath/scripting/2018/03/09/script-create-fixed-size-region.html
Create object based on Viewer position.groovy - Creates an object at the Viewer position. In this case a rectangle. Useful if you want
to create the exact same size object multiple times and then move it to an area of interest for subsampling.
Creating a TMA from script.groovy - Creates a basic TMA, which you would need to then manually position. Easiest to start small and then
add rows and columns through the QuPath TMA menu.
Creating TMA annotations and set missing by area.groovy - Takes a grid, runs a tissue detection (edit this to your own), and then set
missing and delete annotations in any cores below a threshold.
TMA Dearrayer from a script.groovy - More options for the TMA dearrayer than through the GUI. Read through the script carefully.
Tumor invasion areas.groovy - Generates annotation bands from the surface of the tumor inward. Can be used on tumors on the
surface of the tissue (bordering whitespace), but check the link included in the script.
Use points to place cells.groovy - mark locations using the Points tool, then place cells of a given class/size on top of each point.
Zstack or Time series full image annotations.groovy - Go through all layers of a Z stack or time series and place a full image annotation
in each.
Zstack or time series annotation copy.groovy - Similar to previous, but take all current annotations, and copy them to ALL OTHER time
or Z frames.
//Courtesy of Sara McArdle from the La Jolla Institute
//Create multiple bands within tissue, with the distance from the tissue surface (um) determined by the variables below
//Adjust class names within the script
//0.2.0
double firstRadius = -750
double secondRadius = -850
import qupath.lib.roi.*
import qupath.lib.objects.*
def imageData = getCurrentImageData()
def hierarchy = imageData.getHierarchy()
//EDIT THIS LINE TO YOUR TISSUE DETECTION SETTINGS
//runPlugin('qupath.imagej.detect.tissue.SimpleTissueDetection2', '{"threshold": 155, "requestedPixelSizeMicrons": 20.0, "minAreaMicrons": 10000.0, "maxHoleAreaMicrons": 1000000.0, "darkBackground": false, "smoothImage": true, "medianCleanup": true, "dilateBoundaries": false, "smoothCoordinates": true, "excludeOnBoundary": false, "singleAnnotation": true}');
selectAnnotations()
def outer=getAnnotationObjects().find{p -> (p.getLevel()==1) && (p.isAnnotation() == true)}
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": '+firstRadius+', "removeInterior": false, "constrainToParent": true}')
def middle=getAnnotationObjects().find{p -> (p.getLevel()==2) && (p.isAnnotation() == true)}
//Reselecting the outer object turns out to be very important before running the DilateAnnotationPlugin a second (or third) time.
getCurrentHierarchy().getSelectionModel().setSelectedObject(outer)
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": '+secondRadius+', "removeInterior": false, "constrainToParent": true}')
//write some stuff if you have more than 1 outer object for odd shapes
def inner=getAnnotationObjects().find{p -> (p.getLevel()==3) && (p.isAnnotation() == true)}
inner.setName("Medulla")
def toAdd = []
def toRemove = []
outerRing = PathROIToolsAwt.combineROIs(outer.getROI(), middle.getROI(), PathROIToolsAwt.CombineOp.SUBTRACT)
toRemove << middle
def orObject=new PathAnnotationObject(outerRing, outer.getPathClass())
orObject.setName("Cortex")
toAdd << orObject
innerRing = PathROIToolsAwt.combineROIs(middle.getROI(), inner.getROI(), PathROIToolsAwt.CombineOp.SUBTRACT)
def irObject = new PathAnnotationObject(innerRing, outer.getPathClass())
irObject.setName("Middle Zone")
toAdd << irObject
toRemove << outer
hierarchy.addPathObjects(toAdd,false)
hierarchy.removeObjects(toRemove,true)
print "Done"
/**
* Create a region annotation with a fixed size in QuPath, based on the current viewer location.
* 0.1.2
* @author Pete Bankhead
*/
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.objects.classes.PathClassFactory
import qupath.lib.roi.RectangleROI
import qupath.lib.scripting.QPEx
// Define the size of the region to create
double sizeMicrons = 200.0
// Get main data structures
def imageData = QPEx.getCurrentImageData()
def server = imageData.getServer()
// Convert size in microns to pixels - QuPath ROIs are defined in pixel units of the full-resolution image
int sizePixels = Math.round(sizeMicrons / server.getAveragedPixelSizeMicrons())
// Get the current viewer & the location of the pixel currently in the center
def viewer = QPEx.getCurrentViewer()
double cx = viewer.getCenterPixelX()
double cy = viewer.getCenterPixelY()
// Create a new Rectangle ROI
def roi = new RectangleROI(cx-sizePixels/2, cy-sizePixels/2, sizePixels, sizePixels)
// Create & new annotation & add it to the object hierarchy
def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Region"))
imageData.getHierarchy().addPathObject(annotation, false)
//0.1.2 and 0.2.0
import qupath.lib.roi.RectangleROI
import qupath.lib.objects.PathAnnotationObject
// Size in pixels at the base resolution
// note that the actual size will be one pixel larger in each dimension
int size = 255
// Get center pixel
def viewer = getCurrentViewer()
int cx = viewer.getCenterPixelX()
int cy = viewer.getCenterPixelY()
// Create & add annotation
def roi = new RectangleROI(cx-size/2, cy-size/2, size, size)
def rgb = getColorRGB(50, 50, 200)
def pathClass = getPathClass('Other', rgb)
def annotation = new PathAnnotationObject(roi, pathClass)
addObject(annotation)
//For those times when the automatic TMA dearrayer is having trouble picking up cores (too light/sparse)
// from https://github.com/qupath/qupath/issues/77
//0.1.2, metadata would need to be changed for 0.2.0
import qupath.lib.objects.TMACoreObject
import qupath.lib.objects.hierarchy.DefaultTMAGrid
// Enter the number of horizontal & vertical cores here
int numHorizontal = 12
int numVertical = 9
// Enter the core diameter, in millimetres
double diameterMM = 1.2
// Convert diameter to pixels
double diameterPixels = (diameterMM * 1000) / getCurrentImageData().getServer().getAveragedPixelSizeMicrons()
// Get the current ROI
def roi = getSelectedROI()
// Create the cores
def cores = []
double xSpacing = roi.getBoundsWidth() / numHorizontal
double ySpacing = roi.getBoundsHeight() / numVertical
for (int i = 0; i < numVertical; i++) {
for (int j = 0; j < numHorizontal; j++) {
double x = roi.getBoundsX() + xSpacing / 2 + xSpacing * j
double y = roi.getBoundsY() + ySpacing / 2 + ySpacing * i
cores << new TMACoreObject(x, y, diameterPixels, false)
}
}
// Create & set the grid
def tmaGrid = new DefaultTMAGrid(cores, numHorizontal)
getCurrentHierarchy().setTMAGrid(tmaGrid)
//Set TMA cores where the tissue area is below a threshold to Missing so as to avoid inclusion in future calculations.
//0.1.2, but should work with 0.2.0 if the metadata section at the top is adjusted.
MINIMUM_AREA_um2 = 30000
def imageData = getCurrentImageData()
def server = imageData.getServer()
def pixelSize = server.getPixelHeightMicrons()
getTMACoreList().each{
it.setMissing(false)
}
selectTMACores();
runPlugin('qupath.imagej.detect.tissue.SimpleTissueDetection2', '{"threshold": 233, "requestedPixelSizeMicrons": 10.0, "minAreaMicrons": 10000.0, "maxHoleAreaMicrons": 1000000.0, "darkBackground": false, "smoothImage": true, "medianCleanup": true, "dilateBoundaries": false, "smoothCoordinates": true, "excludeOnBoundary": false, "singleAnnotation": true}');
//Potentially alter script to run off of mean intensity in core?
//runPlugin('qupath.lib.algorithms.IntensityFeaturesPlugin', '{"pixelSizeMicrons": 2.0, "region": "ROI", "tileSizeMicrons": 25.0, "colorOD": true, "colorStain1": false, "colorStain2": false, "colorStain3": false, "colorRed": false, "colorGreen": false, "colorBlue": false, "colorHue": false, "colorSaturation": false, "colorBrightness": false, "doMean": false, "doStdDev": false, "doMinMax": false, "doMedian": false, "doHaralick": false, "haralickDistance": 1, "haralickBins": 32}');
getTMACoreList().each{
it.setMissing(true)
list = it.getChildObjects()
println(list)
if ( list.size() > 0){
double total = 0
for (object in list) {
total = total+object.getROI().getArea()
}
total = total*pixelSize*pixelSize
println(it.getName()+ " list "+list.size()+ "total "+total)
if ( total > MINIMUM_AREA_um2 ){
it.setMissing(false)
} else {removeObjects(list, true)}
}
}
fireHierarchyUpdate()
println("done")
// https://forum.image.sc/t/nuclear-dab-disrupts-stardist-detection-of-haematoxylin-in-qupath/50156/2
import qupath.opencv.ops.ImageOp
import qupath.opencv.tools.OpenCVTools
import org.bytedeco.opencv.opencv_core.Mat
import org.bytedeco.opencv.global.opencv_core
import static qupath.lib.gui.scripting.QPEx.*
import qupath.tensorflow.stardist.StarDist2D
import qupath.lib.images.servers.*
// Specify the model directory (you will need to change this!)
def pathModel = '/path/to/dsb2018_heavy_augment'
double originalPixelSize = getCurrentImageData().getServer().getPixelCalibration().getAveragedPixelSizeMicrons();
def stardist = StarDist2D.builder(pathModel)
.threshold(0.5) // Probability (detection) threshold
.channels(
ColorTransforms.createColorDeconvolvedChannel(getCurrentImageData().getColorDeconvolutionStains(), 1),
ColorTransforms.createColorDeconvolvedChannel(getCurrentImageData().getColorDeconvolutionStains(), 2)
) // Select detection channel
.preprocess(new AddChannelsOp())
.normalizePercentiles(1, 99) // Percentile normalization
.pixelSize(originalPixelSize) // Resolution for detection
.cellExpansion(3.0) // Approximate cells based upon nucleus expansion
.cellConstrainScale(1.5) // Constrain cell expansion using nucleus size
.measureShape() // Add shape measurements
.measureIntensity() // Add cell measurements (in all compartments)
.includeProbability(true) // Add probability as a measurement (enables later filtering)
.build()
// Run detection for the selected objects
def imageData = getCurrentImageData()
def pathObjects = getSelectedObjects()
if (pathObjects.isEmpty()) {
Dialogs.showErrorMessage("StarDist", "Please select a parent object!")
return
}
stardist.detectObjects(imageData, pathObjects)
println 'Done!'
class AddChannelsOp implements ImageOp {
@Override
public Mat apply(Mat input) {
def channels = OpenCVTools.splitChannels(input)
if (channels.size() == 1)
return input
def sum = opencv_core.add(channels[0], channels[1])
for (int i = 2; i < channels.size(); i++)
sum = opencv_core.add(sum, channels[i])
return sum.asMat()
}
}
/* 0.2.3, https://forum.image.sc/t/tma-dearrayer-problem/48234/4
* Run QuPath's TMA dearrayer at a different resolution.
* This can give a bit more control over the output if the original dearraying fails.
*
* @author Pete Bankhead
*/
// Change this to adjust the resolution at which cores are detected
// Lower values mean the dearraying is done on a larger image (and will be slower)
double requestedPixelSize = 10
// Adjust these for other detection parameters
double coreDiameter = 1200
double roiScaleFactor = 1.05
def horizontalLabels = 'A-J'
def verticalLabels = '1-16'
boolean horizontalLabelFirst = 100
double densityThreshold = 0.05
boolean isFluorescence = false
// Actually do the dearraying
import qupath.imagej.detect.dearray.TMADearrayerPluginIJ.Dearrayer
def dearrayer = new Dearrayer()
def server = getCurrentServer()
double downsample = requestedPixelSize / server.getPixelCalibration().getAveragedPixelSize()
double fullCoreDiameterPx = coreDiameter / server.getPixelCalibration().getAveragedPixelSize()
def request = RegionRequest.createInstance(server, downsample)
dearrayer.ip = IJTools.convertToImagePlus(server, request).getImage().getProcessor()
def tmaGrid = dearrayer.doDearraying(
fullCoreDiameterPx,
downsample,
densityThreshold,
roiScaleFactor,
isFluorescence,
PathObjectTools.parseTMALabelString(horizontalLabels),
PathObjectTools.parseTMALabelString(verticalLabels),
horizontalLabelFirst
)
getCurrentHierarchy().setTMAGrid(tmaGrid)
println 'Done! ' + println tmaGrid
/**
* Script to help with annotating tumor regions, chopping increasing chunks into the tumor.
* SEE THREAD HERE FOR DESCRIPTION ON USE:
* Here, each of the margin regions is approximately 100 microns in width.
* Should work with both 1.2 and 0.2.0m2 due to code from Thomas Kilvaer found here: https://petebankhead.github.io/qupath/scripts/2018/08/08/three-regions.html
*
* @author Pete Bankhead
* @mangled by Svidro
*/
import qupath.lib.common.GeneralTools
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.objects.PathObject
import qupath.lib.roi.PathROIToolsAwt
import java.awt.Rectangle
import java.awt.geom.Area
//-----
// Some things you might want to change
// How much to expand each region
double expandMarginMicrons = 100.0
// How many times you want to chop into your annotation. Edit color script around line 115 if you go over 5
int howManyTimes = 4
// Define the colors
// Inner layers are given scripted colors, but gretaer than 6 or 7 layers may require adjustments
def colorOuterMargin = getColorRGB(0, 200, 0)
// Choose whether to lock the annotations or not (it's generally a good idea to avoid accidentally moving them)
def lockAnnotations = true
//-----
// Extract the main info we need
def imageData = getCurrentImageData()
def hierarchy = imageData.getHierarchy()
def server = imageData.getServer()
// We need the pixel size
if (!server.hasPixelSizeMicrons()) {
print 'We need the pixel size information here!'
return
}
if (!GeneralTools.almostTheSame(server.getPixelWidthMicrons(), server.getPixelHeightMicrons(), 0.0001)) {
print 'Warning! The pixel width & height are different; the average of both will be used'
}
// Get annotation & detections
def annotations = getAnnotationObjects()
def selected = getSelectedObject()
if (selected == null || !selected.isAnnotation()) {
print 'Please select an annotation object!'
return
}
// We need one selected annotation as a starting point; if we have other annotations, they will constrain the output
annotations.remove(selected)
// If we have at most one other annotation, it represents the tissue
Area areaTissue
PathObject tissueAnnotation
if (annotations.isEmpty()) {
areaTissue = new Area(new Rectangle(0, 0, server.getWidth(), server.getHeight()))
} else if (annotations.size() == 1) {
tissueAnnotation = annotations.get(0)
areaTissue = PathROIToolsAwt.getArea(tissueAnnotation.getROI())
} else {
print 'Sorry, this script only support one selected annotation for the tumor region, and at most one other annotation to constrain the expansion'
return
}
println("Working, give it some time")
// Calculate how much to expand
double expandPixels = expandMarginMicrons / server.getAveragedPixelSizeMicrons()
def roiOriginal = selected.getROI()
def areaTumor = PathROIToolsAwt.getArea(roiOriginal)
// Get the outer margin area
if (getQuPath().getBuildString().split()[1]<"0.2.0-m2"){
def areaOuter = PathROIToolsAwt.shapeMorphology(areaTumor, expandPixels)
}else {areaOuter = PathROIToolsAwt.getArea(PathROIToolsAwt.roiMorphology(roiOriginal, expandPixels))}
areaOuter.subtract(areaTumor)
areaOuter.intersect(areaTissue)
def roiOuter = PathROIToolsAwt.getShapeROI(areaOuter, roiOriginal.getC(), roiOriginal.getZ(), roiOriginal.getT())
def annotationOuter = new PathAnnotationObject(roiOuter)
annotationOuter.setName("Outer margin")
annotationOuter.setColorRGB(colorOuterMargin)
innerAnnotations = []
innerAnnotations << annotationOuter
for (i=0; i<howManyTimes;i++){
//select the current expansion, which the first time is outside of the tumor, then expand it and intersect it
currentArea = PathROIToolsAwt.getArea(innerAnnotations[innerAnnotations.size()-1].getROI())
if (getQuPath().getBuildString().split()[1]<"0.2.0-m2"){
areaExpansion = PathROIToolsAwt.shapeMorphology(currentArea, expandPixels)
}else {areaExpansion = PathROIToolsAwt.getArea(PathROIToolsAwt.roiMorphology(innerAnnotations[innerAnnotations.size()-1].getROI(), expandPixels))}
areaExpansion.intersect(areaTumor)
areaExpansion.intersect(areaTissue)
if(i>=1){
for (k=1; k<=i;k++){
areaExpansion.subtract(PathROIToolsAwt.getArea(innerAnnotations[innerAnnotations.size()-k].getROI()))
}
}
roiExpansion = PathROIToolsAwt.getShapeROI(areaExpansion, roiOriginal.getC(), roiOriginal.getZ(), roiOriginal.getT())
j = i+1
annotationExpansion = new PathAnnotationObject(roiExpansion)
int nameValue = j*expandMarginMicrons
annotationExpansion.setName("Inner margin "+nameValue+" microns")
annotationExpansion.setColorRGB(getColorRGB(20*i, 40*i, 200-30*i))
innerAnnotations << annotationExpansion
}
// Add the annotations
hierarchy.getSelectionModel().clearSelection()
//hierarchy.removeObject(selected, true)
def annotationsToAdd = innerAnnotations;
annotationsToAdd.each {it.setLocked(lockAnnotations)}
if (tissueAnnotation == null) {
hierarchy.addPathObjects(annotationsToAdd, false)
} else {
tissueAnnotation.addPathObjects(annotationsToAdd)
hierarchy.fireHierarchyChangedEvent(this, tissueAnnotation)
if (lockAnnotations)
tissueAnnotation.setLocked(true)
}
println("Done! Wheeeee!")
//0.1.2 only, create detections from points.
//See 0.2.3 below
import qupath.lib.roi.EllipseROI;
import qupath.lib.objects.PathDetectionObject
points = getAnnotationObjects().findAll{it.isPoint() }
//Cycle through each points object (which is a collection of points)
points.each{
//Cycle through all points within a points object
it.getROI().getPointList().each{
//for each point, create a circle on top of it that is "size" pixels in diameter
x = it.getX()
y = it.getY()
size = 5
def roi = new EllipseROI(x-size/2,y-size/2,size,size, 0,0,0)
pathClass = getPathClass("FakeCell")
def aCell = new PathDetectionObject(roi, pathClass)
addObject(aCell)
}
}
//remove points if desired.
removeObjects(points, false)
//0.2.3 version
import qupath.lib.objects.PathDetectionObject
points = getAnnotationObjects().findAll{it.getROI().isPoint() }
//Cycle through each points object (which is a collection of points)
points.each{
plane = it.getROI().getImagePlane()
pathClass = it.getPathClass()
//Cycle through all points within a points object
it.getROI().getAllPoints().each{
//for each point, create a circle on top of it that is "size" pixels in diameter
x = it.getX()
y = it.getY()
size = 5
def roi = ROIs.createEllipseROI(x-size/2,y-size/2,size,size, plane)
//pathClass = getPathClass("FakeCell")
def aCell = new PathDetectionObject(roi, pathClass)
addObject(aCell)
}
}
resolveHierarchy()
//remove points if desired.
removeObjects(points, false)
//0.1.2
//should work for Zstacks OR time series
//Copies ALL existing annotations to ALL other T or Z slices. Use getAnnotationObjects().findAll{it-> if(something)} to limit this
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.roi.PathROIToolsAwt
hierarchy = getCurrentHierarchy()
def imageData = getCurrentImageData()
def server = imageData.getServer()
def xdist = server.getWidth()
def ydist = server.getHeight()
def annotations = getAnnotationObjects()
if (server.nZSlices() >0){
0.upto(server.nZSlices()-1){
for (annotation in annotations){
def roi = annotation.getROI()
def shape = PathROIToolsAwt.getShape(roi)
// There is a method to create a ROI from a shape which allows us to (finally) set the Z (or T)
def roi2 = PathROIToolsAwt.getShapeROI(shape, -1, it, roi.getT(), 0.5)
// We can now make a new annotation
def annotation2 = new PathAnnotationObject(roi2)
// Add it to the current hierarchy. When we move in Z to the desired slice, we should see the annotation
if (roi2.getZ() != roi.getZ())
hierarchy.addPathObject(annotation2, false);
}
}
}
if (server.nTimepoints() >1){
0.upto(server.nTimepoints()-1){
for (annotation in annotations){
def roi = annotation.getROI()
def shape = PathROIToolsAwt.getShape(roi)
// There is a method to create a ROI from a shape which allows us to (finally) set the Z (or T)
def roi2 = PathROIToolsAwt.getShapeROI(shape, -1, roi.getZ(), it)
// We can now make a new annotation
def annotation2 = new PathAnnotationObject(roi2)
// Add it to the current hierarchy. When we move in T to the desired slice, we should see the annotation
if (roi2.getT() != roi.getT())
hierarchy.addPathObject(annotation2, false);
}
}
}
//See below for 0.2.3 version
//0.1.2
//should work for Zstacks OR time series
//Creates a full image annotation in each frame, which can then be used to generate detections.
import qupath.lib.roi.RectangleROI
import qupath.lib.objects.PathAnnotationObject
hierarchy = getCurrentHierarchy()
def imageData = getCurrentImageData()
def server = imageData.getServer()
def xdist = server.getWidth()
def ydist = server.getHeight()
clearAllObjects()
if (server.nZSlices() >0){
0.upto(server.nZSlices()-1){
frame = new PathAnnotationObject(new RectangleROI(0,0,xdist,ydist,-1,it,0));
addObject(frame);
}
}
if (server.nTimepoints() >0){
0.upto(server.nTimepoints()-1){
frame = new PathAnnotationObject(new RectangleROI(0,0,xdist,ydist,-1,0,it));
addObject(frame);
}
}
//0.2.3 version
//should work for Zstacks OR time series
//Creates a full image annotation in each frame, which can then be used to generate detections.
import qupath.lib.roi.RectangleROI
import qupath.lib.objects.PathAnnotationObject
hierarchy = getCurrentHierarchy()
def imageData = getCurrentImageData()
def server = imageData.getServer()
def xdist = server.getWidth()
def ydist = server.getHeight()
clearAllObjects()
if (server.nZSlices() >0){
0.upto(server.nZSlices()-1){
frame = PathObjects.createAnnotationObject(ROIs.createRectangleROI(0,0,xdist,ydist,ImagePlane.getPlane(it,0)));
addObject(frame);
}
}
if (server.nTimepoints() >0){
0.upto(server.nTimepoints()-1){
frame = PathObjects.createAnnotationObject(ROIs.createRectangleROI(0,0,xdist,ydist,ImagePlane.getPlane(0,it)));
addObject(frame);
}
}
selectAnnotations()
//cell detection here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment