Need help quantifying nuclear and cytoplasmic RNAscope foci

Hi all,

New QuPath user. I’m trying to set up a workflow to count nuclear and cytoplasmic RNAscope foci. I figured out how to detect each cell’s nucleus and cytoplasm (Analyze > Cell detection > Cell detection) and detect foci (Analyze > Cell detection > Subcellular detection). I can see how many foci were detected in each cell, but not if the foci is nuclear or cytoplasmic. How can I get this information?

sample image: MAX_sec1_cortex_010.tif (16.0 MB)
(dropbox link if attachment doesn’t work: Dropbox - MAX_sec1_cortex_010.tif - Simplify your life)

My first guess would be an older script found here:
https://gist.github.com/Svidro/5b016e192a33c883c0bd20de18eb7764#file-subcellular-detection-to-nuclear-or-cyto-groovy
But, that was for 0.1.2 when subcellular detections had their own class. If I were to edit this script based on what I ASSUME is your hierarchy structure (and may well be wrong), I would so something like this:

//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.isDetection() == true && p.isCell() == false})

// 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 seems to work for my sample in 0.2.0, but I have also been drinking heavily in order to watch Godzilla vs Kong so your mileage may vary.


Once you have run that script, each foci has a derived classification called nuclear or cytoplasmic. That information could be used in a variety of ways.

The important question, I am guessing, is how you would want to use this information. Do you want the estimated total spot area of cytoplasmic or nuclear spots to be added to a value per cell? A ratio per annotation area? Something else?

All of this assumes you have ONLY cells and subcellular detections, not any other tiles or other detection objects that are not cells.

This is awesome! Right now, it would be very useful to know the number of nuclear and cytoplasmic foci per cell (individually for the C2 (green), C3 (red), and C4 (magenta) channels). From here, it’s trivial to calculate the nuclear to cytoplasmic ratios. How can I get these results to show up in the measurements table? I could be missing it, but when I look at it now, it doesn’t look like it has this quantification. MAX_sec1_cortex_010.txt (72.7 KB)

I see these columns, but not # of foci detected.
Nucleus: Channel 2 mean
Nucleus: Channel 2 sum
Nucleus: Channel 2 std dev
Nucleus: Channel 2 max
Nucleus: Channel 2 min
Nucleus: Channel 2 range
etc.

Also, I’ve done a lot of ImageJ scripting but no Groovy or Java. If I’m understanding correctly, the script is assigning a foci to the nucleus or cytoplasm based on its centroid (as opposed to another parameter like area or intensity)? If so, that’s exactly what I would want!

Right, that is why I asked, because you will have to manually use that classification “for something.”

Number of foci is a bit of a weird measurement for QuPath, and depends on your subcellular detection settings pretty heavily. I tend to use Estimated spot counts instead (if you can estimate a value for an average spot size), since overlapping spots are… one foci? Two? Maybe you want to intensity sum instead? Not sure. Anyway…
I again have to make some assumptions about your project structure - in this case that you only have one class of subcellular detection for simplicity.

//The resolve may or may not be needed. It will slow things down if needed, but if the code works without, great.
//resolveHierarchy() 
getCellObjects().each{cell->

numberOfFociObjectsInCytoplasm = cell.getChildObjects().findAll{it.getPathClass().toString().contains("Cytoplasmic")}.size()

cell.getMeasurementList().putMeasurement("Cytoplasmic spots", numberOfFociObjectsInCytoplasm )
}

Writing that kinda blind without testing it, but that’s the general idea. You will need to generate your measurement (in this case the size of the list of objects with Cytoplasmic in the class name in that particular cell), then add it to the cell. You may need to adapt it as your measurements change, or add additional filters if you have multiple types of subcellular detections. You could do similar but check for nuclear instead, or in addition.

Alternatively, you can export the CSV file of detections, and do all of your sorting and analysis in R, I suppose, though that usually requires naming the cells with unique identifiers.

If you do end up wanting to drill down into a more detailed description of what is in the cell, you may need the measurement list from each subcellular object, to get its area or something.

totalArea = 0
cell.getChildObjects().each{ subcell->
    totalArea += measurement(subcell, "Subcellular Cluster: Channel 3: Area") // or whatever measurement
}

I tried adding this code to my script and running it but I’m getting an error. Here is the code I’m using:

clearAllObjects();


runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImage": "Channel 1",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 10.0,  "medianRadiusMicrons": 3.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 25.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 5.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true}');
runPlugin('qupath.imagej.detect.cells.SubcellularDetection', '{"detection[Channel 1]": -1.0,  "detection[Channel 2]": 40.0,  "detection[Channel 3]": 40.0,  "detection[Channel 4]": 40.0,  "doSmoothing": true,  "splitByIntensity": true,  "splitByShape": true,  "spotSizeMicrons": 0.2,  "minSpotSizeMicrons": 0.1,  "maxSpotSizeMicrons": 10.0,  "includeClusters": true}');


// FROM QUPATH FORUM
// 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.isDetection() == true && p.isCell() == false})

// 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()





//The resolve may or may not be needed. It will slow things down if needed, but if the code works without, great.
//resolveHierarchy() 
getCellObjects().each{cell->

numberOfFociObjectsInCytoplasm = cell.getChildObjects().findAll{it.getPathClass().toString().contains("Cytoplasmic").size()

cell.getMeasurementList().putMeasurement("Cytoplasmic spots", numberOfFociObjectsInCytoplasm )
}

And here is the error:

ERROR: MultipleCompilationErrorsException at line 40: startup failed:
Script9.groovy: 41: Unexpected input: '{'; Expecting <EOF> @ line 41, column 22.
   getCellObjects().each{cell->
                        ^

1 error


ERROR: org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:287)
    org.codehaus.groovy.control.ErrorCollector.addFatalError(ErrorCollector.java:143)
    org.apache.groovy.parser.antlr4.AstBuilder.collectSyntaxError(AstBuilder.java:4544)
    org.apache.groovy.parser.antlr4.AstBuilder.access$000(AstBuilder.java:341)
    org.apache.groovy.parser.antlr4.AstBuilder$1.syntaxError(AstBuilder.java:4559)
    groovyjarjarantlr4.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:44)
    groovyjarjarantlr4.v4.runtime.Parser.notifyErrorListeners(Parser.java:543)
    groovyjarjarantlr4.v4.runtime.DefaultErrorStrategy.notifyErrorListeners(DefaultErrorStrategy.java:154)
    org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy.reportInputMismatch(DescriptiveErrorStrategy.java:104)
    org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy.recover(DescriptiveErrorStrategy.java:55)
    org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy.recoverInline(DescriptiveErrorStrategy.java:68)
    groovyjarjarantlr4.v4.runtime.Parser.match(Parser.java:213)
    org.apache.groovy.parser.antlr4.GroovyParser.compilationUnit(GroovyParser.java:361)
    org.apache.groovy.parser.antlr4.AstBuilder.buildCST(AstBuilder.java:405)
    org.apache.groovy.parser.antlr4.AstBuilder.buildCST(AstBuilder.java:384)
    org.apache.groovy.parser.antlr4.AstBuilder.buildAST(AstBuilder.java:424)
    org.apache.groovy.parser.antlr4.Antlr4ParserPlugin.buildAST(Antlr4ParserPlugin.java:58)
    org.codehaus.groovy.control.SourceUnit.convert(SourceUnit.java:244)
    org.codehaus.groovy.control.CompilationUnit.lambda$addPhaseOperations$1(CompilationUnit.java:191)
    org.codehaus.groovy.control.CompilationUnit$ISourceUnitOperation.doPhaseOperation(CompilationUnit.java:880)
    org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:650)
    org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:627)
    groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:389)
    groovy.lang.GroovyClassLoader.lambda$parseClass$3(GroovyClassLoader.java:332)
    org.codehaus.groovy.runtime.memoize.StampedCommonCache.compute(StampedCommonCache.java:163)
    org.codehaus.groovy.runtime.memoize.StampedCommonCache.getAndPut(StampedCommonCache.java:154)
    groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:330)
    groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:314)
    groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:257)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:336)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:926)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
    qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
    java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    java.base/java.lang.Thread.run(Unknown Source)

Ah, my findAll statement was missing a closure }
fixing it in the above post.
*edit again to put the bracket inside of the .size()

And to emphasize, this is currently getting all of your subcellular detections - you can edit the .contains if you want a specific channel to be less inclusive - use the entire class name if you want.
*Actually, no don’t use the entire class name if you include clusters, since some will be “Subcellular cluster” and others will be “Subcellular spot”

Got it, works now! I should be able to figure out how to do each channel’s objects separately from here.

Thank you so much!

1 Like

No problem, though, please do fill out the QuPath user survey if you have not already!

Help Pete help us.

1 Like

Will do, this software is amazing and I’d be happy to help out in any small way I can!

2 Likes

Another question: how do I call a variable from inside a runPlugin() function? I tried just replacing the numbers (like 40.0) with the variable names but it didn’t work.

double requested_pixel_size = 0.5 // default = 0.5 (0 = more accurate but slower, 1 = less accurate but faster)
double C2_detection_threshold = 40.0
double C3_detection_threshold = 40.0
double C4_detection_threshold = 40.0

clearDetections();
//createSelectAllObject(true);
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": -10.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');
runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImage": "Channel 1",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 10.0,  "medianRadiusMicrons": 3.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 25.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 5.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true}');
runPlugin('qupath.imagej.detect.cells.SubcellularDetection', '{"detection[Channel 1]": -1.0,  "detection[Channel 2]": 50.0,  "detection[Channel 3]": 50.0,  "detection[Channel 4]": 50.0,  "doSmoothing": true,  "splitByIntensity": true,  "splitByShape": true,  "spotSizeMicrons": 0.2,  "minSpotSizeMicrons": 0.1,  "maxSpotSizeMicrons": 10.0,  "includeClusters": true}');

Hi @Soki

You can do it with string concatenation, e.g.

double radius = -10
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": ' + radius + ',  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');

or alternatively by triple quoting the String and using $variable syntax:

double radius = -10
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', """{"radiusMicrons": $radius,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}""");
2 Likes

So close, I tried it with & (from ImageJ) instead of $. Thank you!

1 Like