Alex Tereshenkov
Alex Tereshenkov

Reputation: 3620

Calculating angles between line segments (Python) with math.atan2

I am working on a spatial analysis problem and part of this workflow is to calculate the angle between connected line segments.

Each line segment is composed of only two points, and each point has a pair of XY coordinates (Cartesian). Here is the image from GeoGebra. I am always interested in getting a positive angle in 0 to 180 range. However, I get all kind of angles depending on the order of vertices in input line segments.

enter image description here

The input data I work with is provided as tuples of coordinates. Depending on the vertex creation order, the last/end point for each line segment can be different. Here are some of the cases in Python code. The order of line segments in which I get them is random, but in a tuple of tuples, the first element is the start point and the second one is the end point. DE line segment, for instance, would have ((1,1.5),(2,2)) and the (1,1.5) is the start point because it has the first position in the tuple of coordinates.

I however need to make sure I will get the same angle between DE,DF and ED,DF and so forth.

vertexType = "same start point; order 1"
            #X, Y    X Y coords
lineA = ((1,1.5),(2,2)) #DE
lineB = ((1,1.5),(2.5,0.5)) #DF
calcAngle(lineA, lineB,vertexType)
#flip lines order
vertexType = "same start point; order 2"
lineB = ((1,1.5),(2,2)) #DE
lineA = ((1,1.5),(2.5,0.5)) #DF
calcAngle(lineA, lineB,vertexType)

vertexType = "same end point; order 1"
lineA = ((2,2),(1,1.5)) #ED
lineB = ((2.5,0.5),(1,1.5)) #FE
calcAngle(lineA, lineB,vertexType)
#flip lines order
vertexType = "same end point; order 2"
lineB = ((2,2),(1,1.5)) #ED
lineA = ((2.5,0.5),(1,1.5)) #FE
calcAngle(lineA, lineB,vertexType)

vertexType = "one line after another - down; order 1"
lineA = ((2,2),(1,1.5)) #ED
lineB = ((1,1.5),(2.5,0.5)) #DF
calcAngle(lineA, lineB,vertexType)
#flip lines order
vertexType = "one line after another - down; order 2"
lineB = ((2,2),(1,1.5)) #ED
lineA = ((1,1.5),(2.5,0.5)) #DF
calcAngle(lineA, lineB,vertexType)

vertexType = "one line after another - up; line order 1"
lineA = ((1,1.5),(2,2)) #DE
lineB = ((2.5,0.5),(1,1.5)) #FD
calcAngle(lineA, lineB,vertexType)
#flip lines order
vertexType = "one line after another - up; line order 2"
lineB = ((1,1.5),(2,2)) #DE
lineA = ((2.5,0.5),(1,1.5)) #FD
calcAngle(lineA, lineB,vertexType)

I have written a tiny function that takes combinations of the lines as args and calculates the angle between them. I am using the math.atan2 which seemed to be best suited for this.

def calcAngle(lineA,lineB,vertexType):
    line1Y1 = lineA[0][1]
    line1X1 = lineA[0][0]
    line1Y2 = lineA[1][1]
    line1X2 = lineA[1][0]

    line2Y1 = lineB[0][1]
    line2X1 = lineB[0][0]
    line2Y2 = lineB[1][1]
    line2X2 = lineB[1][0]

    #calculate angle between pairs of lines
    angle1 = math.atan2(line1Y1-line1Y2,line1X1-line1X2)
    angle2 = math.atan2(line2Y1-line2Y2,line2X1-line2X2)
    angleDegrees = (angle1-angle2) * 360 / (2*math.pi)
    print angleDegrees, vertexType

The output I get is:

> -299.744881297 same start point; order 1
> 299.744881297 same start point; order 2
> 60.2551187031 same end point; order 1
> -60.2551187031 same end point; order 2
> -119.744881297 one line after another - down; order 1
> 119.744881297 one line after another - down; order 2
> -119.744881297 one line after another - up; line order 1
> 119.744881297 one line after another - up; line order 2

As you can see, I am getting different values depending on the order of vertices in a line segment and line segments order. I have tried to post-process the angles by finding out what kind of relation the source line had and flipping the lines, editing the angle etc. I've ended with a dozen of such cases and at some point they start to overlap and I cannot any longer find out whether -119.744 should become 60.255 (acute angle) or be left as 119.744 (obtuse angle) etc.

Is there any discrete way to process the output angle values I receive from math.atan2 to get only a positive value in 0 to 180 range? If not, what kind of other approach should I take?

Upvotes: 18

Views: 34652

Answers (3)

Abhinav Ramakrishnan
Abhinav Ramakrishnan

Reputation: 1090

The easiest way to get at this problem is using the dot-product.

Try this code (I've commented practically everything):

import math

def dot(vA, vB):
    return vA[0]*vB[0]+vA[1]*vB[1]

def ang(lineA, lineB):
    # Get nicer vector form
    vA = [(lineA[0][0]-lineA[1][0]), (lineA[0][1]-lineA[1][1])]
    vB = [(lineB[0][0]-lineB[1][0]), (lineB[0][1]-lineB[1][1])]
    # Get dot prod
    dot_prod = dot(vA, vB)
    # Get magnitudes
    magA = dot(vA, vA)**0.5
    magB = dot(vB, vB)**0.5
    # Get cosine value
    cos_ = dot_prod/magA/magB
    # Get angle in radians and then convert to degrees
    angle = math.acos(dot_prod/magB/magA)
    # Basically doing angle <- angle mod 360
    ang_deg = math.degrees(angle)%360
    
    if ang_deg-180>=0:
        # As in if statement
        return 360 - ang_deg
    else: 
        
        return ang_deg

Now try your variations of lineA and lineB and all should give the same answer.

Upvotes: 28

Anderson Oliveira
Anderson Oliveira

Reputation: 484

An alternative solution using the formula:

enter image description here

where 'm1' is the slope of line 1 and 'm2' the slope of line 2. If line 1 is defined by the points P1 = [x1, y1] and P2 = [x2, y2], then slope 'm' is:

enter image description here

By using the formulas above you can find the angle in degrees between two lines as follows:

def slope(x1, y1, x2, y2): # Line slope given two points:
    return (y2-y1)/(x2-x1)

def angle(s1, s2): 
    return math.degrees(math.atan((s2-s1)/(1+(s2*s1))))

lineA = ((0.6, 3.6), (1.6, 3))
lineB = ((1.6, 3), (2, 3.6))

slope1 = slope(lineA[0][0], lineA[0][1], lineA[1][0], lineA[1][1])
slope2 = slope(lineB[0][0], lineB[0][1], lineB[1][0], lineB[1][1])

ang = angle(slope1, slope2)
print('Angle in degrees = ', ang)

Upvotes: 14

Ignacio Vazquez-Abrams
Ignacio Vazquez-Abrams

Reputation: 798476

Too much work. Take the absolute value of the arccosine of the dot product of the two vectors divided by each of the lengths of the lines.

Upvotes: 6

Related Questions