Cell density around vessel

@Research_Associate @melvingelbard @petebankhead @iqmmug

hi everyone, these days I have made some reflections and I have come to these conclusions …

Starting from the beginning:

  1. I created an annotation called PBZ (this is because the process should be performed on a mega-annotation derived from a pixel classifier, so I created a rectangular one to simplify)

  2. I then ran the following script (read the notes that describe each step)

// Select PBZ Annotation

selectObjectsByClassification("PBZ");

//Create Tiles and Delete PBZ Annotation
runPlugin('qupath.lib.algorithms.TilerPlugin', '{"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}');

//Set "Tile" class to tile

getAnnotationObjects().each{

    it.setPathClass(getPathClass("Tile"))
}


//Positive cell detection and delete positive cell

selectObjectsByClassification("Tile");

runPlugin('qupath.imagej.detect.cells.PositiveCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 20.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}');

selectObjectsByClassification("Positive");
clearSelectedObjects();

//Create vessel from Classifier (load it)
selectObjectsByClassification("Tile");
createAnnotationsFromPixelClassifier("vasi ignore", 100.0, 100000.0, "SPLIT")

//Expand Vessels+Remove interior creating Rings. Ring= vessel; Vessel=VesselCenter 
vessels = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
vessels.eachWithIndex{v,i->
    v.setName(i.toString())
}
selectObjectsByClassification("Vessel");
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 20.0,  "lineCap": "Round",  "removeInterior": true,  "constrainToParent": true}');
resolveHierarchy()
vessels.each{it.setPathClass(getPathClass("VesselCenter"))}



The result is the following:

I got the vases (Class VesselCenter) and the ring around them (Class Vessel), related by the same number, contained in the same Tile (Parent)

(Negative) cells respect the hierarchy

  1. At this point I should look for the ratio between the cell density of the ring and the cell density outside the ring.
    Initially I asked you to use the density of the Tile, however it is underestimated as the Area of the tile also includes the area of vessels and vesselcenters.
    I therefore thought of eliminating the sum of these areas from the area of the tile in the calculation of the formula (hoping that the areas of the rings do not cross too much, I should get a fairly precise estimate).

An alternative would be to merge (merging) vessel and vessel ceneter annotation, resolve hierarchy (because the annotation obtained, by default, has as Parent: Image, while I want it to be contained in the Tile), Make inverse and I get the yellow area of the figure (it would be very very very nice if it were possible, the calculations would certainly be more precise)

I kindly ask you to help me and create a script that:
a) calculate the cell density of the rings (Class Vessel) in mm2
Formula = Num Cell / Area Ring

b) Calculate the cell density of the yellow area (cell density/ (Area Tile - (Sum of Vessel and Vessel Centers areas))

c) VCO Ratio = Ring Cell density / Yellow Area Cell Density
VCO Ratio should be positioned in a new column at the VesselCenter row corresponding to the Vessel (Ring)

Thank you in advance, I cross my fingers

1 Like

Hi everyone.
Thanks to the @Research_Associate correction (there was actually an error in the tile loop) the error in the Tile density was solved.

However, the error in local density remains

Can someone help me?
This is the script (starting from an annotation: class “PBZ”):

// Select PBZ Annotation

selectObjectsByClassification("PBZ");

//Create Tiles and Delete PBZ Annotation
runPlugin('qupath.lib.algorithms.TilerPlugin', '{"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}');


//Set "Tile" class to tile

getAnnotationObjects().each{

    it.setPathClass(getPathClass("Tile"))
}

//Positive cell detection and delete positive cell

selectObjectsByClassification("Tile");

runPlugin('qupath.imagej.detect.cells.PositiveCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 20.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}');

selectObjectsByClassification("Positive");
clearSelectedObjects();

//Create vessel from Classifier (load it)
selectObjectsByClassification("Tile");
createAnnotationsFromPixelClassifier("vasi ignore", 100.0, 100000.0, "SPLIT")







imageData = getCurrentImageData()
cal=imageData.getServer().getPixelCalibration()
pixelSize=cal.getAveragedPixelSize()

//Get a list of tiles
tiles = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Tile")}
//Go through each tile and find the density
tiles.each{
    
    totalCells = []
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    it.getMeasurementList().putMeasurement("Tile Density /mm^2", negativeCells.size()/area)
}

//Go through each vessel and index them. This will allow us to find the pairing vessel ring later
vessels = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
vessels.eachWithIndex{v,i->
    v.setName(i.toString())
}
//Perform the expansion on the Vessel objects. You will need to adjust this distance, set to 20.0 currently
selectObjectsByClassification("Vessel");
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 20.0,  "lineCap": "Round",  "removeInterior": true,  "constrainToParent": true}');

//Relabel the center objects
vessels.each{it.setPathClass(getPathClass("VesselCenter"))}

//Find the annotations that are rings for later
rings = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
//Make sure cells are counted within the right annotations
resolveHierarchy()

//In each ring, calculate the local density
rings.each{
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    it.getMeasurementList().putMeasurement("Local Density /mm^2", negativeCells.size()/area)
    
}
//In each vessel, use the local density of its ring, and the density of its parent tile, to calculate the VCO
vessels.each{v->

    currentRing = rings.find{v.getName().equals(it.getName())}
    v.getMeasurementList().putMeasurement("Local Density /mm^2",measurement(currentRing, "Local Density /mm^2"))
    tile = v.getParent()
    print tile
    v.getMeasurementList().putMeasurement("VCO", measurement(v,"Local Density /mm^2")/measurement(tile,"Tile Density /mm^2"))
}

//remove the rings if you want. Uncomment the line below.
//removeObjects(rings, true)

import qupath.lib.objects.PathCellObject

The local density can be fixed in the same way as the tile density, add in a statement to clear the totalCells. I edited the script above to fix this.
totalCells = []

I admit, I’ve been spoiled by getChildObjects() which works for most of my project structures, and will overwrite the previous variable. It seems using getDescendantObjects does not clear the variable.

1 Like

This was more reliable in v0.1.2, but isn’t so good in >= v0.2.0 because one cannot always rely on the hierarchy (depending upon how objects were created, and if relationships between them were explicitly resolved).

Edit: It also wasn’t terribly reliable in v0.1.2, since it is influenced by whether objects are nested further… in general, I’d usually stay away from calling getChildObjects() in a script directly unless the same script has created the objects in the first place (and therefore it is totally clear where everything should be).

2 Likes

Oh, I agree. I have been very careful to make sure it works in my own projects (along with judicious use of resolveHierarchy() ) :slight_smile: It is a little harder poking my nose into someone else’s project without being able to really see what they are doing!

In this case, based on his above post, he might actually want something like getChildObjects().findAll{it.isDetection()}
since it implies (or Tiles) that he does not want to include the cell counts (or areas) from the vessels. Calculating the total area to divide the child cells by would be a little more complicated though, since it would involve also subtracting out the vessels and their rings.

Hmmm, I haven’t followed the thread closely but it may fail if detections are contained within annotations that are within annotations. But it may fail subtly because it may give a plausible-looking result.

There is a method in the hierarchy

def detections = hierarchy.getObjectsForROI(PathDetectionObject, roi)

that uses pixel location relative to the ROI shape rather than hierarchy location. This is the method normally used internally by QuPath v0.2.0+ (for measurements, classifier training etc.).

It can potentially still fail if the previous steps have been particularly weird (e.g. modifying the hierarchy, but suppressing any change events so that the spatial cache is out-of-date)… but shouldn’t in normal use.

In general, complicated scripts that rely upon specific hierarchy behavior can easily go wrong. I think they should be used and shared with some caution…

2 Likes

Yeah, I was thinking, based on their last post, that excluding detections contained within annotations that are within annotations is exactly what they are looking for. Though that was not what I believed 4-5 posts ago!

:slight_smile: :pensive: :frowning: This really seems to be one of those specific use cases where it is necessary, though, due to the weirdness of the analysis.
I can’t think of any non-complicated ways to do what was described here:


but that single post probably has the most detailed description of what is going on, if you can come up with something more streamlined.

Fair enough, sorry I really haven’t had time to get into the details here – I’ve just been dipping in and out.

The main thing is to check everything :slight_smile: I often resort to manually counting cells in specific regions to make sure I’m getting the results I expect…

1 Like

No worries, some of the issue is that the details have kept changing! And some of it has been my coding :frowning:

2 Likes

BTW, this is fixed thanks to @melvingelbard - merged into the master today.

2 Likes

I’m sorry I changed my requests during the conversation, but you often notice errors in the process :frowning:
In this case it seems to me important to correct the calculation of the cell density of the tile (by subtracting AT LEAST the area of the vesselcenters to reduce the error. ALSO subtracting the area of the vessel (rings) would be excellent, but often cross each other).
If this is not possible, I will be satisfied. Meanwhile, thank you again, you have been very important

1 Like

Yes, for each tile you could do something like (typed here, have not checked, assumes all annotations are children and the hierarchy has been resolved, assumes all child annotations should be subtracted)

childAnnotations = it.getChildObjects().findAll{it.isAnnotation()}
//based on Pete's code above you might be able to replace that with 
//childAnnotations = getCurrentHierarchy().getObjectsForROI(PathAnnotationObject, it.getROI() )

tileArea = it.getROI().getArea()
areaToSubtract = 0
childAnnotations.each{a->
areaToSubtract = areaToSubtract+ a.getROI().getArea()
}
finalArea = (tileArea - areaToSubtract) *pixelSize*pixelSize/1000000

Something like that inserted into the Tile loop should give you a measurement that you can use instead of the tile area when calculating the tile density. You would then replace the Tile Density calculation with

negativeCells.size()/finalArea

This change would need to be combined with changing the way of getting negative cells as shown in one of the previous posts: Cell density around vessel
Otherwise you would still be dividing the reduced area by the total number of cells.

I would recommend making the area change first, and see if that is working, then change the negative cell counts.

*Fixed at least one typo.

1 Like

sorry, but I don’t really know where to add this part. Could you edit this script and paste it correct?

// Select PBZ Annotation

selectObjectsByClassification("PBZ");

//Create Tiles and Delete PBZ Annotation
runPlugin('qupath.lib.algorithms.TilerPlugin', '{"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}');


//Set "Tile" class to tile

getAnnotationObjects().each{

    it.setPathClass(getPathClass("Tile"))
}

//Positive cell detection and delete positive cell

selectObjectsByClassification("Tile");

runPlugin('qupath.imagej.detect.cells.PositiveCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}');

selectObjectsByClassification("Positive");
clearSelectedObjects();

//Create vessel from Classifier (load it)
selectObjectsByClassification("Tile");
createAnnotationsFromPixelClassifier("vasi ignore", 100.0, 100000.0, "SPLIT")







imageData = getCurrentImageData()
cal=imageData.getServer().getPixelCalibration()
pixelSize=cal.getAveragedPixelSize()

//Get a list of tiles
tiles = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Tile")}
//Go through each tile and find the density
tiles.each{
    
    totalCells = []
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    it.getMeasurementList().putMeasurement("Tile Density /mm^2", negativeCells.size()/area)
}

//Go through each vessel and index them. This will allow us to find the pairing vessel ring later
vessels = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
vessels.eachWithIndex{v,i->
    v.setName(i.toString())
}
//Perform the expansion on the Vessel objects. You will need to adjust this distance, set to 20.0 currently
selectObjectsByClassification("Vessel");
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 20.0,  "lineCap": "Round",  "removeInterior": true,  "constrainToParent": true}');

//Relabel the center objects
vessels.each{it.setPathClass(getPathClass("VesselCenter"))}

//Find the annotations that are rings for later
rings = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
//Make sure cells are counted within the right annotations
resolveHierarchy()

//In each ring, calculate the local density
rings.each{
    totalCells = []
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    it.getMeasurementList().putMeasurement("Local Density /mm^2", negativeCells.size()/area)
    
}
//In each vessel, use the local density of its ring, and the density of its parent tile, to calculate the VCO
vessels.each{v->

    currentRing = rings.find{v.getName().equals(it.getName())}
    v.getMeasurementList().putMeasurement("Local Density /mm^2",measurement(currentRing, "Local Density /mm^2"))
    tile = v.getParent()
    print tile
    v.getMeasurementList().putMeasurement("VCO", measurement(v,"Local Density /mm^2")/measurement(tile,"Tile Density /mm^2"))
}

//remove the rings if you want. Uncomment the line below.
//removeObjects(rings, true)

import qupath.lib.objects.PathCellObject

I added the code but have not tested it.

// Select PBZ Annotation

selectObjectsByClassification("PBZ");

//Create Tiles and Delete PBZ Annotation
runPlugin('qupath.lib.algorithms.TilerPlugin', '{"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}');


//Set "Tile" class to tile

getAnnotationObjects().each{

    it.setPathClass(getPathClass("Tile"))
}

//Positive cell detection and delete positive cell

selectObjectsByClassification("Tile");

runPlugin('qupath.imagej.detect.cells.PositiveCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}');

selectObjectsByClassification("Positive");
clearSelectedObjects();

//Create vessel from Classifier (load it)
selectObjectsByClassification("Tile");
createAnnotationsFromPixelClassifier("vasi ignore", 100.0, 100000.0, "SPLIT")

resolveHierarchy()  //Make sure cells are all children of the appropriate annotations.

imageData = getCurrentImageData()
cal=imageData.getServer().getPixelCalibration()
pixelSize=cal.getAveragedPixelSize()

//Get a list of tiles
tiles = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Tile")}
//Go through each tile and find the density
tiles.each{
    
    totalCells = it.getChildObjects().findAll{d-> d.isDetection()}
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    childAnnotations = it.getChildObjects().findAll{it.isAnnotation()}
    //based on Pete's code above you might be able to replace that with 
    //childAnnotations = getCurrentHierarchy().getObjectsForROI(PathAnnotationObject, it.getROI() )
    tileArea = it.getROI().getArea()
    areaToSubtract = 0
    childAnnotations.each{a->
        areaToSubtract = areaToSubtract+ a.getROI().getArea()
    }
    finalArea = (tileArea - areaToSubtract) *pixelSize*pixelSize/1000000
    
    /////////////////FOR TESTING, REMOVE NEXT TWO LINES LATER///////////
    it.getMeasurementList().putMeasurement("Tile area excluding vessels", finalArea)
    it.getMeasurementList().putMeasurement("Negative cells outside vessels", negativeCells.size())
    //////////////////////////////////////////////////////////////////////////////
    it.getMeasurementList().putMeasurement("Tile Density /mm^2", negativeCells.size()/finalArea)
}

//Go through each vessel and index them. This will allow us to find the pairing vessel ring later
vessels = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
vessels.eachWithIndex{v,i->
    v.setName(i.toString())
}
//Perform the expansion on the Vessel objects. You will need to adjust this distance, set to 20.0 currently
selectObjectsByClassification("Vessel");
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 20.0,  "lineCap": "Round",  "removeInterior": true,  "constrainToParent": true}');

//Relabel the center objects
vessels.each{it.setPathClass(getPathClass("VesselCenter"))}

//Find the annotations that are rings for later
rings = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
//Make sure cells are counted within the right annotations
resolveHierarchy()

//In each ring, calculate the local density
rings.each{
    totalCells = []
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    it.getMeasurementList().putMeasurement("Local Density /mm^2", negativeCells.size()/area)
    
}
//In each vessel, use the local density of its ring, and the density of its parent tile, to calculate the VCO
vessels.each{v->

    currentRing = rings.find{v.getName().equals(it.getName())}
    v.getMeasurementList().putMeasurement("Local Density /mm^2",measurement(currentRing, "Local Density /mm^2"))
    tile = v.getParent()
    print tile
    v.getMeasurementList().putMeasurement("VCO", measurement(v,"Local Density /mm^2")/measurement(tile,"Tile Density /mm^2"))
}

//remove the rings if you want. Uncomment the line below.
//removeObjects(rings, true)

import qupath.lib.objects.PathCellObject

It also adds two new measurements to the tiles for troubleshooting.

1 Like

it doesn’t work. it loads the classifier, finds the vessels, but stops before index them

INFO: Processing complete in 0.03 seconds
INFO: Completed!
INFO: 
qupath.lib.algorithms.TilerPlugin  {"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}
INFO: 1071 nuclei detected (processing time: 4.92 seconds)
INFO: 1252 nuclei detected (processing time: 5.23 seconds)
INFO: 1351 nuclei detected (processing time: 5.43 seconds)
INFO: 1457 nuclei detected (processing time: 5.74 seconds)
INFO: 896 nuclei detected (processing time: 2.21 seconds)
INFO: 911 nuclei detected (processing time: 2.66 seconds)
INFO: Processing complete in 7.78 seconds
INFO: Completed!
INFO: 
qupath.imagej.detect.cells.PositiveCellDetection  {"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}
WARN: Resolving hierarchy that contains 86 annotations and 5627 detections - this may be slow!
ERROR: MissingMethodException at line 38: No signature of method: org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getChildObjects() is applicable for argument types: () values: []

ERROR: org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.callGlobal(GroovyScriptEngineImpl.java:404)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.access$100(GroovyScriptEngineImpl.java:90)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl$3.invokeMethod(GroovyScriptEngineImpl.java:303)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl$3.invokeMethod(GroovyScriptEngineImpl.java:292)
    groovy.lang.GroovyObject.invokeMethod(GroovyObject.java:39)
    groovy.lang.Script.invokeMethod(Script.java:77)
    org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeOnDelegationObjects(ClosureMetaClass.java:397)
    org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:337)
    org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:61)
    org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:51)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:171)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:176)
    Script4$_run_closure3.doCall(Script4.groovy:41)
    java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    java.base/java.lang.reflect.Method.invoke(Unknown Source)
    org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:107)
    groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
    org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
    groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1029)
    groovy.lang.Closure.call(Closure.java:412)
    groovy.lang.Closure.call(Closure.java:428)
    org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2318)
    org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2303)
    org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:2344)
    org.codehaus.groovy.runtime.dgm$200.invoke(Unknown Source)
    org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:247)
    org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:56)
    org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:139)
    Script4.run(Script4.groovy:39)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155)
    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)


1 Like

Ooops. There should have been an “it” in front of getChildObjects(). It did not know what to get the child objects of! It is fixed in the code above.

1 Like

It works! only one thing I don’t understand. Now from the tile area subtract the sum of the areas of the vesselcenters or the sum of the areas of the vesselcenters AND rings?
I made the calculations and it seems that it only excludes vessels (vesselcenters). Would rings (vessels) also be excluded?

As described, it subtracts all child annotations. It might be worth checking if both the rings and the centers are both child annotations in the Hierarchy tab. Making a very small tile around one ring should also let you test whether it is working the way you want.

1 Like

In fact, in the code you gave me I noticed that there was talk of “ChildAnnotations”. I tried to do the calculations, the hierarchy is correct, but it seems to subtract only the vesselcenters.

This is a simplified case with a single vessel.
“Tile area excluding vessels” is 0.009 (which is the approximation of 0.00879), that is (9211.6 - 415.15) / 1000000. Does not take into account the ring area (class “vessel”)

I think I understand the reason. The rings are created after the calculation of the “tile density excluding vessels”

Rings must be created first, resolve hierarchy so that ring cells are not counted in the final area

I think that’s right

// Select PBZ Annotation

selectObjectsByClassification("PBZ");

//Create Tiles and Delete PBZ Annotation
runPlugin('qupath.lib.algorithms.TilerPlugin', '{"tileSizeMicrons": 500.0,  "trimToROI": true,  "makeAnnotations": true,  "removeParentAnnotation": true}');


//Set "Tile" class to tile

getAnnotationObjects().each{

    it.setPathClass(getPathClass("Tile"))
}

//Positive cell detection and delete positive cell

selectObjectsByClassification("Tile");

runPlugin('qupath.imagej.detect.cells.PositiveCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD",  "requestedPixelSizeMicrons": 0.5,  "backgroundRadiusMicrons": 8.0,  "medianRadiusMicrons": 0.0,  "sigmaMicrons": 1.5,  "minAreaMicrons": 10.0,  "maxAreaMicrons": 400.0,  "threshold": 0.08,  "maxBackground": 2.0,  "watershedPostProcess": true,  "excludeDAB": false,  "cellExpansionMicrons": 2.0,  "includeNuclei": true,  "smoothBoundaries": true,  "makeMeasurements": true,  "thresholdCompartment": "Nucleus: DAB OD mean",  "thresholdPositive1": 0.2,  "thresholdPositive2": 0.4,  "thresholdPositive3": 0.6000000000000001,  "singleThreshold": true}');

selectObjectsByClassification("Positive");
clearSelectedObjects();

//Create vessel from Classifier (load it)
selectObjectsByClassification("Tile");
createAnnotationsFromPixelClassifier("vasi ignore", 100.0, 100000.0, "SPLIT")

//Go through each vessel and index them. This will allow us to find the pairing vessel ring later
vessels = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
vessels.eachWithIndex{v,i->
    v.setName(i.toString())
}
//Perform the expansion on the Vessel objects. You will need to adjust this distance, set to 20.0 currently
selectObjectsByClassification("Vessel");
runPlugin('qupath.lib.plugins.objects.DilateAnnotationPlugin', '{"radiusMicrons": 20.0,  "lineCap": "Round",  "removeInterior": true,  "constrainToParent": true}');

//Relabel the center objects
vessels.each{it.setPathClass(getPathClass("VesselCenter"))}

//Find the annotations that are rings for later
rings = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Vessel")}
//Make sure cells are counted within the right annotations
resolveHierarchy()

imageData = getCurrentImageData()
cal=imageData.getServer().getPixelCalibration()
pixelSize=cal.getAveragedPixelSize()

//In each ring, calculate the local density
rings.each{
    totalCells = []
    area = it.getROI().getArea()*pixelSize*pixelSize/1000000
    qupath.lib.objects.PathObjectTools.getDescendantObjects(it,totalCells, PathCellObject)
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    it.getMeasurementList().putMeasurement("Local Density /mm^2", negativeCells.size()/area)
    
}

resolveHierarchy()  //Make sure cells are all children of the appropriate annotations.



//Get a list of tiles
tiles = getAnnotationObjects().findAll{it.getPathClass() == getPathClass("Tile")}
//Go through each tile and find the density
tiles.each{
    
    totalCells = it.getChildObjects().findAll{d-> d.isDetection()}
    negativeCells = totalCells.findAll{it.getPathClass() == getPathClass("Negative")}
    childAnnotations = it.getChildObjects().findAll{it.isAnnotation()}
    //based on Pete's code above you might be able to replace that with 
    //childAnnotations = getCurrentHierarchy().getObjectsForROI(PathAnnotationObject, it.getROI() )
    tileArea = it.getROI().getArea()
    areaToSubtract = 0
    childAnnotations.each{a->
        areaToSubtract = areaToSubtract+ a.getROI().getArea()
    }
    finalArea = (tileArea - areaToSubtract) *pixelSize*pixelSize/1000000
    
    /////////////////FOR TESTING, REMOVE NEXT TWO LINES LATER///////////
    it.getMeasurementList().putMeasurement("Tile area excluding vessels", finalArea)
    it.getMeasurementList().putMeasurement("Negative cells outside vessels", negativeCells.size())
    //////////////////////////////////////////////////////////////////////////////
    it.getMeasurementList().putMeasurement("Tile Density /mm^2", negativeCells.size()/finalArea)
}


//In each vessel, use the local density of its ring, and the density of its parent tile, to calculate the VCO
vessels.each{v->

    currentRing = rings.find{v.getName().equals(it.getName())}
    v.getMeasurementList().putMeasurement("Local Density /mm^2",measurement(currentRing, "Local Density /mm^2"))
    tile = v.getParent()
    print tile
    v.getMeasurementList().putMeasurement("VCO", measurement(v,"Local Density /mm^2")/measurement(tile,"Tile Density /mm^2"))
}

//remove the rings if you want. Uncomment the line below.
//removeObjects(rings, true)

import qupath.lib.objects.PathCellObject

I created another topic to continue the project (the topic was not inherent in the project, it has a more generic technical aspect)

(I also earn First link badge) :slight_smile:

2 Likes