Problems using the workflow as a macro

Hello,

I am relatively new to QuPath, so I apologize in advance if my question is not very sophisticated.
I did the online tutorial but I cannot solve my problem on my one. I would like to automate the same steps I am doing in QuPath again and again (A description of my exact problem is at the end of this thread: Automatic counting), like writing a macros. I tried to save my workflow in a script, however it does not contain all the steps and it does not really work. It only does step 2 (see below) and than with the standard settings and not the one I choose when I did it by hand.
The steps I am doing are:

  1. I draw a Polyline
  2. I use the buttons: Objects -> Annotations -> Expand annotations and get a ROI around my line
  3. I delete the line
  4. I mark my ROI which I got from step 2
  5. I run a script which I found here in the forum which marks the cells in my ROI and connects the centroids with a line
  6. I convert the detections to points.

So after drawing the line I am always doing the same steps, so I thought it must be possible to automate these steps. Is there a way in QuPath to do so? I really appreciate your help and I am sorry for this really basic question but I cannot solve the problem with the tutorial as only help.

Thank you very much
Tobi

Hi @Tobi,

  1. Step 1 is manual
  2. From Create workflow (assuming only one annotation):
def ann = getAnnotationObjects()[0]
selectObjects(ann)
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 100.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');
  1. Remove the original annotation:
removeObject(ann, false)
resolveHierarchy()
  1. I’m sorry, I’m not sure what you mean by ‘mark a ROI’.
  2. Your script
  3. Your cells have an area and are bigger than Points. I suppose you want to create a point located exactly at the centroid of each nucleus? I am not sure why this would be better than just using the existing cells. But anyway, you can try something like this:
import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject

xs = []
ys = []
getCellObjects().forEach {
    xs << it.getROI().getCentroidX()
    ys << it.getROI().getCentroidY()
}

def roi = ROIs.createPointsROI(xs as double[], ys as double[], getCurrentViewer().getImagePlane())
def newAnn = new PathAnnotationObject(roi, null)
addObject(newAnn)
resolveHierarchy()

So at the end this would look like this:


// Import all packages
import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject

/* Step 1 */
// Manual drawing of polyline

/* Step 2 */
def ann = getAnnotationObjects()[0]
selectObjects(ann)
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 100.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');


/* Step 3 */
removeObject(ann, false)
resolveHierarchy()

/* Step 4 */
// Not sure what 'marking' means here

/* Step 5 */
// Your script goes here..

/* Step 6  (after cell detection, etc..) */
xs = []
ys = []
getCellObjects().forEach {
    xs << it.getROI().getCentroidX()
    ys << it.getROI().getCentroidY()
}

// Create ROI
def roi = ROIs.createPointsROI(xs as double[], ys as double[], getCurrentViewer().getImagePlane())
// Create annotation from ROI
def newAnn = new PathAnnotationObject(roi, null)
// Add it to hierarchy
addObject(newAnn)
// Resolve hierarchy
resolveHierarchy()

If you have more than one line per image, you may want to replace the first two lines of step 2 with
ann = getSelectedObject()

which will run the script on your currently selected object. Hopefully you run it with the line selected.

If you only ever have one line per image, ignore my post!

If you have dozens of lines, it should be possible to cycle through each, but you haven’t been explicit about how your projects look from that perspective.

First of all, thanks to you and your solutions.

However, I tried a lot, but I am not able to run the script as I would like to.

My task is to count the number of cells in one row. As I am working with tree rings, cells are usually arranged in lines. Sometimes they are a bit curvy, but it is clear which cell belongs in which cell row.

In each tree ring I have to count the number of cells in 3 rows. This is the background.

Here I try to illustrate my working steps with some images and after this I will show my script with my questions:

  1. I draw my polyline manually

  2. I expand my annotation/line
    2. Expand annotations

  3. Now I would like to delete the line and chose the new annotation, which were drawn around my line:

  4. In the next step I would like to run a script which detects the cells and draws a line through the cells as I want to measure the length of the whole cell row in the end (I found this script here.

  5. Finally I would like to convert the detections which I got from the step before to points. Points can be corrected manually and my R-Script knows how to handle this information when I export the annotations.

So I adjusted my script to the one of Melvingelbard and it looks like this:

/* Step 1 */
// Manual drawing of polyline

// Import all packages
import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject


/* Step 2 */
def ann = getAnnotationObjects()[0]
selectObjects(ann)
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 8.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');


/* Step 3 */
removeObject(ann, false)
resolveHierarchy()

/* Step 4 */
// Mark the new annotation



/* Step 5 */
runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImage": "Red",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 40.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 7.0,  "minAreaMicrons": 30.0,  "maxAreaMicrons": 1000.0,  "threshold": 50.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 0.1,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true, "thresholdCompartment": "Nucleus: Red mean",  "thresholdPositive1": 10.0,  "thresholdPositive2": 20.0,  "thresholdPositive3": 30.0,  "singleThreshold": true}');

import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject

def toRemove = getSelectedObjects()
def imagePlane = getCurrentViewer().getImagePlane()
resolveHierarchy()
// Get all selected objects

getSelectedObjects().each{
    List<double> points = []
    it.getChildObjects().each{ cell->
        double x = cell.getROI().getCentroidX()
        double y = cell.getROI().getCentroidY()
        points << [x,y]
    }

    pointsX = []
    pointsY = []


    //Use the bounding box to decide if the points should be sorted by left to right or top to bottom.
    def roi = it.getROI()
    def x = roi.getBoundsX()
    def y = roi.getBoundsY()
    def width = roi.getBoundsWidth()
    def height = roi.getBoundsHeight()
    if (roi.getBoundsWidth() > roi.getBoundsHeight()){
    //sort by X
        points.sort({a,b -> a[0]<=>b[0]})
        
        points.each{pointsX << it[0] ; pointsY << it[1]}
        print pointsX
        newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
    }else{
    //otherwise sort by Y
        points.sort({a,b -> a[1]<=>b[1]})
        points.each{pointsX << it[0]; pointsY << it[1]}
        
        newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
    }
        //newRoi = ROIs.createLineROI(x, y, x, y+height, imagePlane)
     
    // Create new Line annotation object (with no class)
    def newAnn = new PathAnnotationObject(newRoi, null)

    // Add it
    addObject(newAnn)

clearSelectedObjects(true);

}

// Remove previous objects and update
//removeObjects(toRemove, false)
resolveHierarchy()




/* Step 6  (after cell detection, etc..) */
xs = []
ys = []
getCellObjects().forEach {
    xs << it.getROI().getCentroidX()
    ys << it.getROI().getCentroidY()
}

// Create ROI

def roi = ROIs.createPointsROI(xs as double[], ys as double[], getCurrentViewer().getImagePlane())

// Create annotation from ROI

def newAnn = new PathAnnotationObject(roi, null)

// Add it to hierarchy

addObject(newAnn)

// Resolve hierarchy

resolveHierarchy()

My problem:
I do not get rid of the line in the new polygon and after I have run my script and I cannot change my detections t points with the script.

So is there a way to only run one script after I have drawn a polyline to get my points with a line?

I was not able to bring the simple steps together. I am really sorry that I am not able to do so on my own. I really appreciate if you could help me to connect the steps in a script which I can simply run.

Thanks you very much!!!

To get rid of the polyline after expanding, you can change false to true in Step 3:

// Step three
removeObject(ann, true)
resolveHierarchy()

The script you posted should take care of the conversion from Detections to Points. What happens when you run it? Does it output anything? Any error/exception/log info?

1 Like

Side note, it would be helpful to use the scripting format for your scripts.
image

Are you sure you have cells?
“cellExpansionMicrons”: 0.1,
Depending on the pixel size of the image, you might have DetectionObjects instead. You should select one of those objects and check what object type it is.

Thanks again for your help!

I run the script and changed “false” to “true” in step 3 now and I realised that the polyline disappears when I click somewhere in the image. Is there a possibility that the line disappears without clicking somewhere and my expanded object around the line becomes selected automatically?

When I run the script and try to convert Detections to points I receive an error message:

ERROR: It looks like you have tried to import a class ‘PathAnnotationObject’ that doesn’t exist!
You should probably remove the broken import statement in your script (around line 12).
Then you may want to check ‘Run -> Include default imports’ is selected, or alternatively add
import qupath.lib.objects.PathObjects
at the start of the script. Full error message below.

ERROR: MultipleCompilationErrorsException at line 11: startup failed:
Script29.groovy: 12: unable to resolve class PathAnnotationObject
@ line 12, column 14.
def newAnn = new PathAnnotationObject(roi, null)
^

1 error

ERROR: org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:287)
org.codehaus.groovy.control.CompilationUnit$ISourceUnitOperation.doPhaseOperation(CompilationUnit.java:893)
org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:623)
groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:389)
groovy.lang.GroovyClassLoader.lambda$parseClass$3(GroovyClassLoader.java:332)
org.codehaus.groovy.runtime.memoize.StampedCommonCache.compute(StampedCommonCache.java:163)
org.codehaus.groovy.runtime.memoize.StampedCommonCache.getAndPut(StampedCommonCache.java:154)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:330)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:314)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:257)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:336)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:926)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
java.base/java.util.concurrent.FutureTask.run(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.base/java.lang.Thread.run(Unknown Source)

Thank you and sorry, but I am really only a user and not a programmer at all =(

No worries, we’re here to help! :slight_smile:

ERROR: It looks like you have tried to import a class ‘PathAnnotationObject’ that doesn’t exist!
You should probably remove the broken import statement in your script (around line 12).
Then you may want to check ‘Run -> Include default imports’ is selected, or alternatively add
import qupath.lib.objects.PathObjects
at the start of the script. Full error message below.

ERROR: MultipleCompilationErrorsException at line 11: startup failed:
Script29.groovy: 12: unable to resolve class PathAnnotationObject
@ line 12, column 14.
def newAnn = new PathAnnotationObject(roi, null)
^

1 error

ERROR: org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:287)
org.codehaus.groovy.control.CompilationUnit$ISourceUnitOperation.doPhaseOperation(CompilationUnit.java:893)
org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:623)
groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:389)
groovy.lang.GroovyClassLoader.lambda$parseClass$3(GroovyClassLoader.java:332)
org.codehaus.groovy.runtime.memoize.StampedCommonCache.compute(StampedCommonCache.java:163)
org.codehaus.groovy.runtime.memoize.StampedCommonCache.getAndPut(StampedCommonCache.java:154)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:330)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:314)
groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:257)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:336)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:926)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
java.base/java.util.concurrent.FutureTask.run(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.base/java.lang.Thread.run(Unknown Source)

Looks like you forgot to import PathAnnotationObject:

import qupath.lib.objects.PathAnnotationObject

This is just a UI thing, what actually happen in the background is what you described as your goal. I’ve tested the script (after changing removeObject(ann, false) to removeObject(ann, true)) and it seems to be doing exactly what you want (I think?):

  1. Select annotation (polyline)
  2. Dilate it (polyline)
  3. Remove it (polyline)
  4. Select the new annotation (polygon)
  5. Run watershed cell detection on it (polygon)
  6. Create points in each cell’s centroid
  7. Connect the centroids with a new polyline

Is the script processing your images differently than this?

PS: Additionally, you can run:

clearDetections()

After your script to get rid of your cell objects (which are by definition Detections), if that’s what you want.

1 Like

The preferred way to create an annotation is now PathObjects.createAnnotationObject(roi) – rather than new PathAnnotationObject(roi, null). Then you don’t need the extra import – see

1 Like

What @melvingelbard said, but more specifically, selected objects that have been deleted still appear onscreen. You can deselect them to automatically remove the visible selection.

If at any point you want to unselect your selections (and get rid of anything that has been removed but still shows up on screen) you can add a:
resetSelection();
line. This will unselect ALL currently selected objects, so make sure to only use it if you want nothing selected prior to what happens next in your scripts.

I am not sure it was ever made clear what you meant by “Mark the new annotation”, but if you only have one annotation in the image, and you want to select it, you could use
selectAnnotations();
instead of doing it manually.

1 Like

Hello together,
we are pretty close to the breakthrough :smiley:

And I guess Reseach_Associate pointed exactly to the missing point.

I have 2 scripts now. After the first one has done it’s job I have to do one manual step and than I can run the second script and receive exactly what I want to.

What I do is, that I draw my line, run the script and receive the expanded annotations around my line (green arrow).

The script also delets the line (although it is still visible in the first moment but I learned now, why). If I run my second script immediately after the first one I get the following message:
WARN: Unable to set parameter thresholdCompartment with value Nucleus: Red mean
WARN: Unable to set parameter thresholdPositive1 with value 10.0
WARN: Unable to set parameter thresholdPositive2 with value 20.0
WARN: Unable to set parameter thresholdPositive3 with value 30.0
WARN: Unable to set parameter singleThreshold with value true
INFO: 0 nuclei detected (processing time: 0.35 seconds)
INFO: Processing complete in 0.35 seconds
INFO: Completed!
INFO:

But If I select the expanded annotation (green arrow) and run my script, everything works perfect.
So my question is if it is possible to select the new annotation (green arrow) automaticaly after the polyline (black arrow) was deleted by the script?

So I need the connecter between my two scripts.
Here are the scripts

Number 1:

Blockquote
import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject
/* Step 2 /
def ann = getAnnotationObjects()[0]
selectObjects(ann)
runPlugin(‘qupath.lib.plugins.objects.DilateAnnotationPlugin’, ‘{“radiusMicrons”: 9.0, “lineCap”: “Round”, “removeInterior”: false, “constrainToParent”: true}’);
/
Step 3 */
removeObject(ann, true)
resolveHierarchy()

Number 2:

Blockquote
runPlugin(‘qupath.imagej.detect.cells.WatershedCellDetection’, ‘{“detectionImage”: “Red”, “requestedPixelSizeMicrons”: 0.5, “backgroundRadiusMicrons”: 40.0, “medianRadiusMicrons”: 0.0, “sigmaMicrons”: 7.0, “minAreaMicrons”: 30.0, “maxAreaMicrons”: 1000.0, “threshold”: 50.0, “watershedPostProcess”: true, “cellExpansionMicrons”: 0.1, “includeNuclei”: true, “smoothBoundaries”: true, “makeMeasurements”: true, “thresholdCompartment”: “Nucleus: Red mean”, “thresholdPositive1”: 10.0, “thresholdPositive2”: 20.0, “thresholdPositive3”: 30.0, “singleThreshold”: true}’);

import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject

def toRemove = getSelectedObjects()
def imagePlane = getCurrentViewer().getImagePlane()
resolveHierarchy()
// Get all selected objects

getSelectedObjects().each{
List points =
it.getChildObjects().each{ cell->
double x = cell.getROI().getCentroidX()
double y = cell.getROI().getCentroidY()
points << [x,y]
}

pointsX = []
pointsY = []


//Use the bounding box to decide if the points should be sorted by left to right or top to bottom.
def roi = it.getROI()
def x = roi.getBoundsX()
def y = roi.getBoundsY()
def width = roi.getBoundsWidth()
def height = roi.getBoundsHeight()
if (roi.getBoundsWidth() > roi.getBoundsHeight()){
//sort by X
    points.sort({a,b -> a[0]<=>b[0]})
    
    points.each{pointsX << it[0] ; pointsY << it[1]}
    print pointsX
    newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
}else{
//otherwise sort by Y
    points.sort({a,b -> a[1]<=>b[1]})
    points.each{pointsX << it[0]; pointsY << it[1]}
    
    newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
}
    //newRoi = ROIs.createLineROI(x, y, x, y+height, imagePlane)
 
// Create new Line annotation object (with no class)
def newAnn = new PathAnnotationObject(newRoi, null)

// Add it
addObject(newAnn)

clearSelectedObjects(true);

}

// Remove previous objects and update
//removeObjects(toRemove, false)
resolveHierarchy()

import qupath.lib.objects.PathAnnotationObject

xs =
ys =
getCellObjects().forEach {
xs << it.getROI().getCentroidX()
ys << it.getROI().getCentroidY()
}

// Create ROI

def roi = ROIs.createPointsROI(xs as double, ys as double, getCurrentViewer().getImagePlane())

// Create annotation from ROI

def newAnn = new PathAnnotationObject(roi, null)

// Add it to hierarchy

addObject(newAnn)

// Resolve hierarchy

resolveHierarchy()

clearDetections()

So you are aware, other users generally cannot copy scripts from posts when you format them like that. They need to be formatted as code, as indicated in the post above.

Did you try selecting it as the previous post indicated?

Sorry, now I know how to do so.

So I tried it and it works for my first cell row. When I try to measure the second row, it always jumps back to the first row again. In the end I would like to measure 3 cell rows.

My final code is here now:

import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject


/* Step 2 */
def ann = getAnnotationObjects()[0]
selectObjects(ann)
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 9.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');

/* Step 3 */
removeObject(ann, true)
resolveHierarchy()

selectAnnotations()

runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImage": "Red",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 40.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 7.0,  "minAreaMicrons": 30.0,  "maxAreaMicrons": 1000.0,  "threshold": 50.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 0.1,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true, "thresholdCompartment": "Nucleus: Red mean",  "thresholdPositive1": 10.0,  "thresholdPositive2": 20.0,  "thresholdPositive3": 30.0,  "singleThreshold": true}');

import qupath.lib.roi.ROIs
import qupath.lib.objects.PathAnnotationObject

def toRemove = getSelectedObjects()
def imagePlane = getCurrentViewer().getImagePlane()
resolveHierarchy()
// Get all selected objects

getSelectedObjects().each{
    List<double> points = []
    it.getChildObjects().each{ cell->
        double x = cell.getROI().getCentroidX()
        double y = cell.getROI().getCentroidY()
        points << [x,y]
    }

    pointsX = []
    pointsY = []


    //Use the bounding box to decide if the points should be sorted by left to right or top to bottom.
    def roi = it.getROI()
    def x = roi.getBoundsX()
    def y = roi.getBoundsY()
    def width = roi.getBoundsWidth()
    def height = roi.getBoundsHeight()
    if (roi.getBoundsWidth() > roi.getBoundsHeight()){
    //sort by X
        points.sort({a,b -> a[0]<=>b[0]})
        
        points.each{pointsX << it[0] ; pointsY << it[1]}
        print pointsX
        newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
    }else{
    //otherwise sort by Y
        points.sort({a,b -> a[1]<=>b[1]})
        points.each{pointsX << it[0]; pointsY << it[1]}
        
        newRoi = ROIs.createPolylineROI(pointsX as double[],pointsY as double[], imagePlane)
    }
        //newRoi = ROIs.createLineROI(x, y, x, y+height, imagePlane)
     
    // Create new Line annotation object (with no class)
    def newAnn = new PathAnnotationObject(newRoi, null)

    // Add it
    addObject(newAnn)

clearSelectedObjects(true);

}

// Remove previous objects and update
//removeObjects(toRemove, false)
resolveHierarchy()

import qupath.lib.objects.PathAnnotationObject

xs = []
ys = []
getCellObjects().forEach {
    xs << it.getROI().getCentroidX()
    ys << it.getROI().getCentroidY()
}

// Create ROI

def roi = ROIs.createPointsROI(xs as double[], ys as double[], getCurrentViewer().getImagePlane())

// Create annotation from ROI

def newAnn = new PathAnnotationObject(roi, null)

// Add it to hierarchy

addObject(newAnn)

// Resolve hierarchy

resolveHierarchy()

clearDetections()

Regarding the warning messages here:

It looks like these are the parameters for the positive cell detection, rather than the ‘simple’ cell detection. This explains why it can’t set the different thresholds (positive cell detection can either have a single threshold or multiple ones (1+, 2+, 3+)). You can replace this line with the line automatically generated after performing cell detection.

This line in the script will always get your first annotation:

def ann = getAnnotationObjects()[0]

This needs to be changed to something like:

getAnnotationObjects().each {
    ...
}

I haven’t tested this, but you can give it a try. Draw your 3 polylines, then run this script that I slightly modified:

import qupath.lib.roi.ROIs

getAnnotationObjects().each { ann ->
    selectObjects(ann)
    runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 9.0,  "lineCap": "Round",  "removeInterior": false,  "constrainToParent": true}');

    removeObject(ann, true)
    resolveHierarchy()
}

selectAnnotations()
runPlugin('qupath.imagej.detect.cells.WatershedCellDetection', '{"detectionImage": "Red",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 40.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 7.0,  "minAreaMicrons": 30.0,  "maxAreaMicrons": 1000.0,  "threshold": 50.0,  "watershedPostProcess": true,  "cellExpansionMicrons": 0.1,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true, "thresholdCompartment": "Nucleus: Red mean"}');

resolveHierarchy()

def imagePlane = getCurrentViewer().getImagePlane()
def toDelete = []
getAnnotationObjects().each { ann ->
    List<double> points = []
    ann.getChildObjects().each { cell ->
        double x = cell.getROI().getCentroidX()
        double y = cell.getROI().getCentroidY()
        points << [x,y]
    }
    
    pointsX = []
    pointsY = []


    //Use the bounding box to decide if the points should be sorted by left to right or top to bottom.
    def roi = ann.getROI()
    def x = roi.getBoundsX()
    def y = roi.getBoundsY()
    def width = roi.getBoundsWidth()
    def height = roi.getBoundsHeight()
    if (roi.getBoundsWidth() > roi.getBoundsHeight()){
        //sort by X
        points.sort({a,b -> a[0]<=>b[0]})
    } else {
        //otherwise sort by Y
        points.sort({a,b -> a[1]<=>b[1]})
    }
    points.each{pointsX << it[0]; pointsY << it[1]}
    newRoi = ROIs.createPolylineROI(pointsX as double[], pointsY as double[], imagePlane)
     
    // Create new Line annotation object (with no class)
    def newAnn = PathObjects.createAnnotationObject(newRoi)

    // Add it
    addObject(newAnn)
    
    // Remember to delete it 
    toDelete << ann
}

removeObjects(toDelete, true)
resolveHierarchy()
    
xs = []
ys = []
getCellObjects().forEach {
    xs << it.getROI().getCentroidX()
    ys << it.getROI().getCentroidY()
}

// Create ROI

def roi = ROIs.createPointsROI(xs as double[], ys as double[], getCurrentViewer().getImagePlane())

// Create annotation from ROI
def newAnn = PathObjects.createAnnotationObject(roi)

// Add it to hierarchy
addObject(newAnn)

// Resolve hierarchy
resolveHierarchy()
clearDetections()

1 Like

Thanks for your help.

Actually I am not sure. I just took the script I found here in the forum and it worked :see_no_evil:

The script worked like you described it. However in this case an other problem arises.


This is an example of one of my samples when the work is done. The green, blue and magenta annotations have to be drawn by hand as I differentiate the different stages of cell development of the current tree ring there. This cannot be automatized as it really needs experience.
In the previous tree rings (black and zyan) all cells are completely developed and only have to be counted. All the points and lines in the current year are already drawn when we start to count the previous tree rings.
The script takes all annotations into acount, so I cannot use it in the way I would like to, as it also applys the steps on the annotations of the previous tree ring.

So if there is a solution that I can run a script after I have drawn 1 polyline this would be perfect. If not, I am already very much faster now, with my 2 scripts solution than before.

Thanks again for your help and patience

If the polylines you want to run the script on are classified (which can essentially mean giving them a color as in your image), you can use
getAnnotationObjects().findAll{it.getPathClass() == getPathClass("The class of the desired polylines")}

Also, this is why you need to describe your entire project when asking questions. Leaving parts out will make scripts interact… badly, with other things that exist in the image.

1 Like

A good solution for this would be to assign a classification to your annotation manually as you draw them, then filter them out when processing them:

getAnnotationObjects().each { ann ->
    if (ann.getPathClass() != getPathClass('yourClassHere'))
        return
1 Like

With this change of code everything disappears except the selected class

Yep, all annotations are cycled through and deleted in the next step.

Could you draw the polylines you want to process first, and then go back and add the ones you don’t want to process (after running the combined script)?