39

How would I use numpy to calculate the intersection between two line segments?

In the code I have segment1 = ((x1,y1),(x2,y2)) and segment2 = ((x1,y1),(x2,y2)). Note segment1 does not equal segment2. So in my code I've also been calculating the slope and y-intercept, it would be nice if that could be avoided but I don't know of a way how.

I've been using Cramer's rule with a function I wrote up in Python but I'd like to find a faster way of doing this.

Dishin H Goyani
  • 7,195
  • 3
  • 26
  • 37
Xavier
  • 8,828
  • 13
  • 64
  • 98

12 Answers12

49

Stolen directly from https://web.archive.org/web/20111108065352/https://www.cs.mun.ca/~rod/2500/notes/numpy-arrays/numpy-arrays.html

#
# line segment intersection using vectors
# see Computer Graphics by F.S. Hill
#
from numpy import *
def perp( a ) :
    b = empty_like(a)
    b[0] = -a[1]
    b[1] = a[0]
    return b

# line segment a given by endpoints a1, a2
# line segment b given by endpoints b1, b2
# return 
def seg_intersect(a1,a2, b1,b2) :
    da = a2-a1
    db = b2-b1
    dp = a1-b1
    dap = perp(da)
    denom = dot( dap, db)
    num = dot( dap, dp )
    return (num / denom.astype(float))*db + b1

p1 = array( [0.0, 0.0] )
p2 = array( [1.0, 0.0] )

p3 = array( [4.0, -5.0] )
p4 = array( [4.0, 2.0] )

print seg_intersect( p1,p2, p3,p4)

p1 = array( [2.0, 2.0] )
p2 = array( [4.0, 3.0] )

p3 = array( [6.0, 0.0] )
p4 = array( [6.0, 3.0] )

print seg_intersect( p1,p2, p3,p4)
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Hamish Grubijan
  • 10,562
  • 23
  • 99
  • 147
  • 6
    Thanks for the hint. After seeing this algorithm I started reading on it. Here is an IMO good explanation http://softsurfer.com/Archive/algorithm_0104/algorithm_0104B.htm . Hope it serves someones else's curiosity as well. – Maik Beckmann May 17 '11 at 10:24
  • 3
    Note to those using the above code: Ensure that you are passing an array of floats to seg_intersect, otherwise unexpected things can happen due to rounding. – schickm Oct 13 '12 at 22:07
  • 4
    Also, remember to check to see if `denom` is zero, otherwise you'll get a division by zero error. (This happens if the lines are parallel.) – Gareth Rees Dec 20 '12 at 22:41
  • @schickm where does this rounding problem happen? during the division? – bennlich Apr 15 '15 at 23:40
  • 1
    Does this handle colinearity? see: https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect – Nathan majicvr.com Jul 03 '18 at 21:20
  • 5
    The link you provided is dead (understandable nine years later...), but fortunately it was saved by the internet archive. It seems useful even now, so here is the link to archived version: https://web.archive.org/web/20111108065352/https://www.cs.mun.ca/~rod/2500/notes/numpy-arrays/numpy-arrays.html – cji Mar 12 '20 at 23:12
  • deadlink was archived at: https://web.archive.org/web/20200107110537/http://www.geomalgorithms.com/a05-_intersect-1.html – J_H Aug 01 '23 at 02:59
  • I don't know why I can't use this answer, but these work https://gist.github.com/kylemcdonald/6132fc1c29fd3767691442ba4bc84018 – M lab Aug 15 '23 at 16:09
31
import numpy as np

def get_intersect(a1, a2, b1, b2):
    """ 
    Returns the point of intersection of the lines passing through a2,a1 and b2,b1.
    a1: [x, y] a point on the first line
    a2: [x, y] another point on the first line
    b1: [x, y] a point on the second line
    b2: [x, y] another point on the second line
    """
    s = np.vstack([a1,a2,b1,b2])        # s for stacked
    h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous
    l1 = np.cross(h[0], h[1])           # get first line
    l2 = np.cross(h[2], h[3])           # get second line
    x, y, z = np.cross(l1, l2)          # point of intersection
    if z == 0:                          # lines are parallel
        return (float('inf'), float('inf'))
    return (x/z, y/z)

if __name__ == "__main__":
    print get_intersect((0, 1), (0, 2), (1, 10), (1, 9))  # parallel  lines
    print get_intersect((0, 1), (0, 2), (1, 10), (2, 10)) # vertical and horizontal lines
    print get_intersect((0, 1), (1, 2), (0, 10), (1, 9))  # another line for fun

Explanation

Note that the equation of a line is ax+by+c=0. So if a point is on this line, then it is a solution to (a,b,c).(x,y,1)=0 (. is the dot product)

let l1=(a1,b1,c1), l2=(a2,b2,c2) be two lines and p1=(x1,y1,1), p2=(x2,y2,1) be two points.


Finding the line passing through two points:

let t=p1xp2 (the cross product of two points) be a vector representing a line.

We know that p1 is on the line t because t.p1 = (p1xp2).p1=0. We also know that p2 is on t because t.p2 = (p1xp2).p2=0. So t must be the line passing through p1 and p2.

This means that we can get the vector representation of a line by taking the cross product of two points on that line.


Finding the point of intersection:

Now let r=l1xl2 (the cross product of two lines) be a vector representing a point

We know r lies on l1 because r.l1=(l1xl2).l1=0. We also know r lies on l2 because r.l2=(l1xl2).l2=0. So r must be the point of intersection of the lines l1 and l2.

Interestingly, we can find the point of intersection by taking the cross product of two lines.

Norbu Tsering
  • 609
  • 5
  • 14
  • Can you give an example usage starting with 2 lines each specified by two 2D points? – Matthias Nov 08 '17 at 19:18
  • @Matthias I added an example – Norbu Tsering Nov 08 '17 at 19:35
  • Thanks! But get_slope_intercept throws an exception of one line is horizontal and the other one vertical perpendicular. example: (1, 1), (3, 1), (2.5, 2), (2.5, 0) – Matthias Nov 08 '17 at 19:43
  • Ah that's right. Vertical lines will make the coefficient matrix singular. Give me a day. I'll take care of it when I get a chance – Norbu Tsering Nov 08 '17 at 20:03
  • @Matthias Updated the answer. I think you'll be very pleased lol – Norbu Tsering Nov 09 '17 at 04:02
  • 1
    Why do you say `t` is the line passing through `p1` and `p2`? Seeing these points as vector offsets relative to the origin of space (the origin is [0,0], so a point [x, y] is an offset away from the origin), when you take the cross product between these two offset vectors you get another vector parallel to them and going out of the "screen", which is not the vector going through the points. – R. Navega Jan 08 '19 at 11:14
  • @R. Navega, because `t`.`p1`=`t`.`p2`=`0` – Norbu Tsering Aug 08 '20 at 03:52
  • We are actually working in just 1 plane of 3d space. The plane is made up of all points with 3rd component equal to 1. That's why I say it is the vector *representing* a line. – Norbu Tsering Aug 08 '20 at 04:09
  • @NorbuTsering okay, correct me if I'm wrong, `t` is actually a vector of the coefficients of a line, same as the `l1` and `l2` that you use. These vectors can't be visualized as lines by themselves, you need to visualize them by using the [x, y] points that solve their line equation, like for `t` it would be the points that solve `t₁ * x + t₂ * y + t₃ = 0` (with t being a 3-component vector) – R. Navega Aug 09 '20 at 05:04
  • Correct. Iirc, I got this from a book about projective geometry. – Norbu Tsering Aug 10 '20 at 17:09
10

This is is a late response, perhaps, but it was the first hit when I Googled 'numpy line intersections'. In my case, I have two lines in a plane, and I wanted to quickly get any intersections between them, and Hamish's solution would be slow -- requiring a nested for loop over all line segments.

Here's how to do it without a for loop (it's quite fast):

from numpy import where, dstack, diff, meshgrid

def find_intersections(A, B):

    # min, max and all for arrays
    amin = lambda x1, x2: where(x1<x2, x1, x2)
    amax = lambda x1, x2: where(x1>x2, x1, x2)
    aall = lambda abools: dstack(abools).all(axis=2)
    slope = lambda line: (lambda d: d[:,1]/d[:,0])(diff(line, axis=0))

    x11, x21 = meshgrid(A[:-1, 0], B[:-1, 0])
    x12, x22 = meshgrid(A[1:, 0], B[1:, 0])
    y11, y21 = meshgrid(A[:-1, 1], B[:-1, 1])
    y12, y22 = meshgrid(A[1:, 1], B[1:, 1])

    m1, m2 = meshgrid(slope(A), slope(B))
    m1inv, m2inv = 1/m1, 1/m2

    yi = (m1*(x21-x11-m2inv*y21) + y11)/(1 - m1*m2inv)
    xi = (yi - y21)*m2inv + x21

    xconds = (amin(x11, x12) < xi, xi <= amax(x11, x12), 
              amin(x21, x22) < xi, xi <= amax(x21, x22) )
    yconds = (amin(y11, y12) < yi, yi <= amax(y11, y12),
              amin(y21, y22) < yi, yi <= amax(y21, y22) )

    return xi[aall(xconds)], yi[aall(yconds)]

Then to use it, provide two lines as arguments, where is arg is a 2 column matrix, each row corresponding to an (x, y) point:

# example from matplotlib contour plots
Acs = contour(...)
Bsc = contour(...)

# A and B are the two lines, each is a 
# two column matrix
A = Acs.collections[0].get_paths()[0].vertices
B = Bcs.collections[0].get_paths()[0].vertices

# do it
x, y = find_intersections(A, B)

have fun

Eric
  • 95,302
  • 53
  • 242
  • 374
marmaduke
  • 101
  • 1
  • 2
9

This is a version of @Hamish Grubijan's answer that also works for multiple points in each of the input arguments, i.e., a1, a2, b1, b2 can be Nx2 row arrays of 2D points. The perp function is replaced by a dot product.

T = np.array([[0, -1], [1, 0]])
def line_intersect(a1, a2, b1, b2):
    da = np.atleast_2d(a2 - a1)
    db = np.atleast_2d(b2 - b1)
    dp = np.atleast_2d(a1 - b1)
    dap = np.dot(da, T)
    denom = np.sum(dap * db, axis=1)
    num = np.sum(dap * dp, axis=1)
    return np.atleast_2d(num / denom).T * db + b1
user1248490
  • 963
  • 9
  • 16
4

I would like to add something small here. The original question is about line segments. I arrived here, because I was looking for line segment intersection, which in my case meant that I need to filter those cases, where no intersection of the line segments exists. Here is some code which does that:

def line_intersection(x1, y1, x2, y2, x3, y3, x4, y4):
    """find the intersection of line segments A=(x1,y1)/(x2,y2) and
    B=(x3,y3)/(x4,y4). Returns a point or None"""
    denom = ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
    if denom==0: return None
    px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom
    py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom
    if (px - x1) * (px - x2) < 0 and (py - y1) * (py - y2) < 0 \
      and (px - x3) * (px - x4) < 0 and (py - y3) * (py - y4) < 0:
        return [px, py]
    else:
        return None
  • Does this really work? I'm testing it with the segments (0, 0) -> (0, 10) and (-1, 5) -> (1, 5) which should intersect at (0, 5), but I get None back? Am I simply misunderstanding something? – alkanen May 21 '22 at 19:59
4

In case you are looking for a vectorized version where we can rule out vertical line segments.

def intersect(a):
    # a numpy array with dimension [n, 2, 2, 2]
    # axis 0: line-pair, axis 1: two lines, axis 2: line delimiters axis 3: x and y coords
    # for each of the n line pairs a boolean is returned stating of the two lines intersect
    # Note: the edge case of a vertical line is not handled.
    m = (a[:, :, 1, 1] - a[:, :, 0, 1]) / (a[:, :, 1, 0] - a[:, :, 0, 0])
    t = a[:, :, 0, 1] - m[:, :] * a[:, :, 0, 0]
    x = (t[:, 0] - t[:, 1]) / (m[:, 1] - m[:, 0])
    y = m[:, 0] * x + t[:, 0]
    r = a.min(axis=2).max(axis=1), a.max(axis=2).min(axis=1)
    return (x >= r[0][:, 0]) & (x <= r[1][:, 0]) & (y >= r[0][:, 1]) & (y <= r[1][:, 1])

A sample invocation would be:

intersect(np.array([
    [[[1, 2], [2, 2]],
     [[1, 2], [1, 1]]], # I
    [[[3, 4], [4, 4]],
     [[4, 4], [5, 6]]], # II
    [[[2, 0], [3, 1]],
     [[3, 0], [4, 1]]], # III
    [[[0, 5], [2, 5]],
     [[2, 4], [1, 3]]], # IV
]))
# returns [False, True, False, False]

Visualization (I need more reputation to post images here).

2

Here's a (bit forced) one-liner:

import numpy as np
from scipy.interpolate import interp1d

x = np.array([0, 1])
segment1 = np.array([0, 1])
segment2 = np.array([-1, 2])

x_intersection = interp1d(segment1 - segment2, x)(0)
# if you need it:
y_intersection = interp1d(x, segment1)(x_intersection)

Interpolate the difference (default is linear), and find a 0 of the inverse.

Cheers!

Andy Reagan
  • 609
  • 9
  • 16
  • Sorry for commenting on an old post, but how is this supposed to work? You can't subtract tuples and using np arrays returns an error that x (segment1) can't have multiple dimensions. – justry Apr 22 '20 at 22:07
  • Yeah good question. I had to really think, I edited my answer to include the data. In short, this just returns the x value. – Andy Reagan Apr 24 '20 at 02:56
  • I'm not sure how this would work for two segments with separate x and Y coordinates, but it did work for me since all I wanted was intersection with the baseline. Thanks! – justry Apr 25 '20 at 04:05
1

This is what I use to find line intersection, it works having either 2 points of each line, or just a point and its slope. I basically solve the system of linear equations.

def line_intersect(p0, p1, m0=None, m1=None, q0=None, q1=None):
    ''' intersect 2 lines given 2 points and (either associated slopes or one extra point)
    Inputs:
        p0 - first point of first line [x,y]
        p1 - fist point of second line [x,y]
        m0 - slope of first line
        m1 - slope of second line
        q0 - second point of first line [x,y]
        q1 - second point of second line [x,y]
    '''
    if m0 is  None:
        if q0 is None:
            raise ValueError('either m0 or q0 is needed')
        dy = q0[1] - p0[1]
        dx = q0[0] - p0[0]
        lhs0 = [-dy, dx]
        rhs0 = p0[1] * dx - dy * p0[0]
    else:
        lhs0 = [-m0, 1]
        rhs0 = p0[1] - m0 * p0[0]

    if m1 is  None:
        if q1 is None:
            raise ValueError('either m1 or q1 is needed')
        dy = q1[1] - p1[1]
        dx = q1[0] - p1[0]
        lhs1 = [-dy, dx]
        rhs1 = p1[1] * dx - dy * p1[0]
    else:
        lhs1 = [-m1, 1]
        rhs1 = p1[1] - m1 * p1[0]

    a = np.array([lhs0, 
                  lhs1])

    b = np.array([rhs0, 
                  rhs1])
    try:
        px = np.linalg.solve(a, b)
    except:
        px = np.array([np.nan, np.nan])

    return px
dashesy
  • 2,596
  • 3
  • 45
  • 61
0
We can solve this 2D line intersection problem using determinant.

To solve this, we have to convert our lines to the following form: ax+by=c. where

 a = y1 - y2
 b = x1 - x2
 c = ax1 + by1 

If we apply this equation for each line, we will got two line equation. a1x+b1y=c1 and a2x+b2y=c2.

Now when we got the expression for both lines.
First of all we have to check if the lines are parallel or not. To examine this we want to find the determinant. The lines are parallel if the determinant is equal to zero.
We find the determinant by solving the following expression:

det = a1 * b2 - a2 * b1

If the determinant is equal to zero, then the lines are parallel and will never intersect. If the lines are not parallel, they must intersect at some point.
The point of the lines intersects are found using the following formula:

equation for finding line intersection using determinant

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


'''
finding intersect point of line AB and CD 
where A is the first point of line AB
and B is the second point of line AB
and C is the first point of line CD
and D is the second point of line CD
'''



def get_intersect(A, B, C, D):
    # a1x + b1y = c1
    a1 = B.y - A.y
    b1 = A.x - B.x
    c1 = a1 * (A.x) + b1 * (A.y)

    # a2x + b2y = c2
    a2 = D.y - C.y
    b2 = C.x - D.x
    c2 = a2 * (C.x) + b2 * (C.y)

    # determinant
    det = a1 * b2 - a2 * b1

    # parallel line
    if det == 0:
        return (float('inf'), float('inf'))

    # intersect point(x,y)
    x = ((b2 * c1) - (b1 * c2)) / det
    y = ((a1 * c2) - (a2 * c1)) / det
    return (x, y)
Sadekujjaman
  • 437
  • 4
  • 7
0

I wrote a module for line to compute this and some other simple line operations. It is implemented in c++, so it works very fast. You can install FastLine via pip and then use it in this way:

from FastLine import Line
# define a line by two points
l1 = Line(p1=(0,0), p2=(10,10))
# or define a line by slope and intercept
l2 = Line(m=0.5, b=-1)

# compute intersection
p = l1.intersection(l2)
# returns (-2.0, -2.0)
0

The reason you would want to use numpy code is because it's faster and it's only really faster when you can broadcast it. The way you make numpy code fast is by doing everything in a series of of numpy operations without loops. If you're not going to do this, don't use numpy.

    def line_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
        denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
        if denom == 0:
            return None  # Parallel.
        ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
        ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
        if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:
            return (x1 + ua * (x2 - x1)), (y1 + ua * (y2 - y1))
        return None

However, let's do use numpy:

It's a bit easier to deal with points as complex numbers (x=real, y=imag). That trick is used elsewhere. And rather than a 2d set of elements we use a numpy 1d complex array for the 2d points.

import numpy as np

def find_intersections(a, b):
    old_np_seterr = np.seterr(divide="ignore", invalid="ignore")
    try:
        ax1, bx1 = np.meshgrid(np.real(a[:-1]), np.real(b[:-1]))
        ax2, bx2 = np.meshgrid(np.real(a[1:]), np.real(b[1:]))
        ay1, by1 = np.meshgrid(np.imag(a[:-1]), np.imag(b[:-1]))
        ay2, by2 = np.meshgrid(np.imag(a[1:]), np.imag(b[1:]))

        # Note if denom is zero these are parallel lines.
        denom = (by2 - by1) * (ax2 - ax1) - (bx2 - bx1) * (ay2 - ay1)

        ua = ((bx2 - bx1) * (ay1 - by1) - (by2 - by1) * (ax1 - bx1)) / denom
        ub = ((ax2 - ax1) * (ay1 - by1) - (ay2 - ay1) * (ax1 - bx1)) / denom
        hit = np.dstack((0.0 <= ua, ua <= 1.0, 0.0 <= ub, ub <= 1.0)).all(axis=2)
        ax1 = ax1[hit]
        ay1 = ay1[hit]
        x_vals = ax1 + ua[hit] * (ax2[hit] - ax1)
        y_vals = ay1 + ua[hit] * (ay2[hit] - ay1)
        return x_vals + y_vals * 1j
    finally:
        np.seterr(**old_np_seterr)

Invoking code:

import svgelements as svge
from random import random
import numpy as np

j = svge.Path(svge.Circle(cx=random() * 5, cy=random() * 5, r=random() * 5)).npoint(
    np.arange(0, 1, 0.001)
)
k = svge.Path(svge.Circle(cx=random() * 5, cy=random() * 5, r=random() * 5)).npoint(
    np.arange(0, 1, 0.001)
)
j = j[:, 0] + j[:, 1] * 1j
k = k[:, 0] + k[:, 1] * 1j

intersects = find_intersections(j, k)
print(intersects)
# Random circles will intersect in 0 or 2 points.

In our code, a and b are segment lists. These expect to be a series of connected points and we mesh them to find any segment n -> n+1 segment that intersects with any or all the other segments.

We return all intersections between the polyline a and the polyline b.

Two tricks (for adaptations):

  1. We mesh all the segments. We check every segment in the polyline a list and every segment in the polyline b list. It's pretty easy to see how you'd arrange this if you wanted other inputs.

  2. Many code examples will check if denom is zero but that's not allowed in pure array code since there's a mesh of different points to check, so conditionals need to be in-lined. We turn off the seterr for dividing by 0 and infinity because we expect to do that if we have parallel lines. Which gets rid of the check for denom being zero. If denom is zero then the lines are parallel which means they either meet at 0 points or infinite many points. The typical conditional checking for the values of ua and ub is done in an array stack of each of the checks which then sees if all of these are true for any elements, and then just returns true for those elements.


If you need the value t or the segments within the lists that intersected this should be readily determined from the ua ub and hit.

Tatarize
  • 10,238
  • 4
  • 58
  • 64
0
import numpy as np

data = np.array([
    #  segment1               segment2
    # [[x1, y1], [x2, y2]],  [[x1, y1], [x2, y2]]
    [[0, 0], [1, 1], [0, 1], [1, 0]],
    [[0, 0], [1, 1], [1, 0], [1, 1]],
    [(0, 1), (0, 2), (1, 10), (2, 10)],
    [(0, 1), (1, 2), (0, 10), (1, 9)],
    [[0, 0], [0, 1], [0, 2], [1, 3]],
    [[0, 1], [2, 3], [4, 5], [6, 7]],  
    [[1, 2], [3, 4], [5, 6], [7, 8]]
])

def intersect(data):
    L = len(data)
    x1, y1, x2, y2 = data.reshape(L * 2, -1).T
    R = np.full([L, 2], np.nan)
    X = np.concatenate([
        (y2 - y1).reshape(L * 2, -1), 
        (x1 - x2).reshape(L * 2, -1)], 
        axis=1
    ).reshape(L, 2, 2)
    B = (x1 * y2 - x2 * y1).reshape(L, 2)
    I = np.isfinite(np.linalg.cond(X))
    R[I] = np.matmul(np.linalg.inv(X[I]), B[I][:,:,None]).squeeze(-1)
    return R

intersect(data)

array([[ 0.5,  0.5],
       [ 1. ,  1. ],
       [ 0. , 10. ],
       [ 4.5,  5.5],
       [ 0. ,  2. ],
       [ nan,  nan],
       [ nan,  nan]])
xmduhan
  • 965
  • 12
  • 14