-
-
Save beosro/da77af6247c51d047cf5663ee678b366 to your computer and use it in GitHub Desktop.
Classification based groovy scripts for QuPath
This file contains hidden or 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 | |
| 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. | |
This file contains hidden or 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 | |
| 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 hidden or 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 | |
| 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() |
This file contains hidden or 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 | |
| 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 hidden or 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 | |
| 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 hidden or 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. | |
| 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 hidden or 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. | |
| setCellIntensityClassifications("Cytoplasm: DAB OD mean", 0.15) |
This file contains hidden or 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
| /** | |
| * 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 hidden or 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 | |
| for (def cell : getCellObjects()) | |
| cell.setPathClass(null) | |
| fireHierarchyUpdate() | |
| println("Done!") |
This file contains hidden or 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. | |
| //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 hidden or 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 | |
| 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