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

3

18

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?

Replica answered 1/2, 2015 at 8:44 Comment(0)
W
28

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.

Wooldridge answered 1/2, 2015 at 9:28 Comment(7)
The gist of the testing code and output is hereWooldridge
thanks for the code snippet, great to start with. The problem with the code is that it never reports any obtuse angles (>90). Try yourself with the lineA = ((0.6,3.6),(1.6,3)) lineB = ((1.6,3),(2,3.6)). It reports 87.27, but should be 92.73. What could be done to fix that?Replica
I've edited my post. I believe I've fixed it to give obtuse angles as well. Take a look.Wooldridge
thanks for the update. I had to use my old code where I had to flip the direction of line segments so the vectors will have the same start point (otherwise you cannot calculate the angle between the way I want) and your code (before the edit). It works fine, thank you so much for the math work.Replica
the line: angle = math.acos(dot_prod/magB/magA) might be changed to angle = math.acos(cos_) How could the code be changed to produce angles between 0 and 360?Aldine
Tried every solution from here and also this question, this was the fastest solution taking 0.00012512699686340056 seconds to process.Schreiner
'%360' is redundant as the output of arccos (math.acos) is within the interval [0, pi] radians (or [0, 180] degrees). Hence you should never get an angle less than 0 or greater than 180 degrees. The if-else statement that follows is also redundant. You should also consider clipping the input to math.acos to -1 (min) and 1 (max).Ronen
A
13

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)
Ardellearden answered 14/8, 2019 at 23:29 Comment(1)
The return result in angle should be wrapped with abs() to avoid negative anglesAurie
N
6

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.

Northrup answered 1/2, 2015 at 8:56 Comment(1)
you mean this? angle = arccos[(xa * xb + ya * yb) / (√(xa2 + ya2) * √(xb2 + yb2))]Smokeproof

© 2022 - 2024 — McMap. All rights reserved.