Classify Annotations with "Create Detection Classifier" possible?

1,050 views
Skip to first unread message

David

unread,
Nov 29, 2017, 8:28:51 AM11/29/17
to QuPath users
Hello all,

I have different types of tissue on one slide, to simple tissue detection with deactivated "Single annotation" checkbox that each ROI is independent / not merged to others.

Then I´d like to calculate intensity features for each ROI and classify them automatically.

But the "Create detection classifier" seems not to accept the measurments for Annotations to classify annotations. It probably only works for cell objects and superpixels.

Does anybody know a way also to classify annotations by classifier instead of manually?

Things would be much less labor intensive then.


Best and thank you very much!

David

David

unread,
Nov 29, 2017, 8:30:46 AM11/29/17
to QuPath users
alternatively to the "Create detection classifier" it would also work just to classify the annotations by a specific feature - e.g. abundance of DAB.

micros...@gmail.com

unread,
Nov 29, 2017, 12:55:42 PM11/29/17
to QuPath users
Well, the answer is yes, but I think you have to write the classifier manually.  The simplest example would be based off of the classifications we added to your dilate annotation script, which shows how to assign a class to an annotation (https://groups.google.com/forum/#!topic/qupath-users/Qebg8o1yqw4).
You also would need to add the measurements you want to base the classifier off of using Calculate Features-> Add intensity features
This is a simplified version of one of Pete's scripts for classifying, re-purposed to target annotations.  I have not tested it, but it should be a good starting point!

import qupath.lib.objects.classes.PathClassFactory

//use add intensity features to add whatever values you need to determine a class.
//Here I used optical density only.
selectAnnotations
();
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": false,  "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
def feature = "Exact string for name of measurement"
//get all annotations in the image
Annotations = getAnnotationObjects();
Annotations.each {
   
double value = it.getMeasurementList().getMeasurementValue(feature)
   
//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.5) {it.setPathClass(ClassA)}
   
else { it.setPathClass(ClassB)}
}
println
("Annotation classification done")


micros...@gmail.com

unread,
Nov 29, 2017, 3:14:36 PM11/29/17
to QuPath users
Your other option would be to convert all of the annotations to detections, create all of the measurements you need, generate and run a classifier, then change the classified detections back into annotations.  If you are only looking at one or two measurements (mean DAB OD, for example) I think the above method will be easier.

David

unread,
Dec 7, 2017, 4:03:34 PM12/7/17
to QuPath users
Hi Micros,

thanks for your answers.
How can i convert the annotations into a detection? I think as soon as I have a detection, I might be able to reach my goal.


concerning the approach 1, it sounds feasible for my first task as well. I could select the right tissues by e.g. DAB threshold. At the moment I fail to use the script in the line

def feature = "Exact string for name of measurement"

I don´t know what a string is. What do I need to write there, if i want to use DAB mean as a feature?

Best and thanks a lot.

David


micros...@gmail.com

unread,
Dec 8, 2017, 3:36:30 PM12/8/17
to QuPath users
I am actually do not have something prepared right now, but I assume it would go something like getting the ROI of each annotation, and using that ROI to create a PathTileObject.  In the sample scripts, I think there is an example of the reverse, converting detections into annotations.  Swapping things around within the script should get you what you want.  Unfortunately San Diego is on fire and I have some other work to do, so I won't be able to look into it too much until next week!  If you aren't in a rush I should be able to look at it then.

The text string for "feature" is the text that describes the feature, for example:
feature = "Nucleus: Area"
or for a measurement added to a SLIC through Add intensity features
feature = "ROI: 0.50 " + qupath.lib.common.GeneralTools.micrometerSymbol() + " per pixel: OD Sum:  Mean"

micros...@gmail.com

unread,
Dec 11, 2017, 5:21:27 PM12/11/17
to QuPath users
And here is the modification of Pete's included detection to annotation script.  I did not delete the original annotations for this, as I figured you would probably just run your classifier then apply the result to the parent annotation, and delete the detection at the end.

/*
 * A script to create detection object(s) having the same ROI as all other annotation objects
 */

import qupath.lib.objects.PathTileObject
import qupath.lib.roi.RectangleROI
import qupath.lib.scripting.QP

// Set this to true to use the bounding box of the ROI, rather than the ROI itself
boolean useBoundingBox = false

// Get the current hierarchy
def hierarchy = QP.getCurrentHierarchy()

// Get the select objects
def selected = getAnnotationObjects()

// Check we have anything to work with
if (selected.isEmpty()) {
   
print("No objects selected!")
   
return
}

// Loop through objects
def newDetections = new ArrayList<>()
for (def pathObject in selected) {

   
// Unlikely to happen... but skip any objects not having a ROI
   
if (!pathObject.hasROI()) {
       
print("Skipping object without ROI: " + pathObject)
       
continue
   
}

   
// Don't create a second annotation, unless we want a bounding box
   
if (!useBoundingBox && pathObject.isDetection()) {
       
print("Skipping annotation: " + pathObject)
       
continue
   
}

   
// Create an annotation for whichever object is selected, with the same class
   
// Note: because ROIs are (or should be) immutable, the same ROI is used here, rather than a duplicate
   
def roi = pathObject.getROI()
   
if (useBoundingBox)
        roi
= new RectangleROI(
                roi
.getBoundsX(),
                roi
.getBoundsY(),
                roi
.getBoundsWidth(),
                roi
.getBoundsHeight(),
                roi
.getC(),
                roi
.getZ(),
                roi
.getT())
   
def detection = new PathTileObject(roi, pathObject.getPathClass())
    newDetections
.add(detection)
   
print("Adding " + detection)
}

// Actually add the objects
hierarchy
.addPathObjects(newDetections, false)
if (newDetections.size() > 1)
   
print("Added " + newDetections.size() + " detections(s)")


David

unread,
Dec 20, 2017, 2:40:53 PM12/20/17
to QuPath users
Hi Mike,

I am not in a rush. Thanks a lot for your ideas. I tried the last script. It converted all annotations into objects, not leaving any annotations on the screen.
I feeded then the detections with statistics and tried to teach a classifier with the "create detection classifier" function.

The create detection classifier plugin seems not to work with the converted objects.
If I do the supervised learning, it will accept the annotated objects as training objects, but the "Build & Apply" Button does not result in any classification of the objects. If I run the process by script, I do not get a failor message.

I will try the absolute approach on top you suggested first. Hope the variety in staining will not cause trouble accross images, if i work with single thresholds.

Best
David


David

unread,
Dec 20, 2017, 2:44:41 PM12/20/17
to QuPath users
ah right now i realized, that the script indeed created classification:
It looks like this: when I open the polygon it contains one object with a class.


I now understand that the annotations are not deleted, and contain a classified object. All objects on the slide became the same class. Maybe coz of unsufficient training. No clue yet.

How do i transfer the class to the parent object?

Best
David

Auto Generated Inline Image 1

micros...@gmail.com

unread,
Dec 20, 2017, 6:03:41 PM12/20/17
to QuPath users
detections = getDetectionObjects();

detections
.each{
itClass
= it.getPathClass()
it
.getParent().setPathClass(itClass)
}
fireHierarchyUpdate
()

For this, you just need to get the list of detection objects, and then for each one, get it's class, and then get it's parent (the annotation is "up" one level) and set the class to the detection's class.

With a possible clearDetections() at the end if you want to remove those.

micros...@gmail.com

unread,
Dec 20, 2017, 6:17:32 PM12/20/17
to QuPath users
I should have noted, this assumes all detections are your classified objects, and all annotations you want to classify are one level up from the detections.

David

unread,
Jan 2, 2018, 2:48:05 PM1/2/18
to QuPath users
Hi Mike

with your suggestion:

detections = getDetectionObjects();

detections
.each{
itClass
= it.getPathClass()
it
.getParent().setPathClass(itClass)
}

fireHierarchyUpdate
();

I finally found a solution.

I can just put a cell into the annotation or large superpixels and classify them and then put the same class of the object to the parent annotation.

I terms of working with that, I figures out, that the lowest bottom superpixel in an annotation will determine its later destiny of class.

It turned out that it runs with better results if I use the tiles to annotations plugin and delete the original annotation.



Lavinia Alberi

unread,
Jun 6, 2018, 11:26:24 AM6/6/18
to QuPath users
I am having the same issue when running the annotation measurement the hierarchy is lost.
The script. 
detections = getDetectionObjects();

detections
.each{ 
itClass 
= it.getPathClass()
it
.getParent().setPathClass(itClass)
}

fireHierarchyUpdate();

 runs but..where should I find the results? 


THX any help is precious

LA

micros...@gmail.com

unread,
Jun 6, 2018, 1:26:58 PM6/6/18
to QuPath users
That script doesn't really generate any results, it just sets sets the parent class to the same class as the detection object.  If you have many detections (cells) within an annotation, it will just rapidly change the class of the annotation over and over again.

I have occasionally lost the hierarchy when scripting and needed to restart QuPath to get it back.  I am unsure what causes that.

Lavinia Alberi

unread,
Jun 6, 2018, 4:09:27 PM6/6/18
to QuPath users
THX for addressing this issue...I think I might have found a solution...yet still need to go through some tweaks.

THX again,

LA
Reply all
Reply to author
Forward
0 new messages