Export, import, re-export annotation question

I think this is a quick question and I’m just missing something. I’ve pieced together various pieces of code but can’t quite get the right behavior

I begin by loading a new image, and running a cell detection

I then save those annotations to a gson/json file using:

def annotations = getDetectionObjects()
boolean prettyPrint = false
def gson = GsonTools.getInstance(prettyPrint)
println gson.toJson(annotations)

File file = new File('d:/testsmall.json')
file.withWriter('UTF-8') {
    gson.toJson(annotations,it)
}

This works as expected

Later on, I reload the image and try to reload the exported cells:

def gson = GsonTools.getInstance(true)
def json = new File('d:/testsmall.json').text
println json


// Read the annotations
def type = new com.google.gson.reflect.TypeToken<List<qupath.lib.objects.PathObject>>() {}.getType()
def deserializedAnnotations = gson.fromJson(json, type)

// Set the annotations to have a different name (so we can identify them) & add to the current image
deserializedAnnotations.eachWithIndex {annotation, i -> annotation.setName('New annotation ' + (i+1))}
addObjects(deserializedAnnotations)   

resolveHierarchy() 

This in essence works, in the sense that the cells are visible in the UI and are listed in the “hierarchy window”

Next I try to re-export these imported objects using the same code as above. This fails as apparently these imported objects are not considered “Detections”

def annotations = getDetectionObjects()
boolean prettyPrint = false
def gson = GsonTools.getInstance(prettyPrint)
println gson.toJson(annotations)

now returns an error:

ERROR: NullPointerException at line 4: null

ERROR: qupath.lib.io.PathObjectTypeAdapters$PathObjectTypeAdapter.write(PathObjectTypeAdapters.java:232)
qupath.lib.io.PathObjectTypeAdapters$PathObjectTypeAdapter.write(PathObjectTypeAdapters.java:190)
com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:97)
com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)

Changing “getDetectedObjects” to “getAnnotationObjects” doesn’t work either, as these imported objects don’t appear to be annotations either

I selected one of the cells and used getSelectedObject and then used getClass, to find that these objects are of type “qupath.lib.objects.PathCellObject”

I then tried to use:

def annotations = getCellObjects()
boolean prettyPrint = false
def gson = GsonTools.getInstance(prettyPrint)
println annotations.getClass()

println gson.toJson(annotations)

which grabs the objects successfully, but then crashes because the toJson component returns null:

INFO: class java.util.ArrayList
INFO: [New annotation 1, New annotation 2, New annotation 3, New annotation 4, New annotation 5, New annotation 6, New annotation 7, New annotation 8, New annotation 9, New annotation 10, New annotation 11, New annotation 12, New annotation 13, New annotation 14, New annotation 15, New annotation 16, New annotation 17, New annotation 18, New annotation 19, New annotation 20, New annotation 21, New annotation 22, New annotation 23, New annotation 24, New annotation 25, New annotation 26, New annotation 27, New annotation 28, New annotation 29, New annotation 30, New annotation 31, New annotation 32, New annotation 33, New annotation 34, New annotation 35, New annotation 36, New annotation 37, New annotation 38, New annotation 39, New annotation 40, New annotation 41, New annotation 42, New annotation 43, New annotation 44, New annotation 45, New annotation 46, New annotation 47, New annotation 48, New annotation 49, New annotation 50, New annotation 51, New annotation 52, New annotation 53, New annotation 54, New annotation 55, New annotation 56, New annotation 57, New annotation 58, New annotation 59, New annotation 60, New annotation 61, New annotation 62, New annotation 63, New annotation 64, New annotation 65, New annotation 66, New annotation 67, New annotation 68, New annotation 69, New annotation 70, New annotation 71, New annotation 72, New annotation 73, New annotation 74, New annotation 75, New annotation 76, New annotation 77, New annotation 78, New annotation 79, New annotation 80, New annotation 81, New annotation 82, New annotation 83, New annotation 84, New annotation 85]
ERROR: NullPointerException at line 7: null

ERROR: qupath.lib.io.PathObjectTypeAdapters$PathObjectTypeAdapter.write(PathObjectTypeAdapters.java:232)
    qupath.lib.io.PathObjectTypeAdapters$PathObjectTypeAdapter.write(PathObjectTypeAdapters.java:190)
    com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
    com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:97)
    com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)
    com.google.gson.Gson.toJson(Gson.java:704)
    com.google.gson.Gson.toJson(Gson.java:683)
    com.google.gson.Gson.toJson(Gson.java:638)
    com.google.gson.Gson.toJson(Gson.java:618)

Ultimately, I think what I’m looking for is not a fix for the second export operation, but an improvement to the import operation, which results in the annotations being imported into the same type of objects/hierarchy that they immediately after the cell detection.

I feel like its looking me right in the face, but I can’t see it

Any ideas would greatly be appreciated!

Aha, the only thing you’re missing is that you’ve discovered an obscure bug :slight_smile:

Basically, deserialization from JSON fails if your object has a name and not a color (i.e. the color is null). Part of the reason this is obscure is that it is very difficult to achieve this state normally… since if you set the properties of an annotation through the UI, this involves setting both the name and color both. Thus I thought/hoped no one would notice.

I found it a couple of weeks ago myself and fixed it here, but it isn’t yet in a release: https://github.com/qupath/qupath/pull/610/commits/9766771a699767d01977dbc8e865d8a7c22b4099

Workarounds for now include:

  • Make the tiny change from the commit locally & rebuild
  • Build from the dev branch (but you’ll get a few more differences as we work towards v0.3)
  • Avoid setting names (e.g. use the PathClass instead, if possible)
  • Set any color to your objects before JSON serialization, e.g.
def defaultColor = getColorRGB(200, 0, 0)
getAnnotationObjects().each {
   if (it.getColorRGB() == null) {
     def newColor = it.getPathClass() == null ? defaultColor : it.getPathClass().getColor()
     it.setColorRGB(newColor)
  }
}
fireHierarchyUpdate()

That script should set the color to match the current classification (if available) or a default, if needed.

1 Like

Perfect! That works great, thanks Pete!

Out of curiority, i looked around in the documentation and saw that one can easily write out things in WKT and WKB, but it doesn’t look like these are “naturally” ingested by QuPath, is that correct? I suspect the issue is that they’re only describing the shape and not the other needed compnents such as class-type and tags like geometry?

The file size differences are (obviously) quite significant.

For others to note, to save space in the output, you can also remove the measurements, this is what my code ended up looking like

// --- remove measurements, not needed but makes file smaller
Set annotationMeasurements = []

getDetectionObjects().each{it.getMeasurementList().getMeasurementNames().each{annotationMeasurements << it}}
println(annotationMeasurements)

annotationMeasurements.each{ removeMeasurements(qupath.lib.objects.PathCellObject, it);}


// write to file
boolean prettyPrint = false // false results in smaller file sizes and thus faster loading times, at the cost of nice formating
def gson = GsonTools.getInstance(prettyPrint)
def annotations = getDetectionObjects()
//println gson.toJson(annotations) // you can check here but this may be HUGE and take a long time to display


File file = new File('d:/testsmall_color.json')
file.withWriter('UTF-8') {
    gson.toJson(annotations,it)
}

Yes, these would only represent the ROI (defining a 2D region) – not the entire object (which can include other properties, such as name, measurements, classification).

More exactly, they’d encode a Java Topology Suite Geometry. If you have a Geometry, you can create a QuPath ROI from this, and subsequently an object (detection or annotation) using something like:

def roi = GeometryTools.geometryToROI(geometry, ImagePlane.getDefaultPlane())
def detection = PathObjects.createDetectionObject(roi)
addObject(detection)

However cells are more awkward, since they are the only QuPath object that has two associated ROIs… therefore would require two geometries.