Instantly share code, notes, and snippets.

What would you like to do?
Making Measurements in QuPath
Collections of scripts harvested mainly from Pete, but also picked up from the forums
Adding cells per mm^2 to annotations.groovy - Use a manually created list to add cells/mm^2 measurements for classes other than
"Positive" to annotations.
Area measurements to annotation.groovy - Kind of terrible holdover, but adds size measurements to Annotations. Could be altered for
detection tiles, which would likely be more useful.
Cell summary measurements to annotation.groovy - Go through the cells and add some sort of summary measurement to their parent
annotation. Examples might be the mean area of all cells, or the min and max intensities of cells of a certain class. Get createive.
Class percentages to TMA measurements.groovy - Checks all cells in each core for membership within a listed set of classes.
Label cells by TMA core.groovy - Rename cells based on their parent core. Could probably be done better with getDecendantObjects()
Nuclear and cytoplasmic color vector means.groovy - Complicated script, but essentially allows you to create sets of color vectors
and obtain cytoplasmic and nuclear mean values for them. Useful in complex brightfield stains, has been used to differentiate cells in
5 stain plus hematoxylin images.
Total subcellular intensity to cell value.groovy - Sums the total intensity of subcellular detections (area*mean intensity summed).
Primary functions here include:
Using "hierarchy = getCurrentHierarchy()" to get access to the hierarchy, so that you can more easily access subsets of cells
Using findAll{true/false statements here} to generate lists of objects you want to perform operations on.
The following gets all objects that are positive within whatever preceeds findAll
.findAll{it.getPathClass() == getPathClass("Positive")}
The following access the measurement list, which is the list you see in the lower right of the Hierarchy tab when selecting
an object.
putMeasurement(key, value)
Sometimes you may want to search an objects list using:
ml = object.getMeasurementList()
to generate a list called ml.
Other times, you may know exactly what you want to modify, and can just use:
object.getMeasurementList().putMeasurement(key, value)
For adding a micrometer symbol in v1.2, use " + qupath.lib.common.GeneralTools.micrometerSymbol() + "
import qupath.lib.objects.PathCellObject
def pixel = getCurrentImageData().getServer().getPixelHeightMicrons()
hierarchy = getCurrentHierarchy()
def list = ["Positive", "Negative", "Macrophage", "T-helper"]
for (annotation in getAnnotationObjects()){
double totalArea = annotation.getROI().getArea()*pixel*pixel
for (className in list) {
def cellCount = hierarchy.getDescendantObjects(annotation,null, PathCellObject).findAll{it.getPathClass() == getPathClass(className)}.size()
annotation.getMeasurementList().putMeasurement(className+" cells per mm^2", cellCount/(totalArea/1000000))
//Useful when using detection objects returned from ImageJ macros. Note that areas are in pixels and would need to be converted to microns
import qupath.lib.objects.PathDetectionObject
hierarchy = getCurrentHierarchy()
for (annotation in getAnnotationObjects()){
//Block 1
def tiles = hierarchy.getDescendantObjects(annotation,null, PathDetectionObject)
double totalArea = 0
for (def tile in tiles){
totalArea += tile.getROI().getArea()
annotation.getMeasurementList().putMeasurement("Marked area px", totalArea)
def annotationArea = annotation.getROI().getArea()
annotation.getMeasurementList().putMeasurement("Marked area %", totalArea/annotationArea*100)
//Sometimes you may want to add a summary measurement from cells within each annotation to the annotation itself.
//This will allow you to see that measurement in the "Show Annotation Measurements" list.
//In this case, it will add the total area taken up by Positive class cells within each annotation to their parent
//annotation as "Positive Area"
import qupath.lib.objects.PathCellObject
hierarchy = getCurrentHierarchy()
for (annotation in getAnnotationObjects()){
//Block 1
def positiveCells = hierarchy.getDescendantObjects(annotation,null, PathCellObject).findAll{it.getPathClass() == getPathClass("Positive")}
double totalArea = 0
for (def cell in positiveCells){
totalArea += cell.getMeasurementList().getMeasurementValue("Cell: Area")
//Comment the following in or out depending on whether you want to see the output
//println("Mean area for Positive is: " + totalArea/positiveCells.size)
//println("Total Positive Area is: " + totalArea)
//Add the total as "Positive Area" to each annotation.
annotation.getMeasurementList().putMeasurement("Positive Area", totalArea)
//Add the percentage positive area to the annotations measurement list
def annotationArea = annotation.getROI().getArea()
annotation.getMeasurementList().putMeasurement("Positive Area %", totalArea/annotationArea*100)
//Block 2 - add as many blocks as you have classes
// Add percentages by cell class to each TMA core
import qupath.lib.objects.PathCellObject
def hierarchy = getCurrentHierarchy()
def cores = hierarchy.getTMAGrid().getTMACoreList()
//Complete list of all classes in YOUR analysis- CHANGE THIS
def list = ["Immune cells", "Stroma", "Dual Positive", "Negative"]
cores.each {
//Find the cell count in this core
def total = hierarchy.getDescendantObjects(it, null, PathCellObject).size()
//Prevent divide by zero errors in empty TMA cores
if (total != 0){
for (className in list) {
def cellType = hierarchy.getDescendantObjects(it,null, PathCellObject).findAll{it.getPathClass() == getPathClass(className)}.size()
it.getMeasurementList().putMeasurement(className+" cell %", cellType/(total)*100)
else {
for (className in list) {
it.getMeasurementList().putMeasurement(className+" cell %", 0)
// label cells within an annotation within a TMA core by the TMA core, not the annotation.
// Remove one getParent if there is no tissue annotation.
getDetectionObjects() each {detection -> detection.setName(detection.getParent().getParent().getName())}
//Calculate the mean OD values in the nucleus and cytoplasm for any number of sets of color vectors
import qupath.lib.objects.*
//This function holds a list of color vectors and their Add Intensity Features command that will add the desired measurements
//to your cells. Make sure you name the stains (for example in the first example, Stain 1 is called "Blue") differently
//so that their Measurements will end up labeled differently. Notice that the Add Intensity Features command includes
//"Colorstain":true, etc. which needs to be true for the measurements you wish to add.
def addColors(){
setColorDeconvolutionStains('{"Name" : "DAB Yellow", "Stain 1" : "Blue", "Values 1" : "0.56477 0.65032 0.50806 ", "Stain 2" : "Yellow", "Values 2" : "0.0091 0.01316 0.99987 ", "Background" : " 255 255 255 "}');
runPlugin('qupath.lib.algorithms.IntensityFeaturesPlugin', '{"pixelSizeMicrons": 0.25, "region": "ROI", "tileSizeMicrons": 25.0, "colorOD": true, "colorStain1": true, "colorStain2": true, "colorStain3": false, "colorRed": false, "colorGreen": false, "colorBlue": false, "colorHue": false, "colorSaturation": false, "colorBrightness": false, "doMean": true, "doStdDev": false, "doMinMax": false, "doMedian": false, "doHaralick": false, "haralickDistance": 1, "haralickBins": 32}');
setColorDeconvolutionStains('{"Name" : "Background1", "Stain 1" : "Blue Background1", "Values 1" : "0.56195 0.77393 0.29197 ", "Stain 2" : "Beige Background1", "Values 2" : "0.34398 0.59797 0.72396 ", "Background" : " 255 255 255 "}');
runPlugin('qupath.lib.algorithms.IntensityFeaturesPlugin', '{"pixelSizeMicrons": 0.25, "region": "ROI", "tileSizeMicrons": 25.0, "colorOD": false, "colorStain1": true, "colorStain2": true, "colorStain3": false, "colorRed": false, "colorGreen": false, "colorBlue": false, "colorHue": false, "colorSaturation": false, "colorBrightness": false, "doMean": true, "doStdDev": false, "doMinMax": false, "doMedian": false, "doHaralick": false, "haralickDistance": 1, "haralickBins": 32}');
//The only thing beyond this point that should need to be modified is the removalList command at the end, which you can disable
//if you wish to keep whole cell measurements
// Get cells & create temporary nucleus objects - storing link to cell in a map
def cells = getCellObjects()
def map = [:]
for (cell in cells) {
def detection = new PathDetectionObject(cell.getNucleusROI())
map[detection] = cell
// Get the nuclei as a list
def nuclei = map.keySet() as List
// and then select the nuclei
getCurrentHierarchy().getSelectionModel().setSelectedObjects(nuclei, null)
// Add as many sets of color deconvolution stains and Intensity features plugins as you want here
//This section ONLY adds measurements to the temporary nucleus objects, not the cell
//etc etc. make sure each set has different names for the stains or else they will overwrite
// Don't need selection now
// Can update measurements generated for the nucleus to the parent cell's measurement list
for (nucleus in nuclei) {
def cell = map[nucleus]
def cellMeasurements = cell.getMeasurementList()
for (key in nucleus.getMeasurementList().getMeasurementNames()) {
double value = nucleus.getMeasurementList().getMeasurementValue(key)
def listOfStrings = key.tokenize(':')
def baseValueName = listOfStrings[-2]+listOfStrings[-1]
nuclearName = "Nuclear" + baseValueName
cellMeasurements.putMeasurement(nuclearName, value)
//I want to remove the original whole cell measurements which contain the mu symbol
// Not yet sure I will find the whole cell useful so not adding it back in yet.
def removalList = []
//Create whole cell measurements for all of the above stains
//Create cytoplasmic measurements by subtracting the nuclear measurements from the whole cell, based total intensity (mean value*area)
for (cell in cells) {
//A mess of things I could probably call within functions
def cellMeasurements = cell.getMeasurementList()
double cellArea = cell.getMeasurementList().getMeasurementValue("Cell: Area")
double nuclearArea = cell.getMeasurementList().getMeasurementValue("Nucleus: Area")
double cytoplasmicArea = cellArea-nuclearArea
for (key in cell.getMeasurementList().getMeasurementNames()) {
//check if the value is one of the added intensity measurements
if (key.contains("per pixel")){
//check if we already have this value in the list.
//probably an easier way to do this outside of every cycle of the for loop
if (!removalList.contains(key)) removalList<<key
double value = cell.getMeasurementList().getMeasurementValue(key)
//calculate the sum of the OD measurements
cellOD = value * cellArea
//break each measurement into component parts, then take the last two
// which will usually contain the color vector and "mean"
def listOfStrings = key.tokenize(':')
def baseValueName = listOfStrings[-2]+listOfStrings[-1]
//access the nuclear value version of the base name, and use it and the whole cell value to
//calcuate the rough cytoplasmic value
def cytoplasmicKey = "Cytopasmic" + baseValueName
def nuclearKey = "Nuclear" + baseValueName
def nuclearOD = nuclearArea * cell.getMeasurementList().getMeasurementValue(nuclearKey)
def cytoplasmicValue = (cellOD - nuclearOD)/cytoplasmicArea
cellMeasurements.putMeasurement(cytoplasmicKey, cytoplasmicValue)
removalList.each {println(it)}
//comment out this line if you want the whole cell measurements.
removalList.each {removeMeasurements(qupath.lib.objects.PathCellObject, it)}
println "Done!"
// Save the total value of your subcellular detection intensities to the cell measurement list so that it may be exported
// with the cell, or used for classification
// This value could then be divided by the total area of subcellular detection (Num spots, if Expected spot size is left as 1)
// for the mean intensity
// Create the name of the new measurement, in this case Channel 3 of a fluorescent image.
// ONLY the "Channel 3" should change to the name of the stain you are measuring, for example "DAB" in a brightfield image
def subcellularDetectionChannel = "Subcellular cluster: Channel 3: "
def newKey = subcellularDetectionChannel+"Mean Intensity"
//This step ensures that there is at least a measurement value of 0 in each cell
for (def cell : getCellObjects()) {
def ml = cell.getMeasurementList()
ml.putMeasurement(newKey, 0)
//Create a list of all subcellular objects
def subCells = getObjects({p -> p.class == qupath.imagej.detect.cells.SubcellularDetection.SubcellularObject.class})
// Loop through all subcellular detections
for (c in subCells) {
// Find the containing cell
def cell = c.getParent()
def ml = cell.getMeasurementList()
double area = c.getMeasurementList().getMeasurementValue( subcellularDetectionChannel+"Area")
double intensity = c.getMeasurementList().getMeasurementValue( subcellularDetectionChannel+"Mean channel intensity")
//calculate the total intensity of stain in this subcellular object, and add it to the total
double stain = area*intensity
double x = cell.getMeasurementList().getMeasurementValue(newKey);
x = x+stain
ml.putMeasurement(newKey, x)
println("Total subcellular stain intensity added to cell measurement list as " + newKey)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment