Bézier curve fitting with SciPy

后端 未结 7 595
醉酒成梦
醉酒成梦 2020-12-04 19:57

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

相关标签:
7条回答
  • 2020-12-04 20:21

    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.

    0 讨论(0)
  • 2020-12-04 20:29

    @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.

    0 讨论(0)
  • 2020-12-04 20:30

    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()
    
    0 讨论(0)
  • 2020-12-04 20:35

    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

    0 讨论(0)
  • 2020-12-04 20:35

    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

    0 讨论(0)
  • 2020-12-04 20:43

    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.

    0 讨论(0)
提交回复
热议问题