Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Scripts mostly taken from Pete, and also from the forums. For easy access and reference.
TOC
Change annotations into Cell objects.groovy - Converts annotations into PathCellObjects, which allows certian functions to work within
them, namely Subcellular detection.
Change some annotations into detections.groovy - Similar to above, except with generic detections.
Copying annotations between images.groovy - Two scripts to save and restore annotations using a separate file.
Create detection objects from annotations.groovy - Another version of creating detection objects, with a few other options (bounding box)
Cytokeratin tool fun.groovy - Remove the stroma and surrounding annotation objects, fill in holes, and remove small bits of tumor
Fill Holes by Size.groovy - Removes holes in annotations based on their size. Can also use options in Simple Tissue Detection, but this
will also work for annotations returned from ImageJ
Fill Holes in Objects.groovy - Fills all holes
Force update selected annotation.groovy - Updates a single annotation in the case when the cells within it are not considered child objects.
Lock all annotations.groovy
Mirroring objects.groovy - Creating a mirror image object (or other transformations)
Object Subtraction.groovy - Performing subtraction between two objects (to create holes, for instance)
Rename annotations and modify annotation lists.groovy - Mass renaming. Creates a subset of a list and incrementally names annotations.
Rotate all annotations.groovy - Another transformation to rotate annotations, partial tissue alignment when overlaying a QPDATA file.
Set all TMA cores to valid.groovy - Newly added cores, through the TMA menu, will not be "valid" and will be ignored. This will fix that.
Set TMA cores to valid if tissue area over threshold.groovy - Same but runs simple tissue detection and chooses which TMA cores to set
as valid based on the area of tissue detected.
Split annotations into contiguous areas.groovy - And version 2, two ways to split annotations.
TMA-add cell measurements by class.groovy - Adds percentage by class, and cells/mm^2 to the TMA core for every existing cell class
Update detections in annotations.groovy - Another version of Force update selected annotation.groovy that works for multiple annotations.
Use points to create cells.groovy - https://gist.github.com/Svidro/ffb6951e70187de5eb007290f61aea4a#file-use-points-to-place-cells-groovy
Duplicate entry.
import qupath.lib.objects.PathCellObject
// Get the current hierarchy
def hierarchy = getCurrentHierarchy()
// Get the non-top level annotations. This assumes you have a tissue area selected, and have hand drawn some cells within that.
def targets = getObjects{return it.getLevel()!=1 && it.isAnnotation()}
// Check we have anything to work with
if ( targets.isEmpty()) {
print("No objects selected!")
return
}
// Loop through objects
def newDetections = new ArrayList<>()
for (def pathObject in targets) {
// Unlikely to happen... but skip any objects not having a ROI
if (!pathObject.hasROI()) {
print("Skipping object without ROI: " + pathObject)
continue
}
def roi = pathObject.getROI()
def detection = new PathCellObject(roi,roi,pathObject.getPathClass())
newDetections.add(detection)
print("Adding " + detection)
}
removeObjects( targets, true)
// Actually add the objects
hierarchy.addPathObjects(newDetections, false)
//Remove nucleus ROI
newDetections.each{it.nucleus = null}
fireHierarchyUpdate()
if (newDetections.size() > 1)
print("Added " + newDetections.size() + " detections(s)")
// https://groups.google.com/forum/#!topic/qupath-users/rKHqWQHhaEE
// Get all annotations with ellipse ROIs
def annotations = getAnnotationObjects()
def ellipses = annotations.findAll {it.getROI() instanceof qupath.lib.roi.EllipseROI}
// Create corresponding detections (name this however you like)
def classification = getPathClass('Node')
def detections = ellipses.collect {
new qupath.lib.objects.PathDetectionObject(it.getROI(), classification)
}
// Remove ellipse annotations & replace with detections
removeObjects(ellipses, true)
addObjects(detections)
//This is actually two scripts, the first should be run in the image you are copying from
//and the second in the image you are copying to.
//Export section
def path = buildFilePath(PROJECT_BASE_DIR, 'annotations-' + getProjectEntry().getImageName() + '.txt')
def annotations = getAnnotationObjects().collect {new qupath.lib.objects.PathAnnotationObject(it.getROI(), it.getPathClass())}
new File(path).withObjectOutputStream {
it.writeObject(annotations)
}
print 'Done!'
//There should now be a saved .txt file in the base project directory that holds the annotation information.
//Swap images and run the second set of code using the file name generated in the first half of the code.
def path = buildFilePath(PROJECT_BASE_DIR, 'annotations-' + getProjectEntry().getImageName() + '.txt')
def annotations = null
new File(path).withObjectInputStream {
annotations = it.readObject()
}
addObjects(annotations)
getAnnotationObjects().each{it.setLocked(true)}
print 'Added ' + annotations
/*
* A script to create detection object(s) having the same ROI as all other annotation objects
*/
import qupath.lib.objects.PathTileObject
import qupath.lib.roi.RectangleROI
import qupath.lib.scripting.QP
// Set this to true to use the bounding box of the ROI, rather than the ROI itself
boolean useBoundingBox = false
// Get the current hierarchy
def hierarchy = QP.getCurrentHierarchy()
// Get all annotation objects: You may want to change this to select only certain annotations based on class
// or just annotations you have selected using =getSelectedObjects()
def selected = getAnnotationObjects()
// Check we have anything to work with
if (selected.isEmpty()) {
print("No objects selected!")
return
}
// Loop through objects
def newDetections = new ArrayList<>()
for (def pathObject in selected) {
// Unlikely to happen... but skip any objects not having a ROI
if (!pathObject.hasROI()) {
print("Skipping object without ROI: " + pathObject)
continue
}
// Don't create a second annotation, unless we want a bounding box
if (!useBoundingBox && pathObject.isDetection()) {
print("Skipping annotation: " + pathObject)
continue
}
// Create an annotation for whichever object is selected, with the same class
// Note: because ROIs are (or should be) immutable, the same ROI is used here, rather than a duplicate
def roi = pathObject.getROI()
if (useBoundingBox)
roi = new RectangleROI(
roi.getBoundsX(),
roi.getBoundsY(),
roi.getBoundsWidth(),
roi.getBoundsHeight(),
roi.getC(),
roi.getZ(),
roi.getT())
def detection = new PathTileObject(roi, pathObject.getPathClass())
newDetections.add(detection)
print("Adding " + detection)
}
// Actually add the objects
hierarchy.addPathObjects(newDetections, false)
if (newDetections.size() > 1)
print("Added " + newDetections.size() + " detections(s)")
//Another version of the split annotations script from : https://github.com/qupath/qupath/issues/99
import static qupath.lib.roi.PathROIToolsAwt.splitAreaToPolygons
import qupath.lib.roi.AreaROI
import qupath.lib.objects.PathAnnotationObject
// Get all the annotations
def annotations = getAnnotationObjects()
annotations.each{
if (it.getPathClass() != getPathClass("Tumor")){removeObject(it, true)}
}
detections = getDetectionObjects()
detections.each{if (it.getPathClass() != getPathClass("Tumor")){removeObject(it, true)}}
def server = getCurrentImageData().getServer()
double pixelWidth = server.getPixelWidthMicrons()
double pixelHeight = server.getPixelHeightMicrons()
// Prepare to add/remove annotations in batch
def toAdd = []
def toRemove = []
// Loop through the annotations, preparing to make changes
for (annotation in annotations) {
def roi = annotation.getROI()
// If we have an area, prepare to remove it -
// and add the separated polygons
if (roi instanceof AreaROI) {
toRemove << annotation
for (p in splitAreaToPolygons(roi)[1]) {
if (p.getScaledArea(pixelWidth, pixelHeight) > 5000){
toAdd << new PathAnnotationObject(p, annotation.getPathClass())
}
}
}
}
// Perform the changes
removeObjects(toRemove, true)
addObjects(toAdd)
fireHierarchyUpdate()
println("Done")
//Base script found here: https://gist.github.com/petebankhead/12cef1421958441c51618f2c80ee0629
import qupath.lib.roi.*
import qupath.lib.objects.*
def areaInSquareMicrons = 500
def imageData = getCurrentImageData()
def server = imageData.getServer()
def pixelSize = server.getPixelHeightMicrons()
// Get selected objects
// If you're willing to loop over all annotation objects, for example, then use getAnnotationObjects() instead
def pathObjects = getSelectedObjects()
// Create a list of objects to remove, add their replacements
def toRemove = []
def toAdd = []
for (pathObject in pathObjects) {
def roi = pathObject.getROI()
// AreaROIs are the only kind that might have holes
if (roi instanceof AreaROI ) {
// Extract exterior polygons
def polygons = PathROIToolsAwt.splitAreaToPolygons(roi)[1] as List
// If we have multiple polygons, merge them
def roiNew = polygons.remove(0)
def roiNegative = PathROIToolsAwt.splitAreaToPolygons(roi)[0] as List
for (temp in polygons){
roiNew = PathROIToolsAwt.combineROIs(temp, roiNew, PathROIToolsAwt.CombineOp.ADD)
}
for (temp in roiNegative){
if (temp.getArea() > areaInSquareMicrons/pixelSize/pixelSize){
roiNew = PathROIToolsAwt.combineROIs(roiNew, temp, PathROIToolsAwt.CombineOp.SUBTRACT)
}
}
// Create a new annotation
toAdd << new PathAnnotationObject(roiNew, pathObject.getPathClass())
toRemove << pathObject
}
}
// Remove & add objects as required
def hierarchy = getCurrentHierarchy()
hierarchy.getSelectionModel().clearSelection()
hierarchy.removeObjects(toRemove, true)
hierarchy.addPathObjects(toAdd, false)
/**
* Replace (selected) annotations with ROIs containing holes with filled-in versions.
*
* @author Pete Bankhead
*/
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.roi.AreaROI
import qupath.lib.roi.PathROIToolsAwt
import qupath.lib.scripting.QPEx
// Get selected objects
// If you're willing to loop over all annotation objects, for example, then use getAnnotationObjects() instead
def pathObjects = QPEx.getSelectedObjects()
// Create a list of objects to remove, add their replacements
def toRemove = []
def toAdd = []
for (pathObject in pathObjects) {
def roi = pathObject.getROI()
// AreaROIs are the only kind that might have holes
if (roi instanceof AreaROI) {
// Extract exterior polygons
def polygons = PathROIToolsAwt.splitAreaToPolygons(roi)[1] as List
// If we have multiple polygons, merge them
def roiNew = polygons.remove(0)
for (temp in polygons)
roiNew = PathROIToolsAwt.combineROIs(roiNew, temp, PathROIToolsAwt.CombineOp.ADD)
// Create a new annotation
toAdd << new PathAnnotationObject(roiNew, pathObject.getPathClass())
toRemove << pathObject
}
}
// Remove & add objects as required
def hierarchy = QPEx.getCurrentHierarchy()
hierarchy.getSelectionModel().clearSelection()
hierarchy.removeObjects(toRemove, true)
hierarchy.addPathObjects(toAdd, false)
//This script forces the annotation to detect whether cells are inside of it
//Useful when pasting an annotation onto a set of detections
//Added warning, this may appear to freeze the program if a lot of detections are being updated. Be patient.
selected = getSelectedObject()
removeObject(selected, true)
addObject(selected)
//Locked is not absolutely necessary, but good practice so you don't "jiggle" your updated animation
selected.setLocked(true)
getAnnotationObjects().each{it.setLocked(true)}
/** https://github.com/qupath/qupath/issues/199
* Flip a QuPath ROI vertically or horizontally.
*
* This creates a new annotation & adds it to the current image hierarchy.
*
* This also shows the method by which any arbitrary AffineTransform may be
* applied to a ROI by scripting.
*
* @author Pete Bankhead
*/
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.roi.PathROIToolsAwt
import qupath.lib.scripting.QPEx
import java.awt.geom.AffineTransform
// Get selected object & its ROI
def selected = QPEx.getSelectedObject()
def roi = selected?.getROI()
if (roi == null) {
print 'No ROI selected!'
return
}
// Create the relevant transforms, incorporating the image dimensions
def server = QPEx.getCurrentImageData().getServer()
def flipHorizontal = AffineTransform.getScaleInstance(-1, 1)
flipHorizontal.translate(-server.getWidth(), 0)
def flipVertical = AffineTransform.getScaleInstance(1, -1)
flipVertical.translate(0, -server.getHeight())
// Get a Shape for the ROI & apply required transform
def shape = PathROIToolsAwt.getShape(roi)
def shape2 = flipHorizontal.createTransformedShape(shape)
//def shape2 = flipVertical.createTransformedShape(shape)
// Create & add a new annotation
def roi2 = PathROIToolsAwt.getShapeROI(shape2, roi.getC(), roi.getZ(), roi.getT(), 0.5)
QPEx.addObject(new PathAnnotationObject(roi2, selected.getPathClass()))
// https://groups.google.com/forum/#!topic/qupath-users/WlxDfgjqrCU
import qupath.lib.roi.*
import qupath.lib.objects.*
classToSubtract = "Endothel"
def topLevel = getObjects{return it.getLevel()==1 && it.isAnnotation()}
for (parent in topLevel){
def total = []
def polygons = []
subtractions = parent.getChildObjects().findAll{it.isAnnotation() && it.getPathClass() == getPathClass(classToSubtract)}
for (subtractyBit in subtractions){
if (subtractyBit instanceof AreaROI){
subtractionROIs = PathROIToolsAwt.splitAreaToPolygons(subtractyBit.getROI())
total.addAll(subtractionROIs[1])
} else {total.addAll(subtractyBit.getROI())}
}
if (parent instanceof AreaROI){
polygons = PathROIToolsAwt.splitAreaToPolygons(parent.getROI())
total.addAll(polygons[0])
} else { polygons[1] = parent.getROI()}
def newPolygons = polygons[1].collect {
updated = it
for (hole in total)
updated = PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
return updated
}
// Remove original annotation, add new ones
annotations = newPolygons.collect {new PathAnnotationObject(updated, parent.getPathClass())}
addObjects(annotations)
removeObjects(subtractions, true)
removeObject(parent, true)
}
//Takes all non ellipses and modifies the names of those annotations
//Classifies all ellipses
// https://groups.google.com/forum/#!topic/qupath-users/rKHqWQHhaEE
// Get all annotations with ellipse ROIs
def annotations = getAnnotationObjects()
def ellipses = annotations.findAll {it.getROI() instanceof qupath.lib.roi.EllipseROI}
// Assign classifications to nodes
def classification = getPathClass('Node')
ellipses.each {it.setPathClass(classification)}
// Assign names to all other annotations
annotations.removeAll(ellipses)
annotations.eachWithIndex {annotation, i -> annotation.setName('Annotation ' + (i+1))}
fireHierarchyUpdate()
// from https://groups.google.com/forum/#!searchin/qupath-users/rotate$20%7Csort:date/qupath-users/UvkNb54fYco/ri_4K6tiCwAJ
import qupath.lib.objects.*
import qupath.lib.roi.*
//As usual, change this to target the annotations you want to rotate
def annotations = getAnnotationObjects()
//Set the number of degrees you wish to rotate your objects
double deg = -60
double degrees = Math.toRadians(deg) // sets the number of degrees to rotate
for (j = 0; j < annotations.size; j++)
{
def roi = annotations[j].getROI()
def roiPoints = annotations[j].getROI().getPolygonPoints()
def roiPointsArx = roiPoints.x.toArray() //convert each point to an array
def roiPointsAry = roiPoints.y.toArray() //for x and y coordinates
//the centroid of the roi - which will be used to center the shape around (0,0)
double centroidX = roi.getCentroidX()
double centroidY = roi.getCentroidY()
for (i= 0; i< roiPointsAry.length; i++)
{
// correct the center to 0
roiPointsArx[i] = roiPointsArx[i] - centroidX
roiPointsAry[i] = roiPointsAry[i] - centroidY
//Makes prime placeholders, which allows the calculations x'=xcos(theta)-ysin(theta), y'=ycos(theta)+xsin(theta) to be performed
double newPointX = roiPointsArx[i]
double newPointY = roiPointsAry[i]
// then rotate
roiPointsArx[i] = (newPointX * Math.cos(degrees)) - (newPointY * Math.sin(degrees))
roiPointsAry[i] = (newPointY * Math.cos(degrees)) + (newPointX * Math.sin(degrees))
// then move it back
roiPointsArx[i] = roiPointsArx[i] + centroidX
roiPointsAry[i] = roiPointsAry[i] + centroidY
}
// then to convert it back into an object
def xFloat = roiPointsArx as float[]
def yFloat = roiPointsAry as float[]
def roiNew = new PolygonROI(xFloat, yFloat, -1, 0, 0)
def pathObjectNew = new PathAnnotationObject(roiNew)
addObject(pathObjectNew)
}
removeObjects(annotations, true)
getTMACoreList().each{
it.setMissing(false)
}
fireHierarchyUpdate()
println("done")
//REPLACE TISSUE DETECTION LINE BELOW WITH YOUR OWN
//Set TMA cores where the tissue area is below a threshold to Missing so as to avoid inclusion in future calculations.
//Any annotations below theMINIMUM_AREA_um2 threshold will also be deleted.
MINIMUM_AREA_um2 = 30000
def imageData = getCurrentImageData()
def server = imageData.getServer()
def pixelSize = server.getPixelHeightMicrons()
getTMACoreList().each{
it.setMissing(false)
}
selectTMACores();
///////////////////////REPLACE WITH YOUR OWN
runPlugin('qupath.imagej.detect.tissue.SimpleTissueDetection2', '{"threshold": 233, "requestedPixelSizeMicrons": 5.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")
import qupath.lib.roi.*
import qupath.lib.objects.*
//selects all annotation areas. Change this if you want only a subset of your areas split into contiguous area components.
def areaAnnotations = getAnnotationObjects().findAll {it.getROI() instanceof AreaROI}
areaAnnotations.each { selected ->
def polygons = PathROIToolsAwt.splitAreaToPolygons(selected.getROI())
def newPolygons = polygons[1].collect {
updated = it
for (hole in polygons[0])
updated = PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
return updated
}
// Remove original annotation, add new ones
annotations = newPolygons.collect {new PathAnnotationObject(it)}
resetSelection()
removeObject(selected, true)
addObjects(annotations)
}
//Another version of the split annotations script from : https://github.com/qupath/qupath/issues/99
import static qupath.lib.roi.PathROIToolsAwt.splitAreaToPolygons
import qupath.lib.roi.AreaROI
import qupath.lib.objects.PathAnnotationObject
// Get all the annotations
def annotations = getAnnotationObjects()
// Prepare to add/remove annotations in batch
def toAdd = []
def toRemove = []
// Loop through the annotations, preparing to make changes
for (annotation in annotations) {
def roi = annotation.getROI()
// If we have an area, prepare to remove it -
// and add the separated polygons
if (roi instanceof AreaROI) {
toRemove << annotation
for (p in splitAreaToPolygons(roi)[1]) {
toAdd << new PathAnnotationObject(p, annotation.getPathClass())
}
}
}
// Perform the changes
removeObjects(toRemove, true)
addObjects(toAdd)
//Checks for all detections within a TMA, DOES NOT EXCLUDE DETECTIONS WITHIN SUB-ANNOTATIONS.
//That last bit should make it compatible with trained classifiers.
//Cells per mm^2 assumes an annotation within the TMA, if there are no annotations within the TMA, remove related lines.
import qupath.lib.objects.PathCellObject
def imageData = getCurrentImageData()
def server = imageData.getServer()
def pixelSize = server.getPixelHeightMicrons()
Set classList = []
for (object in getCellObjects()) {
classList << object.getPathClass()
}
println(classList)
hierarchy = getCurrentHierarchy()
getTMACoreList().each{
totalCells = hierarchy.getDescendantObjects(it,null, PathCellObject)
for (aClass in classList){
if (aClass){
if (totalCells.size() > 0){
def cells = hierarchy.getDescendantObjects(it,null, PathCellObject).findAll{it.getPathClass() == aClass}
//it.getMeasurementList().putMeasurement(aClass.getName()+" cells", cells.size())
it.getMeasurementList().putMeasurement(aClass.getName()+" %", cells.size()*100/totalCells.size())
def annotationArea = it.getChildObjects()[0].getROI().getArea()
it.getMeasurementList().putMeasurement(aClass.getName()+" cells/mm^2", cells.size()/(annotationArea*pixelSize*pixelSize/1000000))
} else {
//it.getMeasurementList().putMeasurement(aClass.getName()+" cells", 0)
it.getMeasurementList().putMeasurement(aClass.getName()+" %", 0)
it.getMeasurementList().putMeasurement(aClass.getName()+" cells/mm^2", 0)
}
}
}
}
println("done")
//This script forces the annotations to detect whether cells are inside of it
//Useful when pasting an annotation onto a set of detections
//Added warning, this may appear to freeze the program if a lot of detections are being updated. Be patient.
import qupath.lib.roi.*
import qupath.lib.objects.*
selected = getSelectedObjects()
def rois = [:]
for (object in selected){
rois.put(object.getROI(),object.getPathClass())
}
removeObjects(selected, true)
rois.each{r,c-> addObject(new PathAnnotationObject(r, c))}
//Locked is not absolutely necessary, but good practice so you don't "jiggle" your updated animation
for (annotation in getAnnotationObjects())
annotation.setLocked(true)
@ab-rawat

This comment has been minimized.

Copy link

commented Jun 5, 2018

Hello Svidro,
In "copying annotations between images" groovy script, what is the file format of the "txt" file that is created?
It seems to be a binary file.
I want to understand the file format so I can maybe import it into other software such as definiens.
any pointers will be appreciated.
thanks!
rawat.

@Svidro

This comment has been minimized.

Copy link
Owner Author

commented Jun 28, 2018

Oh wow, I never got a notification that anyone commented here! I will have to see if there is a way to enable that. Sorry about the much delayed response, I just don't know my Github.

I am afraid most of the details of the scripts like that could probably only be answered by @petebankhead but it looks like ObjectOutputStream is exactly what you think, just a direct data stream object. http://docs.groovy-lang.org/2.4.7/html/groovy-jdk/java/io/OutputStream.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.