Export cell boundaries

Hi guys, I am using the ‘Cell detection’ command in QuPath to detect cells from IHC images. As I am interested in single-cell morphometrics, I would like to export the coordinate of cell boundaries (polygons) and then analyze the shape features (in Matlab, Python, or R). I’ve seen this post: https://github.com/qupath/qupath/issues/70 and I’ve successfully exported the ROI with overlay to the imageJ, but I am not sure what is the next step to export the coordinates, looks like imageJ can export the centroid of each cell, just as QuPath, but can not export the coordinate of the polygon.

Can anyone please help me? Thank you!

(The ROI with boundaries is shown here:

1 Like

There are plenty of discussions regarding the export of the coordinates in J/GeoSON coordinates, and you can find more information here: https://qupath.readthedocs.io/en/latest/docs/advanced/exporting_annotations.html

Having said that, I am already worried that you are doing any analysis of the shape of cell objects from QuPath. Nuclear shapes might be fine, but the cell expansion is blind, and while it is based on the nuclear shape, you can see for yourself that the “cells” that border white space are extending well out into that white space.

Thank you for your reply! I looked into the link you sent, looks like it aims to export annotations, rather than each cell, into coordinates. Am I right?

Also, thank you for pointing out the validity to study the cell boundaries, actually what I care about is the nucleus boundary, not the cell boundary.

Yes, but annotations and cell objects and nuclei are all ROIs. In the very first script, for instance, you might replace
def annotations = getAnnotationObjects()
with
def annotations = getDetectionObjects()
Of course, you could rename the variable too. getDetectionObjects would let you create cells with 0 Cell expansion during the previous step, which would just be nuclei, and use those ROI/sets of coordinates. Alternatively, you could cycle through all cells and get the nuclear ROI, in case you wanted to keep the cytoplasm for some other part of the analysis.

I think annotations are simply the most common things to export since they are created manually, and so you can have the most control.

Thank you! Sorry for asking another newbie question, after running the script, where should I go to find the exported data?

Those scripts just show you how to get the data, you can save it however you want in Groovy. It really depends on how you want to use the data in downstream processes. Find a format that you can access and find out how to save it to that in Groovy.

Usually objects need to be more complicated than a string of coordinates due to things like holes, or having separate pieces, so a list won’t always do it. The example in the link uses WellKnownText.

Another link that might be useful that discusses JSON is


Almost all of these show you how to get access to the object, be it a Geometry or JSON or whatever, then print it out so you can see it. Saving it is mostly using some sort of file writer.

There’s a lot of information if you look around the forum.

Thank you! I will try it out!

1 Like

Sorry to interupt again. In my project, ‘getAnnotationObjects()’ works fine to extract annnotations and export to json file. However, ‘getDetectionObjects()’ command returns nothing (no errors). I cannot save them to file and cannot even print them out. However, I do have detected objects (see attached image). Where did I do wrong?

You would need to share your script.

2 Likes

Here is my script:

import java.io.File
def annotations = getDetectionObjects()
boolean prettyPrint = true
def gson = GsonTools.getInstance(prettyPrint)
Json_file = gson.toJson(annotations)
File file = new File(’/Users/mihaoyang/Desktop/detection_coordinate.json’)
file.write(Json_file)

I can export the annotations, but cannot export detections. ‘print(getDetectionObjects().size())’ is working. I guess the issue is I have too many cells. Maybe I need to export core by core, but I don’t sure how to write the code.

@HAOYANG_MI how many detections do you have?

JSON is ‘wordy’ and Java Strings have a fixed length in terms of characters. Serializing the QuPath objects will also include other data (classifications, measurements). Choosing to pretty print will make them even longer. It’s quite possible your export exceeds the limit.

If you really only care about the ROIs, this should be a lot more concise:

def detections = getDetectionObjects()
def gson = GsonTools.getInstance(false)
def rois = detections.collect {it.getROI()}
println gson.toJson(rois)

(…except you’ll want to write to a file rather than print).

2 Likes

Thank you for your reply!
Yes I would also assume the export is too large… I have 1,303,330 detections and I do want to write to file rather than print. I am thinking export core by core (it’s TMA data set), is it possible? Or could you please suggest alternative solutions?

1 Like

And if you want to cycle through the cores, I think the code structure like this still works:

hierarchy = getCurrentHierarchy()

hierarchy.getTMAGrid().getTMACoreList().each{
    coreName = it.getName()
    cells = it.getChildObjects().findAll{it.isDetection()}
    //Code to write out json file
    }
}

In case the text is still too long and you want to break it up. Actual working code may vary depending on your version of QuPath, though if you are doing JSON export I imagine it is rather recent. You would want to use coreName as part of the file name when writing out the file.

*I suspect this could be better written as it might pull in subcellular detections or other things like that. Maybe it.isCell() ?

1 Like

1,303,330 is rather a lot…

I think a combination of the last script from @Research_Associate and mine (if you can limit to ROIs) should be substantially better.

Also, you don’t need to go via a String - you can write directly to a file. I haven’t tested it, but something like this:

file.withWriter('UTF-8') {
    gson.toJson(rois, it)
}

That should help overcome the String length limit as well.

Alternatively, for such a large number of regions, Well-known binary might be preferable to GeoJSON:
https://qupath.readthedocs.io/en/latest/docs/advanced/exporting_annotations.html#well-structured-binary
But it really depends upon whether you have some code ‘on the other side’ that can make sense of WKB…

Finally, I don’t know what shape features you need - but QuPath can already generate some. See Analyze → Calculate features → Add shape features (rewritten for v0.2.0-m12).

If possible, I’d try to do the shape features in QuPath and avoid having to export the coordinates at all…

2 Likes

That reminds me, @HAOYANG_MI, I was curious what kinds of shape features you were interested in!

@petebankhead @Research_Associate The reason I want to export the coordinates of cell polygon is that I am interested in nucleus orientations , which should be inferred from the cell shape. And I also want to do cell phenotyping. Therefore I need to get cell boundaries and write algorithms to decide which signals fall within the polygon to decide the cell phenotype.

What’s the difference between export ROIs and Detections? Since I also do classifications, I wonder if export ROIs will miss that information.

I already designed a pipeline to process JSON file. So that I prefer if JSON file will finally work. I assume export core by core should be fine.

@HAOYANG_MI yes, you’ll lose the classification if you export the ROI only:
https://qupath.readthedocs.io/en/latest/docs/concepts/objects.html

As you’ve seen, you can script QuPath, so in principle you could do your calculations to compute orientation etc. there, but I totally understand you may have code/libraries outside QuPath/Java that you’d prefer to use - and so exporting the ROIs makes complete sense.

Thank you! That’s good know! The fact is I am really a newbie in terms of Groovy/Java programming. Actually I even don’t know how many classes and functions there are, how to access the coordinate, etc. Is there any material that I can refer to?

There’s lots of information in the docs, but lots more we haven’t had time to write up. A good starting point from the coding side is https://qupath.readthedocs.io/en/latest/docs/scripting/overview.html

1 Like

One thing you might consider then is using the ImageJ function for the Feret angle.


https://imagej.nih.gov/ij/docs/menus/analyze.html

Feret’s Diameter - The longest distance between any two points along the selection boundary, also known as maximum caliper. Uses the Feret heading. FeretAngle (0-180 degrees) is the angle between the Feret’s diameter and a line parallel to the x-axis of the image. MinFeret is the minimum caliper diameter. The starting coordinates of the Feret’s diameter ( FeretX and FeretY ) are also displayed. The DrawFeretDiameter macro draws the Feret’s diameter of the current selection.

Which can be applied in QuPath using a macro like:
https://gist.github.com/Svidro/68dd668af64ad91b2f76022015dd8a45#file-angles-for-cells-groovy

You can also get access to other ImageJ type Measurements through similar means, or other Feret values using positions in the list other than .getFeretValues()[1]. Make sure you read about what the angle actually means, as the fact that it is relative to the X axis means that almost horizontal cells can have angles of either ~0 or ~180, which can make analysis… weird.

//from Pete

import qupath.imagej.objects.*

getCellObjects().each {
    def ml = it.getMeasurementList()
    def roi = it.getNucleusROI()
    //for 0.1.2
    //def roiIJ = ROIConverterIJ.convertToIJRoi(roi, 0, 0, 1)
    roiIJ = IJTools.convertToIJRoi(roi, 0, 0, 1)
    def angle = roiIJ.getFeretValues()[1]
    ml.putMeasurement('Nucleus angle', angle)
    ml.close()
}
fireHierarchyUpdate()
print "done"

LuCa image with some arrows showing similarly aligned cell areas being the same color.

The results are also very heavily dependent on accurate nuclear segmentation, and in this case using StarDist for the nuclear segmentation was much more accurate (not shown, image above is normal Cell Detection segmentation).

1 Like