Skip to content

Instantly share code, notes, and snippets.

@Svidro
Last active October 24, 2024 02:06
Show Gist options
  • Save Svidro/5b016e192a33c883c0bd20de18eb7764 to your computer and use it in GitHub Desktop.
Save Svidro/5b016e192a33c883c0bd20de18eb7764 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. Note you can always run a saved classifier using the
runClassifier() command, with the file path included as a string.
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.
JSON classifier on specific objects.groovy - How to target specific objects with the new classifiers as of M9
JSON object classifier.groovy - simple one line script
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.
Reclassify one class with derived classes.groovy - Takes the Tumor class and allows derived classes based on hematoxylin thresholds
Rename and recolor a class.groovy - Not included here, see: https://gist.github.com/Svidro/e00021dff92ea1173e535008854be72e#file-rename-and-recolor-a-class-groovy
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.
Training set combining.groovy - combine two or more training set files from different projects/images
*Many of these scripts are out of date, and you can get a list of active classes various ways:
Set classList = []
for (object in getAllObjects().findAll{it.isDetection() /*|| it.isAnnotation()*/}) {
classList << object.getPathClass()
}
def classifications = new ArrayList<>(getDetectionObjects().collect {it.getPathClass()} as Set)
// from http://forum.imagej.net/t/counting-double-labeled-cells-in-fiji/3832/2
//0.1.2 and 0.2.0 (though channel names have changed in 0.2.0)
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
//0.1.2 and 0.2.0 (though channel names have changed in 0.2.0)
// 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
//0.1.2 and 0.2.0
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
//0.1.2 and 0.2.0
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.
//0.1.2 and 0.2.0
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!")
//https://forum.image.sc/t/selecting-slic-tiles/34942/12
//0.2.0
def imageData = getCurrentImageData()
def classifier = loadObjectClassifier('Other tiles')
def tiles = getDetectionObjects().findAll {it.isTile()}
classifier.classifyObjects(imageData, tiles, true)
fireHierarchyUpdate()
//https://forum.image.sc/t/scripting-json-classifiers-in-qupath-0-2-0-m9/34614/2?u=research_associate
//0.2.0
//You can also copy the classifier into the current project folder (from a locked in source location) as part of the script, to use this command.
runObjectClassifier("My classifier name")
//Classify cells only by one value. This can be any measurement, including one you created.
//0.1.2 and 0.2.0
// This can be run on top of other classifications, making any current classifications both their original clas, and the subclass positive or negative
setCellIntensityClassifications("Cytoplasm: DAB OD mean", 0.15)
/** 0.1.2
* 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!'
//Based off of script by melvingelbard
//https://forum.image.sc/t/setcellintensityclassifications-for-a-certain-class/33347/3
//0.2.0
def measurementName = "Nucleus: Hematoxylin OD min";
def thresholds = [0.05, 0.2] as double[];
classCells = getCellObjects().findAll{it.getPathClass() == getPathClass("Tumor")}
setIntensityClassifications(classCells, measurementName, thresholds);
//Useful if you want to reset your classifier but do NOT want to reset other classifications, such as subcellular detections
//0.1.2 and 0.2.0
for (def cell : getCellObjects())
cell.setPathClass(null)
fireHierarchyUpdate()
println("Done!")
//Select an object or several objects before running.
//0.1.2 and 0.2.0
//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
//0.1.2
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()
//https://github.com/qupath/qupath/issues/256
//0.1.2
// Paths to training files (here, both relative to the current project)
paths = [
buildFilePath(PROJECT_BASE_DIR, 'training', 'my_training.qptrain'),
buildFilePath(PROJECT_BASE_DIR, 'training', 'my_training2.qptrain'),
]
// Path to output training file
pathOutput = buildFilePath(PROJECT_BASE_DIR, 'training', 'merged.qptrain')
// Count mostly helps to ensure we're adding with unique keys
count = 0
// Loop through training files
def result = null
for (path in paths) {
// .qptrain files just have one object but class isn't public, so
// we take the first one that is deserialized
new File(path).withObjectInputStream {
saved = it.readObject()
}
// Add the training objects, appending an extra number which
// (probably, unless very unfortunate with image names?) means they are unique
map = new HashMap<>(saved.getMap())
if (result == null) {
result = saved
result.clear()
}
for (entry in map.entrySet())
result.put(entry.getKey() + '-' + count, entry.getValue())
count++
}
// Check how big the map is & what it contains
print result.size()
print result.getMap().keySet().each { println it }
// Write out a new training file
new File(pathOutput).withObjectOutputStream {
it.writeObject(result)
}
@Svidro
Copy link
Author

Svidro commented Feb 17, 2018

Updated an error in the Classifier Sample where a variable had not been renamed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment