曲线参数化

如何选择参数

B样条插值/拟合的输入通常一组已知的数据点,曲线参数化第一步是找到一组参数,能够将这些点"固定"在某些特定值上。

例如,如果数据点是D0,,Dn\mathbf{D}_0, \ldots, \mathbf{D}_n,那么需要在曲线的定义域中找到n+1n+1个参数 t0,,tnt_0, \ldots, t_n,使得数据点Dk\mathbf{D}_k对应于参数tkt_k,其中0kn0 \leq k \leq n。如果C(u)\mathbf{C}(u)是一条通过所有数据点的曲线,那么对于所有0kn0 \leq k \leq n,我们都有Dk=C(tk)\mathbf{D}_k = \mathbf{C}(t_k)

在下面的图中,我们有7个数据点(n=6)(n=6),因此需要7个参数来建立数据点和参数的对应关系。

img

参数的选择有无限多可能,我们可以均匀地划分定义域,或者从定义域中随机选择n+1n+1个值。但是,选择不当的参数会导致不可预料的结果。

下图显示了4个数据点和三条插值曲线,每条曲线都使用了不同的参数。其中一条曲线向外侧弯曲,产生了凸起。黑色曲线有一个小的凸起,只有这两者之间的曲线比较符合数据点的分布趋势。因此,参数的选择会影响曲线的形状,从而相应地影响曲线的参数化。

img

下面,我们将讨论一些参数选择的方法,包括均匀分布法弦长法向心法。在确定好一组参数之后,就可以计算相应的节点向量。

关于曲线插值、曲线逼近和曲线拟合的区别

曲线插值、曲线逼近和曲线拟合都是用于构造一条平滑曲线来描述一组离散数据点的方法,但它们的目的和原理有所不同。

  1. 曲线插值(Curve Interpolation)
    • 目的是找到一条平滑曲线精确地通过所有给定的数据点。
    • 常用的插值方法包括拉格朗日插值、牛顿插值、三次样条插值等。
    • 例如,给定一组坐标点(x1,y1),(x2,y2),...,(xn,yn)(x_1,y_1), (x_2,y_2), ..., (x_n,y_n),我们可以使用拉格朗日插值多项式来构造一条曲线精确地通过所有这些点。
  2. 曲线逼近(Curve Approximation)
    • 目的是在某种意义下(如最小二乘法)找到最佳拟合曲线,使曲线和数据点之间的偏差最小。
    • 常用的逼近方法包括最小二乘法拟合多项式、指数函数、三角函数等。
    • 例如,给定一组测量数据点,我们可以用最小二乘法拟合一条直线或抛物线等简单函数来近似描述这些数据。
  3. 曲线拟合(Curve Fitting)
    • 目的是寻找一个适当的函数形式来很好地拟合给定的数据点,使得总误差(根据不同问题有不同的误差距离的衡量方式,一般是均方误差)最小的那个简单函数。
    • 常用的函数形式包括多项式、高斯函数、指数函数、对数函数等参数化模型。
    • 例如,给定一组有规律的数据点,我们可以尝试用一个高斯函数或对数函数等非线性模型来很好地拟合这些数据。

插值和逼近更注重于构造一条平滑曲线来精确地通过或逼近给定的离散数据点,而拟合则更强调寻找一个合适的参数化模型函数来描述潜在的数据生成规律从广义上来讲,曲线拟合包含曲线插值和曲线逼近两种情况,当需要拟合曲线精确经过所有给定数据点时,可以采用曲线插值的方法;当允许拟合曲线与数据点之间存在一定偏差时,可以采用曲线逼近的方法;还可以采用其他参数化模型函数(如高斯函数、对数函数等)进行非线性曲线拟合,来描述数据的内在规律。

线性回归

线性回归是曲线拟合的一种特殊情况,其拟合函数形式是一条直线(一次函数)。在线性回归中,我们试图找到一条最佳拟合直线,使得数据点到直线的残差平方和最小,这个过程就是对线性函数进行曲线拟合。

当使用线性回归模型时, 我们实际上是在用一条直线来逼近数据分布,这种用最小二乘法找到最佳拟合直线的过程,本质上属于曲线逼近的一种方法。由于线性回归模型并不要求拟合直线精确通过所有数据点,因此不属于曲线插值的范畴,但是,如果数据点刚好在一条直线上,那么线性回归得到的结果就是一个插值问题的解。

曲线均匀参数化

最简单的参数选择方法是均匀参数化。假设定义域为[0,1][0,1],需要n+1n+1个均匀分布的参数,那么第一个和最后一个参数必须是0和1,因为我们希望曲线通过第一个和最后一个数据点。因此,我们有t0=0t_0 = 0tn=1t_n = 1

n+1n+1个参数将区间[0,1][0,1]均匀地分成nn个子区间,每个子区间的长度是1/n1/n,划分后的参数是00, 1/n1/n, 2/n2/n, 3/n3/n, …, (n1)/n(n-1)/n11。因此,我们有:

ti=ini=0,1,,nt_i = \frac{i}{n} \quad i = 0, 1, \ldots, n

例如,如果我们需要5个参数,n=4n = 4,那么均匀分布的参数是0, 1/41/4, 1/21/2, 3/43/4 和 1。如果我们需要8个参数,n=7n = 7,那么均匀分布的参数是0, 1/71/7, 2/72/7, 3/73/7, 4/74/7, 5/75/7, 6/76/7 和 1。

如果定义域是[a,b][a, b]而不是[0,1][0,1]呢?在这种情况下,[a,b][a, b]n+1n+1个划分点(包括aabb)分成nn个区间。由于这个区间的长度是bab - a,每个子区间的长度是(ba)/n(b - a)/n。因此,划分点(即参数)是:

ti=a+banii=0,1,,nt_i = a + \frac{b - a}{n} \cdot i \quad i = 0, 1, \ldots, n

尽管均匀参数化很简单,但它会产生一些不稳定的结果。

例如,当数据点不是均匀分布时,使用均匀分布的参数可能会产生异常形状,如凸起、尖锐点和自相交形成的环。

在下面的左图中,在数据点3处有一个环。在右图中,曲线在数据点1、2和3之间出现剧烈摆动。虽然不能说这些问题是均匀参数化引起的,但它确实比使用其他方法时更容易出现问题。

imgimg

曲线弦长参数化

如果插值曲线和数据点连成的多边形非常接近,那么两个相邻数据点之间的曲线段长度将非常接近这两个数据点相连的弦长,并且插值曲线的总长度也非常接近数据点连成的多边形的总长度。

在下图中,插值曲线的每一段长度都非常接近其对应的弦的长度,曲线的总长度接近数据点连成的多边形的总长度。因此,如果根据弦长对定义域进行划分,划分后的节点参数与弧长参数化产生的值近似,这就是弦长参数化的优点。

img

假设数据点是 D0,D1,,Dn\mathbf{D}_0, \mathbf{D}_1, \ldots, \mathbf{D}_nDi1\mathbf{D}_{i-1}Di\mathbf{D}_i 之间的长度为 DiDi1|\mathbf{D}_i - \mathbf{D}_{i-1}|,数据点连成的多边形长度是这些弦的长度之和:

L=i=1nDiDi1L = \sum_{i=1}^{n} |\mathbf{D}_i - \mathbf{D}_{i-1}|

因此,从点 D0\mathbf{D}_0 到点 Dk\mathbf{D}_k 的弦长,记作 LkL_k,与多边形总长度 LL 的比率是:

Lk=i=1kDiDi1LL_k=\frac{ \sum_{i=1}^{k} |\mathbf{D}_i - \mathbf{D}_{i-1}|}{L}

如果我们要对插值曲线进行弦长参数化,那么定义域必须根据比率 LkL_k 进行划分。如果定义域是 [0,1][0,1],那么参数 tkt_k 值为:

t0=0tk=1L(i=1kDiDi1)tn=1\begin{aligned} &t_{0}=0\\ &t_{k}=\frac{1}{L}\left(\sum_{i=1}^{k}|\mathbf{D}_{i}-\mathbf{D}_{i-1}|\right)\\ &t_{n}=1 \end{aligned}

其中 LL 是数据点连成的多边形的总长度。这样,就根据弦长比率划分定义域,从而分配参数值

看一个例子:假设我们有4个数据点 (n=3)(n = 3)D0=(0,0)\mathbf{D}_0 = (0,0)D1=(1,2)\mathbf{D}_1 = (1,2)D2=(3,4)\mathbf{D}_2 = (3,4)D3=(4,0)\mathbf{D}_3 = (4,0),每条弦的长度是:

D1D0=5=2.236D2D1=8=2.828D3D2=17=4.123|\mathbf{D}_1 - \mathbf{D}_0| = \sqrt{5} = 2.236\\ |\mathbf{D}_2 - \mathbf{D}_1| = \sqrt{8} = 2.828\\ |\mathbf{D}_3 - \mathbf{D}_2| = \sqrt{17} = 4.123

总长度是:

L=2.236+2.828+4.123=9.187L = 2.236 + 2.828 + 4.123 = 9.187

最后,我们得到相应的参数:

t0=0t1=D1D0L=0.2434t2=D1D0+D2D1L=0.5512t3=1\begin{array}{rcl} t_0&=&0\\ t_1&=&\frac{|\mathbf{D}_1-\mathbf{D}_0|}{L}=0.2434\\ t_2&=&\frac{|\mathbf{D}_1-\mathbf{D}_0|+|\mathbf{D}_2-\mathbf{D}_1|}{L}=0.5512\\ t_3&=&1 \end{array}

下图显示了使用均匀参数化弦长参数化得到的参数分布

imgimg

如果定义域是 [a,b][a, b] 而不是 [0,1][0,1] 怎么办?上式中 LkL_k 的值在0 到1 之间。由于 [a,b][a, b] 的长度是 bab - a,所以 Lk(ba)L_k(b - a)(其中0kn0 \leq k \leq n) 将 [0,ba][0, b - a] 按照累计的弦长比例进行划分。因此,在 [a,b][a, b]上划分后得到的参数值为:

t0=atk=a+Lk(ba)tn=b\begin{aligned} &t_{0}=a\\ &t_{k}=a+L_{k}(b-a)\\ &t_{n}=b \end{aligned}

对多项式曲线使用弦长参数化效果并不完美,弦长只是一个近似值。有时,较长的弦可能会导致其曲线段出现较大隆起。在下图中,黑色和蓝色曲线都使用7个数据点进行插值计算。

img

两条曲线的形状非常相似,除了最后一段,使用弦长参数化插值的曲线具有较大的摆动,最后几段曲线段截然不同,使用弦长法插值的蓝色曲线与使用均匀法插值的红色曲线相比,有很大的凸起和扭曲。这是弦长法经常出现的问题。

曲线向心参数化

向心参数化E. T. Y. Lee提出,假设我们驾驶一辆汽车穿过一个弯道绕行的赛道。在急转弯处,我们必须非常小心,以免产生过大的法向加速度(即离心力),否则我们的车辆可能会失控。为了安全驾驶,E. T. Y. Lee建议沿路径的法向力应与角度变化成正比。向心参数化方法实际上是对这一模型的近似。我们可以将向心参数化当作是对弦长参数化方法的一种扩展。

假设数据点为 D0,D1,,Dn\mathbf{D}_0, \mathbf{D}_1, \ldots, \mathbf{D}_n,我们取一个正的"幂"值 aa,通常a=1/2a = 1/2 。两个相邻数据点之间的距离通过 DkDk1a|\mathbf{D}_k - \mathbf{D}_{k-1}|^a 而不是 DkDk1|\mathbf{D}_k - \mathbf{D}_{k-1}| 来计算

因此,数据点连接成的多边形的总长度是:

L=i=1nDiDi1aL = \sum_{i=1}^{n} |\mathbf{D}_i - \mathbf{D}_{i-1}|^a

多边形上从 D0\mathbf{D}_0Dk\mathbf{D}_k 的长度与总长度的比是:

Lk=i=1kDiDi1aLL_k=\frac{\sum_{i=1}^k|\mathbf{D}_i-\mathbf{D}_{i-1}|^a}{L}

因此,使用新的弦长计算方法,得到的累计弦长比例为 L0=0,L1,,Ln=1L_0 = 0, L_1, \ldots, L_n = 1 ,假设定义域为[0,1][0,1],划分后的曲线参数为:

t0=0tk=1L(i=1kDiDi1a)tn=1\begin{array}{rcl} t_0&=&0\\ t_k&=&\frac{1}{L}\left(\sum_{i=1}^k|\mathrm D_i-\mathrm D_{i-1}|^a\right)\\ t_n&=&1 \end{array}

如果 a=1a = 1,那么向心参数化退化为弦长法。如果 a<1a < 1,例如 a=1/2a = 1/2,那么DkDk1a|\mathbf{D}_k - \mathbf{D}_{k-1}|^a 的值会小于 DkDk1|\mathbf{D}_k - \mathbf{D}_{k-1}|。那么,较长的弦(长度大于1的弦)对数据点连成的多边形的总长度的贡献会减小,较短的弦(长度小于1的弦)对数据点连成的多边形的总长度的贡献会增加。因此,向心参数化比弦长法能够更好地处理具有较大曲率的曲线段。

下面重新计算弦长参数化中的例子,我们有四个数据点 (n=3)(n = 3)D0=(0,0)\mathbf{D}_0 = (0,0)D1=(1,2)\mathbf{D}_1 = (1,2)D2=(3,4)\mathbf{D}_2 = (3,4)D3=(4,0)\mathbf{D}_3 = (4,0)。取 a=1/2a = 1/2,那么每条弦的长度是:

D1D01/2=5=1.495D2D11/2=22=1.682D3D21/2=17=2.031|\mathbf{D}_1-\mathbf{D}_0|^{1/2}=\sqrt{\sqrt{5}}=1.495\\|\mathbf{D}_2-\mathbf{D}_1|^{1/2}=\sqrt{2\sqrt{2}}=1.682\\|\mathbf{D}_3-\mathbf{D}_2|^{1/2}=\sqrt{\sqrt{17}}=2.031

弦的总长度是:

L=5+22+17=5.208L=\sqrt{\sqrt{5}}+\sqrt{2\sqrt{2}}+\sqrt{\sqrt{17}}=5.208

因此,参数是:

t0=0t1=D1D01/2L=0.2871t2=D1D01/2+D2D11/2L=0.6101t3=1\begin{array}{rcl} t_0&=&0\\ t_1&=&\frac{|\mathbf{D}_1-\mathbf{D}_0|^{1/2}}{L}=0.2871\\ t_2&=&\frac{|\mathbf{D}_1-\mathbf{D}_0|^{1/2}+|\mathbf{D}_2-\mathbf{D}_1|^{1/2}}{L}=0.6101\\ t_3&=&1 \end{array}

下面给出了使用均匀参数化、弦长参数化和向心参数化计算的三组参数的分布情况:

imgimg

下面看一个极端的例子,下图显示了使用均匀参数化(黑色)、弦长参数化(蓝色)和向心参数化(红色)对4个数据点进行插值得到的B样条曲线。均匀参数化得到的曲线有一个峰值,弦长参数化得到的曲线有两个大的凸起,向心参数化则很好地处理了两个距离非常接近的数据点(1号点和2号点)。

img

那么可以说向心参数化比其他两种方法更好吗?下面是一个反例。我们有7个数据点,黑色、蓝色和红色曲线是使用均匀参数化、弦长参数化和向心参数化插值得到的曲线。如图所示,均匀参数化产生了一个非常稳定的插值曲线,向心参数化得到的曲线波动大于均匀参数化得到的曲线,而弦长参数化产生的曲线波动幅度最大。

img

曲线通用方法参数化

1999年,Choong-Gyoo Lim提出了一种有趣的参数化方法。在之前讨论的方法中,我们先确定参数,然后计算一个节点向量。Lim提出的方法称为通用方法,通过使用均匀分布的节点来计算参数,正好相反。

假设我们需要n+1n+1个参数,每个数据点对应一个参数,插值的B样条曲线次数是pp。那么,节点的数量是m+1m+1,满足m=n+p+1m = n + p + 1。Lim认为这些节点应该是均匀分布的。前p+1p+1个节点被设置为0,后p+1p+1个节点被设置为1,其余的npn-p个节点均匀的分布在定义域[0,1][0,1]之间。因此,节点为:

u0=u1==up=0up+i=inp+1for i=1,2,,npump=ump+1==um=1\begin{aligned} u_{0}&=\quad u_1=\cdots=u_p=0\\ u_{p+i}&=\quad\frac i{n-p+1}\quad\mathrm{for~}i=1,2,\ldots,n-p\\ u_{m-p}&=\quad u_{m-p+1}=\cdots=u_m=1 \end{aligned}

这样我们就有了 n+1n+1B样条基函数。然后,选择对应基函数达到最大值时的参数作为数据点的参数。如下图所示,其中n=6n = 6(7个数据点),p=4p = 4,和m=11m = 11(12个节点)。由于使用的是clamped类型节点,0和1是重数为5的两个节点,并且只有两个内部节点,分别为1/3和2/3。有n+1n+1个B样条基函数,第一个和最后一个的最大值分别为0和1,其他基函数的最大值用竖直蓝色线段标记,对应的参数用黄色点标记出。

img

看一个例子。假设我们有4个数据点(n=3n=3),次数p=2p=2。因此,节点的数量是7(即m=n+p+1=6m = n + p + 1 = 6)。由于节点是均匀分布的,它们是:

u0=u1=u2=0,u3=0.5,u4=u5=u6=1u_0 = u_1 = u_2 = 0, \quad u_3 = 0.5, \quad u_4 = u_5 = u_6 = 1

然后,我们可以计算B样条基函数。从第0阶开始:

image-20240528181712523

接下来,我们计算次数为1的基函数。由于N0,0(u)N_{0,0}(u)N1,0(u)N_{1,0}(u)都是0,因此N0,1(u)N_{0,1}(u)在任何区间都是零。同样,N4,1(u)N_{4,1}(u)在任何地方也都是零。因此次数为1的基函数,只有 N1,1(u)N_{1,1}(u)N2,1(u)N_{2,1}(u)N3,1(u)N_{3,1}(u)不为0,如下所示:

image-20240528181815362

次数为2的基函数如下:

image-20240528181832859

下面的图显示了所有四个次数为2的B样条基函数。

img

不难算出,N0,2(u)N_{0,2}(u)N1,2(u)N_{1,2}(u)N2,2(u)N_{2,2}(u)N3,2(u)N_{3,2}(u)的最大值分别为1(在u=0u = 0处)、2/3(在u=1/3u = 1/3处)、2/3(在u=2/3u = 2/3处)和1(在u=1u = 1处)。

因此,使用通用方法,得到的节点向量是0,0,0,0.5,1,1,1{0, 0, 0, 0.5, 1, 1, 1},数据点对应的参数是0,1/3,2/3,1{0, 1/3, 2/3, 1}。在这种情况下,我们的参数是均匀分布的。

使用通用参数化方法插值得到的曲线有一个非常有用的性质。它是仿射不变的。这意味着,可以通过变换数据点来获得变换后的插值B样条曲线。这与B样条曲线的仿射不变性质类似。

如果在原始插值曲线和变换后的插值B样条曲线中使用相同的节点和参数集,那么通过变换数据点来实现曲线的变换。根据这一点,我们知道均匀参数化也是仿射不变的,因为节点向量是从一组均匀分布的参数计算出来的,这些参数在变换前后没有改变。然而,弦长参数化和向心参数化不是仿射不变的,因为变换后的曲线中,弦长分布可能与原始的不一样,我们会得到一组新的弦长,必须重新计算新的参数。

曲面参数化

e+1e+1f+1f+1列控制点定义的次数为(p,q)(p, q)的B样条曲面的方程如下:

S(u,v)=i=0ej=0fNi,p(u)Nj,q(v)Pij\mathbf{S}(u,v) = \sum_{i=0}^{e} \sum_{j=0}^{f} N_{i,p}(u)N_{j,q}(v)\mathbf{P}_{ij}

它需要两组参数(参数点)来进行曲面插值和逼近。

假设我们有m+1m+1n+1n+1列数据点Dij\mathbf{D}_{ij},其中0im,0jn0 \leq i \leq m,0 \leq j \leq n。因此,在uu方向需要m+1m+1个参数s0,...,sms_0, ..., s_mvv方向需要n+1n+1个参数t0,...,tnt_0, ..., t_n,这样,曲面参数域中的点(sc,td)(s_c, t_d)与曲面上的点S(sc,td)\mathbf{S}(s_c, t_d)对应,用方程表示如下:

S(sc,td)=i=0ej=0fNi,p(sc)Nj,q(td)Pij\mathbf{S}(s_c, t_d) = \sum_{i=0}^{e} \sum_{j=0}^{f} N_{i,p}(s_c)N_{j,q}(t_d)\mathbf{P}_{ij}

S(sc,td)\mathbf{S}(s_c, t_d)又与数据点Dcd\mathbf{D}_{cd}相对应,其中 scs_ctdt_d 分别是 uuvv 方向的参数。

在B样条曲面方程中,uu方向对应于Ni,p(u)N_{i,p}(u)Pij\mathbf{P}_{ij}中的索引iiii 的范围为从0到mmN0,p(u),N1,p(u),...,Nm,p(u)N_{0,p}(u), N_{1,p}(u), ..., N_{m,p}(u)是控制点的基函数。因此,在uu方向,我们需要m+1m+1个参数,已知次数 pp 和第 jj 列数据点,我们可以计算m+1m+1个参数u0,j,u1,j,...,um,ju_{0,j}, u_{1,j}, ..., u_{m,j},如下图所示。参数s0,s1,...,sms_0, s_1, ..., s_m是每行的平均值。即参数 sis_i 是第 ii 行参数的平均值,即si=(ui,0+ui,1+...+ui,n)/(n+1)s_i = (u_{i,0} + u_{i,1} + ... + u_{i,n})/(n+1)
在B样条曲面方程中,uu方向对应于Ni,p(u)N_{i,p}(u)Pij\mathbf{P}_{ij}中的索引iiii 的范围为从0到mmN0,p(u),N1,p(u),...,Nm,p(u)N_{0,p}(u), N_{1,p}(u), ..., N_{m,p}(u)是控制点的基函数。因此,在uu方向,我们需要m+1m+1个参数,已知次数 pp 和第 jj 列数据点,我们可以计算m+1m+1个参数u0,j,u1,j,...,um,ju_{0,j}, u_{1,j}, ..., u_{m,j},如下图所示。参数s0,s1,...,sms_0, s_1, ..., s_m是每行的平均值。即参数 sis_i 是第 ii 行参数的平均值,即si=(ui,0+ui,1+...+ui,n)/(n+1)s_i = (u_{i,0} + u_{i,1} + ... + u_{i,n})/(n+1)

img

vv方向的参数计算类似。每行有n+1n+1个数据点,因此需要n+1n+1个参数。因此,对于第 ii 行的数据点,我们可以计算出n+1n+1个参数值vi,0,vi,1,...,vi,nv_{i,0}, v_{i,1}, ..., v_{i,n},有m+1m+1行,这些值可以组织成一个(m+1)×(n+1)(m+1) \times (n+1)矩阵,参数 tjt_j 是第 jj 列参数的平均值,即 tj=(v0,j+v1,j+...+vm,j)/(m+1)t_j = (v_{0,j} + v_{1,j} + ... + v_{m,j})/(m+1)。这样,我们可以计算出 n+1n+1vv 方向的参数。

算法总结如下:

Input: (m+1)×(n+1)(m+1) \times (n+1) 个数据点 Dij\mathbf{D}_{ij}
Output: uu方向的参数 s0,...,sms_0, ..., s_mvv方向的参数 t0,...,tnt_0, ..., t_n

算法:

​ // 计算 s0,...,sms_0, ..., s_m
for jj = 00 to nn do
​ 计算一组 m+1m+1 个参数 u0,j,u1,j,...,um,ju_{0,j}, u_{1,j}, ..., u_{m,j}
for ii = 00 to mm do
si=(ui,0+ui,1+...+ui,n)/(n+1)s_i = (u_{i,0} + u_{i,1} + ... + u_{i,n})/(n+1)
​ (获得uu方向的参数)

​ // 计算 t0,...,tnt_0, ..., t_n
for ii = 00 to mm do
​ 计算一组 n+1n+1 个参数 vi,0,vi,1,...,vi,nv_{i,0}, v_{i,1}, ..., v_{i,n}
for jj = 00 to nn do
tj=(v0,j+v1,j+...+vm,j)/(m+1)t_j = (v_{0,j} + v_{1,j} + ... + v_{m,j})/(m+1)
​ (获得vv方向的参数)

根据参数 s0,s1,...,sms_0, s_1, ..., s_m 和次数 pp ,我们可以计算出uu方向的节点向量 UU,根据参数 t0,t1,...,tnt_0, t_1, ..., t_n 和次数 qq ,我们可以计算出vv方向的节点向量 VV

注意,上述只是一个概念性算法,效率不高。这个算法只适用于均匀参数化、弦长参数化和向心参数化。对于通用参数化方法,因为不涉及数据点,我们可以对每一行和每一列的数据点应用均匀分布的节点来计算参数。

求解线性方程组

插值和逼近的计算过程中都会涉及到求解线性方程组,下面我们讨论一个常用的求解方法。

假设我们有一个 n×nn \times n 的矩阵 A\mathbf{A},一个 n×hn \times h 的"常数"项矩阵 B\mathbf{B},以及一个 n×hn \times h 的未知矩阵 X\mathbf{X},定义如下:

A=[a11a12a1na21a22a2nan1an2ann]n×n\mathbf{A} = \left[\begin{array}{cccc}a_{11}&a_{12}&\cdots&a_{1n}\\a_{21}&a_{22}&\cdots&a_{2n}\\\vdots&\vdots&\ddots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{array}\right]_{n\times n}

B=[b11b12b1hb21b22b2hbn1bn2bnh]n×h\mathbf{B} = \left[\begin{array}{cccc}b_{11}&b_{12}&\cdots&b_{1h}\\b_{21}&b_{22}&\cdots&b_{2h}\\\vdots&\vdots&\ddots&\vdots\\b_{n1}&b_{n2}&\cdots&b_{nh}\end{array}\right]_{n\times h}

X=[x11x12x1hx21x22x2hxn1xn2xnh]n×h\mathbf{X} = \left[\begin{array}{cccc}x_{11}&x_{12}&\cdots&x_{1h}\\x_{21}&x_{22}&\cdots&x_{2h}\\\vdots&\vdots&\ddots&\vdots\\x_{n1}&x_{n2}&\cdots&x_{nh}\end{array}\right]_{n\times h}

它们满足以下关系:

B=AX\mathbf{B} = \mathbf{A}\cdot\mathbf{X}

如果 A\mathbf{A}B\mathbf{B} 已知,我们需要一种快速的方法来求解 X\mathbf{X}。有人可能会说:计算矩阵 A\mathbf{A} 的逆矩阵 A1\mathbf{A}^{-1},然后解就是 X\mathbf{X} = A1B\mathbf{A}^{-1}\mathbf{B},虽然这是正确的思路,但是这样计算量很大。

LU分解

一个有效的求解 B\mathbf{B} = AX\mathbf{A}\mathbf{X} 的方法是LU分解。虽然高斯消元法和Cholesky等其他方法也能计算,但LU分解是一种加速计算的方法

LU分解首先将矩阵 A\mathbf{A} “分解” 为 A=LU\mathbf{A} = \mathbf{L} \mathbf{U}的形式,其中 L\mathbf{L}U\mathbf{U} 分别是下三角和上三角矩阵。如果 A\mathbf{A} 是一个 n×nn \times n 矩阵,那么 L\mathbf{L}U\mathbf{U} 也是 n×nn \times n 矩阵,其形式如下:

L=[l11000l21l2200ln1ln2ln3lnn]U=[u11u12u13u1n0u22u23u2n00u33u3n000unn]\mathbf{L} = \left[\begin{array}{ccccc}l_{11}&0&0&\cdots&0\\l_{21}&l_{22}&0&\cdots&0\\\vdots&\vdots&\ddots&\ddots&\vdots\\l_{n1}&l_{n2}&l_{n3}&\cdots&l_{nn}\end{array}\right]\quad\mathbf{U} = \left[\begin{array}{ccccc}u_{11}&u_{12}&u_{13}&\cdots&u_{1n}\\0&u_{22}&u_{23}&\cdots&u_{2n}\\0&0&u_{33}&\cdots&u_{3n}\\\vdots&\vdots&\vdots&\ddots&\vdots\\0&0&0&\cdots&u_{nn}\end{array}\right]

下三角矩阵 L\mathbf{L} 在对角线上方的所有元素都为零,上三角矩阵 U\mathbf{U} 在对角线下方的所有元素都为零。

如果找到了满足 A=LU\mathbf{A} = \mathbf{L} \mathbf{U}LU\mathbf{L} \mathbf{U}分解,则原始方程变为 B=(LU)X\mathbf{B} = (\mathbf{L} \mathbf{U}) \mathbf{X},根据矩阵的结合律,这个方程可以写为 B=L(UX)\mathbf{B} = \mathbf{L} (\mathbf{U} \mathbf{X})

由于 L\mathbf{L}B\mathbf{B} 是已知的,那么上式方程相当于求解 B=LY\mathbf{B} = \mathbf{L} \mathbf{Y},其中 Y=UX\mathbf{Y} = \mathbf{U} \mathbf{X}。然后,由于 U\mathbf{U}Y\mathbf{Y} 是已知的,因此可以求解出 X\mathbf{X},就得到了最终的结果。

这样,求解方程B=AX\mathbf{B} = \mathbf{A}\mathbf{X} 分解为两步:

  1. B=LY\mathbf{B} = \mathbf{L} \mathbf{Y} 中解出 Y\mathbf{Y}
  2. Y=UX\mathbf{Y} = \mathbf{U} \mathbf{X} 中解出 X\mathbf{X}

前向代换

第一步,展开 B=LY\mathbf{B} = \mathbf{L} \mathbf{Y} 得到:

[b11b12b1hb21b22b2hbn1bn2bnh]=[l1100l21l220ln1ln2lnn][y11y12y1hy21y22y2hyn1yn2ynh]\begin{bmatrix} b_{11} & b_{12} & \cdots & b_{1h} \\ b_{21} & b_{22} & \cdots & b_{2h} \\ \vdots & \vdots & \ddots & \vdots \\ b_{n1} & b_{n2} & \cdots & b_{nh} \end{bmatrix} = \begin{bmatrix} l_{11} & 0 & \cdots & 0 \\ l_{21} & l_{22} & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ l_{n1} & l_{n2} & \cdots & l_{nn} \end{bmatrix} \begin{bmatrix} y_{11} & y_{12} & \cdots & y_{1h} \\ y_{21} & y_{22} & \cdots & y_{2h} \\ \vdots & \vdots & \ddots & \vdots \\ y_{n1} & y_{n2} & \cdots & y_{nh} \end{bmatrix}

不难看出矩阵 B\mathbf{B} 的第 jj 列是矩阵 L\mathbf{L} 和矩阵 Y\mathbf{Y} 的第 jj 列的乘积,因此,我们一次可以求解出 Y\mathbf{Y} 中的一列,如下所示:

[b1b2bn]=[l11000l21l2200ln1ln2ln3lnn][y1y2yn]\left[\begin{array}{c}b_1\\b_2\\\vdots\\b_n\end{array}\right] = \left[\begin{array}{ccccc}l_{11}&0&0&\cdots&0\\l_{21}&l_{22}&0&\cdots&0\\\vdots&\vdots&\ddots&\ddots&\vdots\\l_{n1}&l_{n2}&l_{n3}&\cdots&l_{nn}\end{array}\right]\cdot\left[\begin{array}{c}y_1\\y_2\\\vdots\\y_n\end{array}\right]

上式相当于方程:

b1=l11y1b2=l21y1+l22y2bn=ln1y1+ln2y2+ln3y3++lnnyn\begin{array}{rcl} b_1&=&l_{11}y_1\\ b_2&=&l_{21}y_1&+&l_{22}y_2\\ &\vdots&\vdots\\ b_n&=&l_{n1}y_1&+&l_{n2}y_2&+&l_{n3}y_3&+&\cdots&+&l_{nn}y_n \end{array}

从上面的等式中,我们可以算出 y1=b1l11y_1 = \frac{b_1}{l_{11}},一旦有了 y1y_1,代入第二个方程就可以得出 y2=b2l21y1l22y_2 = \frac{b_2 - l_{21}y_1}{l_{22}},有了 y1y_1y2y_2,代入第三个方程中,我们得出 y3=b3(l31y1+l32y2)l33y_3 = \frac{b_3 - (l_{31}y_1 + l_{32}y_2)}{l_{33}}

因此,我们从第一个方程计算 y1y_1 并将其代入第二个方程,计算出 y2y_2。有了 y1y_1y2y_2,将它们代入第三个方程求解出 y3y_3。依此类推,当计算第 ii 方程时,y1,y2,,yi1y_1, y_2, \ldots, y_{i-1} 可以通过前i1i-1个方程计算出,将它们代入第 ii 个方程进行求解,下面是求解 yiy_i 的公式:

yi=1lii[bik=1i1li,kyk]y_i = \frac{1}{l_{ii}}\left[b_i-\sum_{k=1}^{i-1}l_{i,k}y_k\right]

由于 yiy_i 的值会被代入下一个方程以求解 yi+1y_{i+1} 的值,这个过程被称为前向代换。重复以上过程,可以求解出向量 Y\mathbf{Y}

下面是算法的流程:

Input: 矩阵 Bn×h\mathbf{B}_{n \times h} 和下三角矩阵 Ln×n\mathbf{L}_{n \times n}
Output: 矩阵 Yn×h\mathbf{Y}_{n \times h}

算法:

    for jj = 11 to hh do // 共有 hh
        begin // 对每一列执行以下操作
            y1,j=b1,j/l1,1y_{1,j} = b_{1,j} / l_{1,1}; // 计算当前列的 y1y_1
            for ii = 22 to nn do // 处理该列的元素
                begin
                    sum=0sum = 0; // 求解当前列的 yiy_i
                    for kk = 11 to i1i-1 do
                                sum=sum+li,k×yk,jsum = sum + l_{i,k} \times y_{k,j};
                    yi,j=(bi,jsum)/li,iy_{i,j} = (b_{i,j} - sum) / l_{i,i};
                end
    end

后向代换

求解出Y\mathbf{Y}后, 我们可以根据 Y=UX\mathbf{Y} = \mathbf{U} \mathbf{X} 求解出 X\mathbf{X}。将这个等式展开,只看Y\mathbf{Y}中的某一列和 X\mathbf{X} 的相应列,得到:

[y1y2yn]=[u11u12u13u1n0u22u23u2n00u33u3n000unn][x1x2xn]\left[\begin{array}{c}y_1\\y_2\\\vdots\\y_n\end{array}\right]=\left[\begin{array}{ccccc}u_{11}&u_{12}&u_{13}&\cdots&u_{1n}\\0&u_{22}&u_{23}&\cdots&u_{2n}\\0&0&u_{33}&\cdots&u_{3n}\\\vdots&\vdots&\vdots&\ddots&\vdots\\0&0&0&\cdots&u_{nn}\end{array}\right]\cdot\left[\begin{array}{c}x_1\\x_2\\\vdots\\x_n\end{array}\right]

等价于方程:

y1=u11x1+u12x2+u13x3++u1nxny2=u22x2+u23x3++u2nxnyn=unnxn\begin{array}{cccccccccc}y_1&=&u_{11}x_1&+&u_{12}x_2&+&u_{13}x_3&+&\cdots&+&u_{1n}x_n\\y_2&=&&&u_{22}x_2&+&u_{23}x_3&+&&+&u_{2n}x_n\\&\vdots&&&&&\ddots&&&&\\y_n&=&&&&&&&&u_{nn}x_n\end{array}

现在,可以从第 nn 方程中立即得到 xnx_n,因为 xn=ynunnx_n = \frac{y_n}{u_{nn}}

一旦 xnx_n 可用,将其代入第 n1n-1 个方程 yn1=un1,n1xn1+un1,nxny_{n-1} = u_{n-1,n-1}x_{n-1} + u_{n-1,n}x_n 并解出 xn1x_{n-1} 得到:

xn1=yn1un1,nxnun1,n1x_{n-1} = \frac{y_{n-1} - u_{n-1,n}x_n}{u_{n-1,n-1}}

现在,我们有 xnx_nxn1x_{n-1}。将它们代入第 n2n-2 个方程 yn2=un2,n2xn2+un2,n1xn1+un2,nxny_{n-2} = u_{n-2,n-2}x_{n-2} + u_{n-2,n-1}x_{n-1} + u_{n-2,n}x_n 并解出 xn2x_{n-2} 得到:

xn2=yn2(un2,n1xn1+un2,nxn)un2,n2x_{n-2} = \frac{y_{n-2} - (u_{n-2,n-1}x_{n-1} + u_{n-2,n}x_n)}{u_{n-2,n-2}}

xn,xn1x_n, x_{n-1}xn2x_{n-2},我们可以从第 n3n-3 方程中解出 xn3x_{n-3}。依此类推,当 xn,xn1,,xi+1x_n, x_{n-1}, \ldots, x_{i+1} 已知时,我们可以利用以下关系从第 ii 方程中解出 xix_i

xi=1uii[yik=i+1nui,kxk]x_{i} = \frac{1}{u_{ii}}\left[y_{i}-\sum_{k=i+1}^{n}u_{i,k}x_{k}\right]

重复这个过程直到计算出 x1x_1。然后,所有的 xx 都已经得到,线性方程组就求解出了。

以下算法总结了这个过程:

Input: 矩阵 Yn×h\mathbf{Y}_{n \times h} 和上三角矩阵 Un×n\mathbf{U}_{n \times n}
Output: 矩阵 Xn×h\mathbf{X}_{n \times h}

算法:

    for jj = 11 to hh do // 共有 hh
        begin // 对每一列执行以下操作
            xn,j=yn,j/un,nx_{n,j} = y_{n,j} / u_{n,n}; // 计算当前列的 xnx_n
            for ii = n1n-1 downto 11 do // 处理该列的元素
                begin
                    sum=0sum = 0; // 求解当前列的 xix_i
                    for kk = i+1i+1 to nn do
                        sum=sum+ui,k×xk,jsum = sum + u_{i,k} \times x_{k,j};
                    xi,j=(yi,jsum)/ui,ix_{i,j} = (y_{i,j} - sum) / u_{i,i};
                end
        end

这次我们首先计算 xnx_n ,然后代入前面的方程计算xn1x_{n-1},最后倒推计算得到 x1x_1,因此,这个过程被称为后向代换

全局曲线插值

要用一组点拟合出B样条曲线,最简单的方法是使用全局插值方法。

假设我们有n+1n+1个数据点D0\mathbf{D}_0, D1\mathbf{D}_1, …, Dn\mathbf{D}_n,并希望拟合出一条次数为pp的B样条曲线,其中pnp \leq n是指定值。我们可以选择一组参数值t0,t1,...,tnt_0, t_1, ..., t_n,其中参数tkt_k对应于数据点Dk\mathbf{D}_k,根据这些参数,计算出m+1m+1个节点的节点向量,其中m=n+p+1m = n + p + 1。因此,我们已知节点向量和次数pp,现在缺少的是一组n+1n+1个控制点。全局插值法就是用来找到这些控制点的一种方法。

全局曲线插值给定一组n+1n+1个数据点,D0,D2,...,DnD_0, D_2, ..., D_n 和一个次数pp,找到一条由n+1n+1个控制点定义的次数为pp的B样条曲线,该曲线按给定顺序经过所有数据点。

解决方案

假设次数为pp的插值B样条曲线定义如下:

C(u)=i=0nNi,p(u)PiC(u) = \sum_{i=0}^{n} N_{i,p}(u)P_i

这个B样条曲线有n+1n+1个未知控制点。由于参数tkt_k对应于数据点Dk\mathbf{D}_k,将tkt_k代入上述方程可以得到:

Dk=C(tk)=i=0nNi,p(tk)Pi0kn\mathbf{D}_k = C(t_k) = \sum_{i=0}^{n} N_{i,p}(t_k)P_i \quad 0 \leq k \leq n

上述方程中有n+1n+1个B样条基函数(N0,p(u)N_{0,p}(u), N1,p(u)N_{1,p}(u), N2,p(u)N_{2,p}(u), …, 和 Nn,p(u)N_{n,p}(u))和n+1n+1个参数(t0,t1,t2,..,tnt_0, t_1, t_2, .., 和 t_n,将这些tkt_k的值代入Ni,p(u)N_{i,p}(u)中即可得到(n+1)2(n +1)^2个值。这些值可以写成(n+1)×(n+1)(n +1) \times (n +1)矩阵N\mathbf{N}的形式,其中第kk行是在tkt_k处计算得到的基函数N0,p(u)N_{0,p}(u), N1,p(u)N_{1,p}(u), N2,p(u)N_{2,p}(u), …, 和 Ni,p(u)N_{i,p}(u)的值,如下所示:

N=[N0,p(t0)N1,p(t0)Nn,p(t0)N0,p(t1)N1,p(t1)Nn,p(t1)N0,p(tn)N1,p(tn)Nn,p(tn)]N = \begin{bmatrix} N_{0,p}(t_0) & N_{1,p}(t_0) & \cdots & N_{n,p}(t_0) \\ N_{0,p}(t_1) & N_{1,p}(t_1) & \cdots & N_{n,p}(t_1) \\ \vdots & \vdots & \ddots & \vdots \\ N_{0,p}(t_n) & N_{1,p}(t_n) & \cdots & N_{n,p}(t_n) \end{bmatrix}

我们将向量Dk\mathbf{D}_kPi\mathbf{P}_i也写成两个矩阵D\mathbf{D}P\mathbf{P},如下所示:

D=[d01d02d03d0sd11d12d13d1sdn1dn2dn3dns]P=[p01p02p03p0sp11p12p13p1spn1pn2pn3pns]\left.\mathbf{D}=\left[\begin{array}{ccccc}{d_{01}}&{d_{02}}&{d_{03}}&{\cdots}&{d_{0s}}\\{d_{11}}&{d_{12}}&{d_{13}}&{\cdots}&{d_{1s}}\\{\vdots}&&&{\ddots}&{\vdots}\\{d_{n1}}&{d_{n2}}&{d_{n3}}&{\cdots}&{d_{ns}}\end{array}\right.\right]\quad\mathbf{P}=\left[\begin{array}{ccccc}{p_{01}}&{p_{02}}&{p_{03}}&{\cdots}&{p_{0s}}\\{p_{11}}&{p_{12}}&{p_{13}}&{\cdots}&{p_{1s}}\\{\vdots}&&&{\ddots}&{\vdots}\\{p_{n1}}&{p_{n2}}&{p_{n3}}&{\cdots}&{p_{ns}}\end{array}\right]

Dk\mathbf{D}_k表示一个ss维空间中的向量(即,Dk=[dk1,,dks]\mathbf{D}_k=[d_{k1}, \ldots, d_{ks}]),它是矩阵D\mathbf{D}的第kk行。类似地,Pi\mathbf{P}_i也是一个在ss维空间中的向量(即,Pi=[pi1,,pis]\mathbf{P}_i = [p_{i1}, \ldots, p_{is}])。在三维空间中,我们有s=3s=3,在平面上我们有s=2s=2D\mathbf{D}P\mathbf{P}都是(n+1)×s(n +1) \times s矩阵,因此Dk\mathbf{D}_ktit_i的关系可以写成以下更简单的形式:

D=NP\mathbf{D} = \mathbf{N} \cdot \mathbf{P}

矩阵D\mathbf{D}为输入的数据点,矩阵N\mathbf{N}是给定参数计算出的B样条基函数的值,D\mathbf{D}N\mathbf{N}都是已知的,唯一的未知数是矩阵P\mathbf{P},因此上面的方程是一个关于P\mathbf{P}的线性方程组,求解出P\mathbf{P}即可得到控制点,就能计算出插值的B样条曲线。

算法实现

D\mathbf{D}的第ii列和P\mathbf{P}的第ii列记为Di\mathbf{D}_iPi\mathbf{P}_i。根据上述线性方程组,我们有:

di=Npi\mathbf{d}^i = \mathbf{N} \cdot \mathbf{p}^i

根据N\mathbf{N}di\mathbf{d}^i求解出pi\mathbf{p}^i,其中0ih0\leq i \leq h,对每个ii重复此操作,计算出P\mathbf{P}的每一列。这样,所有控制点都已计算出来。

但是,这是非常低效的。不过,许多数值计算库为我们提供了现成的线性系统求解器,能够有效地求解方程组D=NP\mathbf{D} = \mathbf{N}\mathbf{P}

下面是相关算法的步骤:

Input: n+1n+1个数据点D0\mathbf{D}_0, … Dn\mathbf{D}_n和次数pp
Output: 次数为pp的B样条曲线,按照给定的顺序经过所有数据点

算法:

​ 选择一种方式计算n+1n+1个参数t0,...,tnt_0, ..., t_n,得到节点向量UU
for ii = 00 to nn do
for jj = 00 to nn do
​ 计算矩阵N\mathbf{N}中第ii行和第jj列的值Nj,p(ti)N_{j,p}(t_i)

for ii = 00 to nn do
​ 将数据点Di\mathbf{D}_i存储在矩阵D\mathbf{D}的第ii行;

​ 使用线性方程求解器求解D=NP\mathbf{D} = \mathbf{N}\mathbf{P}得到P\mathbf{P}
P\mathbf{P}的第ii行是控制点Pi\mathbf{P}_i
​ 控制点P0\mathbf{P}_0, …, Pn\mathbf{P}_n,节点向量UU和次数pp确定了一条插值B样条曲线。

下面左图是一个例子。有9个给定数据点(黑色表示),蓝色点为计算出的控制点。插值曲线上的蓝点小圆点是使用弦长法计算出的节点。可以看出,这些“节点”非常接近数据点,控制多边形也紧贴着数据点连成的多边形。但并不是所有的时候都会如此,下图右图中,数据点连成的多边形和控制多边则形非常不同。

imgimg

如果通过对连续pp个参数取平均来计算节点,那么矩阵N\mathbf{N}中每个元素都是正值,且带状的半带宽小于pp(即,如果ikp|i - k| \geq p,则Ni,p(tk)=0N_{i,p}(t_k) = 0),de Boor在1978年证明了这一点。这意味着通过这样的参数化算法得到的线性方程组可以使用高斯消元法求解。

参数和节点的影响

一般来说,所选参数和节点对结果曲线的影响是无法预测的,**但是,如果弦长分布大致相同,那么四种参数化方法生成的曲线应该相似。**此外,通用参数化方法应该与均匀参数化方法表现相似,因为当节点均匀分布时,B样条基函数的最大值分布是均匀的。但是,当弦长分布剧烈变化时,四种参数选择方式产生的曲线则会大不相同。

下图中的四条曲线是使用四种参数化方法获得的,插值B样条曲线的次数为3。均匀参数化产生了一个尖点,弦长参数化生成的曲线在数据点之间会有较大的波动,向心参数化类似于弦长参数化但表现更好,通用参数化方法紧贴着数据点连成的多边形(比均匀参数化更好),但产生了一个小的环。

img(均匀法)

img(弦长法)

img(向心法)

img(通用方法)

参数和节点之间的关系如何?下图显示了所有四种方法的参数和节点分布。通用参数化方法获得的参数和节点比弦长参数化和向心参数化的分布更均匀。弦长参数化获得的较长曲线段在向心参数化中会变短,曲线也不会在数据点之间剧烈摆动。

img

次数的影响

次数对插值B样条曲线形状的影响也很难预测。可以从下图中观察到,均匀参数化和通用参数化法通常很好地跟踪长弦,但是,它们在短弦上会出现问题。由于参数间距几乎相等,因此对于较短的弦,插值曲线会拉伸得更长一些。因此,我们会看到峰值和自交环。当次数增加时,这种情况会变得更加明显,因为高阶曲线提供了更多的摆动自由度。

均匀法 弦长法 向心法 通用方法
2阶 img img img
3阶 img img img img
4阶 img img img img
5阶 img img img img

对于弦长参数化,上图显示它对长弦的效果不是很好,特别是后面或前面有一些较短的弦,可能会发生大的凸起。次数对上图中显示的插值曲线形状没有显著影响。由于向心参数化是弦长参数化的扩展,因此两者具有相同的特点。然而,向心参数化会降低两个相邻参数之间距离之差的影响,它也具有均匀参数化和通用参数化方法相似的特点。例如,生成的插值曲线会紧贴着较长的弦,当次数增加时,短弦处可能会出现自交环。

为什么这种方法是全局的?

即使使用满足局部修改特性的B样条曲线,这种插值方法也是全局的,因为改变单个数据点的位置会完全改变插值曲线的形状。下图中,黄色点是数据点,其中一个被移动到新位置,以浅蓝色标记,并用红色箭头指示。这九个数据点使用4次B样条曲线采用向心参数化插值得到。

可以看到,移动数据点后的插值曲线(蓝色)和原始曲线有八个数据点是相同的,但八个曲线段都不完全相同,因此,改变单个数据点的位置会全局地改变插值曲线的形状!

img

全局曲线逼近

在插值中,插值曲线会按照顺序通过所有给定的数据点,曲线可能会在数据点之间疯狂摆动,而不是紧贴着数据点连成的多边形。曲线逼近则是放宽了要求,不严格要求曲线必须经过所有点。在全局逼近中,除了第一个和最后一个数据点之外,曲线不必经过每一个点。

为了衡量一条曲线“逼近”给定数据点连成的多边形的程度,提出了误差距离的概念。

误差距离是数据点与其在曲线上的对应点之间的距离。因此,如果这些误差距离的总和最小,曲线就会紧贴数据点连成的多边形。插值曲线就是一种特殊结果,因为每个数据点的误差距离为零,以这种方式获得的曲线称为逼近曲线

假设我们有n+1n+1个数据点D0,D1,...,Dn\mathbf{D}_0, \mathbf{D}_1, ..., \mathbf{D}_n,并希望找到一条接近多边形形状的B样条曲线,而不用经过每个数据点。为此,我们需要两个额外的条件:控制点的数量(假设为h+1h+1)和一个次数(设为pp),其中n>hp1n > h \geq p \geq 1。逼近比插值更灵活,因为我们不仅可以选择次数,还可以选择控制点的数量。

全局曲线逼近给定一组n+1n+1个数据点,D0,D1,...,Dn\mathbf{D}_0, \mathbf{D}_1, ..., \mathbf{D}_n,一个次数 p,和数量 h,其中 n>hp1n > h \geq p \geq 1,找到一条由 h+1 个控制点定义的次数为 p 的B样条曲线,并满足以下条件:

  1. 这条曲线经过第一个点和最后一个点(即D0\mathbf{D}_0Dn\mathbf{D}_n);
  2. 这条曲线上各个点的误差距离之和最小。

有了 hhpp,我们可以确定一组参数和一个节点向量。设参数为 t0,t1,...,tnt_0, t_1, ..., t_n。参数的数量必须等于数据点的数量。现在,假设次数为 p 的逼近B样条曲线为:

C(u)=i=0hNi,p(u)PiC(u) = \sum_{i=0}^{h} N_{i,p}(u)P_i

其中 P0,P1,...,Ph\mathbf{P}_0, \mathbf{P}_1, ..., \mathbf{P}_hh+1h+1 个未知控制点。由于我们希望曲线通过第一个和最后一个数据点,我们有 D0=C(0)=P0\mathbf{D}_0 = \mathbf{C}(0) = \mathbf{P}_0Dn=C(1)=Ph\mathbf{D}_n = \mathbf{C}(1) = \mathbf{P}_h。因此,只有 h1h-1 个未知控制点 P1,P2,...,Ph1\mathbf{P}_1, \mathbf{P}_2, ..., \mathbf{P}_{h-1},这样曲线方程变为以下形式:

C(u)=N0,p(u)D0+(i=1h1Ni,p(u)Pi)+Nh,p(u)Dn\mathbf{C}(u)=N_{0,p}(u)\mathbf{D}_{0}+\left(\sum_{i=1}^{h-1}N_{i,p}(u)\mathbf{P}_{i}\right)+N_{h,p}(u)\mathbf{D}_{n}

最小二乘法

如何测量误差距离?

参数 tkt_k 对应于数据点 Dk\mathbf{D}_k, Dk\mathbf{D}_k 和曲线上tkt_k对应的点之间的距离是 DkC(tk)|\mathbf{D}_k - \mathbf{C}(t_k) |。由于这个距离的计算需要开根,我们通常选择使用平方距离DkC(tk)2|\mathbf{D}_k - \mathbf{C}(t_k) |^2 来避免开根计算带来的额外开销。

因此,所有平方误差距离之和是:

f(P1,,Ph1)=k=1n1DkC(tk)2f(P_1, \ldots, P_{h-1}) = \sum_{k=1}^{n-1} | \mathbf{D}_k - C(t_k) |^2

我们的目标是找到一些控制点 P1,...,Ph1\mathbf{P}_1, ..., \mathbf{P}_{h-1},使得函数 f()f() 最小化。

解决方案

我们将DkC(tk)\mathbf{D}_k - \mathbf{C}(t_k)改写成另一种形式:

DkC(tk)=Dk[N0,p(tk)D0+(i=1h1Ni,p(tk)Pi)+Nh,p(tk)Dn]=(DkN0,p(tk)D0Nh,p(tk)Dn)(i=1h1Ni,p(tk)Pi)\begin{aligned}\mathbf{D}_{k}-\mathbf{C}(t_{k})&=\quad\mathbf{D}_k-\left[N_{0,p}(t_k)\mathbf{D}_0+\left(\sum_{i=1}^{h-1}N_{i,p}(t_k)\mathbf{P}_i\right)+N_{h,p}(t_k)\mathbf{D}_n\right]\\&=\quad(\mathbf{D}_k-N_{0,p}(t_k)\mathbf{D}_0-N_{h,p}(t_k)\mathbf{D}_n)-\left(\sum_{i=1}^{h-1}N_{i,p}(t_k)\mathbf{P}_i\right)\end{aligned}

在上面的公式中,D0\mathbf{D}_0, Dk\mathbf{D}_kDn\mathbf{D}_n 是给定的,而 N0,p(tk)N_{0,p}(t_k)Nh,p(tk)N_{h,p}(t_k)N0,p(u)N_{0,p}(u)Nh,p(u)N_{h,p}(u)tkt_k 处的值,为了方便,我们定义一个新的向量 Qk\mathbf{Q}_k 如下:

Qk=DkN0,p(tk)D0Nh,p(tk)Dh\mathbf{Q}_k = \mathbf{D}_k - N_{0,p}(t_k)\mathbf{D}_0 - N_{h,p}(t_k)\mathbf{D}_h

然后,误差距离平方和函数 f()f() 可以写成以下形式:

f(P1,,Ph1)=k=1n1Qk(i=1h1Ni,p(tk)Pi)2f(\mathbf{P}_1,\ldots,\mathbf{P}_{h-1})=\sum_{k=1}^{n-1}\left|\mathbf{Q}_k-\left(\sum_{i=1}^{h-1}N_{i,p}(t_k)\mathbf{P}_i\right)\right|^2

回想一下恒等式 xx=x2\mathbf{x} · \mathbf{x} = | \mathbf{x} |^2。这意味着向量 x\mathbf{x} 与其自身的内积等于 x\mathbf{x} 的长度的平方。因此,误差平方项可以重写为:

Qk(i=1h1Ni,p(tk)Pi)2=(Qk(i=1h1Ni,p(tk)Pi))(Qk(i=1h1Ni,p(tk)Pi))=QkQk2(i=1h1Ni,p(tk)PiQk)+(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)Pi)\begin{aligned} &\left|\mathbf{Q}_{k}-\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\right|^{2} \\ &=\left(\mathbf{Q}_{k}-\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\right)\cdot\left(\mathbf{Q}_{k}-\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\right) \\ &=\mathbf{Q}_{k}\cdot\mathbf{Q}_{k}-2\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\cdot\mathbf{Q}_{k}\right)+\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right) \end{aligned}

因此,函数ff变为:

f(P1,,Ph1)=k=1n1[QkQk2(i=1h1Ni,p(tk)PiQk)+(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)Pi)]\begin{aligned} f(\mathbf{P}_{1},\ldots,\mathbf{P}_{h-1})&=\sum_{k=1}^{n-1}\left[\mathbf{Q}_{k}\cdot\mathbf{Q}_{k}-2\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\cdot\mathbf{Q}_{k}\right)\right.\\ &\quad\left.+\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\right] \end{aligned}

让函数ff对每个Pg\mathbf{P}_{g}求偏导,并找到这些偏导数的零点。在这些共同零点处,函数ff达到最小值。

在计算函数ff对每个Pg\mathbf{P}_{g}的偏导时,Qk\mathbf{Q}_kNi,p(tk)N_{i,p}(t_k)都是常数,它们对Pg\mathbf{P}_{g}的偏导数是零。因此,我们有:

Pg(QkQk)=0\frac{\partial}{\partial\mathbf{P}_{g}}\left(\mathbf{Q}_{k}\cdot\mathbf{Q}_{k}\right)=0

考虑求和中的第二项,即 Ni,p(tk)PiQkN_{i,p}(t_k) \mathbf{P}_i \mathbf{Q}_k 的总和。每个子项的偏导数计算如下:

Pg(Ni,p(tk)PiQk)=Ni,p(tk)PiPgQk\frac{\partial}{\partial\mathbf{P}_{g}}\left(N_{i,p}(t_{k})\mathbf{P}_{i}\cdot\mathbf{Q}_{k}\right)=N_{i,p}(t_{k})\frac{\partial\mathbf{P}_{i}}{\partial\mathbf{P}_{g}}\cdot\mathbf{Q}_{k}

Pi\mathbf{P}_iPg\mathbf{P}_{g} 的偏导数仅在 i=gi = g 时非零。因此,第二项的偏导数如下:

Pg(i=1h1Ni,p(tk)PiQk)=Ng,p(tk)Qk\frac{\partial}{\partial\mathbf{P}_{g}}\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\cdot\mathbf{Q}_{k}\right)=N_{g,p}(t_{k})\mathbf{Q}_{k}

第三项的偏导数比较复杂,需要用到求导规则 (f.g)=f.g+f.g(f.g)' = f'.g + f.g'

Pg(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)Pi)=(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)PiPg)+(i=1h1Ni,p(tk)PiPg)(i=1h1Ni,p(tk)Pi)=2(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)PiPg)\begin{gathered} {\frac{\partial}{\partial\mathbf{P}_{g}}}\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right) \\ =\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\frac{\partial\mathbf{P}_{i}}{\partial\mathbf{P}_{g}}\right)+\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\frac{\partial\mathbf{P}_{i}}{\partial\mathbf{P}_{g}}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right) \\ =2\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\frac{\partial\mathbf{P}_{i}}{\partial\mathbf{P}_{g}}\right) \end{gathered}

由于Pi\mathbf{P}_iPg\mathbf{P}_{g} 的偏导数在 ii 不等于 gg 时为零,求和中第三项对 Pg\mathbf{P}_{g} 的偏导数是:

Pg(i=1h1Ni,p(tk)Pi)(i=1h1Ni,p(tk)Pi)=2Ng,p(tk)(i=1h1Ni,p(tk)Pi)\frac{\partial}{\partial\mathbf{P}_{g}}\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)\cdot\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)=2N_{g,p}(t_{k})\left(\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}\right)

综上,函数 f()f()Pg\mathbf{P}_{g} 的偏导数是:

fPg=2Ng,p(tk)Qk+2Ng,p(tk)i=1h1Ni,p(tk)Pi\frac{\partial f}{\partial\mathbf{P}_{g}}=-2N_{g,p}(t_{k})\mathbf{Q}_{k}+2N_{g,p}(t_{k})\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}

令其等于零,我们得到:

k=1n1Ng,p(tk)i=1h1Ni,p(tk)Pi=k=1n1Ng,p(tk)Qk\sum_{k=1}^{n-1}N_{g,p}(t_k)\sum_{i=1}^{h-1}N_{i,p}(t_k)\mathbf{P}_i=\sum_{k=1}^{n-1}N_{g,p}(t_k)\mathbf{Q}_k

由于我们有 h1h-1 个变量,gg 的取值从 1 到 h1h-1 ,因此有 h1h-1 个这样的方程。

我们定义以下三个矩阵:

P=[P1P2Ph1]Q=[k=1n1N1,p(tk)Qkk=1n1N2,p(tk)Qkk=1n1Nh1,p(tk)Qk]\left.\mathbf{P}=\left[\begin{array}{c}\mathbf{P}_1\\\mathbf{P}_2\\\vdots\\\mathbf{P}_{h-1}\end{array}\right.\right]\quad\mathbf{Q}=\left[\begin{array}{c}\sum_{k=1}^{n-1}N_{1,p}(t_k)\mathbf{Q}_k\\\sum_{k=1}^{n-1}N_{2,p}(t_k)\mathbf{Q}_k\\\vdots\\\sum_{k=1}^{n-1}N_{h-1,p}(t_k)\mathbf{Q}_k\end{array}\right]

N=[N1,p(t1)N2,p(t1)Nh1,p(t1)N1,p(t2)N2,p(t2)Nh1,p(t2)N1,p(tn1)N2,p(tn1)Nh1,p(tn1)]\mathbf{N}=\left[\begin{array}{cccc}N_{1,p}(t_1)&N_{2,p}(t_1)&\cdots&N_{h-1,p}(t_1)\\N_{1,p}(t_2)&N_{2,p}(t_2)&\cdots&N_{h-1,p}(t_2)\\\vdots&&\ddots&\vdots\\N_{1,p}(t_{n-1})&N_{2,p}(t_{n-1})&\cdots&N_{h-1,p}(t_{n-1})\end{array}\right]

在这里,P\mathbf{P} 的第 kk 行是向量 Pk\mathbf{P}_kQ\mathbf{Q} 的第 kk 行是上述第 kk 个方程的右侧,N\mathbf{N} 的第 kk 行是 N1,p(u),N2,p(u),...,Nh1,p(u)N_{1,p}(u), N_{2,p}(u), ..., N_{h-1,p}(u)tkt_k处的值。因此,如果输入的数据点是 ss 维向量,P\mathbf{P}, N\mathbf{N}Q\mathbf{Q} 分别是 $(h-1)×s, (n-1)×(h-1) $和 (h1)×s(h-1)×s 矩阵。

现在,让我们重写第 gg 个线性方程:

k=1n1Ng,p(tk)i=1h1Ni,p(tk)Pi=k=1n1Ng,p(tk)Qk\sum_{k=1}^{n-1}N_{g,p}(t_{k})\sum_{i=1}^{h-1}N_{i,p}(t_{k})\mathbf{P}_{i}=\sum_{k=1}^{n-1}N_{g,p}(t_{k})\mathbf{Q}_{k}

换一种形式,以便可以看出 Pi\mathbf{P}_i 的系数:

i=1h1(k=1n1Ng,p(tk)Ni,p(tk))Pi=k=1n1Ng,p(tk)Qk\sum_{i=1}^{h-1}\left(\sum_{k=1}^{n-1}N_{g,p}(t_k)N_{i,p}(t_k)\right)\mathbf{P}_i=\sum_{k=1}^{n-1}N_{g,p}(t_k)\mathbf{Q}_k

最后,Pi\mathbf{P}_i 的系数是:

k=1n1Ng,p(tk)Ni,p(tk)\begin{aligned}\sum_{k=1}^{n-1}N_{g,p}(t_k)N_{i,p}(t_k)\end{aligned}

如果观察矩阵 N\mathbf{N},就会发现 Ng,p(t1),Ng,p(t2),...,Ng,p(tn1)N_{g,p}(t_1), N_{g,p}(t_2), ..., N_{g,p}(t_{n-1})N\mathbf{N} 的第 gg 列,而 Ni,p(t1),Ni,p(t2),...,Ni,p(tn1)N_{i,p}(t_1), N_{i,p}(t_2), ..., N_{i,p}(t_{n-1})N\mathbf{N} 的第 ii 列。N\mathbf{N} 的第 gg 列是 N\mathbf{N} 转置矩阵 NT\mathbf{N}^T 的第 gg 行,而 Pi\mathbf{P}_i 的系数是 N\mathbf{N}^T 的第 gg 行和 N\mathbf{N} 的第 ii 列的内积。有了这个观察,线性方程组可以重写为:

(NTN)P=Q\begin{pmatrix}\mathrm{N}^T\mathrm{N}\end{pmatrix}\mathrm{P}=\mathrm{Q}

由于 N\mathbf{N}Q\mathbf{Q} 是已知的,求解关于 P\mathbf{P}的线性方程组就得到了所需的控制点。

算法

Input: n+1n+1个数据点D0\mathbf{D}_0, D1\mathbf{D}_1, …, Dn\mathbf{D}_n,次数pp,和所需的控制点数量h+1h+1
Output: 一条由h+1h+1个控制点定义的次数为pp的B样条曲线,由给定的数据点逼近而成;

算法:

    计算一组参数t0,...,tnt_0, ..., t_n和节点向量UU
    让P0=D0\mathbf{P}_0=\mathbf{D}_0Ph=Dn\mathbf{P}_h=\mathbf{D}_n
    for kk = 11 to n1n-1 do
        通过下式计算Qk\mathbf{Q}_k
            Qk=DkN0,p(tk)D0Nh,p(tk)Dn\mathbf{Q}_{k}=\mathbf{D}_{k}-N_{0,p}(t_{k})\mathbf{D}_{0}-N_{h,p}(t_{k})\mathbf{D}_{n}
    for ii = 11 to h1h-1 do
        计算下式并存储到矩阵Q\mathbf{Q}的第ii行中:
            k=1n1Ni,p(tk)Qk\sum_{k=1}^{n-1}N_{i,p}(t_k)\mathbf{Q}_k
    得到矩阵Q\mathbf{Q}

    for kk = 11 to n1n-1 do
        for ii = 11 to h1h-1 do
            计算Ni,p(tk)N_{i,p}(t_k)并存储到矩阵N\mathbf{N}的第kk行第ii
    得到矩阵N\mathbf{N}
    计算M=NTN\mathbf{M} = \mathbf{N}^T \mathbf{N}
    根据方程MP=Q\mathbf{M} \cdot \mathbf{P} = \mathbf{Q}求解出P\mathbf{P}
    矩阵P\mathbf{P}的第iiPi\mathbf{P}_i就是控制点集
    控制点P0\mathbf{P}_0, …, Ph\mathbf{P}_h,节点向量UU和次数pp已知,可以得到逼近的B样条曲线

控制点数量和次数的影响

显然,数据点数量会影响逼近曲线的形状。那么次数 pp 和控制点数量对曲线形状有什么影响?下图显示了10个数据点(n = 9)在不同次数和控制点数量下的逼近曲线。每一行是次数相同但控制点数量不同的逼近曲线,每一列是控制点数量相同但次数不同的逼近曲线,所有曲线都使用向心参数化计算其参数。

控制点数量=4 控制点数量=5 控制点数量=6 控制点数量=7
次数=2 img img img img
次数=3 img img
次数=4 img img img
次数=5 img

可以看出,通常次数较低的曲线无法很好地逼近数据点连成的多边形,而较高次数的曲线结果更好(更接近数据多边形)。类似地,控制点越多,逼近曲线的灵活性就越高。因此,在每一行中,随着控制点数量的增加,曲线更接近数据点连成的多边形。

那是否应该使用高阶曲线和更多的控制点?答案是不,因为全局逼近只需要比全局插值更少的控制点如果控制点的数量等于数据点的数量,全局逼近就变成了全局插值,我们可以直接使用全局插值!至于次数,只要生成的曲线能够接近数据多边形的形状,我们希望次数尽可能的小。

为什么这种方法是全局的?

这种逼近方法是全局的,因为改变一个数据点的位置会导致整个曲线发生变化。下图中的黄点是给定的数据点,用一条由5个控制点定义的3次B样条曲线(n=7,p=3n = 7, p=3h=4h = 4)来逼近数据点。参数是使用向心参数化计算获得的。假设第四个数据点被移动到蓝色点标记的新位置。蓝色曲线是新生成的插值B样条曲线。

如图所示,除了经过第一个和最后一个数据点外,原始曲线和新曲线的形状截然不同,因此,修改数据点引起的变化是全局的!

img

计算节点向量

得到一组参数后,我们就可以计算节点向量,参数点相当于控制点。假设我们有n+1n+1个参数t0,t1,...,tnt_0, t_1, ..., t_n 和 次数 pp。对于一个次数为pp的B样条曲线,我们需要m+1m+1个节点,mm满足条件m=n+p+1m = n + p + 1。如果B样条曲线是clamped的,那么这些节点的分布为:

u0=u1=...=up=0,up+1,...,ump1,ump=ump+1=...=um=1.u_0 = u_1 = ... = u_p = 0, u_{p+1}, ..., u_{m-p-1}, u_{m-p} = u_{m-p+1} = ... = u_m = 1.

p+1p+1 个和最后 p+1p+1 个节点分别是0和1。其余npn-p个节点可以均匀分布,也可以是其他非均匀分布。假设其余的npn-p个内部节点均匀分布。那么有,up=0,up+1,...,ump1,ump=1u_p = 0, u_{p+1}, ..., u_{m-p-1}, u_{m-p} = 1[0,1][0,1] 分成 np+1n-p+1个子区间。因此,完整的节点为:

u0=u1==up=0uj+p=jnp+1for j=1,2,,npump=ump+1==um=1\begin{aligned}u_{0}&=\quad u_1=\cdots=u_p=0\\u_{j+p}&=\quad\frac j{n-p+1}\quad\mathrm{for~}j=1,2,\ldots,n-p\\u_{m-p}&=\quad u_{m-p+1}=\cdots=u_m=1\end{aligned}

例如,如果我们有6个(n=5n = 5)参数点和已知B样条次数 p=3p = 3,那么我们应该找到 (n+p+1)+1=(5+3+1)+1=10(n + p + 1) + 1 = (5 + 3 + 1) + 1 = 10个节点(即,m=9m = 9)。由于我们使用的是clamped曲线,前四个和最后四个(即,p+1p + 1)节点应该是0和1,节点为0, 0, 0, 0, u4u_4, u5u_5, 1, 1, 1, 1, 两个内部节点u4,u5u_4,u_5[0,1][0,1]分成三个子区间,每个区间的长度为1/3。因此,完整的节点向量为:

{0,0,0,0,13,23,1,1,1,1}.\{0, 0, 0, 0, \frac{1}{3}, \frac{2}{3}, 1, 1, 1, 1\}.

均匀分布的节点向量计算起来非常简单,不过,不推荐使用这种方法,因为如果采用弦长参数化得到参数,并使用均匀分布法计算节点,那么最后得到的线性方程组是奇异的。

另一种计算节点向量的方法是由de Boor提出,即平均参数法,计算公式如下:

u0=u1==up=0uj+p=1pi=jj+p1tifor j=1,2,,npump=ump+1==um=1\begin{aligned}u_{0}&=\quad u_1=\cdots=u_p=0\\u_{j+p}&=\quad\frac1p\sum_{i=j}^{j+p-1}t_i\quad\text{for }j=1,2,\ldots,n-p\\u_{m-p}&=\quad u_{m-p+1}=\cdots=u_m=1\end{aligned}

因此,第一个内部节点是 pp 个参数 t1,t2,...,tpt_1, t_2, ..., t_p 的平均值;第二个内部节点是后面 pp 个参数t2,t3,...,tp+1t_2, t_3, ..., t_{p+1}的平均值。假设我们有6个(n=5n = 5)参数,如下所示:

t0t1t2t3t4t50141323341\begin{array}{|c|c|c|c|c|c|c|} \hline t_0 & t_1 & t_2 & t_3 & t_4 & t_5 \\ \hline 0 & \frac{1}{4} & \frac{1}{3} & \frac{2}{3} & \frac{3}{4} & 1 \\ \hline \end{array}

假设我们要计算一个次数为33的B样条曲线的节点向量。需要10个节点(m=9m = 9)。这10个节点中,前四个和后四个节点分别是0和1。因此,第一个内部节点是参数14,13,23\frac{1}{4}, \frac{1}{3}, \frac{2}{3}的平均值,第二个内部节点是接下来的三个参数13,23,34\frac{1}{3}, \frac{2}{3}, \frac{3}{4}的平均值。得到节点向量是:

u0=u1=...=u2=u3u4u5u6=u7=...=u8=u9014+13+23/3=51213+23+34/3=7121\begin{array}{|c|c|c|c|c|} \hline u_0 = u_1 = ... = u_2 = u_3 & u_4 & u_5 & u_6 = u_7 = ... = u_8 = u_9 \\ \hline 0 & \frac{1}{4} + \frac{1}{3} + \frac{2}{3} / 3 = \frac{5}{12} & \frac{1}{3} + \frac{2}{3} + \frac{3}{4} / 3 = \frac{7}{12} & 1 \\ \hline \end{array}

下图说明了参数的位置和得到的节点位置。注意0(4)0(4)1(4)1(4)表示0011是四重节点(即重数=4)。可以看到,第一个非零节点区间[0,512)[0, \frac{5}{12})包含两个参数,第二个非零节点区间 [512,712)[\frac{5}{12}, \frac{7}{12}) 不包含参数,第三个非零节点区间 [712,1)[\frac{7}{12}, 1) 包含两个参数。

img

全局曲面插值

假设我们有m+1m+1n+1n+1列个数据点Dij\mathbf{D}_{ij}0im0 \leq i \leq m0jn0 \leq j \leq n),希望找到一个包含所有这些点的次数为(p,q)(p, q)的B样条曲面。与曲线类似,我们已知数据点和次数 ppqq ,要定义一个B样条插值曲面,还需要两个方向的节点向量U\mathbf{U}V\mathbf{V},以及一组控制点。控制点的数量要和数据点的数量相等(即有(m+1)×(n+1)(m+1) \times (n+1)个控制点)。

之前在 曲面参数化和节点向量计算 中讨论过,我们可以计算出 uuvv 两个方向的参数scs_c0cm0 \leq c \leq m)和tdt_d0dn0 \leq d \leq n),同时还可以计算出 uuvv 方向的节点向量U\mathbf{U}V\mathbf{V}。因此,剩下的就是要找到所需的控制点!

全局曲面插值:给定一个由(m+1)×(n+1)(m+1) \times (n+1)个数据点组成的网格 Dij\mathbf{D}_{ij}0im0 \leq i \leq m0jn0 \leq j \leq n)和次数 (p,q)(p, q),找到一个由 (m+1)×(n+1)(m+1) \times (n+1) 个控制点定义的次数为 (p,q)(p, q) 的B样条曲面,该曲面包含所有的数据点。

解决方案

设B样条曲面定义如下:

S(u,v)=i=0mj=0nNi,p(u)Nj,q(v)PijS(u,v) = \sum_{i=0}^{m} \sum_{j=0}^{n} N_{i,p}(u)N_{j,q}(v)P_{ij}

由于它经过所有数据点,并且参数 scs_ctdt_d 对应于数据点Dcd\mathbf{D}_{cd},将u=scu = s_cv=tdv = t_d代入曲面方程得到:

Dcd=S(sc,td)=i=0mj=0nNi,p(sc)Nj,q(td)PijD_{cd} = S(s_c, t_d) = \sum_{i=0}^{m} \sum_{j=0}^{n} N_{i,p}(s_c)N_{j,q}(t_d)P_{ij}

由于Ni,p(sc)N_{i,p}(s_c) 与索引 jj 无关,因此可以将它从对 jj 的求和中提出来:

Dcd=S(sc,td)=i=0mj=0nNi,p(sc)Nj,q(td)Pij=i=0mNi,p(sc)(j=0nNj,q(td)Pij)\begin{aligned}\mathbf{D}_{cd}&=\mathbf{S}(s_{c},t_{d})=\sum_{i=0}^{m}\sum_{j=0}^{n}N_{i,p}(s_{c})N_{j,q}(t_{d})\mathbf{P}_{ij}\\&=\sum_{i=0}^{m}N_{i,p}(s_{c})\left(\sum_{j=0}^{n}N_{j,q}(t_{d})\mathbf{P}_{ij}\right)\end{aligned}

我们发现索引 ii 只出现在Pij\mathbf{P}_{ij}中。因此,我们可以将括号中的表达式定义成一个新项,如下所示:

Qid=j=0nNj,q(td)Pij\mathbf{Q}_{id}=\sum_{j=0}^nN_{j,q}(t_d)\mathbf{P}_{ij}

准确地说,如果 ii 为某一固定值,Qid\mathbf{Q}_{id} 就是 qq 次B样条曲线(等式右侧为 qq 次B样条曲线的定义)在 tdt_d 处的值,该曲线由 P\mathbf{P}的第 ii 行上的n+1n+1未知控制点(即,Pi0\mathbf{P}_{i0}, Pi1\mathbf{P}_{i1}, …, Pin\mathbf{P}_{in})定义 。将Qid\mathbf{Q}_{id} 代入上面的Dcd\mathbf{D}_{cd}方程得到:

Dcd=i=0mNi,p(sc)QidD_{cd} = \sum_{i=0}^{m} N_{i,p}(s_c) Q_{id}

因此,数据点Dcd\mathbf{D}_{cd}pp 次B样条曲线(等式右侧为pp次B样条曲线的定义)在scs_c处求值的结果,该曲线由Q\mathbf{Q}的第dd列的m+1m+1未知控制点(即,Q0d\mathbf{Q}_{0d}, Q1d\mathbf{Q}_{1d}, …, Qmd\mathbf{Q}_{md})定义。

对每个cc0cm0 \leq c \leq m)重复上述步骤,数据点的第dd列(即,D0d\mathbf{D}_{0d}, D1d\mathbf{D}_{1d}, …, Dmd\mathbf{D}_{md})是根据Q\mathbf{Q}的第dd列(即,Q0d\mathbf{Q}_{0d}, Q1d\mathbf{Q}_{1d}, …, Qmd\mathbf{Q}_{md})和参数s0,s1,...,sms_0, s_1, ..., s_m 计算获得。由于数据点D0d\mathbf{D}_{0d}, D1d\mathbf{D}_{1d}, …, Dmd\mathbf{D}_{md},次数 pp 和参数s0,s1,...,sms_0, s_1, ..., s_m是已知的,问题总结如下:

给定次数pp,参数s0,s1,...,sms_0, s_1, ..., s_m和 第dd列数据点D0d\mathbf{D}_{0d}, D1d\mathbf{D}_{1d}, …, Dmd\mathbf{D}_{md},求第 dd 列的控制点 Q0d\mathbf{Q}_{0d}, Q1d\mathbf{Q}_{1d}, …, Qmd\mathbf{Q}_{md}

因此,这其实就是一个曲线插值问题!全局曲线插值方法可以应用到数据点的每一列,从而求得每一列控制点Qcd\mathbf{Q}_{cd}。我们有n+1n+1列数据点,我们将得带n+1n+1列控制点Q\mathbf{Q}

现在,我们使用相同的思路,应用到Qid\mathbf{Q}_{id}的等式中,该等式如下:

Qid=j=0nNj,q(td)Pij\mathbf{Q}_{id}=\sum_{j=0}^{n}N_{j,q}(t_{d})\mathbf{P}_{ij}

在这个等式中,Q\mathbf{Q}的第ii行数据点(即Qi0\mathbf{Q}_{i0}, Qi1\mathbf{Q}_{i1}, …, Qin\mathbf{Q}_{in})是由n+1n+1个未知控制点Pi0\mathbf{P}_{i0}, Pi1\mathbf{P}_{i1}, …, Pin\mathbf{P}_{in}定义的qq次B样条曲线 在参数 t0,t1,...,tnt_0, t_1, ..., t_n处求值得到的点。因此,已知次数 qq 和参数 t0,t1,...,tnt_0, t_1, ..., t_nQ\mathbf{Q}的第ii行进行曲线插值可以得到 第ii行 的控制点。

一旦找到所有行的控制点,这些控制点连同两个节点向量和次数ppqq就定义了一个插值B样条曲面。因此,使用B样条的曲面插值等价于 (m+1m+1)+(n+1n+1)次曲线插值!

算法

Input: (m+1)×(n+1)(m+1) \times (n+1) 个数据点 Dij\mathbf{D}_{ij} 和次数 (p,qp, q);
Output: 一个经过所有数据点的次数为 (p,qp, q) 的B样条曲面;

    计算uu方向的参数s0,s1,...,sms_0, s_1, ..., s_m和节点向量UU
    计算vv方向的参数t0,t1,...,tnt_0, t_1, ..., t_n和节点向量VV
    for dd = 00 to nn do // 第dd
        begin // 计算第dd列控制点Qd\mathbf{Q}_d
            已知次数pp、参数s0,s1,...,sms_0, s_1, ..., s_m和节点向量U\mathbf{U},对D\mathbf{D}的第dd列数据点应用曲线插值
            (即D0d,D1d,...,Dmd\mathbf{D}_{0d}, \mathbf{D}_{1d}, ..., \mathbf{D}_{md}
            结果为第dd列控制点Q0d,Q1d,...,Qmd\mathbf{Q}_{0d}, \mathbf{Q}_{1d}, ..., \mathbf{Q}_{md}
        end

    for cc = 00 to mm do // 第cc
        begin // 计算第cc行控制点Pc\mathbf{P}_c
            已知次数qq、参数t0,t1,...,tnt_0, t_1, ..., t_n和节点向量V\mathbf{V},对Q\mathbf{Q}的第cc行应用曲线插值
            (即Qc0,Qc1,...,Qcn\mathbf{Q}_{c0}, \mathbf{Q}_{c1}, ..., \mathbf{Q}_{cn}
            结果为第cc行控制点Pc0,Pc1,...,Pcn\mathbf{P}_{c0}, \mathbf{P}_{c1}, ..., \mathbf{P}_{cn}
        end

得到 (m+1)×(n+1)(m+1) \times (n+1) 个控制点 Pij\mathbf{P}_{ij},结合次数 (p,q)(p, q) 和节点向量 U\mathbf{U}V\mathbf{V} ,可以定义一个插值B样条曲面,经过给定的数据点。

第一个 for 循环中使用的矩阵 N\mathbf{N}在循环中计算时不会发生变换,如果我们按照上面的算法步骤计算,最终会求解方程组 D=NQ\mathbf{D}=\mathbf{N}\mathbf{Q} n+1n+1次。因此, 为了加速计算,在第一个 for 循环开始之前,应该先计算 N\mathbf{N}LU分解,这样每次迭代中的插值都是简单的前向代换和后向代换。类似地,第二个 for 循环中的矩阵 N\mathbf{N},它的LU分解 也应该在第二个 for 开始之前计算。如果不这样做,我们将执行 (m+1+n+1=m+n+2)(m+1 + n+1 = m + n + 2) LU分解,而只需要2次就足够了。

为什么这种方法是全局的?

因为插值曲面是通过 m+n+2m + n + 2 次全局曲线插值得到的,由于曲线的插值是全局的,因此这种曲面插值技术也是全局的
下图显示了移动一个数据点对插值曲面的影响。这个B样条曲面是通过6行5列(m=5n=4)(m=5,n=4) 数据点插值生成,曲面次数为(3,3)。第一行的图像显示了节点的等参数曲线,黄圈标记了要移动的数据点。
第二行显示了对应的曲面。显然,改变一个数据点的位置后,蓝色节点曲线的形状发生了剧烈变化,并且向邻近的数据点靠近。影响也会向右侧的节点传播。
从曲面的图中可以看出。数据点移动到右侧后,在左端会产生了一个大的凸起。前侧的边界曲线的形状也稍微发生了改变。

显然,改变数据点的位置会影响到整个曲面,这是一种全局的方法。

移动前 移动后
节点曲线 img img
曲面 img img

全局曲面逼近

我们希望找到一个B样条曲面来逼近(m+1)×(n+1)(m+1) \times (n+1)个数据点,逼近曲面并不需要经过所有给定的数据点,与曲线逼近一样,我们可以控制逼近曲面的次数和控制点的数量。因此,除了数据点已知以外,还需要确定uu方向和vv方向的次数 ppqq ,以及控制点数量,(假设为e+1e+1f+1f+1列)。对于B样条曲面,输入值必须满足 m>ep1m > e \geq p \geq 1n>fq1n > f \geq q \geq 1

全局曲面逼近:给定(m+1)×(n+1)(m+1) \times (n+1)个数据点组成的网格 Dij(0im,0jn)\mathbf{D}_{ij} (0 \leq i \leq m ,0 \leq j \leq n),找到一个由(e+1)×(f+1)(e+1) \times (f+1)个控制点 Pij(0ie,0jf)\mathbf{P}_{ij} (0 \leq i \leq e , 0 \leq j \leq f) 定义的次数为 (p,q)(p,q) 的B样条曲面,该曲面能够逼近给定的数据点网格。

解决方案

设由(e+1)×(f+1)(e+1) \times (f+1)个控制点 Pij\mathbf{P}_{ij} 定义的次数为 (p,q)(p,q) 的B样条曲面定义如下:

S(u,v)=i=0ej=0fNi,p(u)Nj,q(v)PijS(u,v) = \sum_{i=0}^{e} \sum_{j=0}^{f} N_{i,p}(u)N_{j,q}(v)P_{ij}

由于有m+1m+1n+1n+1列数据点,因此我们需要m+1m+1uu方向的参数,s0,s1,...,sms_0, s_1, ..., s_m,和n+1n+1vv方向的参数,t0,t1,...,tnt_0, t_1, ..., t_n。这些参数的计算在 曲面的参数和节点向量 的讨论中给出了计算方法。

有了这些参数,曲面上与数据点 Dcd\mathbf{D}_{cd} 对应的点的计算方式如下:

S(sc,td)=i=0ej=0fNi,p(sc)Nj,q(td)PijS(s_c, t_d) = \sum_{i=0}^{e} \sum_{j=0}^{f} N_{i,p}(s_c)N_{j,q}(t_d)\mathbf{P}_{ij}

Dcd\mathbf{D}_{cd} 与曲面上对应的点的误差距离平方为:

DcdS(sc,td)2\left|\mathrm{D}_{cd}-\mathrm{S}(s_{c},t_{d})\right|^{2}

因此,所有误差距离平方的总和为:

f(P00,P01,...,Pef)=c=0md=0nDcdS(sc,td)2f(P_{00}, P_{01}, ..., P_{ef}) = \sum_{c=0}^{m} \sum_{d=0}^{n} |\mathbf{D}_{cd} - S(s_c, t_d)|^2

这是关于(e+1)×(f+1)(e+1) \times (f+1)个未知控制点 Pij\mathbf{P}_{ij} 的函数。为了求f()f()的最小值,我们计算f()f()的偏导数并令其等于0:

fPij=0\frac{\partial f}{\partial\mathbf{P}_{ij}}=0

然后,我们可以得到(e+1)×(f+1)(e+1) \times (f+1)个方程,它们的共同零点就是所求的控制点。然而这些方程不是线性的,解决非线性方程组非常耗时,与其追求最优解,不如寻找一个合理的、但不是函数 f() 最小值的解。

为了找到一个非最优解,我们采用与全局曲面插值中类似的方法。对每个数据点列应用曲线逼近以计算一些中间数据点

这样,每列 m+1m+1 个数据点就能计算出e+1e+1个“中间”数据点。由于有 n+1n+1 列,这些“中间”数据点构成了一个(e+1)×(n+1)(e+1) \times (n+1)的网格。然后,对这些中间数据点的每一行应用曲线逼近 计算出所需的控制点。由于每一行有 n+1n+1 个“中间”数据点,并且有e+1e+1 行,因此每行都会产生 f+1f+1 个控制点,最终我们将得到 (e+1)×(f+1)(e+1) \times (f+1) 个控制点。

算法

Input: (m+1)×(n+1)(m+1) \times (n+1)个数据点,记作Dij\mathbf{D}_{ij},次数(p,q)(p,q),以及控制点的数量(e+1)×(f+1)(e+1) \times (f+1)
Output: 逼近的(p,q)(p, q)次B样条曲面;

    计算uu方向的参数s0,s1,...,sms_0, s_1, ..., s_m和节点向量U\mathbf{U}
    计算vv方向的参数t0,t1,...,tnt_0, t_1, ..., t_n和节点向量V\mathbf{V}
    for dd = 00 to nn do // D\mathbf{D}的第dd
        begin // 计算"中间数据点"Q\mathbf{Q}
            已知次数pp、参数s0,s1,...,sms_0, s_1, ..., s_m和节点向量U\mathbf{U},对给定数据点的第dd列应用曲线逼近(即D0d\mathbf{D}_{0d}, D1d\mathbf{D}_{1d}, …, Dmd\mathbf{D}_{md})。得到第dd列"中间数据点"Q0d,Q1d,...,Qed\mathbf{Q}_{0d}, \mathbf{Q}_{1d}, ..., \mathbf{Q}_{ed}
            // Q\mathbf{Q}是一个(e+1)×(n+1)(e+1) \times (n+1)矩阵
        end

    for cc = 00 to ee do // Q\mathbf{Q}的第cc
        begin // 计算所需的控制点P\mathbf{P}
            已知次数qq、参数t0,t1,...,tnt_0, t_1, ..., t_n和节点向量V\mathbf{V},对Q\mathbf{Q}的第cc行应用曲线逼近(即Qc0\mathbf{Q}_{c0}, Qc1\mathbf{Q}_{c1}, …, Qcn\mathbf{Q}_{cn}
            得到第cc行控制点Pc0,Pc1,...,Pcf\mathbf{P}_{c0},\mathbf{P}_{c1}, ..., \mathbf{P}_{cf}
            // P\mathbf{P}是一个(e+1)×(f+1)(e+1) \times (f+1)矩阵
        end

计算得到(e+1)×(f+1)(e+1) \times (f+1)个控制点,记作Pij\mathbf{P}_{ij},次数(p,q)(p, q)和节点向量U\mathbf{U}V\mathbf{V}已知,因此可以得到逼近的B样条曲面。

在这个算法中,先对D\mathbf{D}的列进行了n+1n+1次曲线逼近,然后对“中间”数据点的行进行了e+1e+1次曲线逼近。因此,一共执行了n+e+2n + e + 2次曲线逼近。另外,我们也可以对数据点的每一行进行 m+1m+1 次曲线逼近,创建(m+1)×(f+1)(m+1) \times (f+1)个“中间”数据点。然后,对这些“中间”数据点的每一列进行 f+1f+1 次曲线逼近,最终得到 (e+1)×(f+1)(e+1) \times (f+1)个控制点。通过这种方式,一共执行了m+f+2m + f + 2 次曲线逼近。

由于这种算法并没有使得误差度量函数 f()f() 达到最小值,因此它不是最优解,但对于许多应用场景来说,已经满足需要。

注意,上面的算法中,第一个for循环中使用的矩阵 NTN\mathbf{N}^T·\mathbf{N} 不会改变,如果我们按照上述步骤直接计算,最终将会求解方程Q=(NTN)P\mathbf{Q} =(\mathbf{N}^T·\mathbf{N})\mathbf{P} n+1n+1 次。这明显有重复的计算。为了加速计算,在第一个for循环开始之前,应该对NTN\mathbf{N}^T·\mathbf{N} 进行 LU\mathbf{L}\mathbf{U} 分解,在每次循环中,只会计算一个前向代换和后向代换。类似地,第二个for循环会使用一个新的矩阵NTN\mathbf{N}^T·\mathbf{N}。也应该在第二个for开始之前对它进行LU\mathbf{L}\mathbf{U} 分解。如果不这样,将会执行(e+1+(n+1))=e+n+2(e+1 + (n+1)) = e + n + 2LU\mathbf{L}\mathbf{U} 分解,而实际上只需要2次就足够了。

插值和逼近比较

下面是一个6行5列数据点定义的网格。我们用它来说明插值方法和逼近方法之间的区别。第一列的图像是移动黄色数据点之前的结果,第二列是移动了黄色数据点之后的结果。

第一行和第二行显示了使用弦长参数化确定参数的全局插值结果。插值曲面的次数为(3,3)。红色和蓝色曲线是节点处的等参数曲线。
虽然数据点只是稍微移动了位置,但蓝色节点曲线的形状发生了剧烈变化。两个插值曲面之间的差异更大。将数据点向右移动会在曲面左端产生了一个大的凸起。

最后两行显示了全局逼近的结果。uuvv方向的控制点数量分别为5和4。显然,改变数据点位置对逼近曲面形状的影响没有那么明显。

结果表明,逼近曲面对数据点位置的改变不那么敏感,并且逼近曲面比插值曲面更好地贴近数据点网格的形状。

移动前 移动后
插值曲线 img img
插值曲面 img img
逼近曲线 img img
逼近曲面 img img