8

I need a good algorithm for calculating the point that is closest to a collection of lines in python, preferably by using least squares. I found this post on a python implementation that doesn't work:

Finding the centre of multiple lines using least squares approach in Python

And I found this resource in Matlab that everyone seems to like... but I'm not sure how to convert it to python:

https://www.mathworks.com/matlabcentral/fileexchange/37192-intersection-point-of-lines-in-3d-space

I find it hard to believe that someone hasn't already done this... surely this is part of numpy or a standard package, right? I'm probably just not searching for the right terms - but I haven't been able to find it yet. I'd be fine with defining lines by two points each or by a point and a direction. Any help would be greatly appreciated!

Here's an example set of points that I'm working with:

initial XYZ points for the first set of lines

array([[-7.07107037,  7.07106748,  1. ],
       [-7.34818339,  6.78264559,  1. ],
       [-7.61352972,  6.48335745,  1. ],
       [-7.8667115 ,  6.17372055,  1. ],
       [-8.1072994 ,  5.85420065,  1. ]])

the angles that belong to the first set of lines

[-44.504854, -42.029223, -41.278573, -37.145774, -34.097022]

initial XYZ points for the second set of lines

array([[ 0., -20. ,  1. ],
       [ 7.99789129e-01, -19.9839984,  1. ],
       [ 1.59830153e+00, -19.9360366,  1. ],
       [ 2.39423914e+00, -19.8561769,  1. ],
       [ 3.18637019e+00, -19.7445510,  1. ]])

the angles that belong to the second set of lines

[89.13244, 92.39087, 94.86425, 98.91849, 99.83488]

The solution should be the origin or very near it (the data is just a little noisy, which is why the lines don't perfectly intersect at a single point).

Noise in the street
  • 589
  • 1
  • 6
  • 20
  • Can you further describe your problem? Are the lines simple, i.e. y=mx+b type lines? If you can get the expressions for each line you may be able to solve the problem analytically. – John Forbes Aug 30 '18 at 03:51
  • Nevermind, I should have read the mathworks link first. – John Forbes Aug 30 '18 at 03:52
  • 4
    why would this be downvoted? This is a legitimate problem. Does anyone know of an existing solution to this in python? – Noise in the street Aug 30 '18 at 03:56
  • I might be able to make the solution, but I don't know of an existing library that does it. Try reviewing what geometry and math libraries exist. If I get time I'll have a go of coding a solution. – John Forbes Aug 30 '18 at 04:05
  • @Noiseinthestreet it's a legitimate problem for sure, but to be fair you don't develop a minimal complete and verifiable example. You could greatly improve your question by giving us lines and the point you seek (input / expected output). – kevinkayaks Aug 30 '18 at 04:09
  • 1
    @Noiseinthestreet yes, atleast one example would be great. – Deepak Saini Aug 30 '18 at 04:10
  • 1
    Ok, I understand, that's fair - I edited the post to include an example dataset. I didn't want to ask too much, but if someone posts a solution that works with data that's already set up like that, that would be wonderful! – Noise in the street Aug 30 '18 at 04:33
  • 3
    When giving array examples, it's much more useful if you give them with `print(repr(array))` so that people can paste them into their terminals – Eric Aug 30 '18 at 05:28
  • 2
    my apologies - I made the correction. Thanks for the tip! – Noise in the street Aug 30 '18 at 12:24

3 Answers3

12

Here's a numpy solution using the method described in this link

def intersect(P0,P1):
    """P0 and P1 are NxD arrays defining N lines.
    D is the dimension of the space. This function 
    returns the least squares intersection of the N
    lines from the system given by eq. 13 in 
    http://cal.cs.illinois.edu/~johannes/research/LS_line_intersect.pdf.
    """
    # generate all line direction vectors 
    n = (P1-P0)/np.linalg.norm(P1-P0,axis=1)[:,np.newaxis] # normalized

    # generate the array of all projectors 
    projs = np.eye(n.shape[1]) - n[:,:,np.newaxis]*n[:,np.newaxis]  # I - n*n.T
    # see fig. 1 

    # generate R matrix and q vector
    R = projs.sum(axis=0)
    q = (projs @ P0[:,:,np.newaxis]).sum(axis=0)

    # solve the least squares problem for the 
    # intersection point p: Rp = q
    p = np.linalg.lstsq(R,q,rcond=None)[0]

    return p

Works

enter image description here

Edit: here is a generator for noisy test data

n = 6
P0 = np.stack((np.array([5,5])+3*np.random.random(size=2) for i in range(n)))
a = np.linspace(0,2*np.pi,n)+np.random.random(size=n)*np.pi/5.0
P1 = np.array([5+5*np.sin(a),5+5*np.cos(a)]).T
kevinkayaks
  • 2,636
  • 1
  • 14
  • 30
  • 1
    Your picture seems blank. Maybe just on my end though – Daniel F Aug 30 '18 at 06:11
  • What form does the input data need to take for this? I can't seem to get it to run and I'm not familiar enough with the internals to understand the error ("operands could not be broadcast together with shapes (2,2) (5,3,3)"). Would you please share your input data for the plot? – Noise in the street Aug 30 '18 at 13:02
  • yep. it is a messy generator but I'll add it. – kevinkayaks Aug 30 '18 at 13:02
  • 1
    Now I see. Your function seems to assume Nx2 arrays for P0 and P1. I was able to feed it Nx3 by changing `ImnnT = np.eye(2)-nnT` to `ImnnT = np.eye(3)-nnT`. When I did it with the same z values for each point (all in a 2D plane), I got the same answer as just feeding the x & y data in. Does this change make sense for a 3D collection of points? – Noise in the street Aug 30 '18 at 13:51
  • yep. definitely. Sorry, I missed that point for generalization. I'm still learning too – kevinkayaks Aug 30 '18 at 13:52
  • 1
    I included my final code at the bottom that was based on this solution. Thanks to everyone for your help!!! – Noise in the street Aug 30 '18 at 17:39
  • Hi, the link to the pdf document is currently offline. Does anyone know of another resource I could cite? Is there a book or a paper? – macKaiver Jun 12 '19 at 12:42
  • The projector is the same as the wikipedia equation in Eric's answer. Maybe there are references there – kevinkayaks Jun 12 '19 at 16:40
  • Found the like [here](https://silo.tips/download/least-squares-intersection-of-lines) – adentinger Nov 26 '20 at 22:20
6

If this wikipedia equation carries any weight:

then you can use:

def nearest_intersection(points, dirs):
    """
    :param points: (N, 3) array of points on the lines
    :param dirs: (N, 3) array of unit direction vectors
    :returns: (3,) array of intersection point
    """
    dirs_mat = dirs[:, :, np.newaxis] @ dirs[:, np.newaxis, :]
    points_mat = points[:, :, np.newaxis]
    I = np.eye(3)
    return np.linalg.lstsq(
        (I - dirs_mat).sum(axis=0),
        ((I - dirs_mat) @ points_mat).sum(axis=0),
        rcond=None
    )[0]

If you want help deriving / checking that equation from first principles, then math.stackexchange.com would be a better place to ask.

surely this is part of numpy

Note that numpy gives you enough tools to express this very concisely already

Eric
  • 95,302
  • 53
  • 242
  • 374
  • This looks very encouraging since I can easily get direction vectors from my array of angles. I'm struggling with a python-ism though with the arrays. Can I not use `np.array( [[x,y,z],[,x1,y1,z1],...]` to define the `points` and `dirs` inputs? Sorry if this is a stupid question. It seems like a lot of the challenge in doing math in python is just getting syntax like this right. – Noise in the street Aug 30 '18 at 13:14
3

Here's the final code that I ended up using. Thanks to kevinkayaks and everyone else who responded! Your help is very much appreciated!!!

The first half of this function simply converts the two collections of points and angles to direction vectors. I believe the rest of it is basically the same as what Eric and Eugene proposed. I just happened to have success first with Kevin's and ran with it until it was an end-to-end solution for me.

import numpy as np

def LS_intersect(p0,a0,p1,a1):
    """
    :param p0 : Nx2 (x,y) position coordinates
    :param p1 : Nx2 (x,y) position coordinates
    :param a0 : angles in degrees for each point in p0
    :param a1 : angles in degrees for each point in p1    
    :return: least squares intersection point of N lines from eq. 13 in 
             http://cal.cs.illinois.edu/~johannes/research/LS_line_intersect.pdf
    """    

    ang = np.concatenate( (a0,a1) ) # create list of angles
    # create direction vectors with magnitude = 1
    n = []
    for a in ang:
        n.append([np.cos(np.radians(a)), np.sin(np.radians(a))])
    pos = np.concatenate((p0[:,0:2],p1[:,0:2])) # create list of points
    n = np.array(n)

    # generate the array of all projectors 
    nnT = np.array([np.outer(nn,nn) for nn in n ]) 
    ImnnT = np.eye(len(pos[0]))-nnT # orthocomplement projectors to n

    # now generate R matrix and q vector
    R = np.sum(ImnnT,axis=0)
    q = np.sum(np.array([np.dot(m,x) for m,x in zip(ImnnT,pos)]),axis=0)

    # and solve the least squares problem for the intersection point p 
    return np.linalg.lstsq(R,q,rcond=None)[0]


#sample data 
pa = np.array([[-7.07106638,  7.07106145,  1.        ],
       [-7.34817263,  6.78264524,  1.        ],
       [-7.61354115,  6.48336347,  1.        ],
       [-7.86671133,  6.17371816,  1.        ],
       [-8.10730426,  5.85419995,  1.        ]])
paa = [-44.504854321138524, -42.02922380123842, -41.27857390748773, -37.145774853341386, -34.097022454778674]

pb = np.array([[-8.98220431e-07, -1.99999962e+01,  1.00000000e+00],
       [ 7.99789129e-01, -1.99839984e+01,  1.00000000e+00],
       [ 1.59830153e+00, -1.99360366e+01,  1.00000000e+00],
       [ 2.39423914e+00, -1.98561769e+01,  1.00000000e+00],
       [ 3.18637019e+00, -1.97445510e+01,  1.00000000e+00]])
pba = [88.71923357743934, 92.55801427272372, 95.3038321024299, 96.50212060095349, 100.24177145619092]

print("Should return (-0.03211692,  0.14173216)")
solution = LS_intersect(pa,paa,pb,pba)
print(solution)
Noise in the street
  • 589
  • 1
  • 6
  • 20