为保留Bezier方法的优点,B样条曲线的方程定义为
P ( t ) = ∑ i = 0 n P i N i , k ( t ) P(t)=\sum_{i=0}^n P_i N_{i,k}(t) P(t)=i=0∑nPiNi,k(t)
其中, P i ( i = 0 , 1 , … , n ) P_i(i=0,1,\dots,n) Pi(i=0,1,…,n)是控制多边形的顶点, N i , k ( t ) ( i = 0 , 1 , … , n ) N_{i,k}(t)(i=0,1,\dots,n) Ni,k(t)(i=0,1,…,n)称为 k k k阶( k − 1 k-1 k−1次)B样条基函数,其中每一个称为B样条,它是一个由称为节点矢量的费递增参数 t t t的序列 T T T: t 0 ≤ t 1 ≤ ⋯ ≤ t n + k t_0 \le t_1 \le \dots \le t_{n+k} t0≤t1≤⋯≤tn+k所决定的 k k k阶分段多项式,即为 k k k阶( k − 1 k-1 k−1次)B多项式样条。
下面是基函数的递推公式,也称为de Boor-Cox公式:
{ N i , 1 ( t ) = { 1 , t i ≤ t ≤ t i + 1 0 , t < t i 或者 t ≥ t i + k N i , k ( t ) = t − t i t i + k − 1 − t i N i , k − 1 ( t ) + t i + k − t t i + k − t i + 1 N i + 1 , k − 1 ( t ) . k ≥ 2 \begin{cases} N_{i,1}(t) = \begin{cases}&1 ,t_i \le t \le t_{i+1} \\ & 0, t < t_i \text{或者} t \ge t_{i+k} \end{cases} \\ N_{i,k}(t)=\frac{t-t_i}{t_{i+k-1}-t_i}N_{i,k-1}(t)+\frac{t_{i+k}-t}{t_{i+k}-t_{i+1}}N_{i+1,k-1}(t).\quad k \ge 2 \end{cases} ⎩⎪⎨⎪⎧Ni,1(t)={1,ti≤t≤ti+10,t<ti或者t≥ti+kNi,k(t)=ti+k−1−tit−tiNi,k−1(t)+ti+k−ti+1ti+k−tNi+1,k−1(t).k≥2
并约定 0 0 = 0 \frac{0}{0}=0 00=0。
递推公式表明:欲确定第 i i i个 k k k阶B样条 N i , k ( t ) N_{i,k}(t) Ni,k(t)需要用到 t i , t i + 1 , … , t i + k t_i,t_{i+1},\dots,t_{i+k} ti,ti+1,…,ti+k共 k + 1 k+1 k+1个节点,称区间 [ t i , t i + k ] [t_i,t_{i+k}] [ti,ti+k]为 N i , k ( t ) N_{i,k}(t) Ni,k(t)的支撑区间。曲线方程中, n + 1 n+1 n+1个控制节点 P i P_i Pi要用到 n + 1 n+1 n+1个 k k k阶B样条基 N i , k ( t ) N_{i,k}(t) Ni,k(t)。它们的支撑区间的并集定义了这一组B样条基的节点矢量 T = [ t 0 , t 1 , … , t n + k ] T=[t_0,t_1,\dots,t_{n+k}] T=[t0,t1,…,tn+k]。
移动该曲线的第 i i i个控制顶点 P i P_i Pi至多影响定义在区间 ( t i , t i + k ) (t_i,t_{i+k}) (ti,ti+k)上那部分曲线的形状,对曲线的其余部分不产生影响。
P ( t ) P(t) P(t)在 r r r重节点 t i ( k ≤ i ≤ n ) t_i(k \le i \le n) ti(k≤i≤n)处的连续阶不低于 k − 1 − r k-1-r k−1−r;整条曲线 P ( t ) P(t) P(t)的连续阶不低于 k − 1 − r max k-1-r_{\max} k−1−rmax,其中 r max r_{\max} rmax表示位于区间 ( t k − 1 , t n + 1 ) (t_{k-1},t_{n+1}) (tk−1,tn+1)内节点的最大重数。
P ( t ) P(t) P(t)在区间 ( t i , t i + 1 ) (t_i,t_{i+1}) (ti,ti+1), k − 1 ≤ i ≤ n k-1 \le i \le n k−1≤i≤n上的部分位于 k k k个点 P i − k + 1 , … , P i P_{i-k+1},\dots,P_i Pi−k+1,…,Pi的凸包 C i C_i Ci内,整条曲线则位于各凸包 C i C_i Ci的并集 ⋃ i = k − 1 n C i \bigcup_{i=k-1}^n C_i ⋃i=k−1nCi内。
节点矢量中节点为沿参数轴均匀或等距分布,所有节点区间长度 Δ i = t i + 1 − t i = 常 数 > 0 ( i = 0 , 1 , … , n + k − 1 ) \Delta_i = t_{i+1}-t_i=常数 >0(i=0,1,\dots,n+k-1) Δi=ti+1−ti=常数>0(i=0,1,…,n+k−1),这样的节点矢量定义了均匀B样条基。
准均匀B样条与均匀B样条曲线的差别在于两端点具有重复度k,这样的节点矢量定义了准均匀的B样条曲线。
节点矢量中两端点具有重复度 k k k,所有内节点重复度为k-1,这样的节点矢量定义了分段的Bernstein基。
在这种类型里,任意分布的节点矢量 T = [ t 0 , t 1 , … , t n + k ] T=[t_0,t_1,\dots,t_{n+k}] T=[t0,t1,…,tn+k],只要在数学上成立(节点序列递增,两端节点的重复度 ≤ k \le k ≤k,内部节点重复度 ≤ k − 1 \le k-1 ≤k−1)都可以取。这样的节点矢量定义了非均匀B样条基。
先将 t t t固定在区间 [ t j , t j + 1 ) ( k − 1 ≤ j ≤ n ) [t_j,t_{j+1})(k-1 \le j \le n) [tj,tj+1)(k−1≤j≤n)上,由de Boor-Cox公式有
P ( t ) = ∑ i = 0 n P i N i , k ( t ) = ∑ i = j − k + 1 j P i N i , k ( t ) = ∑ i = j − k + 1 j P i [ ( t − t i t i + k − 1 − t i ) N i , k − 1 ( t ) + ( t i + k − t t i + k − t i + 1 ) N i + 1 , k − 1 ( t ) ] = ∑ i = j − k + 1 j [ ( t − t i t i + k − 1 − t i ) P i + ( t i + k − 1 − t t i + k − 1 − t i + 1 ) P i − 1 ] N i , k − 1 ( t ) \begin{aligned} P(t) &=\sum_{i=0}^{n} P_{i} N_{i, k}(t) = \sum_{i=j-k+1}^{j}P_i N_{i,k}(t) \\ &= \sum_{i=j-k+1}^{j} P_{i}\left[\left(\frac{t-t_{i}}{t_{i+k-1}-t_{i}}\right) N_{i, k-1}(t)+\left(\frac{t_{i+k}-t}{t_{i+k}-t_{i+1}}\right) N_{i+1, k-1}(t)\right] \\ & = \sum_{i=j-k+1}^{j} \left[\left(\frac{t-t_{i}}{t_{i+k-1}-t_{i}}\right)P_i+\left(\frac{t_{i+k-1}-t}{t_{i+k-1}-t_{i+1}}\right) P_{i-1}\right]N_{i,k-1}(t) \end{aligned} P(t)=i=0∑nPiNi,k(t)=i=j−k+1∑jPiNi,k(t)=i=j−k+1∑jPi[(ti+k−1−tit−ti)Ni,k−1(t)+(ti+k−ti+1ti+k−t)Ni+1,k−1(t)]=i=j−k+1∑j[(ti+k−1−tit−ti)Pi+(ti+k−1−ti+1ti+k−1−t)Pi−1]Ni,k−1(t)
现令
P i ( r ) ( t ) = { ( 1 − τ i j ) P i − 1 ( r − 1 ) ( t ) + τ i j P i ( r − 1 ) ( t ) if r = 1 , 2 , … , k − 1 P i if r = 0 \mathbf{P}_{i}^{(r)}(t)=\left\{\begin{array}{ll}{\left(1-\tau_{i}^{j}\right) \mathbf{P}_{i-1}^{(r-1)}(t)+\tau_{i}^{j} \mathbf{P}_{i}^{(r-1)}(t)} & {\text { if } r=1,2,\dots,k-1} \\ {\mathbf{P}_{i}} & {\text { if } r=0}\end{array}\right. Pi(r)(t)={(1−τij)Pi−1(r−1)(t)+τijPi(r−1)(t)Pi if r=1,2,…,k−1 if r=0
其中
τ i r = t − t i t i + k − r − t i \tau_{i}^{r}=\frac{t-t_{i}}{t_{i+k-r}-t_{i}} τir=ti+k−r−tit−ti
最终得到
P ( t ) = P j [ k − 1 ] ( t ) P(t) = P_j^{[k-1]}(t) P(t)=Pj[k−1](t)
# -*- coding: utf-8 -*-
import numpy as np
from scipy.special import comb, perm
from matplotlib import pyplot as plt
class MyB:
def __init__(self, line):
self.line = line
self.index_02 = None #保存拖动的这个点的索引
self.press = None # 状态标识,1为按下,None为没按下
self.pick = None # 状态标识,1为选中点并按下,None为没选中
self.motion = None #状态标识,1为进入拖动,None为不拖动
self.xs = list() # 保存点的x坐标
self.ys = list() # 保存点的y坐标
self.cidpress = line.figure.canvas.mpl_connect('button_press_event', self.on_press) # 鼠标按下事件
self.cidrelease = line.figure.canvas.mpl_connect('button_release_event', self.on_release) # 鼠标放开事件
self.cidmotion = line.figure.canvas.mpl_connect('motion_notify_event', self.on_motion) # 鼠标拖动事件
self.cidpick = line.figure.canvas.mpl_connect('pick_event', self.on_picker) # 鼠标选中事件
def on_press(self, event): # 鼠标按下调用
if event.inaxes!=self.line.axes: return
self.press = 1
def on_motion(self, event): # 鼠标拖动调用
if event.inaxes!=self.line.axes: return
if self.press is None: return
if self.pick is None: return
if self.motion is None: # 整个if获取鼠标选中的点是哪个点
self.motion = 1
x = self.xs
xdata = event.xdata
ydata = event.ydata
index_01 = 0
for i in x:
if abs(i - xdata) < 0.02: # 0.02 为点的半径
if abs(self.ys[index_01] - ydata) < 0.02:break
index_01 = index_01 + 1
self.index_02 = index_01
if self.index_02 is None: return
self.xs[self.index_02] = event.xdata # 鼠标的坐标覆盖选中的点的坐标
self.ys[self.index_02] = event.ydata
self.draw_01()
def on_release(self, event): # 鼠标按下调用
if event.inaxes!=self.line.axes: return
if self.pick == None: # 如果不是选中点,那就添加点
self.xs.append(event.xdata)
self.ys.append(event.ydata)
if self.pick == 1 and self.motion != 1: # 如果是选中点,但不是拖动点,那就降阶
x = self.xs
xdata = event.xdata
ydata = event.ydata
index_01 = 0
for i in x:
if abs(i - xdata) < 0.02:
if abs(self.ys[index_01] - ydata) < 0.02: break
index_01 = index_01 + 1
self.xs.pop(index_01)
self.ys.pop(index_01)
self.draw_01()
self.pick = None # 所有状态恢复,鼠标按下到稀放为一个周期
self.motion = None
self.press = None
self.index_02 = None
def on_picker(self, event): # 选中调用
self.pick = 1
def draw_01(self): # 绘图
self.line.clear() # 不清除的话会保留原有的图
self.line.axis([0,1,0,1]) # x和y范围0到1
self.b(self.xs,self.ys) # B样条曲线
self.line.scatter(self.xs, self.ys,color='b',s=200, marker="o",picker=5) # 画点
self.line.plot(self.xs, self.ys,color='r') # 画线
self.line.figure.canvas.draw() # 重构子图
def b(self,*args): # Bezier曲线公式转换,获取x和y
k = 3 # 阶数
n = len(args[0])-1 # 顶点的个数-1
T = np.linspace(1,10,n+k+1) # T 范围1到10,均匀B样条曲线
# if n >= k-1:
# T = [1]*k+(np.linspace(2,9,n-k+1)).tolist()+[10]*k # 准均匀样条
x,y = [],[]
# 递推公式
# def de_Boor(r,t,i):
# if r == 0:
# return [args[0][i],args[1][i]]
# else:
# return ((t-T[i])/(T[i+k-r]-T[i]))*de_Boor(r-1,t,i)+((T[i+k-r]-t)/(T[i+k-r]-T[i]))*de_Boor(r-1,t,i-1)
def de_Boor_x(r,t,i):
if r == 0:
return args[0][i]
else:
if T[i+k-r]-T[i] == 0 and T[i+k-r]-T[i] != 0:
return ((T[i+k-r]-t)/(T[i+k-r]-T[i]))*de_Boor_x(r-1,t,i-1)
elif T[i+k-r]-T[i] != 0 and T[i+k-r]-T[i] == 0:
return ((t-T[i])/(T[i+k-r]-T[i]))*de_Boor_x(r-1,t,i)
elif T[i+k-r]-T[i] == 0 and T[i+k-r]-T[i] == 0:
return 0
return ((t-T[i])/(T[i+k-r]-T[i]))*de_Boor_x(r-1,t,i)+((T[i+k-r]-t)/(T[i+k-r]-T[i]))*de_Boor_x(r-1,t,i-1)
def de_Boor_y(r,t,i):
if r == 0:
return args[1][i]
else:
if T[i+k-r]-T[i] == 0 and T[i+k-r]-T[i] != 0:
return ((T[i+k-r]-t)/(T[i+k-r]-T[i]))*de_Boor_y(r-1,t,i-1)
elif T[i+k-r]-T[i] != 0 and T[i+k-r]-T[i] == 0:
return ((t-T[i])/(T[i+k-r]-T[i]))*de_Boor_y(r-1,t,i)
elif T[i+k-r]-T[i] == 0 and T[i+k-r]-T[i] == 0:
return 0
return ((t-T[i])/(T[i+k-r]-T[i]))*de_Boor_y(r-1,t,i)+((T[i+k-r]-t)/(T[i+k-r]-T[i]))*de_Boor_y(r-1,t,i-1)
def plot(x,y):
for j in range(k-1,n+1):
for t in np.linspace(T[j],T[j+1]):
x.append(de_Boor_x(k-1,t,j))
y.append(de_Boor_y(k-1,t,j))
#print(x,y)
self.line.plot(x,y)
if n >= k-1:
plot(x,y)
fig = plt.figure(2,figsize=(12,6)) #创建第2个绘图对象,1200*600像素
ax = fig.add_subplot(111) #一行一列第一个子图
ax.set_title('My B')
myBezier = MyB(ax)
plt.xlabel('X')
plt.ylabel('Y')
plt.show()