Кривая Bézier с SciPy

У меня есть набор точек, которые аппроксимируют двумерную кривую. Я хотел бы использовать Python с numpy и scipy, чтобы найти кубический маршрут Bézier, который приблизительно соответствует точкам, где я указываю точные координаты двух конечных точек и возвращает координаты двух других контрольных точек.

Первоначально я думал, что scipy.interpolate.splprep() может делать то, что я хочу, но, похоже, заставляет кривую проходить через каждую из точек данных (как я полагаю, вы хотели бы использовать для интерполяции). Я предполагаю, что с этим я ошибся.

Мой вопрос аналогичен этому: как я могу подогнать кривую Безье к набору данных? , за исключением того, что они сказали, что не хотят использовать numpy. Мое предпочтение было бы найти то, что мне нужно, уже реализовано где-то в scipy или numpy. В противном случае я планирую реализовать алгоритм, связанный с одним из ответов на этот вопрос, используя numpy: Алгоритм автоматического подбора оцифрованных кривых (pdf.page 622).

Спасибо за любые предложения!

Изменение: я понимаю, что кубическая кривая Безье не будет проходить через все точки; Я хочу один, который проходит через две заданные конечные точки и как можно ближе к указанным внутренним точкам.

Ответ 1

Вот фрагмент кода python для подбора точек:

'''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

Как правило, кривые Безье проверяют анимированные безье и безье

Ответ 2

Здесь можно сделать кривые Безье с numpy:

import numpy as np
from scipy.misc 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()

Ответ 3

Кривая Безье не будет проходить через каждую точку, которую вы ее снабжаете; контрольные точки произвольны (в том смысле, что нет конкретного алгоритма их поиска, вы сами выбираете их сами) и только тяните кривую в направлении.

Если вам нужна кривая, которая будет проходить через каждую точку, которую вы ее снабжаете, вам нужно что-то вроде натурального кубического сплайна и из-за ограничений этих (вы должны снабдить их увеличивающимися координатами x, или это имеет тенденцию до бесконечности), вам, вероятно, понадобится параметрический естественный кубический сплайн.

Здесь есть интересные уроки:

Кубические сплайны

Параметрические кубические сплайны

Ответ 4

@keynesiancross попросил "комментарии в коде [Роланда] относительно того, что переменные", а другие полностью упустили заявленную проблему. Роланд начал с кривой Безье в качестве входных данных (чтобы добиться идеального соответствия), что затрудняло понимание проблемы и (по крайней мере для меня) решение. Отличие от интерполяции легче увидеть для ввода, оставляющего остатки. Вот как парафразованный код, так и небезирский ввод - и неожиданный результат.

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()

Это хорошо работает для fcn = np.cos но не для log. Я ожидал, что подгонка будет использовать t-компонент контрольных точек в качестве дополнительных степеней свободы, как это было бы, перетаскивая контрольные точки:

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()

Я полагаю, причиной неудачи является то, что норма измеряет расстояние между точками на кривых вместо расстояния между точкой на одной кривой до ближайшей точки на другой кривой.

Ответ 5

Короткий ответ: вы этого не сделаете, потому что это не то, как работают кривые Безье. Более длительный ответ: вместо этого взгляните на сплайны Catmull-Rom. Их довольно легко сформировать (касательный вектор в любой точке P, запирающий начало и конец, параллелен линиям {P-1, P + 1}, поэтому они также легко программируются) и всегда проходят через точки, которые их определяют, в отличие от кривых Безье, которые интерполируют "где-то" внутри выпуклой оболочки, установленной всеми контрольными точками.

Ответ 6

То, что сказал Майк Камерманн, верно, но я также хотел указать, что, насколько мне известно, сплайны камуфляжа можно определить в терминах кубических безьеров. Итак, если у вас есть только библиотека, которая работает с кубиками, вы все равно сможете использовать сплайны catmull-rom: