Positive Cell Detection: How to get more data from it in the measurement exporter

Hi,

I have been performing Positive Cell Detection on DAB stained slides (CD3 stain). I really like it and I can get it working, but I have problems getting cells separated. On itself that is not a big issue, as I can also look at the area instead of single cells.

Hence my question: Is it possible to get more data from the measurement exporter then just the count of positive and negative detection and the % they relate to? For example, would it be possible to generate the total cell area of positive and of the complete annotation in order to calculate the percentage of positive area on the tissue?

I know it can also be generated by asking the measurement exporter to generate results for detections, however when you do that on a rather large image you result into 4x10e6 lines for example on 1 image, which can not be taken by Excel to calculate yourself the positive cell area. (Excel only wants around 1.4x10e6 lines of data…)

My solution would be to have more columns available in “populate” for detections or annotations where you can choose for example to export also the total cell area.

Hope I am either overseeing something or its possible to introduce this in the (pretty close) future :slight_smile:

Hi @svanhal,

You could add your desired measurement via scripting, for instance using something like:

// Script written in v0.2.0
import qupath.lib.gui.commands.SummaryMeasurementTableCommand;
import qupath.lib.gui.measure.ObservableMeasurementTableData;
import qupath.lib.objects.PathAnnotationObject;


// Get the total cell areas ONLY if they belong to the Positive class
cellsArea = 0
getCellObjects().each {
    if (it.getPathClass() == getPathClass('Positive'))
        cellsArea += measurement(it, 'Cell: Area')
}


def entry = getProjectEntry()
def imageData = entry.readImageData()
def model = new ObservableMeasurementTableData()
model.setImageData(imageData, imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, PathAnnotationObject.class))
def data = SummaryMeasurementTableCommand.getTableModelStrings(model, "\t", [])


// Considering you have only 1 annotation with your cells inside it,
// these lines will get the total area of the annotation
def index = data[0].split('\t').findIndexOf { it.equals('Area µm^2')}
def totalArea = data[1].split('\t')[index] as double

// Calculate your measurement
def myMeasurement = totalArea.div(cellsArea)*100

// Printing values
print cellsArea
print totalArea
print myMeasurement

// Add measurement to annotation
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("myMeasurement", myMeasurement)

And then use the measurement exporter to export annotation measurements (don’t forget to save the image first!)

The reason why some measurements are calculated differently for annotations and/or detections is detailed in the docs and involves dynamic and static calculations.

Hope the script helps anyway!

3 Likes

This looks promising and I am trying it as we speak! Thanks also for explaining what the sections do. My scripting knowledge is not as good yet :wink:

It worked! I am so happy, thank you :slight_smile:

Only thing I noticed was this:
WARN: Openslide: Property ‘openslide.objective-power’ not available, will return default value NaN

Doesn’t look like a problem, but that is also becuase I do not understand what it means…

1 Like

I did want to mention that there is some danger to this type of measurement, as it is highly dependent on the Cell expansion set during Cell detection. That means it may not be terribly accurate if you truly want the relative area covered by two different cell types. To see how badly it can be abused, turn up the Cell expansion to something like 100.

Since the Cell expansion doesn’t truly represent the cell area, the area sum will always be suspect (as a scientific measurement. If you just want a rough estimate, that’s fine).

Thanks for mentioning it, I will have a thought about that :slight_smile:

I have updated the script as you made it for me @melvingelbard also, yours contained a small mistake. The cellsArea needs to come first and then be divided by the totalArea :wink:

// Script written in v0.2.0
 import qupath.lib.gui.commands.SummaryMeasurementTableCommand;
 import qupath.lib.gui.measure.ObservableMeasurementTableData;
 import qupath.lib.objects.PathAnnotationObject;
 
 
 // Get the total cell areas and add to corresponding cell area for the respective classes
 cellsAreaPositive = 0
 cellsAreaNegative = 0
 getCellObjects().each {
     if (it.getPathClass() == getPathClass('Positive'))
         cellsAreaPositive += measurement(it, 'Cell: Area')
     if (it.getPathClass() == getPathClass('Negative'))
         cellsAreaNegative += measurement(it, 'Cell: Area')
 }
 
 
def entry = getProjectEntry()
def imageData = entry.readImageData()
def model = new ObservableMeasurementTableData()
model.setImageData(imageData, imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, PathAnnotationObject.class))
def data = SummaryMeasurementTableCommand.getTableModelStrings(model, "\t", [])
 
// Considering you have only 1 annotation with your cells inside it,
// these lines will get the total area of the annotation and the total area of the classes together as earlier defined
def index = data[0].split('\t').findIndexOf { it.equals('Area µm^2')}
def totalAreaAnnotation = data[1].split('\t')[index] as double
def totalAreaPosNeg = cellsAreaPositive + cellsAreaNegative as double
 
 
// Calculate your measurements for different area 
def areaPositive = cellsAreaPositive.div(totalAreaAnnotation)*100
def areaNegative = cellsAreaNegative.div(totalAreaAnnotation)*100
def areaPositiveFromPosNeg = cellsAreaPositive.div(totalAreaPosNeg)*100
def areaNegativeFromPosNeg = cellsAreaNegative.div(totalAreaPosNeg)*100

// Printing values
print cellsAreaPositive
print cellsAreaNegative
print totalAreaAnnotation
print totalAreaPosNeg
print areaPositive
print areaNegative
print areaPositiveFromPosNeg
print areaNegativeFromPosNeg
 
// Add measurements to annotation
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("cellsAreaPositive", cellsAreaPositive)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("cellsAreaNegative", cellsAreaNegative)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("totalAreaAnnotation", totalAreaAnnotation)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("totalAreaPosNeg", totalAreaPosNeg)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("areaPositive", areaPositive)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("areaNegative", areaNegative)
 getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("areaPositiveFromPosNeg", areaPositiveFromPosNeg)
getAnnotationObjects().getAt(0).getMeasurementList().putMeasurement("areaNegativeFromPosNeg", areaNegativeFromPosNeg)


It now gives you:

  • area of the cells that are classified positive
  • area of the cells that are classified negative (I know this is a simple calculation, but nice if QuPath can run it instead of having to do it myself later)
  • total area of the annotation
  • calculated total area for all your detection together
  • calculated % of your positive detection based on the annotation area
  • calculated % of your negative detection based on the annotation area
  • calculated % of your positive detection based on the total detection area
  • calculated % of your negative detection based on the total detection area

It also prints all the above values to the measurement table.

Thanks again Melvin, I learned a lot from your script :slight_smile:

1 Like

Oops indeed you’re right :slight_smile:
I’m glad it works well!

1 Like

Hello! I’m trying to use your script but I keep getting this error:

ERROR: MultipleCompilationErrorsException at line 9: startup failed:
Script21.groovy: 10: Unexpected input: ‘{’; Expecting @ line 10, column 23.
getCellObjects().each {
^

1 error

ERROR: org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:287)

I’m quite new to scripting so I’m no sure what I’m doing wrong. Thank you!

The script was not saved in coding format, so it could not be copied and used in QuPath without significant editing. I adjusted the formatting to “code” format, which is the </> icon you can select when posting.

1 Like

Thank you the error is resolved!
But I think I’m doing something wrong with the Positive Cell Detection bc with the script, I only get the area of the annotation, the other fields are zero.
I run positive cell detection (Nucleus: Channel 2 mean) in one annotation, then applied an object classifier labeling as Positive or Negative. In the detection measurement table, the class says either “Positive: Positive” or “Negative: Negative.” Then click save. I’m not sure if I’m missing a step that impedes the script to detect the other measurements.
Thank you!

1 Like

Based on your other image and your text here, you have no positive or negative cells. As you state, you have Positive:Positive. I don’t know what you have done to get multiple positives, but you may want to look into your upstream steps first. Otherwise, the classes for this script need to be exactly correct.

The script is specifically looking for “Positive” cells. You could adjust the script with some Groovy code to only look for the bit before the first colon, but that’s general Groovy and not QuPath specific. It would probably be easier to figure out why you have multiple levels of positive classes.

Alternatively, build your own script off of one of the generic measurement scripts that cycles through all classes. The script above has fixed classes which limits it’s usefulness.

This script, for example, cycles through all classes to get the percentage of each class and density, though it could be adjusted using the script above to calculate total area. As mentioned above, though, total area is a dangerous calculation to use as it is heavily dependent on the Cell Expansion.
https://gist.github.com/Svidro/68dd668af64ad91b2f76022015dd8a45#file-class-cell-counts-percentages-and-density-to-parent-annotation-0-2-0-groovy