-
-
Save petebankhead/6caf71249f7511288805fb659f877698 to your computer and use it in GitHub Desktop.
/** | |
* Create a 'counts' image in QuPath that can be used to compute the local density of specific objects. | |
* | |
* This implementation uses ImageJ to create and display the image, which can then be filtered as required. | |
* | |
* Written for QuPath v0.2.0. | |
* | |
* @author Pete Bankhead | |
*/ | |
import ij.ImagePlus | |
import ij.process.FloatProcessor | |
import qupath.imagej.gui.IJExtension | |
import qupath.lib.objects.PathObjectTools | |
import qupath.lib.regions.RegionRequest | |
import static qupath.lib.gui.scripting.QPEx.* | |
import qupath.imagej.tools.IJTools | |
// Define the resolution at which the image should be generated | |
double requestedPixelSizeMicrons = 20 | |
// Get the current image | |
def imageData = getCurrentImageData() | |
def server = imageData.getServer() | |
// Set the downsample directly (without using the requestedPixelSize) if you want; 1.0 indicates the full resolution | |
double downsample = requestedPixelSizeMicrons / server.getPixelCalibration().getAveragedPixelSizeMicrons() | |
def request = RegionRequest.createInstance(server, downsample) | |
def imp = IJTools.convertToImagePlus(server, request).getImage() | |
// Get the objects you want to count | |
// Potentially you can add filters for specific objects, e.g. to get only those with a 'Positive' classification | |
def detections = getDetectionObjects() | |
//detections = detections.findAll {it.getPathClass() == getPathClass('Positive')} | |
// Create a counts image in ImageJ, where each pixel corresponds to the number of centroids at that pixel | |
int width = imp.getWidth() | |
int height = imp.getHeight() | |
def fp = new FloatProcessor(width, height) | |
for (detection in detections) { | |
// Get ROI for a detection; this method gets the nucleus if we have a cell object (and the only ROI for anything else) | |
def roi = PathObjectTools.getROI(detection, true) | |
int x = (int)(roi.getCentroidX() / downsample) | |
int y = (int)(roi.getCentroidY() / downsample) | |
fp.setf(x, y, fp.getf(x, y) + 1 as float) | |
} | |
// Show the images | |
IJExtension.getImageJInstance() | |
imp.show() | |
new ImagePlus(imp.getTitle() + "-counts", fp).show() |
I see that this was created a while ago, but just to note that we tried to adapt this to some of our processing and found some edge cases where the downsampling results in selecting x and y positions that are outside of the range of the fp
variable. Usually only shows up when you are downsampling a lot, and there are cells identified at the edges of the scan, which then results in an index location = to the width or height of the matrix and throws an error.
We were able to get around this issue by subtracting 1 before casting to int, but happy to hear if you have other thoughts on a better or more elegant solution (there is a potential edge case where subtracting 1 will result in a negative x
or y
coordinate.
int x = (int) ((roi.getCentroidX() / downsample) - 1)
int y = (int) ((roi.getCentroidY() / downsample) - 1)
Hmmm, tricky. I feel that -1 just shifts the problem... and also all the cells. My guess is that failures occur because the image size is determined when requesting the region, which will involve rounding to the closest appropriate size... but cells might occur outside this 'rounded' size. I guess. Maybe.
If it's not important for the map to have the same size as the original image, you could just use
def fp = new FloatProcessor(width + 1, height + 1)
or alternatively add in
x = Math.min(x, fp.getWidth()-1)
y = Math.min(x, fp.getHeight()-1)
QuPath now has Density maps so I don't really recommend using this script anyway.
Yes, it is definitely a result of rounding. Thanks for the fix on my non-fix (which you are correct, just shifted the error). We were looking into the densityMaps class but have not had the time to figure out the correct way to script them.
https://forum.image.sc/t/cell-density-map/40050