节点矢量重复度在三次样条曲线中,主要表现为节点序列中连续相同节点值的出现次数,它直接决定了曲线在该节点处的连续性(C^k 连续性)和基函数的支撑区间。
在三次B样条曲线中,曲线次数p = 3。根据B样条理论,节点矢量U = {u0, u1, ..., um}需要满足m = n + p + 1,其中n+1是控制点的数量。节点重复度s与曲线在该节点处的连续性关系为:C^(p-s)。这意味着:
- 内部节点的常规重复度 (s=1):对于一个内部节点,如果其重复度
s = 1,则曲线在该节点处具有C^(p-1)连续性,对于三次曲线就是C^2连续性(即位置、一阶导、二阶导均连续)。 - 提高重复度以降低连续性 (s>1):如果某个节点的重复度
s增加,曲线在该节点处的连续性会降低为C^(p-s)。- 当
s = p(对于三次曲线,s=3)时,曲线在该节点处具有C^(p-p) = C^0连续性,即仅位置连续,一阶导和二阶导可能不连续,曲线会出现一个尖锐的角点。 - 当
s = p+1(对于三次曲线,s=4)时,曲线在该节点处甚至可能不连续(取决于控制点位置),这通常用于在曲线中插入一个断点。
- 当
- 端点节点的重复度:为了使曲线通过第一个和最后一个控制点,通常将节点矢量的前
p+1个节点设为最小值,后p+1个节点设为最大值。对于三次曲线,这意味着u0 = u1 = u2 = u3和u_{m-3} = u_{m-2} = u_{m-1} = u_m,重复度s = p+1 = 4。这确保了曲线在端点处与首末控制点重合。
C++代码示例:
下面的示例演示了如何定义不同重复度的节点矢量,并计算对应的B样条基函数,直观展示重复度对基函数形状(进而对曲线局部支撑和连续性)的影响。
#include <iostream> #include <vector> #include <cmath> #include <iomanip> /** * 计算B样条基函数值 (Cox-de Boor递归公式) * @param i 基函数索引 (从0开始) * @param p 曲线次数 * @param u 参数值 * @param knots 节点矢量 * @return 基函数 N_{i,p}(u) 的值 */ double BasisFunction(int i, int p, double u, const std::vector<double>& knots) { // 递归终止条件:零次B样条 if (p == 0) { return (knots[i] <= u && u < knots[i + 1]) ? 1.0 : 0.0; } double leftCoeff = 0.0; double rightCoeff = 0.0; // 计算左项,注意分母为零的情况 double denomLeft = knots[i + p] - knots[i]; if (std::fabs(denomLeft) > 1e-10) { leftCoeff = (u - knots[i]) / denomLeft * BasisFunction(i, p - 1, u, knots); } // 计算右项,注意分母为零的情况 double denomRight = knots[i + p + 1] - knots[i + 1]; if (std::fabs(denomRight) > 1e-10) { rightCoeff = (knots[i + p + 1] - u) / denomRight * BasisFunction(i + 1, p - 1, u, knots); } return leftCoeff + rightCoeff; } int main() { // 定义曲线次数 const int p = 3; // 三次B样条 // --- 示例1:均匀节点矢量 (所有内部节点重复度s=1) --- // 控制点数量 n+1 = 5, 所以 n=4 // 节点矢量长度 m = n + p + 1 = 4 + 3 + 1 = 8 std::vector<double> uniform_knots = {0, 1, 2, 3, 4, 5, 6, 7}; // 标准化到 [0,1] 区间以便比较 for (double& knot : uniform_knots) { knot /= 7.0; } std::cout << "【示例1】均匀节点矢量 (内部节点重复度s=1):" << std::endl; std::cout << "节点矢量 U = {"; for (size_t i = 0; i < uniform_knots.size(); ++i) { std::cout << std::fixed << std::setprecision(2) << uniform_knots[i]; if (i != uniform_knots.size() - 1) std::cout << ", "; } std::cout << "}" << std::endl; // 计算在参数 u=0.5 处,所有非零基函数的值 double u_test = 0.5; std::cout << "在 u=" << u_test << " 处的基函数值 (N0,3 到 N4,3):" << std::endl; for (int i = 0; i <= 4; ++i) { // 控制点索引从0到4 double val = BasisFunction(i, p, u_test, uniform_knots); std::cout << " N" << i << "," << p << "(" << u_test << ") = " << std::setprecision(4) << val << std::endl; } std::cout << std::endl; // --- 示例2:包含高重复度内部节点的节点矢量 --- // 假设我们在参数 u=0.5 处插入一个重复度为3的节点 (s=p)。 // 控制点数量不变 (n=4),但节点矢量长度增加。为了简化,我们重新定义一个节点矢量。 // U = {0,0,0,0, 0.5,0.5,0.5, 1,1,1,1} // 符合m=n+p+1=8?不对,节点数应该是m+1。 // 正确构造:对于n=4, p=3, m=n+p+1=8,节点数应为m+1=9个。 // 我们构造一个在0.5处重复3次的节点矢量。 std::vector<double> repeated_knots = {0.0, 0.0, 0.0, 0.0, // 端点重复 p+1=4 次 0.5, 0.5, 0.5, // 内部节点重复 s=3 次 1.0, 1.0, 1.0, 1.0}; // 端点重复 p+1=4 次 // 检查节点矢量长度:m = 节点数 - 1 = 10 - 1 = 9, 而 n+p+1 = 4+3+1=8,不匹配。 // 这意味着控制点数量需要调整。为了演示,我们使用一个更简单的例子,减少控制点。 // 设 n=3 (4个控制点), 则 m = n+p+1 = 3+3+1=7,节点数应为8个。 // 构造一个在0.6处重复2次(s=2)的节点矢量。 std::vector<double> knots_with_multiplicity = {0.0, 0.0, 0.0, 0.0, // 左端重复4次 0.6, 0.6, // 内部节点重复2次 (s=2) 1.0, 1.0, 1.0, 1.0}; // 右端重复4次 // 此时节点数=10, m=9, n = m - p -1 = 9-3-1=5,所以控制点数量应为6个。 // 这超出了我们简单演示的范围。下面我们直接使用一个定义好的、合法的节点矢量来展示基函数计算。 // 一个合法的、包含重复度2的内部节点的三次B样条节点矢量示例: // 假设有5个控制点 (n=4), p=3, 需要 m = n+p+1 = 8, 节点数9个。 // U = {0,0,0,0, 0.4,0.4, 0.7, 1,1,1,1} // 节点数=11?不对。 // 正确:U = {0,0,0,0, 0.5, 0.5, 0.8, 1,1,1,1} // 节点数=11, m=10, 那么 n = m-p-1=10-3-1=6 (7个控制点)。 // 为了简化,我们固定一个常用于生成闭合曲线或带尖点曲线的节点矢量模式。 std::cout << "【示例2】节点矢量中节点重复度的影响" << std::endl; // 定义一个更直观的节点矢量,其中 u=0.5 是一个重复度 s=2 的节点。 // 使用4个控制点 (n=3), m = n+p+1 = 3+3+1=7, 节点数应为8。 // U = {0,0,0,0, 0.5, 0.5, 1,1,1,1} // 节点数=10? 0重复4次,0.5重复2次,1重复4次,总计10个节点,m=9,需要n=5个控制点。 // 让我们重新计算一个合法的、用于演示的矢量。 // 目标:展示在 u=0.4 处有一个重复度 s=3 (等于次数p) 的节点,这将产生 C^0 连续性(尖点)。 // 设控制点数量为 5 (n=4), m = 4+3+1=8, 节点数9。 // 构造节点矢量:U = {0,0,0,0, 0.4,0.4,0.4, 1,1,1,1} // 节点数=11, m=10, 对应 n=6。 // 让我们直接使用一个已知的、能产生尖点的节点矢量定义。 // 实际编程中,节点矢量的构造需要严格满足 m = n + p + 1 的关系。 // 下面的代码块演示了当参数 u 经过一个高重复度节点时,基函数值的变化,这反映了曲线连续性阶数的降低。 // 定义一个简单的、在中间具有重复节点的非均匀节点矢量(用于演示概念) // 假设有 6 个控制点 (n=5), p=3, 则 m = 5+3+1=9, 节点矢量应有 10 个元素。 // 令 U = {0, 0, 0, 0, 0.3, 0.3, 0.7, 1, 1, 1, 1} // 0.3 的重复度 s=2 std::vector<double> demo_knots = {0.0, 0.0, 0.0, 0.0, 0.3, 0.3, 0.7, 1.0, 1.0, 1.0, 1.0}; int n_demo = 5; // 控制点索引从0到5 std::cout << "演示节点矢量 U = {"; for (size_t i = 0; i < demo_knots.size(); ++i) { std::cout << demo_knots[i]; if (i != demo_knots.size() - 1) std::cout << ", "; } std::cout << "}" << std::endl; std::cout << "控制点数量: " << n_demo + 1 << " (n=" << n_demo << "), 曲线次数 p=" << p << std::endl; std::cout << " 分析节点 u=0.3 (重复度 s=2):" << std::endl; std::cout << "根据连续性公式 C^(p-s) = C^(3-2) = C^1。" << std::endl; std::cout << "这意味着曲线在 u=0.3 处是 C^1 连续的(位置和一阶导连续),但二阶导可能不连续。" << std::endl; // 计算在 u 略小于和略大于 0.3 时,受影响的基函数值 double u_near_left = 0.2999; double u_near_right = 0.3001; int affected_index = 3; // 当u在[0.3, 0.7)区间时,活跃的基函数索引大致从 i=3 开始(取决于节点跨度) std::cout << " 计算 u 接近 0.3 时,基函数 N" << affected_index << "," << p << " 的值:" << std::endl; double N_left = BasisFunction(affected_index, p, u_near_left, demo_knots); double N_right = BasisFunction(affected_index, p, u_near_right, demo_knots); std::cout << " N" << affected_index << "," << p << "(" << u_near_left << ") = " << N_left << std::endl; std::cout << " N" << affected_index << "," << p << "(" << u_near_right << ") = " << N_right << std::endl; std::cout << " 基函数本身在 u=0.3 处是 C^(p-s) = C^1 连续的。" << std::endl; // 讨论如果重复度 s=3 (等于p) 的情况 std::cout << " --- 假设情景:如果节点 u=0.3 的重复度 s=3 ---" << std::endl; std::cout << "则连续性为 C^(p-s) = C^(3-3) = C^0。" << std::endl; std::cout << "曲线在该点仅位置连续,一阶导不连续,可能形成一个尖角。" << std::endl; std::cout << "在节点矢量中,这表现为连续三个节点值都是 0.3(例如 U={..., 0.3,0.3,0.3, ...})。" << std::endl; std::cout << "此时,参数域中对应于该节点的区间长度为零,基函数在该点处不可微,导致曲线导数突变。" << std::4]; return 0; }代码输出分析:
运行上述代码(需补充完整的BasisFunction递归实现),可以观察到:
- 均匀节点矢量:所有内部节点间距均匀,重复度为1。计算得到的基函数在参数域内部是光滑的
C^2连续函数。 - 包含重复节点的非均匀节点矢量:在
u=0.3处,节点重复度s=2。根据公式,曲线在该点具有C^(3-2)=C^1连续性。通过计算u从左侧和右侧趋近于0.3时某个基函数的值,可以发现该基函数的值是连续变化的(位置连续),但其导数(变化率)可能发生突变,这对应了曲线的一阶导连续但二阶导不连续的特性。如果重复度s增加到3,基函数在该点将变得不可微(C^0),导致曲线出现尖点。
总结:在C++实现中,节点矢量通常存储为一个std::vector<double>。节点重复度直接体现在这个向量中连续相等的元素数量上。算法(如Cox-de Boor递归)会根据节点矢量自动处理不同重复度的情况,生成相应连续性的基函数,从而最终影响曲线的形状和光滑度。通过设计和修改节点矢量中的重复度,可以精确控制B样条曲线在各个节点处的连续行为,这是B样条曲线比贝塞尔曲线更灵活强大的关键特性之一。
参考来源
- VC++绘制三次样条插值、贝塞尔曲线与GDI+平滑曲线
- 栅格数据矢量化(附有完整代码)
- 三次样条插值三弯矩matlab_曲线拟合/样条插值的C++及MATLAB实现(三十四)
- 局部路径规划算法 - B样条曲线(B-spline Curves)
- C++矢量运算与java矢量运算
- 栅格数据转换为矢量数据的实用C++代码实现