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.