This should do something relevant:
/**
* QuPath script to create annotations where there is a high density of detections.
*
* @author Pete Bankhead
*/
import ij.plugin.filter.EDM
import ij.plugin.filter.RankFilters
import ij.process.*
import qupath.lib.plugins.parameters.ParameterList
import java.awt.Color
double pixelSizeMicrons = 20.0
double radiusMicrons = 100.0
int minCells = 20
boolean tumorOnly = false
def params = new ParameterList()
.addDoubleParameter("radiusMicrons", "Radius", radiusMicrons, GeneralTools.micrometerSymbol(), "Choose radius of hotspot")
.addIntParameter("minCells", "Minimum cell count", minCells, "", "Minimum number of cells in hotspot")
if (!Dialogs.showParameterDialog("Parameters", params))
return
radiusMicrons = params.getDoubleParameterValue("radiusMicrons")
minCells = params.getIntParameterValue("minCells")
def imageData = getCurrentImageData()
def server = imageData.getServer()
def detections = getDetectionObjects()
double downsample = pixelSizeMicrons / server.getPixelCalibration().getAveragedPixelSizeMicrons()
int w = Math.ceil(server.getWidth() / downsample)
int h = Math.ceil(server.getHeight() / downsample)
// Create centroid map
def fpCounts = new FloatProcessor(w, h)
for (detection in detections) {
def roi = PathObjectTools.getROI(detection, true)
if (roi.isEmpty())
continue
int x = (roi.getCentroidX() / downsample) as int
int y = (roi.getCentroidY() / downsample) as int
fpCounts.setf(x, y, fpCounts.getf(x, y) + 1f as float)
}
// Compute sum of filter elements
def rf = new RankFilters()
double radius = radiusMicrons / pixelSizeMicrons
int dim = Math.ceil(radius * 2 + 5)
def fpTemp = new FloatProcessor(dim, dim)
fpTemp.setf(dim/2 as int, dim/2 as int, radius * radius as float)
rf.rank(fpTemp, radius, RankFilters.MEAN)
def pixels = fpTemp.getPixels() as float[]
double n = Arrays.stream(pixels).filter({f -> f > 0}).count()
// Compute sums
rf.rank(fpCounts, radius, RankFilters.MEAN)
fpCounts.multiply(n)
// Threshold
fpCounts.setThreshold(minCells, Double.POSITIVE_INFINITY, ImageProcessor.NO_LUT_UPDATE)
def roiIJ = new ij.plugin.filter.ThresholdToSelection().convert(fpCounts)
if (roiIJ == null) {
println 'Nothing detected!'
return
}
def roi = IJTools.convertToROI(roiIJ, 0, 0, downsample, ImagePlane.getDefaultPlane())
def hotspot = PathObjects.createAnnotationObject(roi)
addObject(hotspot)
It’s adapted from the script I wrote here: Find highest staining region of a slide - #7 by petebankhead
It is a bit tricky to tune, since it’s necessary to make decisions about what kind of cell density is required. But hopefully it helps.