python - 如何获得 scipy.interpolate.splev 使用的样条基础

标签 python numpy scipy spline

我需要在 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

现在,为了加快速度,我可以计算曲线上所有 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

我的极慢函数在 11 秒内返回了 100k 基值,但由于这些值只需要计算一次,因此计算曲线上的点最终比通过 splev 快 6 倍。

我能够从我的方法和 splev 中获得完全相同的结果这一事实使我相信内部 splev 可能像我一样计算基,只是速度要快得多。

所以我的目标是找出如何快速计算我的基础,将其存储在内存中并仅使用 np.dot() 来计算曲线上的新点,我的问题是:是否可以使用 spicy.interpolate获取(我假设)splev 用于计算其结果的基值?如果是这样,怎么做到的?

[附录]

根据 unutbu 和 ev-br 对 scipy 如何计算样条的基础的非常有用的见解,我查阅了 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 版本。

最佳答案

这是 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)是 bool 数组。 astype 将数组值更改为 0 和 1。因此,在单个 0 或 1 之前 返回,现在返回一整组 0 和 1——每个值对应一个 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

关于python - 如何获得 scipy.interpolate.splev 使用的样条基础,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35236221/

相关文章:

python - 为什么 from __future__ import * 会引发错误?

python - 在 Google-Colab 中使用 Python3 创建词云时应该如何使用 Arial 字体?

python - 序列化大型 scipy 稀疏矩阵的最佳方法是什么?

python - 如何在 matplotlib 中创建密度图?

python - 安装 nimfa 时出现问题(Python 矩阵分解库)

python - 在 3d numpy 数组中查找区域的中心坐标

python - PySide 中的 __init__ 方法

python - 列表理解中函数调用的多次返回

python - 空数组的 Numpy 数组 ndmin 行为

python - numpy 向量运算