Last active
October 24, 2024 02:06
-
-
Save Svidro/5b016e192a33c883c0bd20de18eb7764 to your computer and use it in GitHub Desktop.
Classification based groovy scripts for QuPath
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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!") | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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!' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated an error in the Classifier Sample where a variable had not been renamed.