Export CoreID for TMA when exporting points from ROIs

Hi there, hope someone can help with this:
I am aware of the Hierarchy logic for objects behind Qupath, which I have learnt here(https://github.com/qupath/qupath/wiki/Object-hierarchies).

I have a TMA for which I have imported a TMA map with corresponding coreIDs. Within each TMA core, I have defined an area for stroma and another for tumor. Therefore I have for the same CoreID a stroma region and a tumor region.

With the script#1 below I manage to export in a .txt file the points for my ROIs (all possible ROIs that happen to be drawn on this TMA during processing).

import qupath.lib.gui.QuPathGUI

def annotations=getAnnotationObjects() 
 
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()  
if(dirResults == null) 
    return 
def fileResults = new File(dirResults, "annotations.txt") 
 
int count = 0 
fileResults.withPrintWriter { 
    for(pathObject in annotations) { 
        def roi = pathObject.getROI() 
        it.print(pathObject) 
        it.print("\t") 
        pts = roi.getAllPoints() 
        it.print(pts) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

And with this other script#2 I managed to get the coreIDs exported as well:

import qupath.lib.gui.QuPathGUI
import qupath.lib.objects.TMACoreObject
 
def annotations=getAnnotationObjects() 
def cores=getTMACoreList()
 
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()  
if(dirResults == null) 
    return 
def fileResults = new File(dirResults, "annotations.txt") 
 
int count = 0 
fileResults.withPrintWriter { 
    for(core in cores) { 
        core = core.getUniqueID()
        it.print(core) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

I am missing the way to “combine” the two scripts, in order to have for each pathObject in annotations from the script#1 also the matching coreID.

I tried to include some sort of get.parent() logic in this but all my attempts failed…

Thank you for the help!
Eugenio

1 Like

Hi @eugenio,

If I understood well, you want to relate the stroma and tumour annotations to their ‘parent’ core ID?
Would something like this work? (I haven’t tested it so it might need small changes here and there)

import qupath.lib.gui.QuPathGUI
import qupath.lib.objects.TMACoreObject

def annotations = getAnnotationObjects()
def cores = getTMACoreList()
 
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()  
if (dirResults == null) 
    return
def fileResults = new File(dirResults, "annotations.txt") 
 
int count = 0 
fileResults.withPrintWriter { 
    for (annotation in annotations) { 
        TMACoreObject parent = annotation.getParent()
        it.print(pathObject) 
        it.print("\t") 
        it.print(parent.getID())
        it.print("\t") 
        pts = annotation.getROI().getAllPoints() 
        it.print(pts) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

1 Like

Thank you @melvingelbard, you understand exactly what I want to achieve.
I tried your script, however I get into this error:

ERROR: Error at line 13: Cannot cast object 'Annotation (Geometry) (2/595 objects)' with class 'qupath.lib.objects.PathAnnotationObject' to class 'qupath.lib.objects.TMACoreObject'

ERROR: Script error
    at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnSAM(DefaultTypeTransformation.java:415)
    at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnNumber(DefaultTypeTransformation.java:329)
    at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.castToType(DefaultTypeTransformation.java:243)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.castToType(ScriptBytecodeAdapter.java:615)
    at Script51$_run_closure1.doCall(Script51.groovy:16)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:101)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
    at groovy.lang.Closure.call(Closure.java:405)
    at groovy.lang.Closure.call(Closure.java:421)
    at org.codehaus.groovy.runtime.IOGroovyMethods.withWriter(IOGroovyMethods.java:1133)
    at org.codehaus.groovy.runtime.ResourceGroovyMethods.withPrintWriter(ResourceGroovyMethods.java:2080)
    at org.codehaus.groovy.runtime.dgm$1080.invoke(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:244)
    at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:53)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:127)
    at Script51.run(Script51.groovy:14)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:797)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:731)
    at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:711)
    at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1111)
    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)

I think that it might be related to the hierarchy structure that I have with my ROIs. Here is how it looks:

image

I cannot figure out whether I have to go more levels up or more levels down in the hierarchy to “get back to the core ID”

If you’re sure that you always have the same hierarchy structure, you could replace TMACoreObject parent = annotation.getParent() with:

TMACoreObject grandparent = annotation.getParent().getParent()
it.print(annotation) 
it.print("\t") 
it.print(grandparent .getUniqueID())

EDIT: You could also always go up the hierarchy and stop whenever the parent object is an instance of TMACoreObject!

Thanks for help with this, I can see the logic behind your suggestion with the getParent().getParent() however I still get the same error:

Cannot cast object 'Image' with class 'qupath.lib.objects.PathRootObject' to class 'qupath.lib.objects.TMACoreObject'

So basically now with the getParent().getParent() we went up indeed - that makes perfectly sense. But I don’t get why the CoreID doesn’t get there?

Maybe your hierarchy is not the same everywhere?
Would something like this work?

parent = annotation
while (!parent instanceof TMACoreObject) {
    parent = parent.getParent();
    if (parent instance of PathRootObject)
        break;
}
print(it)
print("\t")
print(parent .getUniqueID())

I’m sorry I can’t test it right now but something like this should probably work.

It is failing on the unclassified annotations.

You would need to get the correct annotations to export. The line where you define what you are exporting could be:

def annotations = getAnnotationObjects().findAll{it.getPathClass() != null}

That will exclude all of your unclassified annotations.

@eugenio if you’re using QuPath v0.2.x, you should know that the hierarchy behavior is a bit different. See here for the latest documentation.

There are also a few potentially-useful methods in PathObjectTools to get the TMACoreObject corresponding to a particular object – based either on the hierarchy or based purely on pixel coordinates.

Basing your query on pixel coordinates is likely to be more reliable, given that the hierarchy behavior is less strict in versions > v0.2.0.

Thank you all for help, I am trying things out - still new to groovy scripting so it will take some time to figure out how to combine things together! I’ll post here as soon as I get it to work!

2 Likes

Small update, with the code below I finally managed to get the get.Parent() to work, but still missing the Core ID which I have added with a TMA map:

import qupath.lib.gui.QuPathGUI
import qupath.lib.objects.TMACoreObject
 
def annotations=getAnnotationObjects() 
def cores=getTMACoreList()
 
def dirResults = Dialogs.promptForDirectory()
if(dirResults == null) 
    return 
def fileResults = new File(dirResults, "annotations_progr.txt") 
 
int count = 0 
fileResults.withPrintWriter { 
    for(pathObject in annotations) { 
        def roi = pathObject.getROI() 
        def ID = pathObject.getParent().getParent()
        it.print(pathObject)
        it.print("\t")
        it.print(ID)
        it.print("\t")
        pts = roi.getAllPoints() 
        it.print(pts) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

@petebankhead have tried to incorporate the getAncestorTMACore(), in order to get the core IDs, in the following way:

import qupath.lib.gui.QuPathGUI
import qupath.lib.objects.TMACoreObject
 
def annotations=getAnnotationObjects() 
def cores=getTMACoreList()
 
def dirResults = Dialogs.promptForDirectory()
if(dirResults == null) 
    return 
def fileResults = new File(dirResults, "annotations_progr.txt") 
 
int count = 0 
fileResults.withPrintWriter { 
    for(pathObject in annotations) { 
        def roi = pathObject.getROI() 
        def ID = pathObject.getParent().getParent()
        it.print(pathObject)
        it.print("\t")
        it.print(ID)
        it.print("\t")
        it.print(ID.getAncestorTMACore())
        it.print("\t")
        pts = roi.getAllPoints() 
        it.print(pts) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

However, I get this error:

ERROR: MissingMethodException at line 13: No signature of method: qupath.lib.objects.TMACoreObject.getAncestorTMACore() is applicable for argument types: () values:

I really think I am missing something in the PathObject structure/logic, but cannot figure out what it is?

You’d call it something like this:

it.print(PathObjectTools.getAncestorCore(pathObject))

Ok, finally got it to work as follow:

@petebankhead I changed into it.print(PathObjectTools.getAncestorTMACore(pathObject)) and this made the get.Parent more efficient, so got rid of those lines in my code.

However my final aim was to get the UniqueID. Found in the meantime another post from @petebankhead which did the trick! Had to set the UniqueID to be the name, but that is fine for me.

Here is the final code:

import qupath.lib.gui.QuPathGUI
import qupath.lib.objects.TMACoreObject
 
def annotations=getAnnotationObjects() 
def cores=getTMACoreList()
 
def dirResults = Dialogs.promptForDirectory()
if(dirResults == null) 
    return 
def fileResults = new File(dirResults, "annotations_progr.txt") 

getTMACoreList().each {it.setName(it.getUniqueID())}
fireHierarchyUpdate()

int count = 0 
fileResults.withPrintWriter { 
    for(pathObject in annotations) { 
        def roi = pathObject.getROI() 
        def ID = PathObjectTools.getAncestorTMACore(pathObject)
        it.print(pathObject)
        it.print("\t")
        it.print(ID)
        it.print("\t")
        pts = roi.getAllPoints() 
        it.print(pts) 
        it.println() 
        count++ 
    } 
} 
 
print "Done! " + count + "results witten to " + fileResults.getAbsolutePath() 

thank you so much for help!

2 Likes