Script for sum of Nucleaus:area of a specific annotation

Hi everyone. I would have a favor to ask you.
I created a pixel classifier (Tumor and Stroma);
I created the corresponding annotations;
In each annotation I performed the “Positive cell detection”.
I need a script that adds two new columns to the “Annotations results” table:

  1. Sum of the “Nucleus: Area” of the Positive cells (visible in the “Detections results” table). So in the row of the annotation Tumor I would like to display the sum of the nuclear areas of the positive cells contained in the tumor, in the row of the annotation Stroma I would like to display the sum of the nuclear areas of the positive cells contained in the stroma);
  2. Percentage of area of annotation occupied by nuclear area of positive cells (hence the previous value / Area of annotation * 100) for both Tumor and Stroma.
    Thanks

I suspect you could put together what you need by adapting the scripts found at:
https://gist.github.com/Svidro/68dd668af64ad91b2f76022015dd8a45
Note that for each cell object, you can use getNucleusROI() instead of getROI() to get the full cell. If you use getROI().getArea(), it is in pixels, versus for each cell using the measurement() function to get the value of “Nucleus: Area”.

sorry, but I’m not very skilled with programming codes. Would you be kind enough to write it to me? :sweat_smile:

I do a bit of coding when it is novel, but this is a fairly straightforward extension of what is already included in the samples!

1 Like

Thanks! (p.s. I’m using 0.2.0 M9)

Hi @Manuel_Baldinu,

Is this what you need?

import qupath.lib.gui.measure.ObservableMeasurementTableData
import qupath.lib.objects.PathCellObject

// Get observable measurement table for the current image
def annotations = getAnnotationObjects()
def ob = new ObservableMeasurementTableData()
ob.setImageData(getCurrentImageData(),  annotations)

annotations.each{ annotation -> {
    // Get children object
    def cells = annotation.getChildObjects()
    if (cells != null && cells.size() > 0) {
        // Get sum of the nucleus area (only if children are cell objects & have name 'Positive')
        def sumPositive = cells.stream()
                .filter(cell -> cell instanceof PathCellObject)
                .filter(cell -> cell.getDisplayedName().equals("Positive"))
                .mapToDouble(cell -> cell.getMeasurementList().getMeasurementValue("Nucleus: Area"))
                .sum()

        // Add measurement "Sum of positive nucleus (areas)" to the annotation's measurement list
        annotation.getMeasurementList().addMeasurement("Sum of positive nucleus (areas)", sumPositive)

        // Calculate percentage of the annotation taken by the positive nucleus
        def areaPercentage = (sumPositive / ob.getNumericValue(annotation, "Area µm^2")) * 100

        // Add measurement "Percentage of positive nucleus (area)" to the annotat9ion's measurement list
        annotation.getMeasurementList().addMeasurement("Percentage of positive nucleus (area)", areaPercentage)
    }
}}

It gets all the annotations in your image, checks the children objects (i.e. your detected cells). Then checks if they are actual ‘Cell Objects’ and if they are positive (name == “Positive”). If they are, their area (in micron) is summed and put into the measurement table (Annotation result table).
Then, the percentage of that sum is calculated, according to its parent annotation.

1 Like

HI! I have this error:

ERROR: MultipleCompilationErrorsException at line 9: startup failed:
Script3.groovy: 10: Ambiguous expression could be either a parameterless closure expression or an isolated open code block;
   solution: Add an explicit closure parameter list, e.g. {it -> ...}, or force it to be treated as an open block by giving it a label, e.g. L:{...} @ line 10, column 33.
   annotations.each{ annotation -> {
                                   ^

1 error


ERROR: Script error (MultipleCompilationErrorsException)
    at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:311)
    at org.codehaus.groovy.control.ErrorCollector.addFatalError(ErrorCollector.java:151)
    at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:121)
    at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:133)
    at org.codehaus.groovy.control.SourceUnit.addError(SourceUnit.java:325)
    at org.codehaus.groovy.antlr.AntlrParserPlugin.transformCSTIntoAST(AntlrParserPlugin.java:224)
    at org.codehaus.groovy.antlr.AntlrParserPlugin.parseCST(AntlrParserPlugin.java:192)
    at org.codehaus.groovy.control.SourceUnit.parse(SourceUnit.java:226)
    at org.codehaus.groovy.control.CompilationUnit$1.call(CompilationUnit.java:197)
    at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:960)
    at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:637)
    at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:613)
    at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:590)
    at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:401)
    at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:89)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:341)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:338)
    at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:336)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:320)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:262)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:331)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:800)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:734)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:714)
    at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1130)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)

another thing: M9, by default, uses pixels, not microns


This is my Detections results table. I don’t know if cells are CellObjects

If your Cell Expansion is set to 0 (no cytoplasm), they will not be cell objects. I can’t tell from that image.

As far as the code is concerned, try getting rid of the indicated { after the -> along with one of the two } at the end.

1 Like

Now i have this error:

ERROR: MultipleCompilationErrorsException at line 15: startup failed:
Script8.groovy: 16: unexpected token: -> @ line 16, column 30.
                   .filter(cell -> cell instanceof PathCellObject)
                                ^

1 error


ERROR: Script error (MultipleCompilationErrorsException)
    at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:311)
    at org.codehaus.groovy.control.ErrorCollector.addFatalError(ErrorCollector.java:151)
    at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:121)
    at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:133)
    at org.codehaus.groovy.control.SourceUnit.addError(SourceUnit.java:325)
    at org.codehaus.groovy.antlr.AntlrParserPlugin.transformCSTIntoAST(AntlrParserPlugin.java:224)
    at org.codehaus.groovy.antlr.AntlrParserPlugin.parseCST(AntlrParserPlugin.java:192)
    at org.codehaus.groovy.control.SourceUnit.parse(SourceUnit.java:226)
    at org.codehaus.groovy.control.CompilationUnit$1.call(CompilationUnit.java:197)
    at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:960)
    at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:637)
    at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:613)
    at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:590)
    at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:401)
    at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:89)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:341)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:338)
    at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:336)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:320)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:262)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:331)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:800)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:734)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:714)
    at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1130)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)

How can I know what kind of object my cells are?

Without resorting to scripting, the best way is usually to check the name field of the object. However, if you have already classified the objects (positive/negative), that name will be overwritten. There are really only two options for cell detection though. Either it has a cytoplasm and is a cell, or it has no cytoplasm and is not a cell.


I am not familiar with that coding style, so I don’t know how to adjust .filter. I generally use .findAll or .each as shown in the linked script page.

These are lambdas (Java 8-style), which are compatible with Groovy 3 (included in M10 onwards, so not in M9)
But anyway I think it should work if you enclose the lambdas in { and }:

.filter({cell -> cell instanceof PathCellObject})

EDIT: Clarified Groovy versions in QuPath

2 Likes


I tried to install M10. Script runs, but does not calculate the percentage

Given that your image is a tif, I’m guessing you don’t have appropriate metadata set (pixel sizes) so all of the measurements are in pixels. Therefor the area measurement name in the script is inaccurate. Adjust the area measurement name to what is shown in your annotation.

        def areaPercentage = (sumPositive / ob.getNumericValue(annotation, "Area µm^2")) * 100

Notice it says microns squared, but you probably don’t have microns squared?

2 Likes

That’s odd, it should work as is…
Let’s print the values to see what might have gone wrong:

// Let's print some variables
print "Sum of positives: " + sumPositive
print "Annotation area: " + ob.getNumericValue(annotation, "Area µm^2")
print "Result: " + areaPercentage

One other thing though: if the measurement exists already, re-running the script will not override the measurement, to be sure to override it, you can ‘remove the measurement’ first:

// Remove measurements to be sure to override it later
annotation.getMeasurementList().removeMeasurements("Sum of positive nucleus (areas)", "Percentage of positive nucleus (area)")

On that note, if you use putMeasurement instead of addMeasurement, it avoids that problem. I have never really used addMeasurement for that reason. putMeasurement will work even if the measurement doesn’t exist yet. There may be a good reason to avoid put, but I don’t think I have run into it yet.

Good point - there isn’t! Always use ‘put’.

‘Add’ exists because it is faster whenever you know that there will be no duplicates, and QuPath uses it internally when this is definitely the case (e.g. creating objects for the first time). In practice, this should probably be removed at some point as the trivial performance benefits probably aren’t worth the confusion (since I accept ‘add’ seems natural…).

1 Like

I replaced “Area micron2” with “Area px ^ 2”. Now it runs!

Another question. is it possible change pixel scale with micron scale on Qupath m10?

1 Like

Yes, manually you can double click in the missing metadata entries in the Image tab. You can also script it.
Some information here on metadata:

I’ve forgotten what the current best way to adjust the metadata by script is, but I still have this hanging around from M5. Maybe it will work?
https://gist.github.com/Svidro/68dd668af64ad91b2f76022015dd8a45#file-metadata-by-script-in-m5-groovy

1 Like