Intersection point between line and surface in 3D python - python

I want to be able to find the intersection between a line and a three-dimensional surface.
Mathematically, I have done this by taking the following steps:
Define the (x, y, z) coordinates of the line in a parametric manner. e.g. (x, y, z) = (1+t, 2+3t, 1-t)
Define the surface as a function. e.g. z = f(x, y)
Substitute the values of x, y, and z from the line into the surface function.
By solving, I would be able to get the intersection of the surface and the line
I want to know if there is a method for doing this in Python. I am also open to suggestions on more simple ways to solving for the intersection.

You can use the following code:
import numpy as np
import scipy as sc
import scipy.optimize
from matplotlib import pyplot as plt
def f(x, y):
""" Function of the surface"""
# example equation
z = x**2 + y**2 -10
return z
p0 = np.array([1, 2, 1]) # starting point for the line
direction = np.array( [1, 3, -1]) # direction vector
def line_func(t):
"""Function of the straight line.
:param t: curve-parameter of the line
:returns xyz-value as array"""
return p0 + t*direction
def target_func(t):
"""Function that will be minimized by fmin
:param t: curve parameter of the straight line
:returns: (z_line(t) - z_surface(t))**2 – this is zero
at intersection points"""
p_line = line_func(t)
z_surface = f(*p_line[:2])
return np.sum((p_line[2] - z_surface)**2)
t_opt = sc.optimize.fmin(target_func, x0=-10)
intersection_point = line_func(t_opt)
The main idea is to reformulate the algebraic equation point_of_line = point_of_surface (condition for intersection) into a minimization problem: |point_of_line - point_of_surface| → min. Due to the representation of the surface as z_surface = f(x, y) it is convenient to calculate the distance for a given t-value only on basis of the z-values. This is done in target_func(t). And then the optimal t-value is found by fmin.
The correctness and plausibility of the result can be checked with some plotting:
from mpl_toolkits.mplot3d import Axes3D
ax = plt.subplot(projection='3d')
X = np.linspace(-5, 5, 10)
Y = np.linspace(-5, 5, 10)
tt = np.linspace(-5, 5, 100)
XX, YY = np.meshgrid(X, Y)
ZZ = f(XX, YY)
ax.plot_wireframe(XX, YY, ZZ, zorder=0)
LL = np.array([line_func(t) for t in tt])
ax.plot(*LL.T, color="orange", zorder=10)
ax.plot([x], [y], [z], "o", color="red", ms=10, zorder=20)
Note that this combination of wire frame and line plots does not handle well, which part of the orange line should be below the blue wire lines of the surface.
Also note, that for this type of problem there might be any number of solutions from 0 up to +∞. This depends on the actual surface. fmin finds an local optimum, this might be a global optimum with target_func(t_opt)=0 or it might not. Changing the initial guess x0 might change which local optimum fmin finds.

Related

Plotting arrows perpendicular to coordinates

I have a plot like this, plotting a semicircle with x and y
I want to add arrows at each point like so (ignore the horrible paint job):
Is there an easy way to add arrows perpendicular to the plot?
Current code:
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
x = np.linspace(x0,x1,9)
y = k + np.sqrt(r**2 - (x-h)**2)
plt.scatter(x,y)
plt.xlim(-4,4)
plt.ylim(-4,4)
PERPENDICULAR TO THE TANGENT OF THE CURVE I'M SORRY I FORGOT TO ADD THIS
A point in space has no idea what "perpendicular" means, but assuming your y is some function of x that has a derivate, you can think of the derivate of the function at some point to be the tangent of the curve at that point, and to get a perpendicular vector you just need to rotate the vector counter-clockwise 90 degrees:
x1, y1 = -y0, x0
We know that these points come from a circle. So given three points we can easily find the center using basic geometry notions. If you need a refresher, take a look here.
For this particular case, the center is at the origin. Knowing the center coordinates, the normal at each point is just the vector from the center to the point itself. Since the center is the origin, the normals' components are just given by the coordinates of the points themselves.
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
x = np.linspace(x0, x1, 9)
y = k + np.sqrt(r**2 - (x-h)**2)
center = np.array([0.0, 0.0])
plt.scatter(x, y)
plt.quiver(x, y, x, y, width=0.005)
plt.xlim(-4, 4)
plt.ylim(-4, 4)
plt.show()
If you are in a hurry and you do not have time to implement equations, you could use the scikit-spatial library in the following way:
from skspatial.objects import Circle, Vector, Points
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
x = np.linspace(x0, x1, 9)
y = k + np.sqrt(r**2 - (x-h)**2)
points = Points(np.vstack((x, y)).T)
circle = Circle.best_fit(np.vstack((x, y)).T)
center = circle.point
normals = np.array([Vector.from_points(center, point) for point in points])
plt.scatter(x, y)
plt.quiver(x, y, normals[:, 0], normals[:, 1], width=0.005)
plt.xlim(-4, 4)
plt.ylim(-4, 4)
plt.show()
Postulate of blunova's and simon's answers is correct, generally speaking: points have no normal, but curve have; so you need to rely on what you know your curve is. Either, as blunova described it, by the knowledge that it is a circle, and computing those normal with ad-hoc computation from that knowledge.
Or, as I am about to describe, using the function f such as y=f(x). and using knowledge on what is the normal to such a (x,f(x)) chart.
Here is your code, written with such a function f
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
x = np.linspace(x0,x1,9)
def f(x):
return k + np.sqrt(r**2 - (x-h)**2)
y=f(x)
plt.scatter(x,y)
plt.xlim(-4,4)
plt.ylim(-4,4)
So, all I did here is rewriting your line y=... in the form of a function.
From there, it is possible to compute the normal to each point of the chart (x,f(x)).
The tangent to a point (x,f(x)) is well known: it is vector (1,f'(x)), where f'(x) is the derivative of f. So, normal to that is (-f'(x), 1).
Divided by √(f'(x)²+1) to normalize this vector.
So, just use that as entry to quiver.
First compute a derivative of your function
dx=0.001
def fprime(x):
return (f(x+dx)-f(x-dx))/(2*dx)
Then, just
plt.quiver(x, f(x), -fprime(x), 1)
Or, to have all vector normalized
plt.quiver(x, f(x), -fprime(x)/np.sqrt(fprime(x)**2+1), 1/np.sqrt(fprime(x)**2+1))
(note that fprime and the normalization part are all vectorizable operation, so it works with x being a arange)
All together
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
def f(x):
return k+ np.sqrt(r**2 - (x-h)**2)
dx=0.001
x = np.linspace(x0+dx,x1-dx,9)
y = f(x)
def fprime(x):
return (f(x+dx)-f(x-dx))/(2*dx)
plt.scatter(x,y)
plt.quiver(x,f(x), -fprime(x)/np.sqrt(fprime(x)**2+1), 1/np.sqrt(fprime(x)**2+1))
plt.xlim(-4,4)
plt.ylim(-4,4)
plt.show()
That is almost an exact copy of your code, but for the quiver line, and with the addition of fprime.
One other slight change, specific to your curve, is that I changed x range to ensure the computability of fprime (if first x is x0, then fprime need f(x0-dx) which does not exist because of sqrt. Likewise for x1. So, first x is x0+dx, and last is x1-dx, which is visually the same)
That is the main advantage of this solution over blunova's: it is your code, essentially. And would work if you change f, without assuming that f is a circle. All that is assume is that f is derivable (and if it were not, you could not define what those normal are anyway).
For example, if you want to do the same with a parabola instead, just change f
import numpy as np
import matplotlib.pyplot as plt
r = 2
h = 0
k = 0
x0 = h-r
x1 = h+r
def f(x):
return x**2
dx=0.001
x = np.linspace(x0+dx,x1-dx,9)
y = f(x)
def fprime(x):
return (f(x+dx)-f(x-dx))/(2*dx)
plt.scatter(x,y)
plt.quiver(x,f(x), -fprime(x)/np.sqrt(fprime(x)**2+1), 1/np.sqrt(fprime(x)**2+1))
plt.xlim(-4,4)
plt.ylim(-2,5)
plt.show()
All I changed here is the f formula. Not need for a new reasoning to compute the normal.
Last remark: an even more accurate version (not forcing the approximate computation of fprime with a dx) would be to use sympy to define f, and then compute the real, symbolic, derivative of f. But that doesn't seem necessary for your case.

Determine outer contour of boolean function

I would like to determine the contour of a boolean function defined over two variables, each in the 0, 1 range. I do not have access to the explicit form of the function: it is a black box that returns a boolean for each pairs of variables, so I don't know how to convert it to a continuous function and use the first answer below. The region is convex, and it may reach the edges of the ranges of the parameters. Language is python. The function may be expensive to calculate, so the brute force grid approach I have below is slow in addition to being inaccurate. How can I improve things?
import numpy as np
import matplotlib.pyplot as plt
def f(x, y): return ((x-0.3)**2+(y-0.4)**2 <= 0.1) # Example of function; may extend outside region. In reality, form is not known.
xs, ys = np.linspace(0, 1, 11), np.linspace(0, 1, 11) # Brute force grid
for x in xs:
for y in ys: plt.plot(x, y, 'ro' if f(x, y) else 'b+')
cntr = [] # List of unique, approximate contour points
for x in xs:
col = [y for y in ys if f(x, y)]
if (col != []) and (not ((x, min(col)) in cntr)): cntr.append((x, min(col)))
if (col != []) and (not ((x, max(col)) in cntr)): cntr.append((x, max(col)))
for y in ys:
row = [x for x in xs if f(x, y)]
if (row != []) and (not ((min(row), y) in cntr)): cntr.append((min(row), y))
if (row != []) and (not ((max(row), y) in cntr)): cntr.append((max(row), y))
plt.plot(np.transpose(cntr)[0], np.transpose(cntr)[1], 'kv') # Contour in black
plt.show()
EDIT: illustration of effect of level on #Arne's answer:
Of course this is always going to be somewhat approximate, because you can't sample every point, but there are two functions that can help to simplify the code and speed things up: np.meshgrid() and plt.contour(). The latter allows specifying the levels you want to plot, so you can define a real-valued function by leaving the threshold out of your Boolean function, and pass the threshold as the level to plot to plt.contour() instead:
import numpy as np
import matplotlib.pyplot as plt
def f_continuous(x, y):
return (x - 0.3)**2 + (y - 0.4)**2 # real-valued instead of Boolean;
# define the 0.1 threshold in the contour plot function instead
xs, ys = np.linspace(0, 1, 11), np.linspace(0, 1, 11) # Brute force grid
X, Y = np.meshgrid(xs, ys)
plt.contour(X, Y, f_continuous(X, Y), levels=[0.1])
plt.show()
Edit: What if the function is a black box and can't be turned into a continuous one?
You can still use plt.contour(), by passing a value between the Boolean values 0 and 1 as the contour level. The interpolation just doesn't turn out as smooth in this case:
plt.contour(X, Y, f(X, Y), levels=[0.5])
plt.show()
As the answer you linked to notes, you can get the coordinates of the calculated points on the contour line by catching the QuadContourSet object returned by plt.contour() and accessing its allsegs attribute:
contour = plt.contour(X, Y, f(X, Y), levels=[0.5])
contour.allsegs
[[array([[0. , 0.25],
[0.05, 0.2 ],
[0.1 , 0.15],
...
[0.1 , 0.65],
[0.05, 0.6 ],
[0. , 0.55]])]]

Differentiate a 2d cubic spline in python

I'm using interpolate.interp2d() to fit a 2-D spline over a function. How can I get the first derivative of the spline w.r.t. each of the dependent variables? Here is my code so far, Z are the descrete points on a mesh-grid that I have
from scipy import interpolate
YY, XX = np.meshgrid(Y, X)
f = interpolate.interp2d(AA, XX, Z, kind='cubic')
So, I need df/dx and df/dy. Note also that my Y-grid is not evenly spaced. I guess I can numerically differentiate Z and then fit a new spline, but it seemed like too much hassle. Is there an easier way?
You can differentiate the output of interp2d by using the function bisplev on the tck property of the interpolant with the optional arguments dx and dy.
If you've got some meshed data which you've interpolated:
X = np.arange(5.)
Y = np.arange(6., 11)
Y[0] = 4 # Demonstrate an irregular mesh
YY, XX = np.meshgrid(Y, X)
Z = np.sin(XX*2*np.pi/5 + YY*YY*2*np.pi/11)
f = sp.interpolate.interp2d(XX, YY, Z, kind='cubic')
xt = np.linspace(X.min(), X.max())
yt = np.linspace(Y.min(), Y.max())
then you can access the appropriate structure for bisplev as f.tck: the partial derivative of f with respect to x can be evaluated as
Z_x = sp.interpolate.bisplev(xt, yt, f.tck, dx=1, dy=0)
Edit: From this answer, it looks like the result of interp2d can itself take the optional arguments of dx and dy:
Z_x = f(xt, yt, dx=1, dy=0)

Symmetric streamplot with matplotlib

I'm trying to plot the streamlines of a magnetic field around a sphere using matplotlib, and it does work quite nicely. However, the resulting image is not symmetric, but it should be (I think).
This is the code used to generate the image. Excuse the length, but I thought it would be better than just posting a non-working snippet. Also, it's not very pythonic; that's because I converted it from Matlab, which was easier than I expected.
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
def cart2spherical(x, y, z):
r = np.sqrt(x**2 + y**2 + z**2)
phi = np.arctan2(y, x)
theta = np.arccos(z/r)
if r == 0:
theta = 0
return (r, theta, phi)
def S(theta, phi):
S = np.array([[np.sin(theta)*np.cos(phi), np.cos(theta)*np.cos(phi), -np.sin(phi)],
[np.sin(theta)*np.sin(phi), np.cos(theta)*np.sin(phi), np.cos(phi)],
[np.cos(theta), -np.sin(theta), 0]])
return S
def computeB(r, theta, phi, a=1, muR=100, B0=1):
delta = (muR - 1)/(muR + 2)
if r > a:
Bspherical = B0*np.array([np.cos(theta) * (1 + 2*delta*a**3 / r**3),
np.sin(theta) * (delta*a**3 / r**3 - 1),
0])
B = np.dot(S(theta, phi), Bspherical)
else:
B = 3*B0*(muR / (muR + 2)) * np.array([0, 0, 1])
return B
Z, X = np.mgrid[-2.5:2.5:1000j, -2.5:2.5:1000j]
Bx = np.zeros(np.shape(X))
Bz = np.zeros(np.shape(X))
Babs = np.zeros(np.shape(X))
for i in range(len(X)):
for j in range(len(Z)):
r, theta, phi = cart2spherical(X[0, i], 0, Z[j, 0])
B = computeB(r, theta, phi)
Bx[i, j], Bz[i, j] = B[0], B[2]
Babs[i, j] = np.sqrt(B[0]**2 + B[1]**2 + B[2]**2)
fig=plt.figure()
ax=fig.add_subplot(111)
plt.streamplot(X, Z, Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3,
minlength=0.9, arrowstyle='-')
ax.add_patch(Circle((0, 0), radius=1, facecolor='none', linewidth=2))
plt.axis('equal')
plt.axis('off')
fig.savefig('streamlines.pdf', transparent=True, bbox_inches='tight', pad_inches=0)
First of all, for curiosity, why would you want to plot symmetric data? Why plotting half of isn't fine?
Said that, this is a possible hack. You can use mask arrays as Hooked suggested to plot half of it:
mask = X>0
BX_OUT = Bx.copy()
BZ_OUT = Bz.copy()
BX_OUT[mask] = None
BZ_OUT[mask] = None
res = plt.streamplot(X, Z, BX_OUT, BZ_OUT, color='k',
arrowstyle='-',linewidth=1,density=2)
then you save in res the result from streamplot, extract the lines and plot them with the opposite X coordinate.
lines = res.lines.get_paths()
for l in lines:
plot(-l.vertices.T[0],l.vertices.T[1],'k')
I used this hack to extract streamlines and arrows from a 2D plot, then apply a 3D transformation and plot it with mplot3d. A picture is in one of my questions here.
Quoting from the documentation:
density : float or 2-tuple
Controls the closeness of streamlines. When density = 1,
the domain is divided into
a 25x25 grid—density linearly scales this grid.
Each cell in the grid can have, at most, one traversing streamline.
For different densities in each direction, use [density_x, density_y].
so you are getting aliasing effects between the cells it uses to decide where the stream lines are, and the symmetries of your problem. You need to carefully choose your grid size (of the data) and the density.
It is also sensitive to where the box boundaries are relative to the top of the sphere. Is the center of your sphere on a data grid point or between the data grid points? If it is on a grid point then the box that contains the center point will be different than the boxes adjacent to it.
I am not familiar with exactly how it decides which stream lines to draw, but I could imagine that it is some sort of greedy algorithm and hence will give different results walking towards the high density region and away density region.
To be clear, you issue is not that the stream lines are wrong, they are valid stream lines, it is that you find the result not aesthetically pleasing.
Use a mask to separate the two regions of interest:
mask = np.sqrt(X**2+Z**2)<1
BX_OUT = Bx.copy()
BZ_OUT = Bz.copy()
BX_OUT[mask] = None
BZ_OUT[mask] = None
plt.streamplot(X, Z, BX_OUT, BZ_OUT, color='k',
arrowstyle='-', density=2)
BX_IN = Bx.copy()
BZ_IN = Bz.copy()
BX_IN[~mask] = None
BZ_IN[~mask] = None
plt.streamplot(X, Z, BX_IN, BZ_IN, color='r',
arrowstyle='-', density=2)
The resulting plot isn't exactly symmetric, but by giving the algorithm a hint, it's far closer than what you had before. Play with the density of the grid via meshgrid and the density parameter to achieve the effect you are looking for.
Use physics, instead... The magnetic field is symmetrical with respect to the z (vertical) axis! So you just need two streamplot's:
plt.streamplot(X, Z, Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3, minlength=0.9, arrowstyle='-')
plt.streamplot(-X, Z, -Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3, minlength=0.9, arrowstyle='-')

Bézier curve fitting with SciPy

I have a set of points which approximate a 2D curve. I would like to use Python with numpy and scipy to find a cubic Bézier path which approximately fits the points, where I specify the exact coordinates of two endpoints, and it returns the coordinates of the other two control points.
I initially thought scipy.interpolate.splprep() might do what I want, but it seems to force the curve to pass through each one of the data points (as I suppose you would want for interpolation). I'll assume that I was on the wrong track with that.
My question is similar to this one: How can I fit a Bézier curve to a set of data?, except that they said they didn't want to use numpy. My preference would be to find what I need already implemented somewhere in scipy or numpy. Otherwise, I plan to implement the algorithm linked from one of the answers to that question, using numpy: An algorithm for automatically fitting digitized curves (pdf.page 622).
Thank you for any suggestions!
Edit: I understand that a cubic Bézier curve is not guaranteed to pass through all the points; I want one which passes through two given endpoints, and which is as close as possible to the specified interior points.
Here's a way to do Bezier curves with numpy:
import numpy as np
from scipy.special import comb
def bernstein_poly(i, n, t):
"""
The Bernstein polynomial of n, i as a function of t
"""
return comb(n, i) * ( t**(n-i) ) * (1 - t)**i
def bezier_curve(points, nTimes=1000):
"""
Given a set of control points, return the
bezier curve defined by the control points.
points should be a list of lists, or list of tuples
such as [ [1,1],
[2,3],
[4,5], ..[Xn, Yn] ]
nTimes is the number of time steps, defaults to 1000
See http://processingjs.nihongoresources.com/bezierinfo/
"""
nPoints = len(points)
xPoints = np.array([p[0] for p in points])
yPoints = np.array([p[1] for p in points])
t = np.linspace(0.0, 1.0, nTimes)
polynomial_array = np.array([ bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints) ])
xvals = np.dot(xPoints, polynomial_array)
yvals = np.dot(yPoints, polynomial_array)
return xvals, yvals
if __name__ == "__main__":
from matplotlib import pyplot as plt
nPoints = 4
points = np.random.rand(nPoints,2)*200
xpoints = [p[0] for p in points]
ypoints = [p[1] for p in points]
xvals, yvals = bezier_curve(points, nTimes=1000)
plt.plot(xvals, yvals)
plt.plot(xpoints, ypoints, "ro")
for nr in range(len(points)):
plt.text(points[nr][0], points[nr][1], nr)
plt.show()
Here is a piece of python code for fitting points:
'''least square qbezier fit using penrose pseudoinverse
>>> V=array
>>> E, W, N, S = V((1,0)), V((-1,0)), V((0,1)), V((0,-1))
>>> cw = 100
>>> ch = 300
>>> cpb = V((0, 0))
>>> cpe = V((cw, 0))
>>> xys=[cpb,cpb+ch*N+E*cw/8,cpe+ch*N+E*cw/8, cpe]
>>>
>>> ts = V(range(11), dtype='float')/10
>>> M = bezierM (ts)
>>> points = M*xys #produces the points on the bezier curve at t in ts
>>>
>>> control_points=lsqfit(points, M)
>>> linalg.norm(control_points-xys)<10e-5
True
>>> control_points.tolist()[1]
[12.500000000000037, 300.00000000000017]
'''
from numpy import array, linalg, matrix
from scipy.misc import comb as nOk
Mtk = lambda n, t, k: t**(k)*(1-t)**(n-k)*nOk(n,k)
bezierM = lambda ts: matrix([[Mtk(3,t,k) for k in range(4)] for t in ts])
def lsqfit(points,M):
M_ = linalg.pinv(M)
return M_ * points
Generally on bezier curves check out
Animated bezier and
bezierinfo
Resulting Plot
Building upon the answers from #reptilicus and #Guillaume P., here is the complete code to:
Get the Bezier Parameters i.e. the control points from a list of points.
Create the Bezier Curve from the Bezier Parameters i.e. the control points.
Plot the original points, the control points and the resulting Bezier Curve.
Getting the Bezier Parameters i.e. the control points from a set of X,Y points or coordinates. The other parameter needed is the degree for the approximation and the resulting control points will be (degree + 1)
import numpy as np
from scipy.special import comb
def get_bezier_parameters(X, Y, degree=3):
""" Least square qbezier fit using penrose pseudoinverse.
Parameters:
X: array of x data.
Y: array of y data. Y[0] is the y point for X[0].
degree: degree of the Bézier curve. 2 for quadratic, 3 for cubic.
Based on https://stackoverflow.com/questions/12643079/b%C3%A9zier-curve-fitting-with-scipy
and probably on the 1998 thesis by Tim Andrew Pastva, "Bézier Curve Fitting".
"""
if degree < 1:
raise ValueError('degree must be 1 or greater.')
if len(X) != len(Y):
raise ValueError('X and Y must be of the same length.')
if len(X) < degree + 1:
raise ValueError(f'There must be at least {degree + 1} points to '
f'determine the parameters of a degree {degree} curve. '
f'Got only {len(X)} points.')
def bpoly(n, t, k):
""" Bernstein polynomial when a = 0 and b = 1. """
return t ** k * (1 - t) ** (n - k) * comb(n, k)
#return comb(n, i) * ( t**(n-i) ) * (1 - t)**i
def bmatrix(T):
""" Bernstein matrix for Bézier curves. """
return np.matrix([[bpoly(degree, t, k) for k in range(degree + 1)] for t in T])
def least_square_fit(points, M):
M_ = np.linalg.pinv(M)
return M_ * points
T = np.linspace(0, 1, len(X))
M = bmatrix(T)
points = np.array(list(zip(X, Y)))
final = least_square_fit(points, M).tolist()
final[0] = [X[0], Y[0]]
final[len(final)-1] = [X[len(X)-1], Y[len(Y)-1]]
return final
Create the Bezier curve given the Bezier Parameters i.e. control points.
def bernstein_poly(i, n, t):
"""
The Bernstein polynomial of n, i as a function of t
"""
return comb(n, i) * ( t**(n-i) ) * (1 - t)**i
def bezier_curve(points, nTimes=50):
"""
Given a set of control points, return the
bezier curve defined by the control points.
points should be a list of lists, or list of tuples
such as [ [1,1],
[2,3],
[4,5], ..[Xn, Yn] ]
nTimes is the number of time steps, defaults to 1000
See http://processingjs.nihongoresources.com/bezierinfo/
"""
nPoints = len(points)
xPoints = np.array([p[0] for p in points])
yPoints = np.array([p[1] for p in points])
t = np.linspace(0.0, 1.0, nTimes)
polynomial_array = np.array([ bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints) ])
xvals = np.dot(xPoints, polynomial_array)
yvals = np.dot(yPoints, polynomial_array)
return xvals, yvals
Sample data used (can be replaced with any data, this is GPS data).
points = []
xpoints = [19.21270, 19.21269, 19.21268, 19.21266, 19.21264, 19.21263, 19.21261, 19.21261, 19.21264, 19.21268,19.21274, 19.21282, 19.21290, 19.21299, 19.21307, 19.21316, 19.21324, 19.21333, 19.21342]
ypoints = [-100.14895, -100.14885, -100.14875, -100.14865, -100.14855, -100.14847, -100.14840, -100.14832, -100.14827, -100.14823, -100.14818, -100.14818, -100.14818, -100.14818, -100.14819, -100.14819, -100.14819, -100.14820, -100.14820]
for i in range(len(xpoints)):
points.append([xpoints[i],ypoints[i]])
Plot the original points, the control points and the resulting Bezier Curve.
import matplotlib.pyplot as plt
# Plot the original points
plt.plot(xpoints, ypoints, "ro",label='Original Points')
# Get the Bezier parameters based on a degree.
data = get_bezier_parameters(xpoints, ypoints, degree=4)
x_val = [x[0] for x in data]
y_val = [x[1] for x in data]
print(data)
# Plot the control points
plt.plot(x_val,y_val,'k--o', label='Control Points')
# Plot the resulting Bezier curve
xvals, yvals = bezier_curve(data, nTimes=1000)
plt.plot(xvals, yvals, 'b-', label='B Curve')
plt.legend()
plt.show()
#keynesiancross asked for "comments in [Roland's] code as to what the variables are" and others completely missed the stated problem. Roland started with a Bézier curve as input (to get a perfect match), which made it harder to understand both the problem and (at least for me) the solution. The difference from interpolation is easier to see for input that leaves residuals. Here is both paraphrased code and non-Bézier input -- and an unexpected outcome.
import matplotlib.pyplot as plt
import numpy as np
from scipy.special import comb as n_over_k
Mtk = lambda n, t, k: t**k * (1-t)**(n-k) * n_over_k(n,k)
BézierCoeff = lambda ts: [[Mtk(3,t,k) for k in range(4)] for t in ts]
fcn = np.log
tPlot = np.linspace(0. ,1. , 81)
xPlot = np.linspace(0.1,2.5, 81)
tData = tPlot[0:81:10]
xData = xPlot[0:81:10]
data = np.column_stack((xData, fcn(xData))) # shapes (9,2)
Pseudoinverse = np.linalg.pinv(BézierCoeff(tData)) # (9,4) -> (4,9)
control_points = Pseudoinverse.dot(data) # (4,9)*(9,2) -> (4,2)
Bézier = np.array(BézierCoeff(tPlot)).dot(control_points)
residuum = fcn(Bézier[:,0]) - Bézier[:,1]
fig, ax = plt.subplots()
ax.plot(xPlot, fcn(xPlot), 'r-')
ax.plot(xData, data[:,1], 'ro', label='input')
ax.plot(Bézier[:,0],
Bézier[:,1], 'k-', label='fit')
ax.plot(xPlot, 10.*residuum, 'b-', label='10*residuum')
ax.plot(control_points[:,0],
control_points[:,1], 'ko:', fillstyle='none')
ax.legend()
fig.show()
This works well for fcn = np.cos but not for log. I kind of expected that the fit would use the t-component of the control points as additional degrees of freedom, as we would do by dragging the control points:
manual_points = np.array([[0.1,np.log(.1)],[.27,-.6],[.82,.23],[2.5,np.log(2.5)]])
Bézier = np.array(BézierCoeff(tPlot)).dot(manual_points)
residuum = fcn(Bézier[:,0]) - Bézier[:,1]
fig, ax = plt.subplots()
ax.plot(xPlot, fcn(xPlot), 'r-')
ax.plot(xData, data[:,1], 'ro', label='input')
ax.plot(Bézier[:,0],
Bézier[:,1], 'k-', label='fit')
ax.plot(xPlot, 10.*residuum, 'b-', label='10*residuum')
ax.plot(manual_points[:,0],
manual_points[:,1], 'ko:', fillstyle='none')
ax.legend()
fig.show()
The cause of failure, I guess, is that the norm measures the distance between points on the curves instead of the distance between a point on one curve to the nearest point on the other curve.
Short answer: you don't, because that's not how Bezier curves work. Longer answer: have a look at Catmull-Rom splines instead. They're pretty easy to form (the tangent vector at any point P, barring start and end, is parallel to the lines {P-1,P+1}, so they're easy to program, too) and always pass through the points that define them, unlike Bezier curves, which interpolates "somewhere" inside the convex hull set up by all the control points.
A Bezier curve isn't guaranteed to pass through every point you supply it with; control points are arbitrary (in the sense that there is no specific algorithm for finding them, you simply choose them yourself) and only pull the curve in a direction.
If you want a curve which will pass through every point you supply it with, you need something like a natural cubic spline, and due to the limitations of those (you must supply them with increasing x co-ordinates, or it tends to infinity), you'll probably want a parametric natural cubic spline.
There are nice tutorials here:
Cubic Splines
Parametric Cubic Splines
I had the same problem as detailed in the question. I took the code provided Roland Puntaier and was able to make it work. Here:
def get_bezier_parameters(X, Y, degree=2):
""" Least square qbezier fit using penrose pseudoinverse.
Parameters:
X: array of x data.
Y: array of y data. Y[0] is the y point for X[0].
degree: degree of the Bézier curve. 2 for quadratic, 3 for cubic.
Based on https://stackoverflow.com/questions/12643079/b%C3%A9zier-curve-fitting-with-scipy
and probably on the 1998 thesis by Tim Andrew Pastva, "Bézier Curve Fitting".
"""
if degree < 1:
raise ValueError('degree must be 1 or greater.')
if len(X) != len(Y):
raise ValueError('X and Y must be of the same length.')
if len(X) < degree + 1:
raise ValueError(f'There must be at least {degree + 1} points to '
f'determine the parameters of a degree {degree} curve. '
f'Got only {len(X)} points.')
def bpoly(n, t, k):
""" Bernstein polynomial when a = 0 and b = 1. """
return t ** k * (1 - t) ** (n - k) * comb(n, k)
def bmatrix(T):
""" Bernstein matrix for Bézier curves. """
return np.matrix([[bpoly(degree, t, k) for k in range(degree + 1)] for t in T])
def least_square_fit(points, M):
M_ = np.linalg.pinv(M)
return M_ * points
T = np.linspace(0, 1, len(X))
M = bmatrix(T)
points = np.array(list(zip(X, Y)))
return least_square_fit(points, M).tolist()
To fix the end points of the curve, ignore the first and last parameter returned by the function and use your own points.
What Mike Kamermans said is true, but I also wanted to point out that, as far as I know, catmull-rom splines can be defined in terms of cubic beziers. So, if you only have a library that works with cubics, you should still be able to do catmull-rom splines:
http://schepers.cc/getting-to-the-point
https://github.com/DmitryBaranovskiy/raphael/blob/d8fbe4be81d362837f95e33886b80fb41de443b4/dev/raphael.core.js#L1021

Categories

Resources