Importing ROI and make editable

Hello. I am using QuPath 0.2.0-m3

I was looking for a way to import annotations after exporting from QuPath, classifying outside and importing again to check and change in QuPath.

At first I had trouble creating the ROI, as I had the similar problem as here: Example of creating polygonROI

In the end, after reading the code directly on github, the code I came up with is the one below. The problem is that I can’t correct the annotations once they are created, I was hoping to be able to modify them inside QuPath by selecting them and clicking change class.

import qupath.lib.objects.PathObjects
import qupath.lib.roi.ROIs
import qupath.lib.regions.ImagePlane
import qupath.lib.io.GsonTools
import qupath.lib.geom.Point2

def plane = ImagePlane.getPlane(0, 0)

def gson=GsonTools.getInstance(true)
BufferedReader bufferedReader = new BufferedReader(new FileReader("myjsonfile.json"));
HashMap<String, String> myjson = gson.fromJson(bufferedReader, HashMap.class); 

xCoords = myjson["allx"]
yCoords = myjson["ally"]
classes_index = myjson["allc"]

c0 = getPathClass("Class0")
c1 = getPathClass("Class1")
c2 = getPathClass("Class2")
c3 = getPathClass("Class3")
c4 = getPathClass("Class4")
c5 = getPathClass("Class5")

classes=[c0,c1,c2,c3,c4,c5]

for (c=0; c < xCoords.size(); c++) {

    List<Point2> points = []
    
    def xarr= xCoords[c] as double[]
    def yarr= yCoords[c] as double[]
    
    for( i=0; i< xarr.size();i++){
        points.add(new Point2(xarr[i], yarr[i]));        
    }

    def cell_roi = ROIs.createPolygonROI(points, plane)
    def cell = PathObjects.createDetectionObject(cell_roi)
    addObject(cell)
    cell.setPathClass(classes[(int) classes_index[c]])
}

First, any hierarchy update results in an error. Second running my script manages to add the polygons with their correct classes but I get a popup with the log that appears as many times as I have classes. Then I also get an error like this one per annotation:

ERROR: QuPath exception
    at java.base/java.util.LinkedHashMap$LinkedHashIterator.nextNode(Unknown Source)
    at java.base/java.util.LinkedHashMap$LinkedKeyIterator.next(Unknown Source)
    at java.base/java.util.AbstractCollection.toArray(Unknown Source)
    at java.base/java.util.Collections$UnmodifiableCollection.toArray(Unknown Source)
    at qupath.lib.gui.panels.PathObjectHierarchyView$3.getChildren(PathObjectHierarchyView.java:480)
    at qupath.lib.gui.panels.PathObjectHierarchyView$3.isLeaf(PathObjectHierarchyView.java:491)
    at javafx.scene.control.TreeItem$4.invalidated(TreeItem.java:558)
    at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
    at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:145)
    at javafx.beans.property.BooleanProperty.setValue(BooleanProperty.java:81)
    at javafx.scene.control.TreeItem.setExpanded(TreeItem.java:539)
    at javafx.scene.control.TreeView.updateRootExpanded(TreeView.java:1090)
    at javafx.scene.control.TreeView$1.invalidated(TreeView.java:462)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at javafx.scene.control.TreeView.setRoot(TreeView.java:474)
    at qupath.lib.gui.panels.PathObjectHierarchyView.hierarchyChanged(PathObjectHierarchyView.java:510)
    at qupath.lib.gui.panels.PathObjectHierarchyView.lambda$hierarchyChanged$8(PathObjectHierarchyView.java:506)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
    at java.base/java.security.AccessController.doPrivileged(Unknown Source)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
    at java.base/java.lang.Thread.run(Unknown Source)

and it results in permanent objects whose class I can’t change in the interface.

Can you tell me what can I do? what am I missing in my ROI creation to make it selectable and editable in QuPath?

In the script you posted you are creating detection object by using createDetectionObject. Detection objects are not meant to be modified. If you want to modify the object after creating use createAnnotationObject. Annotation can be modified and later you can convert your annotations to detections if you’d like.

2 Likes

Another option, from the sounds of your workflow, would be to reclassify the objects that already exist within QuPath, rather than recreating them. Unless your annotations are frequently overlapping and have the same centroid coordinates, you can generally export objects/data structures, classify them or modify their measurements, then reimport data for those same objects based on the XY coordinates. That may be easier than recreating the objects.

That is essentially what is done in the CytoMAP scripts, the objects are exported and classified in some sort of cluster analysis, then the XY coordinates of each object are used to update that object’s class or measurement list.

Also, did you mean 0.2.0m3? I haven’t kept up, but not aware of a version of 0.2.3.

2 Likes

To create annotation objects you can use something like this:

// create the rois from x/y coordinates
def polyROI = ROIs.createPolygonROI(xCoords as double[], yCoords as double[], plane);
polyROIs.add(polyROI);

// ...
// create pathclass 
PathClass pc = PathClassFactory.getPathClass(annotation_name, annotation_color);

// ...
// create annotations for that class
//...
annotation = PathObjects.createAnnotationObject(polyROIs[j] as PolygonROI, pc);

// add to image, and reload it from File menu to see changes
imageData = entry.readImageData();
imageData.getHierarchy().addPathObjects(annotationList);
entry.saveImageData(imageData)
2 Likes

Yes this was it, I changed:

PathObjects.createDetectionObject(cell_roi)

for

PathObjects.createAnnotationObject(cell_roi)

Thanks!

1 Like
  • Yeah my mistake, 0.2.0 m3 I have several versions in my computer.
  • Yes this would be ideal, but would I have to compare always the X and the Y? I have and ID for my cell, wish I could use it in QuPath. I mean in one way X and Y can be a composite ID but since floating points are on the way, sometimes when importing and exporting, decimals are added.

  • Since I am observing something like 5k cells at a time it is not too bad, it doesn’t take too much to recreate. But for a bigger sample I will definitely do this.

Thank you @darshats!

For some reason

def polyROI = ROIs.createPolygonROI(xCoords as double[], yCoords as double[], plane);

Didn’t work for me, which is why I did that cumbersome point list :woman_shrugging:t2:

PathClass pc = PathClassFactory.getPathClass(annotation_name, annotation_color);

Yours looks better than my approach:

c0 = getPathClass("Class0")
c0color=  getColorRGB(41, 83, 185)
c0.setColor(c0color)

Also since I am using the internal script editor in QuPath

I can use addObject(cell) directly without calling imageData and Hierarchy. But I will try it your way to see if I don’t get errors.

Thank you!

For anyone looking for the full script here it is, also in my gist:

import qupath.lib.objects.PathObjects
import qupath.lib.roi.ROIs
import qupath.lib.regions.ImagePlane
import qupath.lib.io.GsonTools
import qupath.lib.geom.Point2
import qupath.lib.objects.classes.PathClass
import qupath.lib.objects.classes.PathClassFactory

def plane = ImagePlane.getPlane(0, 0)

//Dialogs didn't work so I just add the filename manually
def loc="/home/leslie/"

def gson=GsonTools.getInstance(true)
BufferedReader bufferedReader = new BufferedReader(new FileReader(loc+"file.json"));
HashMap<String, String> myjson = gson.fromJson(bufferedReader, HashMap.class); 

def classmap=["Class0":[41, 83, 185] ,"Class1":[255, 127, 14] ,"Class2":[234, 134, 213] ,
"Class3":[234, 35, 37] ,"Class4":[148, 103, 189] ,"Class5":[38, 153, 177]]

pathClasses=[]

for(c in classmap){
    v=c.value;k=c.key;
    pathClasses.add(PathClassFactory.getPathClass(k, getColorRGB(v[0],v[1],v[2])));
}

xCoords = myjson["allx"]; yCoords = myjson["ally"]
classes_index = myjson["allc"]

//this should be empty , I just call it to get the right data type for the list
annotations = getAnnotationObjects()

for (c=0; c < xCoords.size(); c++) {
    //copy points because (double[], double[], plane) didn't work
    List<Point2> points = []    
    def xarr= xCoords[c] as double[]
    def yarr= yCoords[c] as double[]
    
    for( i=0; i< xarr.size();i++){
        points.add(new Point2(xarr[i], yarr[i]));        
    }

    def cell_roi = ROIs.createPolygonROI(points, plane)
    def cell = PathObjects.createAnnotationObject(cell_roi)
    annotations.add(cell)
    cell.setPathClass(pathClasses[ classes_index[c] ])
}

def viewer = getCurrentViewer()
def imageData = viewer.getImageData()
def hierarchy = imageData.getHierarchy()
hierarchy.getRootObject().addPathObjects(annotations);
hierarchy.fireHierarchyChangedEvent(this)

print "Done"

And this one doesn’t show any errors and works very fast!

2 Likes

I agree, I would normally use an objectID (as I have a few other places on the forum), but XY are more generalizable since they are “always there” and you don’t have to trust that the user will add the object ID or call it the exact same thing.

In the example mentioned above, the XY coordinates are truncated to… oh, I forget, some smaller number of significant digits to account for most of that.