Selecting annotations within a QuPath script

Hi, hoping for some help with scripting again.

I’ve written a script to iteratively run cell detection with different parameters - it is working perfectly for a single image but I’ve run into a problem getting it to run for a whole project. (I can’t just use Run for Project because I want the user to be able to enter the range of required parameters at the beginning just once instead of for every image).

The issue appears to be that the annotation is no longer being selected so, while the code runs, the cell detection analysis doesn’t.

This is the part of the script that selects the annotation in the version that works:

    hierarchy = getCurrentHierarchy()
    def annotations = getAnnotationObjects()

    for (annotation in annotations) {
        hierarchy.getSelectionModel().clearSelection();
        selectObjects { p -> p == annotation }

When I script it to run for the whole project, the hierachy is defined earlier with the following:

def project = getProject()
for (entry in project.getImageList()) {   // Loop through all images in the project
    def imageData = entry.readImageData()
    def hierarchy = imageData.getHierarchy()
    def annotations = hierarchy.getAnnotationObjects()

    for (annotation in annotations) {   // Loop through each annotation in the image
            hierarchy.getSelectionModel().clearSelection()
            selectObjects { p -> p == annotation }

So I’m assuming this is the source of the problem but I can’t work out what to change. It is still correctly looping through the annotations, it’s just not selecting them.

Thanks in advance for any suggestions!

If you are running it for project outside of the run for project, are you sure you are saving the changes to the imageData? I recall that coming up several times. Run for project saves the data for you, if you don’t use it, you need to call the save function yourself.

Thanks for the quick response!

I think the issue is with selection rather than saving. The script doesn’t actually change anything in each image, just runs the cell detection plugin.multiple times and writes the outputs to a csv file. All the annotations in all the images are pre-set and don’t change, I just need to cycle through selecting them.

I’m pretty sure the issue is with this line:
selectObjects { p -> p == annotation }
That works fine for a single image but doesn’t actually select anything when running through the project.

Any ideas?

1 Like

Hi @jo-maree, you’re right (although like @Research_Associate I also initially thought it was connected with saving) – that line will just operate on the ‘current’ image at the time the script was run. Here’s the method it calls:

So in your case, something like this should work:

hierarchy.getSelectionModel().setSelectedObject(annotation)
1 Like

Thanks @petebankhead - I just tried using setSelectedObject but still no luck, I must stil be missing something basic.

I’ve made this test script with the same basic structure to replicate the problem:

def project = getProject()
for (entry in project.getImageList()) {
    def imageData = entry.readImageData()
    
    def hierarchy = imageData.getHierarchy()
    def annotations = hierarchy.getAnnotationObjects()
    for (annotation in annotations) {
        print annotation.getName()
        hierarchy.getSelectionModel().setSelectedObject(annotation)
        def myObject = getSelectedROI()
        for (m in myObject.getClass().getMethods()){
            println(m)
        }
    }
    print entry.getImageName() + '\t' + annotations.size()
}

This successfully loops through the project, getting the correct names for all the images and annotations but it doesn’t actually select any annotations (the printouts of the methods are all for null objects).

Any other ideas?

Cheers,
Jo-Maree

1 Like

getSelectedROI() has the same issue as selectObjects had – it works on whatever QuPath considers to be the current image when the script is run.

If you post the entire script I can flag other likely troublesome lines, but in general if a line does something with the image/hierarchy/selection but doesn’t specify precisely which image/object is involved, the current one is probably being used.

Most such methods are implemented here, and many (not all) have a version that allows you to specify the image/hierarchy you specifically want to use.

1 Like

Thanks again for your help @petebankhead - I’m still pretty new to scripting!
Here’s my whole script…

import qupath.lib.objects.PathDetectionObject

def thresholdSteps = 5
def thresholdStart = 100
def thresholdIncrement = 50
def sigmaSteps = 3
def sigmaStart = 1.5
def sigmaIncrement = 0.5
def expansion = 2
def radius = 8.0

def dir = buildFilePath(PROJECT_BASE_DIR, 'Optimisation Results')  
mkdirs(dir)

def project = getProject()
for (entry in project.getImageList()) {   // Loop through all images in the project
    def imageData = entry.readImageData()
    def hierarchy = imageData.getHierarchy()
    def annotations = hierarchy.getAnnotationObjects()
    def name = entry.getImageName()
    def filepath = dir + '\\' + name + '.csv'

    File csvFile = new File(filepath)   // Create a CSV file to collect data about the current image
    csvFile.createNewFile()
    
    new File(filepath).withWriter { fw -> 
        fw.writeLine(name)  // Write the image name
        fw.writeLine("Threshold, Sigma, Cells")  // Write headers

        clearDetections() 

        for (annotation in annotations) {   // Loop through each annotation in the image
            hierarchy.getSelectionModel().clearSelection()
            selectObjects { p -> p == annotation }   // Select the annotation  NOT WORKING!!!!

            fw.writeLine(annotation.getName())  // Write the annotation name

            for (int sig = 0; sig < sigmaSteps; sig++) {  // Loop through the Sigma increments
                def sigma = sigmaStart + sigmaIncrement * sig

                for (int th = 0; th < thresholdSteps; th++) {  // Loop through the Threshold increments
                    def threshold = thresholdStart + thresholdIncrement * th

                    runPlugin('qupath.imagej.detect.cells.WatershedCellDetection',
                            '{"detectionImage": "DAPI",  ' +
                                    '"requestedPixelSizeMicrons": 0.5,  ' +
                                    '"backgroundRadiusMicrons": ' + radius + ',  ' +
                                    '"medianRadiusMicrons": 0.0,  ' +
                                    '"sigmaMicrons": ' + sigma + ',  ' +
                                    '"minAreaMicrons": 10.0,  ' +
                                    '"maxAreaMicrons": 400.0,  ' +
                                    '"threshold": ' + threshold + ',  ' +
                                    '"watershedPostProcess": true,  ' +
                                    '"cellExpansionMicrons": ' + expansion + ',  ' +
                                    '"includeNuclei": true,  ' +
                                    '"smoothBoundaries": true,  ' +
                                    '"makeMeasurements": true}')

                    // Get the number of cells detected
                    def objects = hierarchy.getObjectsForROI(null, annotation.getROI()).findAll {
                        it.isDetection()
                    }
                    hierarchy.getObjectsForROI(PathDetectionObject, annotation.getROI())
                    def detectionCount = objects.size()
                    def data = Double.toString(threshold) + ',' + Double.toString(sigma) + ',' + Double.toString(detectionCount)
                    fw.writeLine(data) // Write data to csv file
                }
            }
        }
    }
}
print 'DONE!'
1 Like

This might work (only changed a few lines, hopefully caught all the important ones):

import qupath.lib.objects.PathDetectionObject

def thresholdSteps = 5
def thresholdStart = 1
def thresholdIncrement = 1
def sigmaSteps = 3
def sigmaStart = 1.5
def sigmaIncrement = 0.5
def expansion = 2
def radius = 8.0

def dir = buildFilePath(PROJECT_BASE_DIR, 'Optimisation Results')  
mkdirs(dir)

def project = getProject()
for (entry in project.getImageList()) {   // Loop through all images in the project
    def imageData = entry.readImageData()
    def hierarchy = imageData.getHierarchy()
    def annotations = hierarchy.getAnnotationObjects()
    def name = entry.getImageName()
    def filepath = dir + '\\' + name + '.csv'

    File csvFile = new File(filepath)   // Create a CSV file to collect data about the current image
    csvFile.createNewFile()
    
    new File(filepath).withWriter { fw -> 
        fw.writeLine(name)  // Write the image name
        fw.writeLine("Threshold, Sigma, Cells")  // Write headers

        def detections = hierarchy.getDetectionObjects()
        hierarchy.removeObjects(detections, true)

        for (annotation in annotations) {   // Loop through each annotation in the image
            hierarchy.getSelectionModel().clearSelection()
            hierarchy.getSelectionModel().setSelectedObject(annotation)   // Select the annotation  NOT WORKING!!!!

            fw.writeLine(annotation.getName())  // Write the annotation name

            for (int sig = 0; sig < sigmaSteps; sig++) {  // Loop through the Sigma increments
                def sigma = sigmaStart + sigmaIncrement * sig

                for (int th = 0; th < thresholdSteps; th++) {  // Loop through the Threshold increments
                    def threshold = thresholdStart + thresholdIncrement * th

                    runPlugin('qupath.imagej.detect.cells.WatershedCellDetection',
                            imageData,
                            '{"detectionImage": "DAPI",  ' +
                                    '"requestedPixelSizeMicrons": 0.5,  ' +
                                    '"backgroundRadiusMicrons": ' + radius + ',  ' +
                                    '"medianRadiusMicrons": 0.0,  ' +
                                    '"sigmaMicrons": ' + sigma + ',  ' +
                                    '"minAreaMicrons": 10.0,  ' +
                                    '"maxAreaMicrons": 400.0,  ' +
                                    '"threshold": ' + threshold + ',  ' +
                                    '"watershedPostProcess": true,  ' +
                                    '"cellExpansionMicrons": ' + expansion + ',  ' +
                                    '"includeNuclei": true,  ' +
                                    '"smoothBoundaries": true,  ' +
                                    '"makeMeasurements": true}')

                    // Get the number of cells detected
                    def objects = hierarchy.getObjectsForROI(null, annotation.getROI()).findAll {
                        it.isDetection()
                    }
                    hierarchy.getObjectsForROI(PathDetectionObject, annotation.getROI())
                    def detectionCount = objects.size()
                    def data = Double.toString(threshold) + ',' + Double.toString(sigma) + ',' + Double.toString(detectionCount)
                    fw.writeLine(data) // Write data to csv file
                }
            }
        }
    }
}
print 'DONE!'
1 Like

I wrote the scripting, but the necessary changes weren’t at all obvious to me :slight_smile:

If it’s possible to reformat things so that you can use Run for project then scripting should be easier, since you don’t have to worry about opening files yourself – although that does tend to mean you need unique filenames for every image.

There is one trick that sometimes helps*:

// Create a file when you need it for the first time
def file = new File("/Whatever/your/file/path/is")
if (!file.exists())
   file.text = "Whatever you want as the first line in your file"
else
   file << "Whatever you want to append"

It’s not super-efficient, but it means you can use QuPath to do the loops via Run for project and still write to the same file – you just need to remember to delete the file each time you want to start anew, so it doesn’t continually append to the same file indefinitely.

*-Written from memory, so possibly with typos.

1 Like

Great question though, and I might be coming back to this answer at some point in the future! Definitely something I had run into a couple of times before, though not with selecting objects specifically.

1 Like

Ah, thanks so much @petebankhead - that version works perfectly :grinning:

1 Like