Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save beosro/da77af6247c51d047cf5663ee678b366 to your computer and use it in GitHub Desktop.
Save beosro/da77af6247c51d047cf5663ee678b366 to your computer and use it in GitHub Desktop.
Classification based groovy scripts for QuPath
Collection of scripts mostly from Pete, but also taken from the forums
TOC
A simple cell classifier.groovy - One way to classify cells.
A simple classifier 2.groovy - Another way.
Annotation Classifications to Name field.groovy - Sets the Name of the annotation to its classification. Useful for applying a second
classifier without losing the results of the first.
Annotation classifier.groovy - Classify annotations.
Classifier sample.groovy - Another cell classifier, but more complicated format.
OneStep Classifier.groovy - In case I forget the name of the function.
Pete base classifier.groovy - One of the original classification scripts, has the most protections against errors.
Reset Cell Classifications only.groovy - Important when you have subcellular detections. You would not want to reset their class as that
alters the summary values in the cell measurement list.
Set Selected Object Class.groovy - Change the class of a specific object. Much like the annotation context menu, but works for detections
Subcellular detection to nuclear or cyto.groovy - Pete's script for checking whether a subcellular detection is within the nucleus.
Creates a derived class.
// from http://forum.imagej.net/t/counting-double-labeled-cells-in-fiji/3832/2
positive = getPathClass('Positive')
negative = getPathClass('Negative')
for (cell in getCellObjects()) {
ch1 = measurement(cell, 'Cell: Channel 1 mean')
ch2 = measurement(cell, 'Cell: Channel 2 mean')
if (ch1 > 100 && ch2 > 200)
cell.setPathClass(positive)
else
cell.setPathClass(negative)
}
fireHierarchyUpdate()
// From Pete's post on Gitter, another way of applying cell classifications
import static qupath.lib.scripting.QPEx.*
// Get cells & reset all the classifications
def cells = getCellObjects()
resetDetectionClassifications()
cells.each {it.setPathClass(getPathClass('Negative'))}
// Get channel 1 & 2 positives
def ch1Pos = cells.findAll {measurement(it, "Nucleus: Channel 1 mean") > 5}
ch1Pos.each {it.setPathClass(getPathClass('Ch 1 positive'))}
def ch2Pos = cells.findAll {measurement(it, "Nucleus: Channel 2 mean") > 0.2}
ch2Pos.each {it.setPathClass(getPathClass('Ch 2 positive'))}
// Overwrite classifications for double positives
def doublePos = ch1Pos.intersect(ch2Pos)
doublePos.each {it.setPathClass(getPathClass('Double positive'))}
fireHierarchyUpdate()
//Remove annotation classification and rename the annotation to the original class
for (annotation in getAnnotationObjects()) {
def pathClass = annotation.getPathClass()
if (pathClass == null)
continue
annotation.setName(pathClass.getName())
// annotation.setColorRGB(pathClass.getColor())
annotation.setPathClass(null)
}
fireHierarchyUpdate()
//Restore the classification based on the annotation name (reverse the above effects)
for (annotation in getAnnotationObjects()) {
def name = annotation.getName()
if (name == null)
continue
def pathClass = getPathClass(name)
annotation.setPathClass(pathClass)
}
fireHierarchyUpdate()
//Here I used optical density only. You will need to add any intensity features you want to the classifier
import qupath.lib.objects.classes.PathClassFactory
//Use add intensity features to add whatever values you need to determine a class
selectAnnotations();
//If you have already added features to your annotations, you will not need this line
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": true, "doStdDev": false, "doMinMax": false, "doMedian": false, "doHaralick": false, "haralickDistance": 1, "haralickBins": 32}');
def ClassA = PathClassFactory.getPathClass("ClassA")
def ClassB = PathClassFactory.getPathClass("ClassB")
//feature has to be exact, including spaces. This can be tricky
//for the above you would need: "ROI: 2.00 " + qupath.lib.common.GeneralTools.micrometerSymbol() + " per pixel: OD Sum: Mean"
def feature = "ROI: 2.00 " + qupath.lib.common.GeneralTools.micrometerSymbol() + " per pixel: OD Sum: Mean"
//get all annotations in the image
Annotations = getAnnotationObjects();
Annotations.each {
double value = it.getMeasurementList().getMeasurementValue(feature)
print(it.getMeasurementList())
//use logic here to determine whether each "it" or annotation will be a given class
//this can be as complicated as you want, or as simple as a single if statement.
if (value > 0.45) {it.setPathClass(ClassA)}
else { it.setPathClass(ClassB)}
}
println("Annotation classification done")
//Simplified script for classifying cells based on their values. Can easily be dramatically expanded as much as you may like
//by adding features and thresholds
//If all of your results are showing up as one class, it is probably because the feature variable is not exaaaactly correct.
//It has to be character for character the same as what the program uses, sorry! Some of the scripts in Coding Helper Functions
//might help with this if you are having trouble.
import qupath.lib.objects.classes.PathClass
import qupath.lib.objects.classes.PathClassFactory
//This part resets the classifier so that you can run it again. Clearing detection classifications would also clear your subcellular detections
for (def cell : getCellObjects())
cell.setPathClass(null)
fireHierarchyUpdate()
// Parameters to modify
def feature = "Subcellular: Channel 2: Num spots estimated"
//Not using these in this example, but they could be used to further expand the classifier below
//def threshold = 1
//def feature2 = "Cytoplasm: Channel 2 mean"
//def threshold2 = 30
// Check what base classifications we should be worried about
// It's possible to specify 'All', or select specific classes and exclude others
def Negative = PathClassFactory.getPathClass("Negative")
def Lowest = PathClassFactory.getPathClass("1-3 spots")
def Low = PathClassFactory.getPathClass("4-9 spots")
def Medium = PathClassFactory.getPathClass("10-15 spots")
def High = PathClassFactory.getPathClass("15+ spots")
// Loop through all cells
for (def cell : getCellObjects()) {
//Assign the measurement for "feature" from above for this cell to a variable. In this case I named it spots
double spots = cell.getMeasurementList().getMeasurementValue(feature)
//If you need further measurements for your classifier, get them here
//double val2 = pathObject.getMeasurementList().getMeasurementValue(feature2)
// Set class based on the value(s) obtained
if ( spots > 15){
cell.setPathClass(High)
}else if ( spots > 9 ){
cell.setPathClass(Medium)
}else if ( spots > 3 ){
cell.setPathClass(Low)
}else if ( spots > 1 ){
cell.setPathClass(Lowest)
}else cell.setPathClass(Negative)
}
// Fire update event
fireHierarchyUpdate()
// Make sure we know we're done
println("Done!")
//Classify cells only by one value. This can be any measurement, including one you created.
setCellIntensityClassifications("Cytoplasm: DAB OD mean", 0.15)
/**
* Create a QuPath classifier by scripting, rather than the 'standard' way with annotations.
*
* This selects training regions according to a specified criterion based on staining,
* and then creates a classifier that uses other features.
*
* The main aim is to show the general idea of creating a classifier by scripting.
*
* @author Pete Bankhead
*/
import qupath.lib.classifiers.Normalization
import qupath.lib.classifiers.PathClassificationLabellingHelper
import qupath.lib.objects.PathObject
import qupath.lib.objects.classes.PathClassFactory
import qupath.lib.scripting.QPEx
// Optionally check what will be used for training -
// setting the training classification for each cell & not actually building the classifier
// (i.e. just do a sanity check)
boolean checkTraining = false
// Get all cells
def cells = QPEx.getCellObjects()
// Split by some kind of DAB measurement
def isTumor = {PathObject cell -> return cell.getMeasurementList().getMeasurementValue('Cell: DAB OD mean') > 0.2}
def tumorCells = cells.findAll {isTumor(it)}
def nonTumorCells = cells.findAll {!isTumor(it)}
print 'Number of tumor cells: ' + tumorCells.size()
print 'Number of non-tumor cells: ' + nonTumorCells.size()
// Create a relevant map for training
def map = [:]
map.put(PathClassFactory.getPathClass('Tumor'), tumorCells)
map.put(PathClassFactory.getPathClass('Stroma'), nonTumorCells)
// Check training... if necessary
if (checkTraining) {
print 'Showing training classifications (not building a classifier!)'
map.each {classification, list ->
list.each {it.setPathClass(classification)}
}
QPEx.fireHierarchyUpdate()
return
}
// Get features & filter out the ones that shouldn't be used (here, any connected to intensities)
def features = PathClassificationLabellingHelper.getAvailableFeatures(cells)
features = features.findAll {!it.toLowerCase().contains(': dab') && !it.toLowerCase().contains(': hematoxylin')}
// Print the features
print Integer.toString(features.size()) + ' features: \n\t' + String.join('\n\t', features)
// Create a new random trees classifier with default settings & no normalization
print 'Training classifier...'
// This would show available parameters
// print classifier.getParameterList().getParameters().keySet()
def classifier = new qupath.opencv.classify.RTreesClassifier()
classifier.updateClassifier(map, features as List, Normalization.NONE)
// Actually run the trained classifier
print 'Applying classifier...'
classifier.classifyPathObjects(cells)
QPEx.fireHierarchyUpdate()
print 'Done!'
//Useful if you want to reset your classifier but do NOT want to reset other classifications, such as subcellular detections
for (def cell : getCellObjects())
cell.setPathClass(null)
fireHierarchyUpdate()
println("Done!")
//Select an object or several objects before running.
//Change Tumor to the class you want to add
def Class = getPathClass('Tumor')
selected = getSelectedObjects()
for (def detection in selected){
detection.setPathClass(Class)
}
fireHierarchyUpdate()
println("Done!")
//Classify your subcellular detections based on their X-Y coordinate centers.
// https://github.com/qupath/qupath/wiki/Spot-detection
def clusters = getObjects({p -> p.class == qupath.imagej.detect.cells.SubcellularDetection.SubcellularObject.class})
// Loop through all clusters
for (c in clusters) {
// Find the containing cell
def cell = c.getParent()
// Check the current classification - remove the last part if it
// was generated by a previous run of this command
def pathClass = c.getPathClass()
if (["Nuclear", "Cytoplasmic"].contains(c.getName())) {
pathClass = pathClass.getParentClass()
}
// Check the location of the cluster centroid relative to the nucleus,
// and classify accordingly
def nucleus = cell.getNucleusROI()
if (nucleus != null && nucleus.contains(c.getROI().getCentroidX(), c.getROI().getCentroidY())) {
c.setPathClass(getDerivedPathClass(c.getPathClass(), "Nuclear"))
} else
c.setPathClass(getDerivedPathClass(c.getPathClass(), "Cytoplasmic"))
}
fireHierarchyUpdate()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment