2016-02-06 3 views
2

Мне нужно оценить b-сплайны в python. Для этого я написал код, который работает очень хорошо.Как получить сплайн-основу, используемую scipy.interpolate.splev

import numpy as np 
import scipy.interpolate as si 

def scipy_bspline(cv,n,degree): 
    """ bspline basis function 
     c  = list of control points. 
     n  = number of points on the curve. 
     degree = curve degree 
    """ 
    # Create a range of u values 
    c = cv.shape[0] 
    kv = np.clip(np.arange(c+degree+1)-degree,0,c-degree) 
    u = np.linspace(0,c-degree,n) 

    # Calculate result 
    return np.array(si.splev(u, (kv,cv.T,degree))).T 

Придав ему 6 контрольных точек и попросить его оценить 100k точек на кривой ветер:

# Control points 
cv = np.array([[ 50., 25., 0.], 
     [ 59., 12., 0.], 
     [ 50., 10., 0.], 
     [ 57., 2., 0.], 
     [ 40., 4., 0.], 
     [ 40., 14., 0.]]) 

n = 100000 # 100k Points 
degree = 3 # Curve degree 
points_scipy = scipy_bspline(cv,n,degree) #cProfile clocks this at 0.012 seconds 

Вот сюжет "points_scipy": enter image description here

Теперь, чтобы сделать это быстрее я мог бы вычислить основу для всех 100k точек на кривой, сохранить это в памяти, и когда мне нужно нарисовать кривую, все, что мне нужно будет сделать, - это умножить новые позиции контрольной точки с сохраненной основой, чтобы получить новую кривую , Для того, чтобы доказать свою точку зрения я написал функцию, которая использует DeBoor's algorithm вычислить свою базу:

def basis(c, n, degree): 
    """ bspline basis function 
     c  = number of control points. 
     n  = number of points on the curve. 
     degree = curve degree 
    """ 
    # Create knot vector and a range of samples on the curve 
    kv = np.array([0]*degree + range(c-degree+1) + [c-degree]*degree,dtype='int') # knot vector 
    u = np.linspace(0,c-degree,n) # samples range 

    # Cox - DeBoor recursive function to calculate basis 
    def coxDeBoor(u, k, d): 
     # Test for end conditions 
     if (d == 0): 
      if (kv[k] <= u and u < kv[k+1]): 
       return 1 
      return 0 

     Den1 = kv[k+d] - kv[k] 
     Den2 = 0 
     Eq1 = 0 
     Eq2 = 0 

     if Den1 > 0: 
      Eq1 = ((u-kv[k])/Den1) * coxDeBoor(u,k,(d-1)) 

     try: 
      Den2 = kv[k+d+1] - kv[k+1] 
      if Den2 > 0: 
       Eq2 = ((kv[k+d+1]-u)/Den2) * coxDeBoor(u,(k+1),(d-1)) 
     except: 
      pass 

     return Eq1 + Eq2 


    # Compute basis for each point 
    b = np.zeros((n,c)) 
    for i in xrange(n): 
     for k in xrange(c): 
      b[i][k%c] += coxDeBoor(u[i],k,degree) 

    b[n-1][-1] = 1 

    return b 

Теперь позволяет использовать это, чтобы вычислить новую основу, умножаем на контрольных точках и подтверждают, что мы получаем те же результаты, splev:

b = basis(len(cv),n,degree) #5600011 function calls (600011 primitive calls) in 10.975 seconds 
points_basis = np.dot(b,cv) #3 function calls in 0.002 seconds 
print np.allclose(points_basis,points_scipy) # Returns True 

Моя очень медленно функция вернула 100k базисные значения в 11 секунд, но так как эти значения должны только быть вычислен один раз, вычисления точки на кривой заканчивал тем, что в 6 раз быстрее, таким образом, чем делать это через splev ,

Тот факт, что я смог получить точные результаты как от моего метода, так и от splev, заставляет меня поверить, что внутренне splev, вероятно, вычисляет базу, как я, за исключением гораздо более быстрого.

Итак, моя цель - выяснить, как быстро вычислить мой базис, сохранить его в памяти и просто использовать np.dot() для вычисления новых точек на кривой, и мой вопрос: можно ли использовать пряный. интерполировать, чтобы получить базовые значения, которые (я полагаю) splev использует для вычисления его результата? И если да, то как?

[ДОБАВЛЕНИЕ]

После unutbu годов и очень полезное понимание ЭВ-БР о том, как SciPy вычисляет основы сплайна, я посмотрел Фортран код и написал эквивалент в меру моих способностей:

def fitpack_basis(c, n=100, d=3, rMinOffset=0, rMaxOffset=0): 
    """ fitpack's spline basis function 
     c = number of control points. 
     n = number of points on the curve. 
     d = curve degree 
    """ 
    # Create knot vector 
    kv = np.array([0]*d + range(c-d+1) + [c-d]*d, dtype='int') 

    # Create sample range 
    u = np.linspace(rMinOffset, rMaxOffset + c - d, n) # samples range 

    # Create buffers 
    b = np.zeros((n,c)) # basis 
    bb = np.zeros((n,c)) # basis buffer 
    left = np.clip(np.floor(u),0,c-d-1).astype(int) # left knot vector indices 
    right = left+d+1 # right knot vector indices 

    # Go! 
    nrange = np.arange(n) 
    b[nrange,left] = 1.0 
    for j in xrange(1, d+1): 
     crange = np.arange(j)[:,None] 
     bb[nrange,left+crange] = b[nrange,left+crange]   
     b[nrange,left] = 0.0 
     for i in xrange(j): 
      f = bb[nrange,left+i]/(kv[right+i] - kv[right+i-j]) 
      b[nrange,left+i] = b[nrange,left+i] + f * (kv[right+i] - u) 
      b[nrange,left+i+1] = f * (u - kv[right+i-j]) 

    return b 

Тестирование против версии unutbu в моей исходной базисной функции:

fb = fitpack_basis(c,n,d) #22 function calls in 0.044 seconds 
b = basis(c,n,d) #81 function calls (45 primitive calls) in 0.013 seconds ~5 times faster 
print np.allclose(b,fb) # Returns True 

Моя функция в 5 раз медленнее, но все еще относительно быстро. Что мне нравится в этом, так это то, что он позволяет использовать диапазоны образцов, выходящие за границы, что является чем-то полезным в моем приложении. Например:

print fitpack_basis(c,5,d,rMinOffset=-0.1,rMaxOffset=.2) 
[[ 1.331 -0.3468 0.0159 -0.0002 0.  0. ] 
[ 0.0208 0.4766 0.4391 0.0635 0.  0. ] 
[ 0.  0.0228 0.4398 0.4959 0.0416 0. ] 
[ 0.  0.  0.0407 0.3621 0.5444 0.0527] 
[ 0.  0.  -0.0013 0.0673 -0.794 1.728 ]] 

Поэтому по этой причине я, вероятно, буду использовать fitpack_basis, так как это относительно быстро. Но мне бы хотелось, чтобы предложения по улучшению его производительности и, надеюсь, приблизились к версии оригинальной исходной функции unutbu, которую я написал.

+0

Пожалуйста, разместите код, который вы использовали для расчета основы для каждого образца. – unutbu

+0

Что именно вы подразумеваете под «на выборку, используемой splev при оценке сплайна»? Вы поняли, как дать splev узлы и коэффициенты, что именно вы ищете? –

+0

@unutbu я полностью переписал свой вопрос + добавленный код (медленно) получить базу – Fnord

ответ

1

fitpack_basis использует двойную петлю, которая итеративно модифицирует элементы в bb и b. Я не вижу способа использовать NumPy для векторизации этих циклов, поскольку значение из bb и b на каждом этапе итерации зависит от значений от предыдущих итераций. В подобных ситуациях иногда Cython можно использовать для , чтобы улучшить производительность петель.

Настоящая Cyton-isized версия fitpack_basis, которая бежит так быстро, как bspline_basis.Основные идеи , используемые для ускорения работы с использованием Cython, - это объявление типа каждой переменной, а переписывать все применения индексирования NumPy как циклы с использованием простого целочисленного индексирования.

См. this page для инструкции о том, как создать код и запустить его из python.

import numpy as np 
cimport numpy as np 
cimport cython 

ctypedef np.float64_t DTYPE_f 
ctypedef np.int64_t DTYPE_i 

@cython.boundscheck(False) 
@cython.wraparound(False) 
@cython.nonecheck(False) 
def cython_fitpack_basis(int c, int n=100, int d=3, 
         double rMinOffset=0, double rMaxOffset=0): 
    """ fitpack's spline basis function 
     c = number of control points. 
     n = number of points on the curve. 
     d = curve degree 
    """ 
    cdef Py_ssize_t i, j, k, l 
    cdef double f 

    # Create knot vector 
    cdef np.ndarray[DTYPE_i, ndim=1] kv = np.array(
     [0]*d + range(c-d+1) + [c-d]*d, dtype=np.int64) 

    # Create sample range 
    cdef np.ndarray[DTYPE_f, ndim=1] u = np.linspace(
     rMinOffset, rMaxOffset + c - d, n) 

    # basis 
    cdef np.ndarray[DTYPE_f, ndim=2] b = np.zeros((n,c)) 
    # basis buffer 
    cdef np.ndarray[DTYPE_f, ndim=2] bb = np.zeros((n,c)) 
    # left knot vector indices 
    cdef np.ndarray[DTYPE_i, ndim=1] left = np.clip(np.floor(u), 0, c-d-1).astype(np.int64) 
    # right knot vector indices 
    cdef np.ndarray[DTYPE_i, ndim=1] right = left+d+1 

    for k in range(n): 
     b[k, left[k]] = 1.0 

    for j in range(1, d+1): 
     for l in range(j): 
      for k in range(n): 
       bb[k, left[k] + l] = b[k, left[k] + l] 
       b[k, left[k]] = 0.0 
     for i in range(j): 
      for k in range(n): 
       f = bb[k, left[k]+i]/(kv[right[k]+i] - kv[right[k]+i-j]) 
       b[k, left[k]+i] = b[k, left[k]+i] + f * (kv[right[k]+i] - u[k]) 
       b[k, left[k]+i+1] = f * (u[k] - kv[right[k]+i-j]) 
    return b 

Используя этот timeit код для сравнения его производительность,

import timeit 
import numpy as np 
import cython_bspline as CB 
import numpy_bspline as NB 

c = 6 
n = 10**5 
d = 3 

fb = NB.fitpack_basis(c, n, d) 
bb = NB.bspline_basis(c, n, d) 
cfb = CB.cython_fitpack_basis(c,n,d) 

assert np.allclose(bb, fb) 
assert np.allclose(cfb, fb) 
# print(NB.fitpack_basis(c,5,d,rMinOffset=-0.1,rMaxOffset=.2)) 

timing = dict() 
timing['NB.fitpack_basis'] = timeit.timeit(
    stmt='NB.fitpack_basis(c, n, d)', 
    setup='from __main__ import NB, c, n, d', 
    number=10) 
timing['NB.bspline_basis'] = timeit.timeit(
    stmt='NB.bspline_basis(c, n, d)', 
    setup='from __main__ import NB, c, n, d', 
    number=10) 
timing['CB.cython_fitpack_basis'] = timeit.timeit(
    stmt='CB.cython_fitpack_basis(c, n, d)', 
    setup='from __main__ import CB, c, n, d', 
    number=10) 

for func_name, t in timing.items(): 
    print "{:>25}: {:.4f}".format(func_name, t) 

оказывается Cython может сделать fitpack_basis код запуска так быстро, как (и, возможно, немного быстрее), чем bspline_basis:

  NB.bspline_basis: 0.3322 
    CB.cython_fitpack_basis: 0.2939 
     NB.fitpack_basis: 0.9182 
+0

Nice @unutbu! К сожалению, у меня очень мало опыта работы с Cython, и я не знаю, как использовать ваш код в своем приложении. Я собрал Cython для работы с настраиваемым интерпретатором Python, который мы используем (2.7.3 MSC v.1600 64 бит). Я могу импортировать cython в качестве модуля и вызывать его функции и классы (например: cython.array), но все команды cython centric, которые у вас есть (cdef, cimport и т. Д.), Вызывают ошибку с недопустимым синтаксисом. – Fnord

+0

См. [Build Cython code] (http://docs.cython.org/src/quickstart/build.html). – unutbu

+0

Я смог создать файл .pyd!Но я получаю эту ошибку при запуске cython_fitpack_basis (6): строка 34: несоответствие dtype буфера, ожидаемое «DTYPE_i», но получившее «long» # строка 34 - cdef np.ndarray [DTYPE_i, ndim = 1] left = np.clip (np .floor (u), 0, cd-1) .astype (int) – Fnord

2

Вот версия coxDeBoor которая (на моей машине) 840x быстрее оригинала.

In [102]: %timeit basis(len(cv), 10**5, degree) 
1 loops, best of 3: 24.5 s per loop 

In [104]: %timeit bspline_basis(len(cv), 10**5, degree) 
10 loops, best of 3: 29.1 ms per loop 

import numpy as np 
import scipy.interpolate as si 

def scipy_bspline(cv, n, degree): 
    """ bspline basis function 
     c  = list of control points. 
     n  = number of points on the curve. 
     degree = curve degree 
    """ 
    # Create a range of u values 
    c = len(cv) 
    kv = np.array(
     [0] * degree + range(c - degree + 1) + [c - degree] * degree, dtype='int') 
    u = np.linspace(0, c - degree, n) 

    # Calculate result 
    arange = np.arange(n) 
    points = np.zeros((n, cv.shape[1])) 
    for i in xrange(cv.shape[1]): 
     points[arange, i] = si.splev(u, (kv, cv[:, i], degree)) 

    return points 


def memo(f): 
    # Peter Norvig's 
    """Memoize the return value for each call to f(args). 
    Then when called again with same args, we can just look it up.""" 
    cache = {} 

    def _f(*args): 
     try: 
      return cache[args] 
     except KeyError: 
      cache[args] = result = f(*args) 
      return result 
     except TypeError: 
      # some element of args can't be a dict key 
      return f(*args) 
    _f.cache = cache 
    return _f 


def bspline_basis(c, n, degree): 
    """ bspline basis function 
     c  = number of control points. 
     n  = number of points on the curve. 
     degree = curve degree 
    """ 
    # Create knot vector and a range of samples on the curve 
    kv = np.array([0] * degree + range(c - degree + 1) + 
        [c - degree] * degree, dtype='int') # knot vector 
    u = np.linspace(0, c - degree, n) # samples range 

    # Cox - DeBoor recursive function to calculate basis 
    @memo 
    def coxDeBoor(k, d): 
     # Test for end conditions 
     if (d == 0): 
      return ((u - kv[k] >= 0) & (u - kv[k + 1] < 0)).astype(int) 

     denom1 = kv[k + d] - kv[k] 
     term1 = 0 
     if denom1 > 0: 
      term1 = ((u - kv[k])/denom1) * coxDeBoor(k, d - 1) 

     denom2 = kv[k + d + 1] - kv[k + 1] 
     term2 = 0 
     if denom2 > 0: 
      term2 = ((-(u - kv[k + d + 1])/denom2) * coxDeBoor(k + 1, d - 1)) 

     return term1 + term2 

    # Compute basis for each point 
    b = np.column_stack([coxDeBoor(k, degree) for k in xrange(c)]) 
    b[n - 1][-1] = 1 

    return b 

# Control points 
cv = np.array([[50., 25., 0.], 
       [59., 12., 0.], 
       [50., 10., 0.], 
       [57., 2., 0.], 
       [40., 4., 0.], 
       [40., 14., 0.]]) 

n = 10 ** 6 
degree = 3 # Curve degree 
points_scipy = scipy_bspline(cv, n, degree) 

b = bspline_basis(len(cv), n, degree) 
points_basis = np.dot(b, cv) 
print np.allclose(points_basis, points_scipy) 

Большинство ускорения достигается путем coxDeBoor вычислить вектор результатов вместо одного значения в то время. Обратите внимание, что u удаляется из аргументов , переданных в coxDeBoor. Вместо этого новый coxDeBoor(k, d) вычисляет , что было до np.array([coxDeBoor(u[i], k, d) for i in xrange(n)]).

Поскольку арифметика массива NumPy имеет тот же синтаксис, что и скалярная арифметика, очень небольшой код, необходимый для изменения. Единственное синтаксическое изменение было в состоянии конца :

if (d == 0): 
    return ((u - kv[k] >= 0) & (u - kv[k + 1] < 0)).astype(int) 

(u - kv[k] >= 0) и (u - kv[k + 1] < 0) являются логическими массивами. astype изменяет значения массива на 0 и 1. Таким образом, до тех пор, пока не возвращается один 0 или 1 , теперь возвращается целый массив из 0s и 1s - по одному для каждого значения в u.

Memoization также может повысить производительность, так как рекуррентные соотношения вызывает coxDeBoor(k, d), чтобы быть вызваны для одних и тех же значений k и d более чем один раз. Синтаксис декоратора

@memo 
def coxDeBoor(k, d): 
    ... 

эквивалентен

def coxDeBoor(k, d): 
    ... 
coxDeBoor = memo(coxDeBoor) 

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


scipy_bspline еще быстрее, но, по крайней мере, bspline_basis плюс np.dot в футбольном поле, и может быть полезно, если вы хотите повторно использовать b со многими контрольными точками, cv.

In [109]: %timeit scipy_bspline(cv, 10**5, degree) 
100 loops, best of 3: 17.2 ms per loop 

In [104]: %timeit b = bspline_basis(len(cv), 10**5, degree) 
10 loops, best of 3: 29.1 ms per loop 

In [111]: %timeit points_basis = np.dot(b, cv) 
100 loops, best of 3: 2.46 ms per loop 
+0

(+1) Вероятно, это путь, по крайней мере, до тех пор, пока https://github.com/scipy/scipy/pull/3174 не будет объединен. –

+0

@unutbu благодарит за информацию о memoization. Это будет отлично работать для моих нужд, пока scipy не предоставит fpbspl. Существует одно преимущество splev: мне нужно будет выяснить: когда заданы значения вне границ (например: я хочу запросить u = -1.3), splev вернет точку, которая следует за границей. (в scipy_bspline, если вы измените u = np.linspace (-1, c-degree + 1, n), вы увидите, что я имею в виду) Я не уверен, что это что-то, что splev достигает чисто с основанием (с отрицательными значениями или значения> 1?), или если он просто ловит это условие и вилки что-то еще. Это была хорошая функция, которую я использовал. – Fnord

+0

@ ev-br спасибо за информацию. Я буду следить за новыми функциями, скоро они скоро вернутся! – Fnord

Смежные вопросы