Scripting advice and assistance

Hi All,

I am new to programming with Groovy and was wondering if anyone had any suggestions for a modification to a script I am working on. Overall, I have it designed to be used with the Line ROI, but recently with the release of v0.2.0-m2 which has a Polyline ROI feature, I have been trying to adapt it so it can be used with the polyline ROI.

The goal of this script is to calculate the shortest distance between two lines which the user draws and then creates new lines connecting the user drawn lines. Finally, it will calculate the area between each of the interval lines and determine the largest combination of adjacent intervals and create a new polygon such that the deviation of the mean total area is no greater than 50um.

The user will draw 2 semi-parallel lines (shown below). Note although one set/pair of lines in the image, the script is designed such that the user should be able to draw several pairs/sets.


Then after running the script, an image similar to the one below should appear

I would like to modify the script such that it can be used with polylines such as in the image below

I would greatly appreciate any time or assistance from anyone as this has been something I have been struggling with for quite sometime.

Please see the script below:

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)


//selectAnnotations();
selectObjects { p -> p.getPathClass() == getPathClass("Greatest GM Sampling zone") }; 

print('Done')

Thank you so very much for your time, patience, advice, and assistance! This is a wonderful platform and I look forward as it continues to grow and improve!

Looks involved, and I haven’t played with polylines much :frowning:
Are you making the assumption that the user would have to create bends in approximately the same places for the polyline? If so, you could essentially run the single line code after splitting the polyline by vertices. If you can’t make that assumption though… I am less certain. It would get involved around the bendy bits.

Thanks so much @Research_Associate for your response!

I think given my limited knowledge of Groovy, for now, we can work under the assumption that bends will be in approximately the same place for the polylines. How would you suggest splitting the polylines by vertices?

1 Like

Well, right off the bat, I don’t know, but taking a look at the options available to that class of object…
[Woah, linking that script directly results in horrible formatting. I keep forgetting about Gists being terrible like that.]

Which gives me the following with a polyline selected

So first I tried:
getAnnotationObjects().each{println(it.getPolygonPoints())}
This crashed and failed since it needs the ROI first, so next:
getAnnotationObjects().each{println(it.getROI().getPolygonPoints())}
This gives me, for my line:
INFO: [Point: 400.5811767578125, 999.3134155273438, Point: 700.7701416015625, 569.7666625976562, Point: 1366.9791259765625, 802.80810546875, Point: 1203.7183837890625, 1166.19482421875, Point: 1074.6898193359375, 1266.2578125]
INFO: Result: [Polyline]
Pushing it a little further:
getAnnotationObjects().each{it.getROI().getPolygonPoints().each{println it}}

Cycles through each of those points, resulting in:
INFO: Point: 400.5811767578125, 999.3134155273438
INFO: Point: 700.7701416015625, 569.7666625976562
INFO: Point: 1366.9791259765625, 802.80810546875
INFO: Point: 1203.7183837890625, 1166.19482421875
INFO: Point: 1074.6898193359375, 1266.2578125
INFO: Result: [Polyline]
You could use each of those points to create a temporary line ROI, run your current script on that ROI segment, then never actually add that ROI to the hierarchy.
You would still need to find some way to deal with corners “cleanly,” as you might end up with two very close to overlapping lines there, and I suspect this is not what you want. I don’t have a great way to get around that right off the top of my head. I don’t have much experience actually manipulating lines, so I might not be as much help the rest of the way.