QuPath: Geometry issue from SLIC pixels

Hi @petebankhead,
SLIC pixels sometimes causes problems while adding the shape feature ‘Circularity’ (and maybe others too).
This happens when a SLIC pixel is not a Polygon but a Geometry.

After creating the SLICs and adding shape features to the detection one example looks like this:

In this case the detection is a polygon and the circularity feature is available in the feature list.

An example of a detection causing the problem looks like this:

In this case the detection is a geometry and the circularity feature is NOT available in the feature list because the SLIC pixel is fragmented and is not a valid polygon.

I think the problem is located in
qupath/lib/analysis/features/ObjectMeasurements.java addShapeMeasurements()

If a ‘Single Measurement Classifier’ based on the circularity is applied to the detections the message “Missing feature: Circularity” is displayed.

Maybe this is an issue for your list of geometry problems.

I don’t think the definition of circularity really makes sense when a ROI has multiple pieces as in this case, therefore I wouldn’t expect a measurement to be made in this case. It’s not really an issue of geometry validity.

It’s good to know the issue exists but it’s not clear to me what the fix can/should be; since the workflow is quite specific, I’m inclined to just advise not to use circularity as a measurement in such cases :slight_smile:

Alternatively, I presume it should only occur around annotation boundaries – in which case the script I wrote last week may be of some help in removing these objects:

2 Likes

Thanks @petebankhead for your fast response and pointing to the script.
Yes, you are right, the workflow is specific. And yes it is not the validity of the geometry which causes problem. It makes total sense not to calculate circularity for such split object.

But what makes no sense is to have different feature lists for different detections. This can - as I tried to show - break down following processes of the workflow.
Instead of simply omitting the feature you could think about registering 0 as circular feature value instead.

This is not a huge thing … as said, just for your list.

1 Like

In some cases it can be useful to check when a measurement doesn’t exist, but having the option to fill nulls with 0 might be nice.
Of course, that is one more checkbox/option/other.

Also something fun I have made use of recently, objects with missing values do not show up when that measurement is selected in the Measurement Maps. I had stacked detections, each with different measurements, and I could flip between measurements to view each group of detections in turn.

I’m not convinced this would be a good idea, since 0 would have a meaning for a circular measurement… but not appropriate in this case.

When requesting an unavailable measurement value, QuPath will return NaN – rather than throw an error – and therefore the current behaviour should at least work consistently. Some classifiers may accept NaNs (I think the RTrees implementation in OpenCV does; I’m pretty sure the neural networks implementation doesn’t).

Thinking about it some more, I think the ‘correct’ behaviour may be for the superpixel command to forbid generating split objects. In practice, they shouldn’t happen – except I can see how they might in (rare?) cases where a superpixel ‘curls’ around the outside of an annotation, and is then truncated to form two disconnected pieces when the annotation ROI mask is applied. I think that QuPath should perhaps generate two superpixels in such cases (although one may be very small).

Changing the behavior isn’t entirely without risk, since someone, somewhere, might rely upon the current (admittedly suboptimal) behavior – and any small modification will make scripts subtly inconsistent with previous versions.

I’ll add it to the list – although since the list is very long, and a change could take a long time to check, I can’t promise to get to it very soon/

In the meantime, if you are scripting you can set a default value with:

def toFix = getDetectionObjects().findAll { d -> Double.isNaN(measurement(d, 'Circularity')) }
toFix.each { d -> 
    try (def ml = d.getMeasurementList()) {
        ml.putMeasurement('Circularity', 0)
    }
}
fireHierarchyUpdate()
2 Likes

I see your point. And also the next one …

The behaviour is consistant. :white_check_mark:
In my case, detections without Circularity feature are simply not classified if I design my classifier accordingly (by using the Positive and Negative class). Then the Unclassified objects can be selected by their class and deleted.

Also your script is a perfect solution for me. :+1:

I agree. But as you said, my workflow is specific, split objects are rare, there is a workaround and your list is long.

No worries. I’m fine with what we have elaborated (Thank you and @Research_Associate )

One more detail:
I have to specify my first statement. The fact that the ‘problematic’ detections are Geometries and not Polygons is NOT the determining chracteristics. In fact, the most (at least a huge fraction) of the SLIC detections are Geometries.
The decision wether or not to calculate Circularity is based on isArea().

2 Likes