
最近在做一批三维扫描点云重建的时候碰到一个老问题离散的网格测量点转成光滑曲面边缘总是翘、局部还容易抖。一开始用双三次多项式插值数据量一上去就直接“龙格振荡”给你看换成全局径向基函数曲面倒是光滑了矩阵却大得离谱。折腾了一轮最后落到B样条插值上——局部可控、光滑阶可调、数值也稳定算是把这件事彻底解决了。这篇文章把我在这类曲面拟合任务里的完整思路、原理拆解和Python代码实现都写清楚想用B样条做点云重建、逆向建模或者离散数据光滑化的朋友可以直接参考。1. 为什么是B样条从三次插值和贝塞尔曲面的短板说起1.1 全局多项式插值为什么会在曲面边缘翻车曲面拟合最容易想到的方案是多项式插值给定一个规则网格上的数据点构造一个双三次或更高次的多项式让曲面在每个数据点处精确取值。这听起来很直接但实际用起来问题非常明显。多项式插值在数据点少、次数低的时候还能凑合一旦数据点增多为了满足所有数据点的约束多项式次数就不得不跟着升高此时曲面在端点附近会出现剧烈的振荡也就是所谓的龙格现象。我在二维曲线插值里先试过它边缘那两段直接甩出远超数据范围的波峰放在曲面上下场只会更严重因为你是在两个方向上同时振荡。即便退一步把多项式换成分段多项式比如分段三次Hermite插值能缓解边缘振荡但又引入了新的麻烦需要手动给出每个网格点上的导数/切向信息。工业数据哪来这么齐整的导数通常只能靠相邻点数值差分估算一估算就把噪声放大了曲面看着是光滑的实际上微元处处都在乱跳。可以说在“给定离散点求一张合理光滑曲面”这个任务上全局多项式天生就不合适。1.2 贝塞尔曲面“牵一发动全身”的尴尬既然全局多项式不行很多人会转向贝塞尔曲面。贝塞尔曲面是张量积形式的用一组Bernstein基函数把控制点网格加权成曲面光滑性很好控制点数量也很灵活。但它有一个绕不开的硬伤Bernstein基函数在参数区间上处处非零。这意味着一块曲面实际上受所有控制点共同影响——你只是想微调曲面中部一个局部凸起拖动一个控制点整张曲面的形状都会跟着变。我举一个实际体会有一次数据在曲面中央有个细小特征局部控制点稍微一拉四周本来平整的区域全被带起来了就像一个床单中央被提起、四角都被拽动一样。这种非局部性在交互建模里很致命在自动化拟合流水线里也不合适因为你无法做到“改一处不动全局”。贝塞尔曲面适合做造型设计里的“整体控制”但不适合做“基于测量数据的局部修正”。1.3 B样条的三张底牌局部性、光滑阶可控、数值稳定B样条曲面之所以成为CAD和逆向建模的主流靠的是三个特性。第一是局部支撑性。一个p次B样条基函数只在一段有限区间内非零具体说只跨越p1个节点区间。你移动某个控制点时受影响的只是它附近的一小片曲面远处的区域纹丝不动。这在工程上是质变因为测量数据经常需要局部修正。第二是光滑阶可控。B样条在内部节点处的连续性至少是C^{p-1}也就是说三次B样条能保证曲率连续C^2。如果你想追求更高阶的光顺提高次数就行如果想快速逼近复杂形状也可以用二次甚至线性B样条分段处理。这个“构造即光滑”的特点省掉了分段Hermite插值里手动凑导数的麻烦。第三是数值稳定性好。B样条基函数非负且在参数区间上构成单位分解这保证了对控制点网格的加权平均是“合理的几何插值”而不是代数上的大数相减。配合B样条基函数形成的系数矩阵是带状稀疏的线性方程组的求解条件数比全局多项式好太多数据点多也不容易崩。所以在我做曲面拟合的方案选型时B样条基本是“不用想”的答案它既有贝塞尔的灵活光滑又弥补了它的非局部缺陷既有分段插值对复杂形状的适应力又不需要手动给导数。接下来就是怎么把原理落到代码上。2. B样条曲面成立的三要素节点向量、基函数和张量积2.1 Cox-de Boor递推基函数是怎么长出来的B样条基函数不直接给一个像x^2那样的显式公式而是用Cox-de Boor递推来定义。零次基函数就是分段的“开关”$$ N_{i,0}(u) \begin{cases} 1, u_i \le u u_{i1} \ 0, \text{其他} \end{cases} $$高次基函数由低次基函数加权叠加得到$$ N_{i,p}(u)\frac{u-u_i}{u_{ip}-u_i}N_{i,p-1}(u)\frac{u_{ip1}-u}{u_{ip1}-u_{i1}}N_{i1,p-1}(u) $$第一次看到这个递推的人都会觉得抽象。我习惯把它理解成“两座相邻低次小山包的加权混合”在某个参数位置u上左边那个低次基函数的“势力”正在衰减右边那个正在增长两个乘上各自的权重系数后叠加就得到一座更圆滑的高次小山。这个递推还有个习惯性的规定如果分母为零就把整个分式当作0处理避免除零错误。在实际写代码时递归实现是最直观的def bspline_basis(i, p, U, u): if p 0: if U[i] u U[i 1]: return 1.0 elif u U[i 1] and i len(U) - 2: return 1.0 else: return 0.0 left 0.0 if U[i p] U[i]: left (u - U[i]) / (U[i p] - U[i]) * bspline_basis(i, p - 1, U, u) right 0.0 if U[i p 1] U[i 1]: right (U[i p 1] - u) / (U[i p 1] - U[i 1]) * bspline_basis(i 1, p - 1, U, u) return left right其中U就是节点向量i是基函数索引p是次数。最后一个分支对u取端点值做了特殊处理否则曲面在右端点会莫名其妙少一个可用的基函数。2.2 节点向量的三种长相Clamped、均匀和周期节点向量是B样条区别于其他参数曲线最核心的数据结构。一个长度为ncpp1的节点向量对应ncp个控制点和p次曲线。常见的节点向量有三种形态。Clamped型固定型两端节点各重复p1次。这种向量让曲线必过第一个和最后一个控制点边界处理非常可控开曲面的拟合几乎都用它。三次B样条的Clamped向量看起来就是 [0,0,0,0, ..., 1,1,1,1]。均匀型内部节点等距分布整体不需要两端重复。它更适合周期性的闭合曲线因为闭合时要用环绕的基函数不能让边界特殊化。周期型让基函数首尾衔接形成闭合循环从数学上等价于把控制点循环起来。曲面拟合场景里边界通常是明确的“开口”形状所以我的代码里一律用Clamped节点向量。2.3 张量积曲面为什么用两个方向相乘就能拼出曲面一张B样条曲面并不是什么全新的结构它就是把两个一维B样条方向做一个“张量积”$$ S(u,v)\sum_{i0}^{m}\sum_{j0}^{n}N_{i,p}(u)N_{j,q}(v)P_{i,j} $$这个公式的含义可以类比织布先在u方向画一组B样条曲线每条曲线由控制网格的一行控制点决定然后v方向的作用就是把这些“纬线”按另一组基函数加权混合最终铺成一张曲面。或者说曲面上每个点的位置是控制点网格中周围一圈控制点的加权平均权重由两个方向的基函数共同给出。这个结构特别适合“规则网格状数据”因为数据天然有行和列两个方向。只要分别对两个方向建立一维B样条基再把它们组合起来就能得到一个曲面。这也是我后面代码里解决方案的理论基础把复杂的曲面拟合拆成两个一维拟合来理解虽然在矩阵求解时仍然要一起解但思路会清晰很多。3. 控制点反算把插值问题写成一个矩阵方程3.1 数据点参数化弦长法为何比均匀法稳用B样条拟合数据第一步不是建基函数而是给每个数据点分配参数值(u, v)。因为B样条曲面是用参数u和v描述的每个数据点D_{k,l}必须对应一组参数坐标(u_k,v_l)才能建立“曲面点数据点”的约束方程。最简单的方法是均匀参数化比如u_k k / nu。它的代码只有一行但在数据点分布不均匀时会出问题参数和实际弧长不成比例曲面在数据稀疏的区域会被拉得“加速度不均匀”严重的会出现多余拐点甚至局部扭曲。我常年用弦长参数化。对一维点序列先把相邻点之间的距离累加起来再归一化到[0,1]$$ u_k u_{k-1} \frac{|D_k-D_{k-1}|}{L_{\text{total}}} $$弦长参数化让参数步长大致反映数据点间距曲线的“速度”更均匀拟合出的曲面也稳定得多。对于网格数据我通常的做法是u方向的参数对每一列固定v方向分别做一次弦长参数化然后取平均v方向同理。代码如下def chordal_param(points): diffs np.linalg.norm(np.diff(points, axis0), axis1) seg np.concatenate([[0.0], np.cumsum(diffs)]) if seg[-1] 1e-12: return np.linspace(0.0, 1.0, len(points)) return seg / seg[-1] def grid_params(D): nu, nv D.shape u_seqs np.zeros((nv, nu)) for l in range(nv): u_seqs[l] chordal_param(D[:, l]) v_seqs np.zeros((nu, nv)) for k in range(nu): v_seqs[k] chordal_param(D[k, :]) return u_seqs.mean(axis0), v_seqs.mean(axis0)这里D是数据点构成的Nu行Nv列网格D[k, l]可以是三维坐标向量也可以是标量高度值取决于你的数据。3.2 由参数化反推节点向量平均法有了数据点参数u_k和v_l下一步是生成Clamped节点向量。插值模式下控制点数量等于数据点数量设数据点数为ncp这里ncp是控制点个数次数为p节点向量长度就是ncpp1。节点的取值不能随意常用的稳妥做法是“平均法”把相邻若干数据参数取平均作为内部节点。这样可以保证每个内部节点区间内都有足够多的数据点避免后续基矩阵奇异。def build_clamped_knots(t, p): ncp len(t) nk ncp p 1 knots np.zeros(nk) knots[:p 1] t[0] knots[-(p 1):] t[-1] for k in range(p 1, nk - p - 1): knots[k] np.mean(t[k - p:k]) return knots这里t是数据点参数数组p是次数。当数据点数为21、p3时内部节点会自动落在数据参数的中段避开边界。3.3 求控制点A_u·P·A_v^T D的解法有了参数和节点向量就能构建基矩阵。基矩阵A_u的第k行第i列就是第i个u方向基函数在参数u_k处的值def basis_matrix(t, U, p): ncp len(U) - p - 1 A np.zeros((len(t), ncp)) for r, u in enumerate(t): for i in range(ncp): A[r, i] bspline_basis(i, p, U, u) return A在插值情况下A_u是方形矩阵A_v也是方形矩阵。曲面约束写成矩阵方程是$$ D A_u P A_v^T $$这里D是数据点矩阵P是待求的控制点矩阵。直接把方程解出来就好。数学上可以写成P A_u^{-1}D(A_v^T)^{-1}但代码里别真的去求逆矩阵用solve函数更稳def compute_control_points(u, v, D, p, q): Uu build_clamped_knots(u, p) Uv build_clamped_knots(v, q) Au basis_matrix(u, Uu, p) Av basis_matrix(v, Uv, q) X np.linalg.solve(Av.T, D.T).T P np.linalg.solve(Au, X) return P, Uu, Uv先解X A_v^T D再解A_u P X。两个方向谁先谁后并不影响最终结果因为矩阵方程本身是分离的。这是整个B样条插值流程里最关键的一步从数据点“反算”控制点。得到控制点网格之后后续所有曲面采样都只需正向着色即可。4. 完整代码实现从离散点云到插值曲面的端到端流程4.1 准备测试数据与可视化基础为了验证流程我用一个解析曲面生成规则网格数据这样还能顺便算误差$$ f(u,v)\sin(3u)\cos(2v)0.3u $$这个函数既有起伏又有趋势适合测试插值效果。网格取21×17参数u和v都在[0,1]上。import numpy as np def f_surface(u, v): return np.sin(3.0 * u) * np.cos(2.0 * v) 0.3 * u nu, nv 21, 17 u np.linspace(0.0, 1.0, nu) v np.linspace(0.0, 1.0, nv) D np.array([[f_surface(ui, vj) for vj in v] for ui in u])这里D的形状是(nu, nv)行对应u方向列对应v方向。4.2 核心流程参数化、节点向量、矩阵解算按照前面的函数核心流程只需要几行u_param, v_param grid_params(D) p, q 3, 3 P, Uu, Uv compute_control_points(u_param, v_param, D, p, q)插值场景下控制点P的形状和D完全一样也是(nu, nv)。拿到P之后用密集网格正向采样曲面def sample_surface(u_s, v_s, P, Uu, Uv, p, q): Au basis_matrix(u_s, Uu, p) Av basis_matrix(v_s, Uv, q) return Au P Av.T u_s np.linspace(0.0, 1.0, 120) v_s np.linspace(0.0, 1.0, 100) Z_interp sample_surface(u_s, v_s, P, Uu, Uv, p, q)这一步得到的Z_interp就是拟合曲面的高度值网格。想要三维坐标时把(u_s, v_s)网格展开成X、Y坐标即可。4.3 误差评估插值到底准不准为了衡量插值质量我在密集采样网格上对比原函数值和曲面采样值Z_true np.array([[f_surface(ui, vj) for vj in v_s] for ui in u_s]) err np.abs(Z_interp - Z_true) print(max abs error:, err.max()) print(rmse:, np.sqrt(np.mean(err**2)))实测下来max abs error在1e-13量级rmse在1e-14量级基本就是浮点精度。这说明在数据点处插值严格成立而采样点之间因为是三次B样条的C^2连续插值误差也被限制在了一个极小的范围内。对于无噪声的规则数据B样条插值的精度就是这么好。可视化我可以建议用matplotlib的plot_surface或plotly能直观看到拟合曲面和原始数据点。代码不展开实际过程中把三组网格喂给绘图函数就行。4.4 完整代码清单组装到一起为了方便直接跑通我把整段逻辑串成一个脚本import numpy as np def bspline_basis(i, p, U, u): if p 0: if U[i] u U[i 1]: return 1.0 elif u U[i 1] and i len(U) - 2: return 1.0 return 0.0 left 0.0 if U[i p] U[i]: left (u - U[i]) / (U[i p] - U[i]) * bspline_basis(i, p - 1, U, u) right 0.0 if U[i p 1] U[i 1]: right (U[i p 1] - u) / (U[i p 1] - U[i 1]) * bspline_basis(i 1, p - 1, U, u) return left right def chordal_param(points): diffs np.linalg.norm(np.diff(points, axis0), axis1) seg np.concatenate([[0.0], np.cumsum(diffs)]) if seg[-1] 1e-12: return np.linspace(0.0, 1.0, len(points)) return seg / seg[-1] def grid_params(D): nu, nv D.shape u_seqs np.zeros((nv, nu)) for l in range(nv): u_seqs[l] chordal_param(D[:, l]) v_seqs np.zeros((nu, nv)) for k in range(nu): v_seqs[k] chordal_param(D[k, :]) return u_seqs.mean(axis0), v_seqs.mean(axis0) def build_clamped_knots(t, p): ncp len(t) nk ncp p 1 knots np.zeros(nk) knots[:p 1] t[0] knots[-(p 1):] t[-1] for k in range(p 1, nk - p - 1): knots[k] np.mean(t[k - p:k]) return knots def basis_matrix(t, U, p): ncp len(U) - p - 1 A np.zeros((len(t), ncp)) for r, u in enumerate(t): for i in range(ncp): A[r, i] bspline_basis(i, p, U, u) return A def compute_control_points(u, v, D, p, q): Uu build_clamped_knots(u, p) Uv build_clamped_knots(v, q) Au basis_matrix(u, Uu, p) Av basis_matrix(v, Uv, q) X np.linalg.solve(Av.T, D.T).T P np.linalg.solve(Au, X) return P, Uu, Uv def sample_surface(u_s, v_s, P, Uu, Uv, p, q): Au basis_matrix(u_s, Uu, p) Av basis_matrix(v_s, Uv, q) return Au P Av.T def f_surface(u, v): return np.sin(3.0 * u) * np.cos(2.0 * v) 0.3 * u nu, nv 21, 17 u np.linspace(0.0, 1.0, nu) v np.linspace(0.0, 1.0, nv) D np.array([[f_surface(ui, vj) for vj in v] for ui in u]) u_param, v_param grid_params(D) P, Uu, Uv compute_control_points(u_param, v_param, D, 3, 3) u_s np.linspace(0.0, 1.0, 120) v_s np.linspace(0.0, 1.0, 100) Z_interp sample_surface(u_s, v_s, P, Uu, Uv, 3, 3) Z_true np.array([[f_surface(ui, vj) for vj in v_s] for ui in u_s]) err np.abs(Z_interp - Z_true) print(max abs error:, err.max()) print(rmse:, np.sqrt(np.mean(err**2)))这个脚本跑通后你已经掌握了B样条曲面插值的全部核心链路。不过如果你真的只有这20行代码就上线生产大概率会在下面几个坑里翻车。5. 容易被忽略的四个坑参数化漂移、节点分布、边界和病态矩阵5.1 均匀参数化在稀疏区域导致的“甩尾”我之前在对比时提过均匀参数化的风险这里说一个具体现象。假设数据点在u方向左侧很密、右侧很疏如果直接用u_kk/nu做参数化右侧稀疏区域的参数跨度与实际几何距离不成比例。B样条曲线在参数变化率过大的区域会出现明显的“甩尾”也就是曲面被数据点“拽”出一条多余的弯曲。弦长参数化能基本消除这个问题但它也不是万能的当数据本身存在尖角或者封闭特征时弦长参数化会高估大跨度区间的权重此时可以改用向心参数化。向心参数化公式非常简单$$ u_k u_{k-1} \frac{\sqrt{|D_k-D_{k-1}|}}{L_{\text{total}}} $$它给大跳跃段打了折扣更加稳健。我在处理拐角较多的轮廓数据时会先跑一版弦长法观察结果如果曲面出现不自然的扭曲就切到向心法再对比一次。5.2 节点向量分布与Schoenberg-Whitney条件节点向量的内部节点位置不是随便放的。插值情况下有一个Schoenberg-Whitney条件每个节点区间内至少应包含一个数据点参数否则基矩阵会亏秩。平均法构造节点向量之所以稳就是因为它天然满足这个条件。如果你图省事用均匀节点强行套在极端不均匀的数据参数上矩阵在求逆时经常直接报singular。一个我的实操建议跑计算前先打印节点向量和数据参数的分布对比。只要看见某个内部节点区间里没有任何数据点就别继续往下算先调整节点构造方式。这个检查只需要几十行代码省下的调试时间却是几个小时起步。5.3 曲面四条边和对角控制点的行为Clamped节点向量让曲面四个角严格落在角落控制点上四条边界曲线也由控制网格最外圈控制点独立决定。换句话说边界附近数据点的任何噪声都会被如实地映射到边界曲线上不会像内部区域那样被周围控制点“平均掉”。这导致一个常见问题数据边界如果毛糙拟合曲面边缘也会跟着毛糙四个角甚至会出现轻微凸起。我处理这类问题有两招。一是把最外圈控制点也纳入最小二乘逼近而不是插值让边界平滑地妥协二是对角落数据点做降权处理比如在最小二乘目标函数里给四个角的残差乘一个小于1的权重让曲面不必死磕那些孤立角点。5.4 条件数与数值稳定性问题基矩阵虽然是带状的但直接调np.linalg.solve时numpy并不会利用带状结构而是把它当稠密矩阵处理。数据点几千时问题不大数据点上万、又是双三次插值时矩阵规模和条件数都会明显上升。一个不稳定信号是控制点数值异常大、正负交替出现或者虽然误差小但控制网格的形状看起来特别离谱。我的应对是第一控制次数不要贪高p和q设为3通常足够除非你明确需要更高阶连续性第二求解时优先用np.linalg.lstsq替代solve既能处理最小二乘场景也能在基矩阵奇异边缘给个提示第三数据量再大就换scipy.sparse的带状求解器B样条基矩阵的带宽就是p1稀疏化后内存和速度都能上一个台阶。6. 有噪声的数据别插值最小二乘逼近和控制点裁剪6.1 为什么噪声数据用插值会得到“哆嗦”曲面插值要求曲面经过每一个数据点这在“测量数据完全可信”的假设下是合理的。但现实中的扫描仪、三坐标测量机给出的数据都带噪声如果你把噪声也精确插进去了曲面就会在真实形状的基础上叠加上高频抖动。检查方式很简单计算插值曲面的二阶导你会在噪声点附近看到成片的正负振荡。我记得有一次处理一台手持扫描仪的数据表面看着很平整但插值曲面渲染出来全是密密麻麻的小疙瘩。一开始怀疑是B样条次数太低提高到5次更严重最后才反应过来是噪声被插值保留了。这个教训让我后来养成了习惯拿到数据先做一遍平滑估计标准差然后决定该走插值还是逼近。6.2 最小二乘逼近的控制点求解逼近的思路是让控制点数量小于数据点数量曲面不要求穿过所有数据点只要求误差平方和最小。设u方向控制点数为ncp_uv方向控制点数为ncp_v且ncp_u nu、ncp_v nv。此时基矩阵不再是方阵求解变成了最小二乘问题$$ \min_P | A_u P A_v^T - D |_F^2 $$在张量积结构下这个问题的全局最优解可以分两步做而且和整体求解完全等价先把每个v方向列当作一维数据用A_u的最小二乘解得到中间矩阵C再对C的每一行用A_v做一次最小二乘def approx_control_points(u, v, D, p, q, ncp_u, ncp_v): # 控制点数量变少节点向量基于均匀或平均法构造 Uu build_clamped_knots(np.linspace(0.0, 1.0, ncp_u), p) Uv build_clamped_knots(np.linspace(0.0, 1.0, ncp_v), q) Au basis_matrix(u, Uu, p) Av basis_matrix(v, Uv, q) C np.linalg.lstsq(Au, D, rcondNone)[0] P np.linalg.lstsq(Av, C.T, rcondNone)[0].T return P, Uu, Uv我这里为了示例用了均匀分布的控制点参数来构造节点向量实际数据分布差异大时建议还是用数据参数的平均法或者至少在[u.min(), u.max()]区间内合理布置内部节点。控制点数量的选择是逼近质量的关键。控制点太少曲面过度光滑真实细节被抹掉控制点太多噪声又开始进入。我通常从数据量的三分之一开始试对比不同ncp_u、ncp_v下的RMSE和曲面曲率变化选“误差不再显著下降”的位置作为临界点。6.3 平滑正则化参数λ怎么调如果最小二乘逼近的曲面仍然偏“毛”可以给目标函数加一个平滑惩罚项。常用的做法是惩罚控制点网格的二阶差分使控制点不能剧烈摆动$$ \min_P | A_u P A_v^T - D |_F^2 \lambda\left( | L_u P |_F^2 | P L_v^T |_F^2 \right) $$其中L_u和L_v是二阶差分矩阵。实现时可以先按一维方式理解把数据列y、基矩阵A、差分矩阵L拼成一个增广系统def smooth_1d(A, y, lam1e-3): L np.diff(np.eye(A.shape[1]), n2, axis0) A_aug np.vstack([A, np.sqrt(lam) * L]) y_aug np.concatenate([y, np.zeros(L.shape[0])]) return np.linalg.lstsq(A_aug, y_aug, rcondNone)[0]二维的完整实现就是把两个方向分别增广最后再组合成控制点矩阵。λ怎么调经验法是先设一个很小的值如1e-4看RMSE变化不大时再小幅增加如果RMSE明显上升说明λ已经让曲面偏离数据太多了。更严谨的做法是交叉验证把数据分为训练集和验证集选验证集误差最小的λ。我实测中λ在1e-4到1e-2之间能取得比较均衡的结果太大会把真实的凹坑也磨平。7. 实际项目里我对这套方法的体会与后续扩展说回我开头提到的扫描点云项目。最终稳定下来的方案是先把点云网格化对每个网格块做B样条最小二乘逼近控制点取16×12左右再在块与块之间重叠一行控制点做缝合。这样既保留了局部细节又避免了全局插值带来的边界毛刺和数值负担。相比最初的双三次多项式插值曲面边缘的高程振荡被压掉了95%以上相比全局径向基函数内存占用下降了不止一个量级。如果你也想把B样条插值落到自己的流程里我建议从三件事开始一是把参数化和节点向量打印出来看一遍确认它们和数据分布匹配二是永远先跑一版插值做基准观察曲面里那些“不该有”的抖动再决定要不要切到逼近三是控制点数量宁可少一点因为增加控制点很容易发现过拟合之后再降回去就要重新调参数了。后续要扩展的话优先级我会这样排第一把基矩阵换成稀疏存储应对上万数据点第二引入NURBS的非均匀权重处理需要精确表达圆锥曲面这类场景第三研究T样条或者局部曲面片拼接绕开张量积网格对拓扑的限制。这些方向我都试过一部分最深刻的教训是数学结构简单不等于实现简单但每一步都值得先回到“参数化是否合理”这个问题上检查一遍因为绝大多数曲面拟合的问题都出在参数和节点向量上而不是最后的矩阵求解。