Delete Annotations of Class 1 of it does NOT contain another Annotation of Class 2

548 views
Skip to first unread message

David

unread,
May 10, 2018, 11:56:54 AM5/10/18
to QuPath users
Hi,

is it possible to selectively delete annotations of class 1, only if they do not contain another annotation of class 2?

I detect Regions of interest with large Superpixels. Then I convert them into an annotation with class 1. Creat small superpixels within the class 1 annotations to excatly detect the objects of interest with correct size.

Not it happens, that there is no object in the class 1 annotation and it goes as false positive ROI into the statistics.

If there is a solution with scripting, it will help a lot!

Best and thanks for any help.

David

David

unread,
May 10, 2018, 12:04:20 PM5/10/18
to QuPath users
typing error in the title: it is an "if" instead of "of"

micros...@gmail.com

unread,
May 10, 2018, 1:05:13 PM5/10/18
to QuPath users
Another way to handle this would be with the classification.  The false positive usually happens when an entry does not exist, so by adding the particular entry to all of your annotations (look up some putMeasurement() examples) with a value of 0 might avoid the classification error.

Are you using Version 0.1.3?

David

unread,
May 11, 2018, 5:39:51 AM5/11/18
to QuPath users
Hi Mike,

I still use Version Version: 0.1.2

Version 0.1.3 is Pete´s new fork?

How do I integrate the upMeasurement() thing into a script?

micros...@gmail.com

unread,
May 11, 2018, 10:27:27 AM5/11/18
to QuPath users
Yes, it is available from one of his blog posts and can be accessed through Gradle. One of the standard annotation values is how many sub-annotations it has.  It also lists the current parent annotation as another value.

Code
https://groups.google.com/forum/#!searchin/qupath-users/putmeasurement%7Csort:date/qupath-users/44HBd-5nbaQ/QJvK-3xPBAAJ

Note the extra spaces in the code (comment below), there are a lot of other examples if you search for putMeasurement.  It sounds like you will want to add the measurement to all parent annotations.  More scripts:
https://gist.github.com/Svidro/68dd668af64ad91b2f76022015dd8a45

David

unread,
May 11, 2018, 4:36:58 PM5/11/18
to QuPath users
do I understand it right, that this step add a parameter to the measurement list? / Annotation results list?

So I could train a classifier and classify Class 1 ROIs without Class 2 ROIs inside in a different way?

Acutally I finished my workflow and only want to get rid of empty "ROIs". I searched for Events inside ROIs and if QuPath did not find an event, I want to delete the ROI as well.

micros...@gmail.com

unread,
May 11, 2018, 7:30:18 PM5/11/18
to QuPath users
Oh, I thought you were actually classifying the ROIs to see which were empty and which were not.  I think I get it now.

You want something more like (and this is going to be rough, from memory):
annotations = getAnnotationObjects()
annotations
.each{
   
if (!it.getChildObjects())  {
         removeObject
(it,true)
   
}
}
I am a little hazy on the removeObject part, but basically you are getting all of the child objects, and checking if that list exists.  You may have to do something more like
it.getChildObjects().length >0
or something similar for your if statement, I am not sure.

Pete

unread,
May 12, 2018, 3:25:18 AM5/12/18
to QuPath users
Based on the original description, I'd use this:

// Define classes
class1
= getPathClass('class1')
class2
= getPathClass('class2')


// Get all the class1 annotations that don't contain a class2 object as a direct child
toRemove
= getAnnotationObjects().findAll {
   
if (it.getPathClass() != class1)
       
return false
    children
= it.getChildObjects()
   
return !children.any {it.getPathClass() == class2}
}


// Remove annotations meaning that criteria
removeObjects
(toRemove, true)

But yes, if 'emptiness' is the criteria then just checking the getChildObjects() is empty is probably better.

I'd just recommend bringing together all the objects you want to remove in one single list first (as I did with findAll) and removing them all in one go.

Removing the objects one at a time will involve triggering hierarchy (and user interface) updates for every object that is removed.  At best, this could reduce performance a bit unnecessarily.  At worst, it might result in concurrent modification exceptions if you end up removing objects from a list that is currently being used elsewhere... at least when running interactively (because the GUI will be handling the updates on a different thread).  I haven't tested it so don't know for sure if it happens... but better avoid it anyway.

In general it's better to modify the object hierarchy fewer times in bigger ways, both for performance and stability.

David

unread,
May 13, 2018, 7:16:20 AM5/13/18
to QuPath users
Hi Pete, Hi Mike,

both scripts work. Thanks a lot. Mike´s script will also remove empty annotations of all classes, which is not desired in this case, but might be usefull later on.

Pete´s script is excatly doing what we needed! Thank you.
Concerining the hierarchie and performance. Yes I observed that it is better to have the annotation result list being closed while removing the annotations.

Can the performance problem or any problems be avoided by using the "fire hierarchy update" command in the script?



David

unread,
May 13, 2018, 7:17:30 AM5/13/18
to QuPath users
I we want to have an entry in the annotation measurements list that shows how many class 2 annotations are in each class 1 annotation, we can achieve that eith the "putMeasurement()" command?

Pete

unread,
May 13, 2018, 7:25:12 AM5/13/18
to QuPath users
Can the performance problem or any problems be avoided by using the "fire hierarchy update" command in the script?

I'm afraid not - hierarchy updates will always be triggered when removing objects in this way (which is why you don't need to run that line in the script separately).  But the update made here will be very directed; components will be notified that specific objects were removed, but they won't be expecting any other changes (e.g. objects added, classifications changed).

Therefore if you do make any other changes you should also call the fireHierarchyUpdate() line in the script; that line basically says 'something changed in the hierarchy, so everything that depends on it needs to update'.  For all the various components that relate to the hierarchy to update their status takes a bit of time.

Performance should be better if running the script in batch mode (Run for project); it's the interactive updates needing to be reflected in the user interface that cause the biggest problems.  Especially the 'tree view' display under the 'Hierarchy' tab; synchronizing that when there's a lot of objects in the hierarchy can be slow.

Pete

unread,
May 13, 2018, 7:42:40 AM5/13/18
to QuPath users
I we want to have an entry in the annotation measurements list that shows how many class 2 annotations are in each class 1 annotation, we can achieve that eith the "putMeasurement()" command?

Yes, but I don't usually like doing that because if the number of annotations changes then the measurement in the measurement list won't update automatically.  So if you have a script to add that measurement you always need to run it at the end... in case it doesn't reflect the current status. 

Where necessary, I like to add such scripts as part of the export and then use Run -> Run for project (without save).  Then the results are calculated, exported but not saved in the .qpdata file.

Anyway, here's how it might look:
// Define classes
class1
= getPathClass('class1')
class2
= getPathClass('class2')


// Loop through and add child/descendant counts
hierarchy
= getCurrentHierarchy()
getAnnotationObjects
().each {

   
if (it.getPathClass() != class1)
       
return false

   
def children = it.getChildObjects()
   
def nClass2Children = children.count {it.getPathClass() == class2}
   
def descendants = hierarchy.getDescendantObjects(it, null, null)
   
def nClass2Descendants = descendants.count {it.getPathClass() == class2}
    it
.getMeasurementList().putMeasurement('Number of ' + class2 + ' child objects', nClass2Children)
    it
.getMeasurementList().putMeasurement('Number of ' + class2 + ' descendant objects', nClass2Descendants)
    it
.getMeasurementList().closeList()
}


Note that there is a difference between child objects (directly below the parent) and descendant objects (which might be children, or children of children, or children of children of children...).

Also, this should give the counts of all children/descendants of the annotations with the specified class - it doesn't check for annotations specifically, and will also count detections.
Message has been deleted

David

unread,
May 13, 2018, 11:17:01 AM5/13/18
to QuPath users
Hi Pete,

I integrated the delete empty annotation step into the large script now.
It works, but causes a QuPath Exception in some images. I could not figure out why.
Using print("Checkpoint 1,2,3,...") I was able to find out, that the line of code producing the exception was a sleep thread:
Thread.sleep(100);

Now the weird thing: The sleep thread causes only an excption, if the remove empty annotations part is included in the script. Even if the remove emtpy annotations part comes in a later step of the script than the sleep thread. 

Any idea how this can be?  Some lines of code, coming later in the script, changing the behavior of a sleep thread in an earlier place of the script?

I could solve the issue by just removing the sleep thread - with no consequences till now.
By the way, the sleep thread does not cause the exception, if I put "print("Checkpoint 1) " behind it. Normally it is followed by "selectObjects { p -> p.getPathClass() == getPathClass("Other") };"
Also a surprising outcome ^-^

Here the exception:

ERROR: QuPath exception
    at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.subList(ReadOnlyUnbackedObservableList.java:136)
    at javafx.collections.ListChangeListener$Change.getAddedSubList(ListChangeListener.java:242)
    at com.sun.javafx.scene.control.behavior.ListViewBehavior.lambda$new$177(ListViewBehavior.java:269)
    at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
    at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
    at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
    at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.callObservers(ReadOnlyUnbackedObservableList.java:75)
    at javafx.scene.control.MultipleSelectionModelBase.selectIndices(MultipleSelectionModelBase.java:529)
    at qupath.lib.gui.panels.PathAnnotationPanel.selectedPathObjectChanged(PathAnnotationPanel.java:762)
    at qupath.lib.gui.panels.PathAnnotationPanel.lambda$selectedPathObjectChanged$15(PathAnnotationPanel.java:704)
    at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)

Pete

unread,
May 13, 2018, 11:53:31 AM5/13/18
to QuPath users
The exception is from one of the user interface components trying to synchronize the status of the selected objects.

I made a couple of changes to that method in the snapshot version v0.1.3 (see blog), so it might already be addressed there.

But I suspect it will not be possible to fully eliminate all these problems with the current design.  I hadn't envisaged scripts of this level of complexity would be run in interactive mode; it might be necessary to introduce a new way of doing things to avoid trouble (like with ImageJ's 'batch mode' that can be used for macros).  In the meantime you can try v0.1.3, experiment with guiscript=true and try running your script in batch mode (Run for project) without the current image being open, to try to work around any lingering issue like this not already fixed in v0.1.3.

micros...@gmail.com

unread,
May 13, 2018, 4:28:47 PM5/13/18
to QuPath users
Ah, selection is still something that is best avoided, I think.  Reminded me of : https://github.com/qupath/qupath/issues/129

And for myself, I do need to work on not using for loops so much.  It is just the first thing my coding brain reaches for when I want to do something a bunch of times. 
Reply all
Reply to author
Forward
0 new messages