Scripting assistance and development

Hi,

I have a script a collaborator created for me, but I was wondering if someone could help me modify it. Currently, it creates and determines the largest potential polygon of the green lines and assigns it the class “Greatest GM Sampling zone”. Please see the picture script below:

Note lines below are an excerpt from a larger script shown at the end of post.

P=[ ]
imax=polysizes.indexOf(Collections.max(polysizes))
def maxPolyClass=new PathClass('Greatest GM Sampling zone',ColorTools.makeRGB(0,94,32))
for (pA in getCurrentHierarchy().getFlattenedObjectList()[1..-1]){
    if (pA.getROI().getRoiName()=='Polygon'){
        P.add(pA)
    }

P[imax].setPathClass(maxPolyClass)

But I need to add another polygon (with the displayed name of “WM”) to sample a different area of the image. When I run the script with the additional polygon, it assigns the class to the wrong polygon - I understand this is because the size is larger than the one between the red lines - see image below:

What I really need is for the class only to be assigned to newly created polygons created between the red lines (as the WM polygon already exists prior to running the script) - I have attached a picture of the desired output below:

If anyone could please help I would greatly appreciate it. It seems simple, but I have limited coding knowledge.

Thanks in advance!

Here is the entire script:

import qupath.lib.roi.LineROI
import qupath.lib.roi.PolygonROI
import qupath.lib.geom.Point2
import qupath.lib.objects.classes.PathClassFactory
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.objects.classes.PathClass
import qupath.lib.gui.scripting.QPEx
import qupath.lib.common.ColorTools

def imageData = QPEx.getCurrentImageData()
def server = imageData.getServer()

t=server.getAveragedPixelSizeMicrons() //echelle pixel/micron

X1=[]
X2=[]
Y1=[]
Y2=[]
slope=[]
i=0
def double inf=Double.POSITIVE_INFINITY
def double ninf=Double.NEGATIVE_INFINITY
gmSampName=[]
gmHierar=[]
polysizes=[]
//Setting such that the mean total area can deviate by 50um to be considered within the greatest GM ROI - this is equivlent to 0.2mm in HALO protocol
deviation=50
//Sets increments/layer thickness to 250um
step=250
acc=250

for (pathAnnotation in getCurrentHierarchy().getFlattenedObjectList()[1..-1]){
   if (pathAnnotation.getPathClass()==null && pathAnnotation.getROI().getRoiName()=='Line'){
       X1.add(pathAnnotation.getROI().getX1()*t)
       X2.add(pathAnnotation.getROI().getX2()*t)
       Y1.add(pathAnnotation.getROI().getY1()*t)
       Y2.add(pathAnnotation.getROI().getY2()*t)
       slope.add((Y2[i]-Y1[i])/(X2[i]-X1[i]))
       i+=1
   }   
}

0.step X1.size(),2,{
   x11=X1[it]
   y11=Y1[it]
   x21=X2[it]
   y21=Y2[it]
   s1=slope[it]
   d1=Math.sqrt(Math.pow(x21-x11,2)+Math.pow(y21-y11,2))
   
   x12=X1[it+1]
   y12=Y1[it+1]
   x22=X2[it+1]
   y22=Y2[it+1]
   s2=slope[it+1]
   d2=Math.sqrt(Math.pow(x22-x12,2)+Math.pow(y22-y12,2))
   
   testmax1=1e5
   testmax2=1e5
   if (s1==-(double)0){s1=0}
   if (s2==-(double)0){s2=0}
   switch(s1){
       case 0:
           ysop1=0
       if (x11<x21){
           xsop1=250
       }else{
           xsop1=-250
       }
       break
   case inf:
       xsop1=0
       ysop1=250
       break
   case ninf:   
           xsop1=0.
           ysop1=-250
       break
   default:   
       if (s1<0){
           if (x11>=x21 && y11<=y21){
   	    for (int i=1;i<step*acc;i++){
   	        xs1=-i/acc
   		ys1=Math.sqrt(Math.pow(step,2)-Math.pow(xs1,2))
   		test1=Math.abs(ys1/(xs1*s1)-1)
   		if(test1<testmax1){
   		    testmax1=test1
   		    xsop1=xs1
   		    ysop1=ys1
   		}
   	    }
   	}else if (x11<=x21 && y11>=y21){
   	    for (int i=1;i<step*acc;i++){
   		xs1=i/acc
   		ys1=-Math.sqrt(Math.pow(step,2)-Math.pow(xs1,2))
   		test1=Math.abs(ys1/(xs1*s1)-1)
   		if(test1<testmax1){
   		    testmax1=test1
   		    xsop1=xs1
   		    ysop1=ys1
   		}
   	    }
   	}
       }else if (s1>0){
   	if (x11<=x21 && y11<=y21){
   	    for (int i=1;i<step*acc;i++){
   		xs1=i/acc
   	        ys1=Math.sqrt(Math.pow(step,2)-Math.pow(xs1,2))
   		test1=Math.abs(ys1/(xs1*s1)-1)
   		if(test1<testmax1){
   		    testmax1=test1
   		    xsop1=xs1
   		    ysop1=ys1
   		}
   	    }
   	}else if (x11>=x21 && y11>=y21){
   	    for (int i=1;i<step*acc;i++){
   		xs1=-i/acc
   	        ys1=-Math.sqrt(Math.pow(step,2)-Math.pow(xs1,2))
   		test1=Math.abs(ys1/(xs1*s1)-1)
   		if(test1<testmax1){
   		    testmax1=test1
   		    xsop1=xs1
   		    ysop1=ys1
   		}
   	    }
   	}
       }
           break
   }
   
   switch(s2){
       case 0:
           ysop2=0
       if (x12<x22){
           xsop2=250
       }else{
   	        xsop2=-250
   	    }
       break
   case inf:
           xsop2=0
           ysop2=250
       break
   case ninf:   
           xsop2=0
           ysop2=-250
       break
   default: 
       if (s2<0){
           if (x12>=x22 && y12<=y22){
   	    for (int i=1;i<step*acc;i++){
   	        xs2=-i/acc
   		ys2=Math.sqrt(Math.pow(step,2)-Math.pow(xs2,2))
   		test2=Math.abs(ys2/(xs2*s2)-1)
   		if(test2<testmax2){
   		    testmax2=test2
   		    xsop2=xs2
   		    ysop2=ys2
   		}
   	    }
   	}else if (x12<=x22 && y12>=y22){
   	    for (int i=1;i<step*acc;i++){
   		xs2=i/acc
   		ys2=-Math.sqrt(Math.pow(step,2)-Math.pow(xs2,2))
   		test2=Math.abs(ys2/(xs2*s2)-1)
   		if(test2<testmax2){
   		    testmax2=test2
   		    xsop2=xs2
   		    ysop2=ys2
   		}
   	    }
   	}
       }else if (s2>0){
   	if (x12<=x22 && y12<=y22){
   	    for (int i=1;i<step*acc;i++){
   		xs2=i/acc
   	        ys2=Math.sqrt(Math.pow(step,2)-Math.pow(xs2,2))
   		test2=Math.abs(ys2/(xs2*s2)-1)
   		if(test2<testmax2){
   		    testmax2=test2
   		    xsop2=xs2
   		    ysop2=ys2
   		}
   	    }
   	}else if (x12>=x22 && y11>=y22){
   	    for (int i=1;i<step*acc;i++){
   		xs2=-i/acc
   	        ys2=-Math.sqrt(Math.pow(step,2)-Math.pow(xs2,2))
   		test2=Math.abs(ys2/(xs2*s2)-1)
   		if(test2<testmax2){
   		    testmax2=test2
   		    xsop2=xs2
   		    ysop2=ys2
   		}
   	    }
   	}
       }
           break
   }
   classname='GM sampling '+(it/2+1).toString()
   def newPassClass=new PathClass(classname,ColorTools.makeRGB(18,255,87))
   
   while (d1>=2*step && d2>=2*step){ 
       
       x11+=xsop1
       y11+=ysop1
       x12+=xsop2
       y12+=ysop2
               
       d1=Math.sqrt(Math.pow(x21-x11,2)+Math.pow(y21-y11,2))
       d2=Math.sqrt(Math.pow(x22-x12,2)+Math.pow(y22-y12,2))
       
       def roi = new LineROI(x11/t,y11/t,x12/t,y12/t)
       def newLine=new PathAnnotationObject(roi,newPassClass)
       imageData.getHierarchy().addPathObject(newLine,false)
       
   } 
}

for (pathAnnotation in getCurrentHierarchy().getFlattenedObjectList()[1..-1]){
   if (pathAnnotation.getPathClass()!=null && pathAnnotation.getROI().getRoiName()=='Line'){
       if (!gmSampName.contains(pathAnnotation.getPathClass().getName())){
           gmSampName.add(pathAnnotation.getPathClass().getName())
       }
       gmHierar.add(pathAnnotation)
   }
}

for (lines in gmSampName){
   mean=0.0
   N=0
   poly=[]
   for (gmSample in gmHierar){
       if (gmSample.getPathClass().getName()==lines){
           mean+=gmSample.getROI().getLength()*t
           N+=1
       }
   }
   mean/=N
   
   for (gmSample in gmHierar){
       l=Math.abs(gmSample.getROI().getLength()*t-mean)
       if (gmSample.getPathClass().getName()==lines && l<=deviation){
           poly.add(gmSample)
           //imageData.getHierarchy().removeObject(gmSample,false)
       }
   }
   polysizes.add(poly.size()*step)
   def point1 = new Point2(poly[0].getROI().getX1(),poly[0].getROI().getY1())
   def point2 = new Point2(poly[0].getROI().getX2(),poly[0].getROI().getY2())
   def point3 = new Point2(poly[-1].getROI().getX1(),poly[-1].getROI().getY1())
   def point4 = new Point2(poly[-1].getROI().getX2(),poly[-1].getROI().getY2())
   points=[point1,point3,point4,point2,point1]
   
   def roi = new PolygonROI(points)
   def newPoly=new PathAnnotationObject(roi)
   imageData.getHierarchy().addPathObject(newPoly,false)
   
}

P=[]
imax=polysizes.indexOf(Collections.max(polysizes))
def maxPolyClass=new PathClass('Greatest GM Sampling zone',ColorTools.makeRGB(0,94,32))
for (pA in getCurrentHierarchy().getFlattenedObjectList()[1..-1]){
   if (pA.getROI().getRoiName()=='Polygon'){
       P.add(pA)
   }
}

P[imax].setPathClass(maxPolyClass)

It looks like this is 0.1.2 or 0.1.3? I am guessing you would just need to select by class when getting your input, but I will take a look.

Actually, I take that back, the code doesn’t seem to be working for any version of QuPath that I have tried.

Sorry, I am using v0.2.0-m2

Ahah, ok. My least favourite version (portability and file issues!). Will have to load that one up.

I’m not sure I understand the problem. It ignores all other polygons in the image, and only changes the classes of the newly created polygons, which is what I thought you wanted. Any larger polygons I create are ignored.

How are you generating the other polygon?

Edit, I think I see, the other object was created using a similar method, you deleted all of the extra stuff, and then ran it again?
**Further edit, ah, it is because it is a polygon. I created a rectangle object.

1 Like

So I start by drawing the parallel lines and drawing the polygon. Then I rename the existing polygon “WM”.

The other polygon (in the class “Greatest GM Sampling zone”) is created using the script in the post. I tried to use “getDisplayedName().toString()” but unfortunately I don’t believe it is a method for the polygon ROI

Ok, try: *Edit for clarity, replace the line that looks like this one with this one
if (pA.getROI().getRoiName()=='Polygon' && pA.getPathClass() == null){
at line 266ish.
Then it will ignore any object that already has a class.

I swapped the whole section around a little bit, as getAnnotationObjects is a bit more intuitive for me, but that part shouldn’t change anything.

for (pA in getAnnotationObjects()){
   if (pA.getROI().getRoiName()=='Polygon' && pA.getPathClass() == null){
       P.add(pA)
   }
}
1 Like

Hmm… its seeming to work for some images but not all of the images in the project…

That specifically excludes any objects where you have added a class. If you have other objects that do not yet have a class, it will not ignore them.

It might be cleaner to only search added objects, if they are stored somewhere during the script.

1 Like

I only read quickly & not sure I quite understand the goal or problem, but two quick points:

  • Never use new PathClass(name, color) - rather, use getPathClass(name, color) instead. This is really important, since the PathClass for any name always be the same.
  • getAnnotationObjects() is to be preferred rather than the current approach of getting the flattened object list & discarding the first entry… assuming it’s annotations that you want
  • getDisplayedName() isn’t really intended to be something for you to rely on; you can use getPathClass() or getName() since these are well defined (and have corresponding methods to set them). All these are accessible from the object, not the ROI.

The ‘displayed name’ basically represents the kind of object (e.g. detection or annotation), ROI (e.g. polygon or line) and classification somehow in a way that is readable. But it changes according to what properties are set, so isn’t stable in the way the others are.

Ah! Okay, thank you for the clarification. In that case, would you please help me set a class for the “WM” annotation. In other words, this script would select all annotations NOT a line and set the class. I have pieced together some lines from the forums to rename all of the annotations NOT lines to “WM” - see below

// Script only works if there are ONLY LINES AND POLYGONS/AREA/RECTANGLES on images
// Get all annotations with Line ROIs
def annotations = getAnnotationObjects()
def lines = annotations.findAll {it.getROI() instanceof qupath.lib.roi.LineROI}


// Removes lines
annotations.removeAll(lines)
// Assign names to all other annotations
annotations.eachWithIndex {annotation, i -> annotation.setName('WM')}

fireHierarchyUpdate()

If the lines are allowed to have classes it might be easiest to use something like:
getAnnotationObjects().each{it.setPathClass(getPathClass("WM"))}

Stick that at the top of the script, and it will make EVERY annotation object including lines WM, before the rest of the script proceeds.

Or this:

getAnnotationObjects().each{ if (!it.getROI().isLine()){it.setPathClass(getPathClass("WM"))}}
fireHierarchyUpdate()

To ignore the lines.

2 Likes

This is perfect and is appearing to work well for all images! Thank you so very much!

2 Likes