如何获取scipy.interpolate.splev使用的样条基函数

6

我需要在Python中评估B样条。为此,我编写了下面的代码,它非常有效。

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

现在,为了使其更快,我可以计算曲线上所有10万个点的基础,并将其存储在内存中。当我需要绘制曲线时,我只需要将新的控制点位置与存储的基础相乘即可得到新的曲线。为了证明我的观点,我编写了一个使用DeBoor算法来计算我的基础的函数:

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

我的极慢的函数在11秒内返回了100k个基础值,但由于这些值只需要计算一次,因此通过这种方式计算曲线上的点比通过splev计算要快6倍。
我能够从我的方法和splev获得完全相同的结果,这让我相信,在内部,splev可能像我一样计算基础,只是速度更快。
因此,我的目标是找出如何快速计算我的基础,将其存储在内存中,并只使用np.dot()来计算曲线上的新点。我的问题是:是否可以使用spicy.interpolate获取splev用于计算其结果的基础值?如果可以,怎么做?
[补充]
在unutbu和ev-br非常有用的见解之后,我查找了Fortran代码并尽力编写了一个等效的代码:
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版本的原始基础函数。

请发布您用于计算每个样本基础的代码。 - unutbu
你所说的“在评估样条时使用splev的每个样本基础”是什么意思?你已经知道如何给splev提供节点和系数,那你具体要找什么呢? - ev-br
@ev-br 我重新写了我的问题,希望这能澄清我正在寻找什么。 - Fnord
@ev-br 稍微离题了:我看到splev有一个参数可以获取样条的导数(der)。查看源代码,我发现splev将该参数传递给_fitpack._spl_,但是我找不到这个函数。我的目标是弄清楚如何计算导数,以便我可以使用这些信息在特定位置上计算样条线的切线。 - Fnord
@Fnord https://github.com/scipy/scipy/blob/master/scipy/interpolate/src/__fitpack.h#L868; 还请查看例程splder和https://github.com/scipy/scipy/pull/3174。然而,更多讨论可能需要另外一个问题。祝好运! - ev-br
显示剩余3条评论
2个回答

4
这里是一个版本的coxDeBoor,在我的电脑上比原始版本快840倍。
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,现在返回整个由0和1组成的数组——每个值都对应u中的一个值。

记忆化也可以提高性能,因为递归关系会导致多次调用具有相同kd值的coxDeBoor(k, d)函数。这里使用了装饰器语法。

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

等同于

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

使用memo修饰符会导致coxDeBoor记录从参数(k, d)到返回值的映射,存储在一个cache中。如果再次调用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

这很可能是正确的方法,至少在https://github.com/scipy/scipy/pull/3174没有合并之前。 - ev-br
@unutbu 感谢您提供的记忆化信息。在scipy暴露fpbspl之前,这对我的需求很有用。Splev有一个好处我需要弄清楚:当给定越界值(例如:假设我想查询u=-1.3)时,splev会返回遵循边界切线的点。(如果在scipy_bspline中更改u=np.linspace(-1,c-degree+1,n),您会看到我是什么意思)但我不确定这是否仅通过基础实现的(具有负值或大于1的值?)还是只是捕捉此条件并将其分支到其他地方。那是一个很棒的'特性'我一直在使用。 - Fnord
@ev-br 感谢提供的信息。我会密切关注新功能的推出,希望它们能尽快到来! - Fnord

3
fitpack_basis使用双重循环迭代修改bbb中的元素。我没有看到使用NumPy将这些循环向量化的方法,因为迭代的每个阶段bbb的值都取决于以前迭代的值。在这种情况下,有时可以使用Cython来改进循环的性能。
这是一个经过Cython处理的fitpack_basis版本,它的运行速度与bspline_basis一样快。使用Cython提高速度的主要思想是声明每个变量的类型,并使用普通整数索引重写所有NumPy花式索引的用法。
请参见此页面了解如何构建代码并从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

不错的@unutbu!不幸的是,我对Cython几乎没有经验,也不知道如何在我的应用程序中使用你的代码。我编译了Cython以便与我们使用的自定义Python解释器(2.7.3 MSC v.1600 64位)一起工作。我可以将cython作为模块导入并调用它的函数和类(例如:cython.array),但是你所拥有的所有基于cython的命令(cdef、cimport等)都会出现无效语法的错误。 - Fnord
我成功生成了一个 .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, c-d-1).astype(int)。 - Fnord
我的错误。.astype(int) 应该是 .astype(np.int64)。我已经编辑了帖子以反映这一点。 - unutbu
我也刚发现这个问题,正准备告诉你!太棒了!非常感谢! - Fnord
你对Cython化这里描述的函数有什么建议吗:http://stackoverflow.com/questions/35520384/improving-a-numpy-implementation-of-a-simple-spring-network/35525713#35525713。我目前正在尝试让它工作,但是在定义一个三维点数组的正确方式上遇到了困难。非常感谢您的意见。 - Fnord

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接