Cluster mean centroids incorrect in qupath 0.2.1?


I am updating some image analysis scripts to use QuPath 0.2.1 (and will eventually move up to the latest 0.2.2 or later) and have run into some errors with the Delaunay clustering output.

Analysis goals

I’m using the cluster mean measurements for the Centroid X and Y of the cluster.


The main challenge is the X and Y appear to be incorrect - clustered in the center of the image, and they don’t appear to correspond with the classified cells they are attached to.

  • What have you tried already?
    In the older version of QuPath (0.1.2) these XY values appear correct - that is, if you have a cluster of 2 cells, the centroid XY is perfectly in between the two items.

Ive been looking at differences between px and uM but scaling doesn’t appear to be the problem.

Has anyone else run into this? Is it a known issue?


I don’t see any of the scripts themselves, so it doesn’t seem possible to judge where your XY coordinates are coming from. As you seem to have already noted, getting the measurement from the measurement list vs the centroid of the ROI will result in different numbers.

The Delaunay features themselves do not seem to include these types of measurements. Are you averaging the XY coordinates from each member of a cluster yourself? Or are you accessing the Delaunay clusters themselves as objects?

I also don’t understand the question. The cluster measurements obtained via Delaunay cluster 2D don’t include centroid measurements, and aren’t intended to.

Can you describe your process step-by-step, including any scripts involved?

Sorry, of course. Apologies for not attaching the info.

In 0.1.2, the script (below) does the following:

  • sets the stain vectors for an H&E image
  • runs watershed cell detection on a 40x H&E image
  • runs a legacy object classifier
  • runs the qupath.opencv.features.DelaunayClusteringPlugin to add additional measurements, and this where I assumed Cluster Mean: Centroid X and Cluster Mean: Centroid Y were added
  • outputs the measurements to a file

In 0.2.1, I had to update the cell detection script (now asking for micron measurements) but the core of the script appeared to be the same

The data I extracted looks like the following for 0.1:

Class	Centroid X	Centroid Y	Cluster mean: Cluster mean:   Cluster size   Cluster mean:   Cluster mean:.   
                                Centroid X	  Centroid Y                     Delaunay:       Delaunay: 
                                                                             Mean triangle   Max triangle
                                                                             area            area
Tumor	2.1126	    1014.1837	2.1126	    1014.1837	      1	             NaN	         NaN
Tumor	3.1126	    968.1396	28.9001	    952.2041	      6	            NaN	         NaN
Tumor	3.1591	    939.9977	28.9001   	952.2041.         6	            NaN	         NaN
Tumor	3.2385	    4876.4502	3.2385	    4876.4502	      1	            NaN	         NaN
Immune	3.4258	    3988.0852	3.4258	    3988.0852	      1	            NaN	         NaN
Other	4984.9805	864.7894	4972.6846	867.4989	      2	            NaN	         NaN
Tumor	4985.2031	2887.9033	4985.2031	2887.9033	      1	            NaN	         NaN
Tumor	4985.5864	902.7181	4970.3271	910.4127	      2	            NaN	         NaN
Tumor	4986.333	2946.6963	4981.623	2963.2764	      2	            NaN	         NaN
Other	4987.2432	1318.7078	4964.9033	1308.3369	      3	            NaN	         NaN
Tumor	4987.6245	706.0053	4987.6245	706.0053	      1	            NaN	         NaN

and for 0.2:

Class	Centroid X	Centroid Y	Cluster mean: Cluster mean:   Cluster size   Cluster mean:   Cluster mean:.   
                                Centroid X	  Centroid Y                     Delaunay:       Delaunay: 
                                                                             Mean triangle   Max triangle
                                                                             area            area
Immune	4.3296	    303.1197	2073.9075	2425.1501	      1	           27.0159	           27.3722
Immune	4.3666	    1738.4387	2108.3594	2439.7048	      1	           26.5655	           26.9005
Tumor	4.7627	    236.5061	2037.7327	2395.6453	      2	           27.3736	           27.7156
Tumor	4.8758	    4090.6487	1989.6343	2385.6663	      7	           27.8381	           28.172
Other	4979.9585	957.9927	2060.0403	2412.5054	     11	           27.4378	           27.7913
Other	4981.0166	743.3763	1866.7214	1955.0779	      2	           29.8036	           29.8036
Tumor	4983.0947	1414.9407	1882.735	2343.3923	      1	           27.5031	           27.8053
Other	4985.252	864.946	    2072.6904	2428.012	      2	           27.0159             27.3722
Tumor	4986.0503	903.4723	2106.2058	2439.2739	      1	           26.5655	           26.9005
Tumor	4986.5107	2946.7944	2078.6411	2425.9231	      1	           26.8938	           27.244

There appears to be a strong correlation to CentroidX/Y and Cluster mean: CentroidX/Y in the 0.1 results (I charted them and they overlay perfectly, and when there are cell clusters of 2 or more cells, the centroids land in the center of them)

In the 0.2 results however, they have a pattern, but are centered in the results which is confusing - it doesn’t appear to be a simple scaling px/micron issue.

The two scripts are reproduced below - does this provide more info? If the Cluster Mean fields don’t come from the Delaunay output, is there another plugin being run that I am not aware of?

Thanks for the quick reply, I appreciate the help!

0.1.2 script


setColorDeconvolutionStains('{"Name" : "H&E", "Stain 1" : "Hematoxylin", "Values 1" : "0.51346 0.77067 0.37739 ", "Stain 2" : "Eosin", "Values 2" : "0.21093 0.93879 0.27238 ", "Background" : " 221 214 220 "}');

runPlugin('qupath.imagej.detect.nuclei.WatershedCellDetection', '{"detectionImageBrightfield": "Optical density sum",  "backgroundRadius": 25.0,  "medianRadius": 3.0,  "sigma": 10.0,  "minArea": 90.0,  "maxArea": 1600.0,  "threshold": 0.2,  "maxBackground": 0.0,  "watershedPostProcess": true,  "cellExpansion": 7.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true}');



runPlugin('qupath.opencv.features.DelaunayClusteringPlugin', '{"distanceThreshold": 40.0,  "limitByClass": true,  "addClusterMeasurements": true}');

saveDetectionMeasurements('/Users/xxxxxx/xxxxxxx/', );

0.2.1 script


setColorDeconvolutionStains('{"Name" : "H&E", "Stain 1" : "Hematoxylin", "Values 1" : "0.51346 0.77067 0.37739 ", "Stain 2" : "Eosin", "Values 2" : "0.21093 0.93879 0.27238 ", "Background" : " 221 214 220 "}');


runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImageBrightfield": "Optical density sum",  "requestedPixelSizeMicrons": 0.25,  "backgroundRadiusMicrons": 6.0,  "medianRadiusMicrons": 0.75,  "sigmaMicrons": 2.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 150.0,  "threshold": 0.2,  "maxBackground": 0.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 1.5,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true}');

runPlugin('qupath.opencv.features.DelaunayClusteringPlugin', '{"distanceThresholdMicrons": 10.0,  "limitByClass": true,  "addClusterMeasurements": true}');

1 Like

Ah, all I can say is that in my versions of QuPath, neither 0.1.2 nor 0.2.1 has those two Delaunay measurements, and I don’t see them being added in the script, so I am guessing that someone, at some point, modified your copy of QuPath or included an extra plugin.

Did you download and install QuPath yourself?

Both copies were downloaded from GitHub - there was a portion of the script that at first I did not think was responsible but now that I’ve looked through the sources I think is probably part of the equation:

int n = 0
for (detection in getDetectionObjects()) {

    def roi = detection.getROI()

    // Use a Groovy metaClass trick to check if we can get a nucleus ROI... if we need to
    // (could also use Java's instanceof qupath.lib.objects.PathCellObject)
    if (useNucleusROI && detection.metaClass.respondsTo(detection, "getNucleusROI") && detection.getNucleusROI() != null)
        roi = detection.getNucleusROI()
    // ROI shouldn't be null...
    if (roi == null)
    // Get class
    def pathClass = detection.getPathClass()
    def className = pathClass == null ? "" : pathClass.getName()
    // Get centroid
    double cx = roi.getCentroidX()
    double cy = roi.getCentroidY()
    detection.getMeasurementList().addMeasurement("Centroid X", cx)
    detection.getMeasurementList().addMeasurement("Centroid Y", cy)
    // Append to String
    sb.append(String.format("%s\t%.2f\t%.2f\n", className, cx, cy))
    // Count

I believe the routine here was used to generate a Centroid of the cell, but it also appears to work over all the measurements in the measurement list. The measurement at first didn’t appear until I ran the Delaunay step, so I had assumed it came from there.

So I guess that points me to something about the ROI?

1 Like

It looks like it is taking the pixel based nucleus centroid, so the cluster mean should be the mean of those coordinate values. It looks like the Cluster mean from Delaunay is run on any of the standard measurements (so you can add Intensity features or the X coordinate and it will take the mean of those values).

The means of these additional measurements does not seem to be calculated correctly, however, in 0.2.0. I put a fake measurement with a constant value of 1 for all cells, and it’s Delaunay cluster mean hovered around 19 (no matter what the size of the cluster is).

I am fairly certain this is something that will need to be fixed in the code, though… it is working for some of my added measurements. I had a measurement for the number of cells within an X micron radius, and those numbers seem to have worked correctly.

*Is there any chance you are running the Delaunay clustering twice? I’ve found that I can put in measurements before running the clustering and they will work just fine. If I

  1. Run Delaunay measurements first
  2. Then add a new measurement to the measurement list
  3. Run Delaunay cluster measurements again
    The measurement added in 2 will have a weird value. Measurements I added prior to 1. will be fine.

If I had to guess, it is here:
The measurement list position it accesses includes all of the Cluster measurements (with Cluster at the beginning of the name), but the “i” value being used to reference the position in that list comes from another list that excluded all of those Cluster measurements.

I would have to see the rest of your script to be sure that is the problem, but it seems likely that it is related.

1 Like

Thanks for your help Mike in somewhat replicating the behavior. The looping through the detections is after the classifier and before the Delaunay step, and those two parts comprise the whole script.

You beat me to the specific Java source file, but that would be a good guess. I think this might be better suited to an issue on the GitHub if you are seeing the same behavior.

I will try moving the detections iterations after the Delaunay plugin though to see if that makes a difference!

1 Like

This looks like an attempted optimization that turns into a bug whenever measurement lists vary across objects, and perhaps also when objects may have multiple measurements with the same names.

I’ll look into fixing it in the code. In the meantime one thing you can do regarding the second issue is to replace addMeasurement with putMeasurement.


Actually, investigating this revealed a bigger problem with the cluster measurements… I think introduced in v0.2.0-m5.

I’ve made a pull request here that I believe fixes things:

I am on the verge of a vacation, but there is another bug fix release planned for the week starting 20 July when I’m back. If any of you could check it (in any way possible) before then that would be excellent :slight_smile:

1 Like

Enjoy the vacation! Given the mass of coding changes to QuPath recently, you certainly deserve it!

1 Like

Thanks so much for digging into this right away and even developing a proposed fix! I haven’t built QuPath before, but I know enough about GitHub that I might be able to before you get back - something new to learn.

I’m super glad to catch you before you are off… 0.2 is a huge step up and it’s great to finally be porting this project. Enjoy the break!


Thanks to you both :slight_smile: Steps for building are at - hopefully relatively painless (although if you’re on Linux, it seems should should avoid OpenJ9).

My mistake

I was able to build the newest version (as of 10:30AM PST, 20 minutes ago), and I still see a problem when I run Delaunay cluster measurements, add a measurement afterwards, and then run cluster measurements again.
There is no way the OD sum Cluster Mean should be 25.9 for most of the cells. :frowning:
However, the Cluster mean: Nucleus Area is about 25 in all cases, so it looks like it is still pulling that value (1 step into the cluster measurements that were excluded).

I spammed the update on Git Desktop, but maybe I needed to wait longer and my newest build was still missing the fix. Will try again tonight.

Based on the changelog, I might not have waited long enough.

Version 0.2.2-SNAPSHOT

In progress

This is a minor release that aims to be fully compatible with v0.2.0 while fixing bugs.

List of bugs fixed:

It’s available now but you’ll need to get it from – there’s an Open in Desktop button at the bottom. You should see the fix referenced in the changelog then. It hasn’t been merged into the main repo yet (waiting until it is checked).


That did it. I can confirm it fixed the situations where I was having issues.

Also, if anyone, say @chrisb wants a link to that build for Windows, I have it hosted on GoogleDrive, and can send the link by message.

1 Like

Thanks @Mike_Nelson for the offer - I’m on a Mac and was successfully able to build and verify the fix. This is great and unblocks me, thanks again @petebankhead!