Cut an Annotation into two pieces. How to make two or more annotation objects from it?

1,073 views
Skip to first unread message

David Haumann

unread,
Nov 7, 2017, 10:43:04 AM11/7/17
to QuPath users
Hello all,

after simple tissue detection I face the task to cut the ROI into pieces and give each tissue piece a class. Originally I thought that will be easy.
But now I realized that also after cutting with the Alt+brush tool, it is still the same object and can have only one class.

Is there a way to make two objects from one annotation? And if not, can it be done by scripting?

At the moment I do it via menu "Objects" ==> "Dublicate Annotation". That is very cumbersome. Because then I need to manually  delete all parts of the roi that do not belong to the tissue area.

Best
David

Pete

unread,
Nov 7, 2017, 11:46:57 AM11/7/17
to QuPath users
If you unselect the 'Single annotation' checkbox in the 'Simple tissue detection' parameter dialog box, does that give you what you want immediately - without any splitting required?

David Haumann

unread,
Nov 7, 2017, 12:09:40 PM11/7/17
to QuPath users
Hi Pete,
no that is not what I mean. I deactivated this checkbox already.

But later I still need to cut one tissue piece into two ore more to become able to set different tissue classes for each region. It does not work by superpixel classification in this case. It need to be done manually.

After simple tissue detection, the slide looks like this:


Finally after manuall cutting and set different classes it shall look like this:


Best
David

Auto Generated Inline Image 1
Auto Generated Inline Image 2

Pete

unread,
Nov 7, 2017, 12:30:24 PM11/7/17
to QuPath users
You could try this:

import qupath.lib.roi.*
import qupath.lib.objects.*

def selected = getSelectedObject()
if (!(selected.getROI() instanceof AreaROI)) {
   
print 'Selected object does not have an AreaROI!'
   
return
}

// Try to do split, and ensure holes are taken into consideration
def polygons = PathROIToolsAwt.splitAreaToPolygons(selected.getROI())
def newPolygons = polygons[1].collect {
    updated
= it
   
for (hole in polygons[0])
        updated
= PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
   
return updated
}

// Remove original annotation, add new ones
annotations
= newPolygons.collect {new PathAnnotationObject(it)}
resetSelection
()
removeObject
(selected, true)
addObjects
(annotations)

It operates on the (one) annotation object currently selected.  I'd definitely recommend saving your data before running it... since it will remove the existing selected annotation after attempting the split.

David Haumann

unread,
Nov 7, 2017, 1:33:03 PM11/7/17
to QuPath users
Hi Pete,
thanks again for your always so fast reaction!

I tried it. Exactly with this code:

import qupath.lib.roi.*
import qupath.lib.objects.*
def selected = getSelectedObject();
if (!(selected.getROI() instanceof AreaROI)) {
    print 'Selected object does not have an AreaROI!'
    return
};
// Try to do split, and ensure holes are taken into consideration
def polygons = PathROIToolsAwt.splitAreaToPolygons(selected.getROI())
def newPolygons = polygons[1].collect {
    updated = it
    for (hole in polygons[0])
        updated = PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
    return updated
};
// Remove original annotation, add new ones
annotations = newPolygons.collect {new PathAnnotationObject(it)};
resetSelection();
removeObject(selected, true);
addObjects(annotations);


It generates this error message:

INFO: Starting script at Tue Nov 07 19:30:37 GMT+01:00 2017
ERROR: Error at line 5: Cannot invoke method getROI() on null object

ERROR: Script error
    at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:91)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:48)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:35)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
    at Script2.run(Script2.groovy:6)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:343)
    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:152)
    at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:765)
    at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:695)
    at qupath.lib.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:677)
    at qupath.lib.scripting.DefaultScriptEditor.access$400(DefaultScriptEditor.java:136)
    at qupath.lib.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1029)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

Best
David

micros...@gmail.com

unread,
Nov 7, 2017, 2:13:02 PM11/7/17
to QuPath users
It works perfectly for me, thanks!  I actually wished I could do this a couple times during the last year, so this could be very helpful.

I get the same error, David, when I run the script with nothing selected (highlighted), as this results in passing the function a "null object" as described in the error.

David Haumann

unread,
Nov 7, 2017, 5:01:03 PM11/7/17
to QuPath users
Hi Micros,

yes indeed!  That was my fault. Now it works for me as well!

The possibilies are so amazing with IntelliJ. I am really impressed about the divers things one can do.

Thank you, Micros and Pete, for your fast help again!

Best
David

David Haumann

unread,
Nov 7, 2017, 5:15:34 PM11/7/17
to QuPath users
oh I need to correct myself once more.
Actually the script works only if a activate one annotations manually by double-click.

But what would really reduce workload is to cut all ROIs into their areas and then run the script across the project in batch mode without selecting each ROI by hand.

I tried this script for it:

selectAnnotations();

import qupath.lib.roi.*
import qupath.lib.objects.*
def selected = getSelectedObject();
if (!(selected.getROI() instanceof AreaROI)) {
    print 'Selected object does not have an AreaROI!'
    return
};
// Try to do split, and ensure holes are taken into consideration
def polygons = PathROIToolsAwt.splitAreaToPolygons(selected.getROI())
def newPolygons = polygons[1].collect {
    updated = it
    for (hole in polygons[0])
        updated = PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
    return updated
};
// Remove original annotation, add new ones
annotations = newPolygons.collect {new PathAnnotationObject(it)};
resetSelection();
removeObject(selected, true);
addObjects(annotations);

But it generates the above mentioned error message.
Only manual selection helps.

Do you have an idea why is it so?

Pete

unread,
Nov 7, 2017, 5:29:08 PM11/7/17
to QuPath users
You'll need to use

def selectedObjects = getSelectedObjects();

(note the plural) and then modify the script to loop through them one by one.  Although in that case you might not need to bother with selecting them at all.  You could just use this to get all the potentially-splittable annotations:

def areaAnnotations = getAnnotationObjects().findAll {it.getROI() instanceof AreaROI}

Either way, you'll get a list of objects rather than one object.  So some kind of loop is needed.  The way to do it with minimal changes might be:

def areaAnnotations = getAnnotationObjects().findAll {it.getROI() instanceof AreaROI}
areaAnnotations
.each { selected ->
 
// ...All the other stuff...
}

where it uses Groovy's 'each' to do the looping.

David Haumann

unread,
Nov 10, 2017, 12:13:13 PM11/10/17
to QuPath users
Hello Pete,

thank you for your post. My understanding of the language is not good enough to finish it by myself.
We have the above two parts of the script now. I tried to fuse them but I dont know how to build a loop and how to go further after selected ->:

import qupath.lib.roi.*
import qupath.lib.objects.*
def selectedObjects = getSelectedObjects();

def areaAnnotations = getAnnotationObjects().findAll {it.getROI() instanceof AreaROI};
areaAnnotations.each { selected ->

//second part here. Above "Selected" is used but not defined yet, right?
def selected = getSelectedObject();
if (!(selected.getROI() instanceof AreaROI)) {
    print 'Selected object does not have an AreaROI!'
    return
};
// Try to do split, and ensure holes are taken into consideration
def polygons = PathROIToolsAwt.splitAreaToPolygons(selected.getROI())
def newPolygons = polygons[1].collect {
    updated = it
    for (hole in polygons[0])
        updated = PathROIToolsAwt.combineROIs(updated, hole, PathROIToolsAwt.CombineOp.SUBTRACT)
    return updated
};
// Remove original annotation, add new ones
annotations = newPolygons.collect {new PathAnnotationObject(it)};
resetSelection();
removeObject(selected, true);
addObjects(annotations);

Best and thanks a lot!
David

micros...@gmail.com

unread,
Nov 10, 2017, 1:33:06 PM11/10/17
to QuPath users
Combination of Pete's scripts that should split ALL annotations in an image.  I don't think I understand the hole detection code and the structure of the "polygon" list though.
import qupath.lib.roi.*
import qupath.lib.objects.*

def areaAnnotations = getAnnotationObjects().findAll {it.getROI() instanceof AreaROI}
areaAnnotations.each { selected ->

David Haumann

unread,
Nov 10, 2017, 6:37:58 PM11/10/17
to QuPath users
Hi Mike,

thanks a lot! It works well!

Cheers
David
Reply all
Reply to author
Forward
0 new messages