Skip to content

Instantly share code, notes, and snippets.

@petebankhead
Created July 8, 2020 16:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save petebankhead/6caf71249f7511288805fb659f877698 to your computer and use it in GitHub Desktop.
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
/**
* 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()
@petebankhead
Copy link
Author

@steelec
Copy link

steelec commented Mar 2, 2023

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)

@petebankhead
Copy link
Author

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.

@steelec
Copy link

steelec commented Mar 8, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment