Qupath distance between annotations

Hi everybody,

In a project I’ve been working on, we are comparing two different staining patterns, HIF-1a and pimonidazole, with each other. We have annotated the positive PIMO and HIF areas and would now like to analyze this by calculating the overlap or, if there is no overlap, the distance between the areas.

image

This image shows a part of a PIMO-stained tissue fragment. Here we have annotated the PIMO-positive area and have inserted the HIF-positive areas from the corresponding HIF-stained tissue.

I have found the Qupath function that measures the distance to annotations from individual cells, but I can’t seem to figure out how to accomplish this for the distance between annotations without having to manually measure it.
Does anyone have any tips on how to do this?

All help is appreciated

Kind Regards,
Hilde

Hi @hsmits there’s no command to do exactly what you’re looking for – I think it’s specialised enough that it is best solved using a script.

I’ve quickly written an example below. I haven’t checked it very carefully, and made some assumptions (e.g. the naming of your annotations), but have mostly documented these in the script comments.

Hopefully it’s useful as a starting point for you and others. I do recommend thorough checking to make sure it’s doing what you expect (e.g. manually draw lines between some annotations and check the measurements are roughly the same as the script displays).

// Get the objects to compare
// This assumes that just *one* annotation has each specified name
def hifPos = getAnnotationObjects().find {it.getName() == 'HIF-positive area 2'}
def pimoPos = getAnnotationObjects().find {it.getName() == 'PIMO-positive area 2'}

// Make sure we're on the same plane (only really relevant for z-stacks, time series)
def plane = hifPos.getROI().getImagePlane()
if (plane != pimoPos.getROI().getImagePlane()) {
    println 'Annotations are on different planes!'
    return    
}

// Convert to geometries & compute distance
// Note: see https://locationtech.github.io/jts/javadoc/org/locationtech/jts/geom/Geometry.html#distance-org.locationtech.jts.geom.Geometry-
def g1 = hifPos.getROI().getGeometry()
def g2 = pimoPos.getROI().getGeometry()
double distancePixels = g1.distance(g2)
println "Distance between annotations: ${distancePixels} pixels"

// Attempt conversion to calibrated units
def cal = getCurrentServer().getPixelCalibration()
if (cal.pixelWidth != cal.pixelHeight) {
    println "Pixel width != pixel height ($cal.pixelWidth vs. $cal.pixelHeight)"
    println "Distance measurements will be calibrated using the average of these"
}
double distanceCalibrated = distancePixels * cal.getAveragedPixelSize()
println "Distance between annotations: ${distanceCalibrated} ${cal.pixelWidthUnit}"

// Check intersection as well
def intersection = g1.intersection(g2)
if (intersection.isEmpty())
    println "No intersection between areas"
else {
    def roi = GeometryTools.geometryToROI(intersection, plane)
    def annotation = PathObjects.createAnnotationObject(roi, getPathClass('Intersection'))
    addObject(annotation)
    selectObjects(annotation)
    println "Annotated created for intersection"
}
6 Likes

Hi Pete,

Thank you so much for your speedy reply. I’ve tested it and so far it seems to be working great.
Thanks for your help!

Kind regards,
Hilde

2 Likes

This is pretty amazing, and something quite a few people have been interested in, as I recall. I always came up with terrible converting things into detection objects workarounds… because me.

I adjusted the script to no longer search for overlap, but instead it looks for all other annotations of all classes, and records the distance to the nearest “other” annotation of every possible annotation class. If you only have one annotation of a given class, say “Tissue,” it’s distance to “Tissue” would be 0. If there are two “Tissue” annotations, it will record the distance between them for each.

Primarily, it should record distances between different annotation objects, per class.

Measurements show up in each Annotation’s measurement list.

// Script modified from Pete Bankhead's post https://forum.image.sc/t/qupath-distance-between-annotations/47960/2
// Calculate the distances to the nearest annotation, per class. Saved to the annotation measurement list.
// Distances recorded should stay 0 if there are no other annotations, but it should find a non-zero distance if there are multiple of the same class

// Does NOT work between multiple points in a Points object.

classList = getAnnotationObjects().collect{it.getPathClass()} as Set
def cal = getCurrentServer().getPixelCalibration()
if (cal.pixelWidth != cal.pixelHeight) {
    println "Pixel width != pixel height ($cal.pixelWidth vs. $cal.pixelHeight)"
    println "Distance measurements will be calibrated using the average of these"
}


getAnnotationObjects().each{ a ->
    //Store the shortest non-zero distance between an annotation and another class of annotation
    Map classDistanceMap = [:]
    classList.each{c->
        classDistanceMap[c.toString()] = 0
    }
    def g1 = a.getROI().getGeometry()
    def plane = a.getROI().getImagePlane()
    //If there are multiple annotations of the same type, prevent checking distances against itself
    otherAnnotations = getAnnotationObjects().findAll{it != a}
    otherAnnotations.each{oa->
        // Make sure we're on the same plane (only really relevant for z-stacks, time series)        
        if (plane != oa.getROI().getImagePlane()) {
            println 'Annotations are on different planes!'
            return    
        }
        classList.each{c->
            classAnnotations = otherAnnotations.findAll{it.getPathClass() == c}
            classAnnotations.each{ca->
                // Convert to geometries & compute distance
                // Note: see https://locationtech.github.io/jts/javadoc/org/locationtech/jts/geom/Geometry.html#distance-org.locationtech.jts.geom.Geometry-

                def g2 = ca.getROI().getGeometry()
                double distancePixels = g1.distance(g2)
                double distanceCalibrated = distancePixels * cal.getAveragedPixelSize()
                //check if the distance between annotations is less than what was previously calculated, or if no distance had yet been calculated
                if(classDistanceMap[c.toString()] == 0 || distanceCalibrated < classDistanceMap[c.toString()]){
                    classDistanceMap[c.toString()] = distanceCalibrated
                }
            }
        }
    }
    saveDistancesToAnnotation(a, classDistanceMap)
}

print "Done! Distances saved to each annotation's measurement list"
def saveDistancesToAnnotation (annotation,classDistanceMap){
    classDistanceMap.each{
        annotation.getMeasurementList().putMeasurement("Distance in um to nearest "+it.key+" annotation", it.value)
        
    }
}

As noted in the script, it does not make any special considerations for Points annotations, and while distance from some area annotation to the nearest Point will work, you will not see the distance between two points in a given Points object.

3 Likes

Thank you so much for your addition to the script. It works like a charm!

2 Likes

One further update. I had someone working with creating Detection objects using the pixel classifier, which can be all sorts of weird shapes. I adjusted the script to work for all objects, and merged all of the objects of each class (in JTS form) in order to make the distance calculations faster. It can still be VERY slow for large numbers of objects. It also no longer checks for objects being on the same Plane (time point, Z-stack), so users beware.

// Script further modified from Pete Bankhead's post https://forum.image.sc/t/qupath-distance-between-annotations/47960/2
// Calculate the distances from each object to the nearest object of each other class. Saved to the object's measurement list.
// Distances recorded should stay 0 if there are no other objects, but it should find a non-zero distance if there are multiple of the same class

// Does NOT work *between* multiple points in a Points object, though Points objects will count as a single object.
/////////////////////////////////////////////////////
// DOES NOT WORK/CHECK FOR ZSTACKS AND TIME SERIES //
/////////////////////////////////////////////////////
print "Caution, this may take some time for large numbers of objects."
print "If the time is excessive for your project, you may want to consider size thresholding some objects, or adjusting the objectsToCheck"
objectsToCheck = getAllObjects().findAll{it.isDetection() || it.isAnnotation()}
classList = objectsToCheck.collect{it.getPathClass()} as Set
def cal = getCurrentServer().getPixelCalibration()
if (cal.pixelWidth != cal.pixelHeight) {
    println "Pixel width != pixel height ($cal.pixelWidth vs. $cal.pixelHeight)"
    println "Distance measurements will be calibrated using the average of these"
}
Map combinedClassObjects = [:]
classList.each{c->
    currentClassObjects = getAllObjects().findAll{it.getPathClass() == c}
    geom = null
    currentClassObjects.eachWithIndex{o, i->
        if(i==0){geom = o.getROI().getGeometry()}else{
            geom = geom.union(o.getROI().getGeometry())
            
        }
    }
    combinedClassObjects[c] = geom
}

objectsToCheck.each{ o ->
    //Store the shortest non-zero distance between an annotation and another class of annotation

    def g1 = o.getROI().getGeometry()
    //If there are multiple annotations of the same type, prevent checking distances against itself
    combinedClassObjects.each{cco->
        double distancePixels = g1.distance(cco.value)
        double distanceCalibrated = distancePixels * cal.getAveragedPixelSize()
        o.getMeasurementList().putMeasurement("Distance in um to nearest "+cco.key+" annotation", distanceCalibrated)
    }
}

print "Done! Distances saved to each object's measurement list"

I fully realize developer time/resources are limited, but I think a similar built in function might be a fairly nice addition to the Spatial analysis list, especially given the utility of the pixel classifiers in creating objects that are not well represented by their centroids.