Python: point on a line closest to third point - python

I have a line/vector between two XY points (p1 and p2) and a third XY point (p3) that is outside the line. According to this post I know how to get the distance of that point to the line. But what I'm actually looking for is a point (p4) on that line that is in a minimum distance (d) to the third point (p3). I found this post, but I feel it's not the correct solution. Maybe there's something included in Numpy or Python?
According to #allo I tried the following. You can download my code as Python file or Jupyter Notebook (both Python3).
points = [[1, 1], [3, 1], [2.5, 2], [2.5, 1]]
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots()
fig.set_size_inches(6,6)
x, y = zip(*points[:2])
l1, = ax.plot(x,y, color='blue')
scatter1 = ax.scatter(x=x,y=y, color='blue', marker='x', s=80, alpha=1.0)
x, y = zip(*points[2:])
l2, = ax.plot(x,y, color='red')
scatter2 = ax.scatter(x=x,y=y, color='red', marker='x', s=80, alpha=1.0)
p1 = Vector2D(*points[0])
p2 = Vector2D(*points[1])
p3 = Vector2D(*points[2])
p1p2 = p2.sub_vector(p1)
p1p3 = p3.sub_vector(p1)
angle_p1p2_p1p3 = p1p2.get_angle_radians(p1p3)
length_p1p3 = p1p3.get_length()
length_p1p2 = p1p2.get_length()
p4 = p1.add_vector(p1p2.multiply(p1p3.get_length()/p1p2.get_length()).multiply(math.cos(p1p2.get_angle_radians(p1p3))))
#p4 = p1 + p1p2 * length(p1p3)/length(p1p2)*cos(angle(p1p2, p1p3))
p4 = p1.add_vector(p1p2.multiply(length_p1p3/length_p1p2*math.cos(angle_p1p2_p1p3)))
p4
Which results in p4 = (1.8062257748298551, 1.0) but should obviously be (2.5, 1.0).

Analytical Geometry
Let's start with the assigned line, we define the line in terms of two points on it (x1, y1) and (x2, y2).
With dx = x2-x1 and dy = y2-y1 we can formally write every point on the line as (x12, y12) = (x1, y1) + a*(dx, dy) where a is a real number.
Using an analogous notation a point on the line passing in (x3, y3) and perpendicular to the assigned one is (x34, y34) = (x3, y3) + b*(-dy, +dx).
To find the intersection we have to impose (x12, y12) = (x34, y34) or
(x1, y1) + a*(dx, dy) = (x3, y3) + b*(-dy, +dx).
Writing separately the equations for x and y
y1 + a dy - y3 - b dx = 0
x1 + a dx + b dy - x3 = 0
it is a linear system in a and b whose solution is
a = (dy y3 - dy y1 + dx x3 - dx x1) / (dy^2 + dx^2)
b = (dy x3 - dy x1 - dx y3 + dx y1) / (dy^2 + dx^2)
The coordinates of the closest point to (x3, y3) lying on the line
are (x1+a*dx, y1+a*dy) — you need to compute only the coefficient a.
Numerically speaking, the determinant of the linear system is dx**2+dy**2 so you have problems only when the two initial points are extremely close to each other with respect to their distance w/r to the third point.
Python Implementation
We use a 2-uple of floats to represent a 2D point and we define a function whose arguments are 3 2-uples representing the points that define the line (p1, p2) and the point (p3) that determines the position of p4 on said line.
In [16]: def p4(p1, p2, p3):
...: x1, y1 = p1
...: x2, y2 = p2
...: x3, y3 = p3
...: dx, dy = x2-x1, y2-y1
...: det = dx*dx + dy*dy
...: a = (dy*(y3-y1)+dx*(x3-x1))/det
...: return x1+a*dx, y1+a*dy
To test the implementation I am using the three points used by the OP
to demonstrate their issues with this problem:
In [17]: p4((1.0, 1.0), (3.0, 1.0), (2.5, 2))
Out[17]: (2.5, 1.0)
It seems that the result of p4(...) coincides with the OP expectation.
A Matplotlib Example
import matplotlib.pyplot as plt
def p(p1, p2, p3):
(x1, y1), (x2, y2), (x3, y3) = p1, p2, p3
dx, dy = x2-x1, y2-y1
det = dx*dx + dy*dy
a = (dy*(y3-y1)+dx*(x3-x1))/det
return x1+a*dx, y1+a*dy
p1, p2, p3 = (2, 4), (7, 3), (1, 1)
p4 = p(p1, p2, p3)
fig, ax = plt.subplots()
# if we are after right angles, anything else would be wrong
ax.set_aspect(1)
plt.plot(*zip(p1, p2, p4, p3), marker='*')

Shapely's distance() function returns the minimum distance:
>>> from shapely.geometry import LineString as shLs
>>> from shapely.geometry import Point as shPt
>>> l = shLs([ (1,1), (3,1)])
>>> p = shPt(2,2)
>>> dist = p.distance(l)
1.0
>>> l.interpolate(dist).wkt
'POINT (2 1)'

What you want to do is a vector projection.
The edge p1p3 is rotated onto the edge p1p2 and you need to find the correct length of the segment p1p4. Then you can just use p1+FACTOR*p1p2 / length(p1p2). The needed factor is given by the cosine of the angle between p1p2 and p1p3. Then you get
p4 = p1 + p1p2 * length(p1p3)/length(p1p2)*cos(angle(p1p2, p1p3))
Here the two edge cases as example:
The cosinus is 0 if p1p3 is orthogonal to p1p2, so p4 lies on p1.
The cosinus is 1 when p1p3 lies on p1p2, so p1p2 is just scaled by length(p1p3)/length(p1p2) to get p1p4.
You further can replace the cosinus by the dot product dot(p1p2 / length(p1p2), p1p3 / length(p1p3).
You can find more details and nice illustrations in the wikibook about linear algebra.
Here an full example derived from your python code. I used numpy instead of Vector2D here.
points = [[1, 1], [3, 1], [2.5, 2], [2.5, 1]]
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
fig, ax = plt.subplots()
fig.set_size_inches(6,6)
x, y = zip(*points[:2])
l1, = ax.plot(x,y, color='blue')
scatter1 = ax.scatter(x=x,y=y, color='blue', marker='x', s=80, alpha=1.0)
x, y = zip(*points[2:])
l2, = ax.plot(x,y, color='red')
scatter2 = ax.scatter(x=x,y=y, color='red', marker='x', s=80, alpha=1.0)
p1 = np.array(points[0])
p2 = np.array(points[1])
p3 = np.array(points[2])
p1p2 = p2 - p1
p1p3 = p3 - p1
p4 = p1 + p1p2 / np.linalg.norm(p1p2) * np.linalg.norm(p1p3) * ((p1p2/np.linalg.norm(p1p2)).T * (p1p3/np.linalg.norm(p1p3)))
p1, p2, p3, p4, p1p2, p1p3
We can shorten the p4 line a bit like this, using the linearity of the scalar product:
p4 = p1 + p1p2 / np.linalg.norm(p1p2) * ((p1p2/np.linalg.norm(p1p2)).T * (p1p3))
p4 = p1 + p1p2 / np.linalg.norm(p1p2)**2 * (p1p2.T * (p1p3))

Related

Failed attempt to plot a parallel line through a known point using axline and/or solving for y=mx+b

I am trying to use plt.axline, within python, to draw a line parallel to another line running through two points. I know the coordinates of an adjacent point that I would like the parallel line to run through.
I cant make plt.axline work at all using the slope call. i.e. plt.axline((x0, y0),slope=m)
So, two things:
It would make life easier if I could just used axline with a slope call
why is it when I solve for y=mx+b my parallel line fails?
Thanks in advance!
My failed code is below:
x0 = 2457460.6130471006;y0 = 552976.7722723632
x1 = 2455541.591031033 ; y1 = 553865.0073953888
x2 = 2456838.6408896423; y2 = 550689.8369019731
%matplotlib notebook
fig,ax = plt.subplots(figsize=(5,5))
plt.scatter(x1,y1,color="red",marker="x",s=100,label="p1 = x1,y1")
plt.scatter(x2,y2,color="blue",marker="x",s=100,label="p2 = x2,y2")
plt.axline((x1, y1), (x2, y2), linestyle='--', color='black', zorder=0)
plt.scatter(x0,y0,color="green",marker="x",s=100,label="p0 = x0,y0")
m = (y2-y1)/(x2-x1) # slope of the line p1 -> p2
print('m = Slope of line through p1 and p2 = '+str(m))
b = y0/(m * x0) # y=mx+b -> b=y/mx
print('\nb = y intercept = '+str(b))
xnew = x0+10000 # picks some x point off in the distance
ynew = m*xnew+b # solves for the y at the x point off in the distance
# Draws a line with same slope running through p0
# plt.axline((x0, y0),(xnew, ynew), linestyle='--', color='red', zorder=0)
plt.axline((x0, y0),slope=m, linestyle='--', color='red', zorder=0)
plt.legend()
Here, it works, when not attempting to use axline:
x0 = 2457460.6130471006;y0 = 552976.7722723632
x1 = 2455541.591031033 ; y1 = 553865.0073953888
x2 = 2456838.6408896423; y2 = 550689.8369019731
%matplotlib notebook
fig,ax = plt.subplots(figsize=(5,5))
plt.scatter(x1,y1,color="red",marker="x",s=100,label="p1 = x1,y1")
plt.scatter(x2,y2,color="blue",marker="x",s=100,label="p2 = x2,y2")
plt.axline((x1, y1), (x2, y2), linestyle='--', color='black', zorder=0)
plt.scatter(x0,y0,color="green",marker="x",s=100,label="p0 = x0,y0")
m = (y2-y1)/(x2-x1) # slope of the line p1 -> p2
print('m = Slope of line through p1 and p2 = '+str(m))
b = y0/(m * x0) # y=mx+b -> b=y/mx
print('\nb = y intercept = '+str(b))
xnew = x0+10000 # picks some x point off in the distance
ynew = m*xnew+b # solves for the y at the x point off in the distance
# Draws a line with same slope running through p0
# plt.axline((x0, y0),(xnew, ynew), linestyle='--', color='red', zorder=0)
# plt.axline((x0, y0),slope=m, linestyle='--', color='red', zorder=0)
plt.legend()
I think it has to be b = y0 - m*x0, as
y = mx + b |-mx
b = y - mx

Generating a random float below and above a line created by numpy arrays python

I would like to generate a random float point above and below a line created by numpy arrays.
For example I have these line equations:
x_values = np.linspace(-1, 1, 100)
y1 = 2 * x_values -5
y2= -3 * x_values +2
plt.plot(x_values,y1, '-k')
plt.plot(x_values,y2, '-g')
I have tried this method from Generate random points above and below a line in Python and it works if np.arrange is used like so:
lower, upper = -25, 25
num_points = 1
x1 = [random.randrange(start=1, stop=9) for i in range(num_points)]
x2 = [random.randrange(start=1, stop=9) for i in range(num_points)]
y1 = [random.randrange(start=lower, stop=(2 * x -5) )for x in x1]
y2 = [random.randrange(start=(2 * x -5), stop=upper) for x in x2]
plt.plot(np.arange(10), 2 * np.arange(10) -5)
plt.scatter(x1, y1, c='blue')
plt.scatter(x2, y2, c='red')
However, I wanted to find a way to generate a random point if np.linspace(-1, 1, 100) was used to create the line graph. The difference is involving/allowing float coordinates to be picked to. But unsure how.
Any ideas will be appreciated.
Here is an approach, using functions for the y-values. Random x positions are chosen uniformly over the x-range. For each random x, a value is randomly chosen between its y-ranges.
import numpy as np
import matplotlib.pyplot as plt
x_values = np.linspace(-1, 1, 100)
f1 = lambda x: 2 * x - 5
f2 = lambda x: -3 * x + 2
y1 = f1(x_values)
y2 = f2(x_values)
plt.plot(x_values, y1, '-k')
plt.plot(x_values, y2, '-g')
plt.fill_between (x_values, y1, y2, color='gold', alpha=0.2)
num_points = 20
xs = np.random.uniform(x_values[0], x_values[-1], num_points)
ys = np.random.uniform(f1(xs), f2(xs))
plt.scatter(xs, ys, color='crimson')
plt.show()
PS: Note that the simplicity of the approach chooses x uniform over its length. If you need an even distribution over the area of the trapezium, you need the x less probable at the right, and more at the left. You can visualize this with many more points and using transparency. With the simplistic approach, the right will look denser than the left.
The following code first generates x,y points in a parallelogram, and remaps the points on the wrong side back to its mirror position. The code looks like:
import numpy as np
import matplotlib.pyplot as plt
x0, x1 = -1, 1
x_values = np.linspace(x0, x1, 100)
f1 = lambda x: 2 * x - 5
f2 = lambda x: -3 * x + 2
y1 = f1(x_values)
y2 = f2(x_values)
plt.plot(x_values, y1, '-k')
plt.plot(x_values, y2, '-g')
plt.fill_between(x_values, y1, y2, color='gold', alpha=0.2)
num_points = 100_000
h0 = f2(x0) - f1(x0)
h1 = f2(x1) - f1(x1)
xs1 = np.random.uniform(x0, x1, num_points)
ys1 = np.random.uniform(0, h0 + h1, num_points) + f1(xs1)
xs = np.where(ys1 <= f2(xs1), xs1, x0 + x1 - xs1)
ys = np.where(ys1 <= f2(xs1), ys1, f1(xs) + h0 + h1 + f1(xs1) - ys1)
plt.scatter(xs, ys, color='crimson', alpha=0.2, ec='none', s=1)
plt.show()
Plot comparing the two approaches:
First of all, if you have 2 intersecting lines, there will most likely be a triangle in which you can pick random points. This is dangerously close to Bertrand's paradox, so make sure that your RNG suits its purpose.
If you don't really care about how "skewed" your randomness is, try this:
import numpy as np
left, right = -1, 1
# x_values = np.linspace(left, right, 100)
k1, k2 = 2, -3
b1, b2 = -5, 2
y1 = lambda x: k1*x + b1
y2 = lambda x: k2*x + b2
# If you need a point above the 1st equation, but below the second one.
# Check the limits where you can pick the points under this condition.
nosol = False
if k1==k2:
if b1>=b2:
inters = -100
nosol = True
else:
rand_x = np.random.uniform(left,right)
rand_y = np.random.uniform(y1(rand_x),y2(rand_x))
print(f'Random point is ({round(rand_x,2)}, {round(rand_y,2)})')
else:
inters = (b2-b1)/(k1-k2)
if inters<=left:
if k1>=k2:
nosol=True
elif inters>=right:
if k1<=k2:
nosol=True
if nosol:
print('No solution')
else:
if k1>k2:
right = inters
else:
left = inters
# Pick random X between "left" and "right"
# Pick whatever distribution you like or need
rand_x = np.random.uniform(left,right)
rand_y = np.random.uniform(y1(rand_x),y2(rand_x))
print(f'Random point is ({round(rand_x,2)}, {round(rand_y,2)})')
If your random X needs to belong to a specific number sequence, use some other np.random function: randint, choice...

Python, matplotlib. Plot a function between two points

I want to plot a function between 2 points using matplotlib.
The similar problem, but for 3d case is without working answer: How to plot a function oriented on a local x axis matplotlib 3d?
I think it should work something like that:
import matplotlib.pyplot as plt
import math
def foo(x, L):
# some polynomial on [0, L]
return x**2 # usually it's more difficult
def magic(plt, foo, point1, point2):
"""
Plot foo from point1 to point2
:param plt: matplotlib.pyplot
:param foo: function
:param point1: tuple (x1, y1) or list [x1, y1]
:param point2: tuple (x2, y2) or list [x2, y2]
:return:
"""
# do magic
# create modified function along new local x' axis using points in initial x,y axis?
# create new axis, rotate and move them?
pass
x1, y1 = 1, 1 # first point coordinates
x2, y2 = 2, 2 # second point coordinates
dx = x1 - x2
dy = y1 - y2
# length, this ratio is always True in my case (see picture below)
L = math.sqrt(dx * dx + dy * dy)
ax, fig = plt.subplots()
magic(plt, foo, (x1,y1), (x2, y2))
plt.show()
Important: The length along a new axis does not change. If there is a function on [0, L] it means it will have the same domain (or 'length') after rotation/moving or representing it along a new axis.
Here is the image of what I'm trying to do
There's no magic involved. Just a translation from (0,0) to (x1,y1) and a rotation by an angle defined by dx and dy. The sine of that angle is dy/L and the cosine dx/L. Using numpy arrays is both handy to write the function is a concise form as well as speeding up the calculations.
In the code below I changed the example function to make it more clear how the function is transformed. Also, the aspect ratio is set to 'equal'. Otherwise the rotated function would look distorted.
import matplotlib.pyplot as plt
import numpy as np
def foo(x):
# some function on [0, L]
return np.sin(x*20)/(x+1)
def function_on_new_axis(foo, point1, point2):
"""
Plot foo from point1 to point2
:param foo: function
:param point1: tuple (x1, y1) or list [x1, y1]
:param point2: tuple (x2, y2) or list [x2, y2]
:return:
"""
# create modified function along new local x' axis using points in initial x,y axis?
# create new axis, rotate and move them?
dx = x2 - x1
dy = y2 - y1
L = np.sqrt(dx * dx + dy * dy)
XI = np.linspace(0, L, 500) # initial coordinate system
YI = foo(XI)
s = dy / L # sine of the rotation angle
c = dx / L # cosine of the rotation angle
XM = c * XI - s * YI + x1 # modified coordinate system
YM = s * XI + c * YI + y1
plt.plot(XI, YI, color='crimson', alpha=0.3)
plt.plot(XM, YM, color='crimson')
plt.plot([x1, x2], [y1, y2], 'go')
x1, y1 = 1, 1 # first point coordinates
x2, y2 = 2, 3 # second point coordinates
fig, ax = plt.subplots()
function_on_new_axis(foo, (x1,y1), (x2, y2))
ax.axis('equal') # equal aspect ratio between x and y axis to prevent that the function would look skewed
# show the axes at (0,0)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
plt.show()

How to draw a cylinder using matplotlib along length of point (x1,y1) and (x2,y2) with specified radius?

I would like to draw a cylinder using matplotlib along length of point (x1,y1) and (x2,y2) with specified radius r. Please let me know how to do this.
Just for fun, I'm going to generalize this to any axis (x0, y0, z0) to (x1, y1, z1). Set z0 and z1 to 0 if you want an axis in the xy plane.
You can find a vector equation for the axis pretty easily by finding the unit vector in the same direction as the axis, then adding it to p0 and scaling it along the length of the axis. Normally you can find the coordinates of a circle with x = x0 + cos(theta) * R and y = y0 + sin(theta) * R, but the circles aren't in the xy plane, so we're going to need to make our own axes with unit vectors perpendicular to the axis of the cylinder and each other and then get the xyz coordinates from that. I used this site to help me figure this out: Link. Here's the code:
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.linalg import norm
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
origin = np.array([0, 0, 0])
#axis and radius
p0 = np.array([1, 3, 2])
p1 = np.array([8, 5, 9])
R = 5
#vector in direction of axis
v = p1 - p0
#find magnitude of vector
mag = norm(v)
#unit vector in direction of axis
v = v / mag
#make some vector not in the same direction as v
not_v = np.array([1, 0, 0])
if (v == not_v).all():
not_v = np.array([0, 1, 0])
#make vector perpendicular to v
n1 = np.cross(v, not_v)
#normalize n1
n1 /= norm(n1)
#make unit vector perpendicular to v and n1
n2 = np.cross(v, n1)
#surface ranges over t from 0 to length of axis and 0 to 2*pi
t = np.linspace(0, mag, 100)
theta = np.linspace(0, 2 * np.pi, 100)
#use meshgrid to make 2d arrays
t, theta = np.meshgrid(t, theta)
#generate coordinates for surface
X, Y, Z = [p0[i] + v[i] * t + R * np.sin(theta) * n1[i] + R * np.cos(theta) * n2[i] for i in [0, 1, 2]]
ax.plot_surface(X, Y, Z)
#plot axis
ax.plot(*zip(p0, p1), color = 'red')
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_zlim(0, 10)
plt.show()

Draw a curve connecting two points instead of a straight line

I want to do something like this:
I have the points but don't know how to plot the curves instead of straight lines.
Thank you.
For people interested in this question, I followed Matthew's suggestion and came up with this implementation:
def hanging_line(point1, point2):
import numpy as np
a = (point2[1] - point1[1])/(np.cosh(point2[0]) - np.cosh(point1[0]))
b = point1[1] - a*np.cosh(point1[0])
x = np.linspace(point1[0], point2[0], 100)
y = a*np.cosh(x) + b
return (x,y)
Here is what the result looks like:
import matplotlib.pyplot as plt
point1 = [0,1]
point2 = [1,2]
x,y = hanging_line(point1, point2)
plt.plot(point1[0], point1[1], 'o')
plt.plot(point2[0], point2[1], 'o')
plt.plot(x,y)
You are going to need some expression for the curve you want to plot, then you can make the curve out of many line segments.
Here's a parabola:
x = np.linspace(-1, 1, 100)
y = x*x
plt.plot(x, y)
Here's a sin curve:
x = np.linspace(-2*np.pi, 2*np.pi, 100)
y = np.sin(x)
plt.plot(x, y)
Each of these looks smooth, but is actually made up of many small line segments.
To get a collection of curves like you showed, you are going to need some expression for a curve you want to plot in terms of its two endpoints. The ones in your picture look like catenarys which are (approximately) the shape a hanging chain assumes under the force of gravity:
x = np.linspace(-2*np.pi, 2*np.pi, 100)
y = 2*np.cosh(x/2)
plt.plot(x, y)
You will have to find a way of parameterizing this curve in terms of its two endpoints, which will require you substituting your values of y and x into:
y = a*cosh(x/a) + b
and solving the resulting pair of equations for a and b.
There is a cool (at least for me) way to draw curve lines between two points, using Bezier curves. Just with some simple code you can create lists with dots connecting points and chart them with matplotlib, for example:
def recta(x1, y1, x2, y2):
a = (y1 - y2) / (x1 - x2)
b = y1 - a * x1
return (a, b)
def curva_b(xa, ya, xb, yb, xc, yc):
(x1, y1, x2, y2) = (xa, ya, xb, yb)
(a1, b1) = recta(xa, ya, xb, yb)
(a2, b2) = recta(xb, yb, xc, yc)
puntos = []
for i in range(0, 1000):
if x1 == x2:
continue
else:
(a, b) = recta(x1, y1, x2, y2)
x = i*(x2 - x1)/1000 + x1
y = a*x + b
puntos.append((x,y))
x1 += (xb - xa)/1000
y1 = a1*x1 + b1
x2 += (xc - xb)/1000
y2 = a2*x2 + b2
return puntos
Then, just run the function for some starting, mid and ending points, and use matplotlib:
lista1 = curva_b(1, 2, 2, 1, 3, 2.5)
lista2 = curva_b(1, 2, 2.5, 1.5, 3, 2.5)
lista3 = curva_b(1, 2, 2.5, 2, 3, 2.5)
lista4 = curva_b(1, 2, 1.5, 3, 3, 2.5)
fig, ax = plt.subplots()
ax.scatter(*zip(*lista1), s=1, c='b')
ax.scatter(*zip(*lista2), s=1, c='r')
ax.scatter(*zip(*lista3), s=1, c='g')
ax.scatter(*zip(*lista4), s=1, c='k')
This should be the results:
several Bezier quadratic curves
By extending the code a little more, you can get forms like this:
Bezier quartic curve

Categories