无论是曲线拟合,能量优化还是分段函数模拟的应用中,通过一组离散点拟合出一条完整的曲线,都是不可避免的工作。一般来说,像贝塞尔曲线,b样条等曲线拟合方法,是通过控制点来生成曲线,控制点本身不经过曲线,这就带来一些不便。我们希望使用一种控制点在曲线上,同时保证曲线光滑,拟合结果良好方法,来实现曲线拟合。这就引出了我们今天的主题:三次样条曲线。

一. 背景知识

该部分参考博客:三次样条(cubic spline)插值 - 知乎

如果用一句话概括,三次样条就是有条件下的分段三次函数拟合问题。条件包括要通过特定的点序列以及保持函数的平滑。每一对相邻的点序列表示一个三次函数,每一个点是两个三次函数交汇处,代表起点和终点。平滑表示高阶导数具有连续性,基本表示到2阶。有了以上的了解,我们就能够对三次样条的求解进行数学建模:

1. 点序列{x0, x1, ...., xn}, 每一对点xm与xm+1之间会有一个对应的三次函数:Sm,且Sm(xm+1)=Sm+1(xm+1), 表示每一个点是两个三次函数的交汇。

2. 曲线光滑,即到二阶连续(S'').

根据端点设定不同的二阶约束,会产生不同的边界,包括自然边界,固定边界以及非节点边界。这里我们只使用自然边界的情况,即S''(x0)=S''(xn)=0。

具体推导,假设三次函数的参数为a,b,c,d,对应不同的次数,则能够得到:

三次样条R语言 简述三次样条函数_三次样条

 这里我们不在展开推导过程,按照已知条件对上述方程进行变换,其问题转换为求解方程组:

三次样条R语言 简述三次样条函数_c++_02

 

三次样条R语言 简述三次样条函数_三次样条R语言_03

 其中h表示x的步长值,hm=xm+1-xm。m为求解中间变量。解上述线性方程,得到m的值。将m和h带入下面的方程,就得到了分段的三次函数表达式:(y是每一个已知点对应x的纵坐标)

三次样条R语言 简述三次样条函数_matplotlib-cpp_04

 到此,我们就得到了三次样条函数自然边界条件下的求解方法。

二. 程序实现

基于Eigen的C++程序实现,我参考的是以下两篇博客,分别介绍了Eigen的基础配置和HelloWorld程序以及对Ax=b的线性方程求解。

Eigen介绍:


Eigen求解:


根据上面两篇博客的介绍,我写了一个类,来实现三次样条函数的C++程序实现,具体如下:

/*************************************************************
* 
*                      CubicSpline
* 
*                       2021.12.14 
*                       
* 
*       Function: Input discrete points and Output curve
*                 Points corss the curve.               
* 
* 
*************************************************************/

#pragma once
#include <iostream>
#include <Eigen/Dense>

//using Eigen::MatrixXd;
using namespace Eigen;
using namespace Eigen::internal;
using namespace Eigen::Architecture;

using namespace std;

class CubicSpline {

private:

	vector<vector<double>> point2D;
	vector<vector<double>> CubicSplineParameter;//a, b, c, d.
	vector<double> h;
	vector<double> m;

public:

	void CubicSpline_init(vector<vector<double>> point2D_input) {

		point2D = point2D_input;

		//init h
		h.clear();
		h.resize(point2D.size() - 1);
		for (int i = 0; i < point2D.size() - 1; i++) {
			double x1 = point2D[i][0];
			double x2 = point2D[i + 1][0];
			double h_i = abs(x2 - x1);
			h[i] = h_i;
		}

		//init m. m.size = point2D.size()
		//1, compute yh coefficient
		vector<double> yh(point2D_input.size());
		for (int i = 0; i < yh.size(); i++) {
			if (i == 0 || i == yh.size() - 1) {
				yh[i] = 0;
			}
			else {
				yh[i] = 6 * ((point2D[i + 1][1] - point2D[i][1]) / h[i] - (point2D[i][1] - point2D[i - 1][1]) / h[i - 1]);
			}
		}

		MatrixXf A(point2D.size(), point2D.size());
		MatrixXf B(point2D.size(), 1);
		MatrixXf m;

		//2, init A, B
		B(0, 0) = yh[0];
		B(point2D.size() - 1, 0) = yh[point2D.size() - 1];

		for (int i = 0; i < point2D.size() - 1; i++) {

			A(0, i) = 0;
			A(point2D.size() - 1, i) = 0;

		}
		A(0, 0) = 1;
		A(point2D.size() - 1, point2D.size() - 2) = 1;

		for (int i = 1; i < point2D.size() - 1; i++) {

			B(i, 0) = yh[i];

			for (int j = 0; j < point2D.size(); j++) {

				if (j == i) {
					A(i, j) = 2 * (h[i - 1] + h[i]);
				}
				else if (j == i - 1) {
					A(i, j) = h[i - 1];
				}
				else if (j == i + 1) {
					A(i, j) = h[i];
				}
				else {
					A(i, j) = 0;
				}

			}

		}

		m = A.llt().solve(B);
		vector<double> mV(point2D.size());
		for (int i = 0; i < point2D.size(); i++) {
			mV[i] = m(i, 0);
		}

		for (int i = 0; i < m.size() - 1; i++) {

			vector<double> CubicSplineParameter_i;
			double a = point2D[i][1];
			double b = (point2D[i + 1][1] - point2D[i][1]) / h[i] - h[i] / 2 * mV[i] - h[i] / 6 * (mV[i + 1] - mV[i]);
			double c = mV[i] / 2;
			double d = (mV[i + 1] - mV[i]) / (6 * h[i]);
			CubicSplineParameter_i.push_back(a);
			CubicSplineParameter_i.push_back(b);
			CubicSplineParameter_i.push_back(c);
			CubicSplineParameter_i.push_back(d);
			CubicSplineParameter.push_back(CubicSplineParameter_i);

		}

	}

	vector<vector<double>> CubicSpline_Insert(int step) {

		vector<vector<double>> insertList;

		for (int i = 0; i < CubicSplineParameter.size(); i++) {
			double h_i = h[i] / (double)step;
			insertList.push_back(point2D[i]);
			double a = CubicSplineParameter[i][0];
			double b = CubicSplineParameter[i][1];
			double c = CubicSplineParameter[i][2];
			double d = CubicSplineParameter[i][3];
			for (int j = 1; j < step; j++) {
				double x_new = point2D[i][0] + h_i * j;
				double y_new = a + b * (x_new - point2D[i][0])
					+ c * (x_new - point2D[i][0]) * (x_new - point2D[i][0])
					+ d * (x_new - point2D[i][0]) * (x_new - point2D[i][0]) * (x_new - point2D[i][0]);
				vector<double> p_new_ij;
				p_new_ij.push_back(x_new);
				p_new_ij.push_back(y_new);
				insertList.push_back(p_new_ij);
			}
		}

		insertList.push_back(point2D[point2D.size() - 1]);
		return insertList;

	}

	vector<vector<double>> CubicSpline_Insert(double step) {

		vector<vector<double>> insertList;

		for (int i = 0; i < CubicSplineParameter.size(); i++) {
			int h_i = h[i] / (double)step;
			insertList.push_back(point2D[i]);
			double a = CubicSplineParameter[i][0];
			double b = CubicSplineParameter[i][1];
			double c = CubicSplineParameter[i][2];
			double d = CubicSplineParameter[i][3];
			for (int j = 1; j < h_i; j++) {
				double x_new = point2D[i][0] + step * j;
				double y_new = a + b * (x_new - point2D[i][0])
					+ c * (x_new - point2D[i][0]) * (x_new - point2D[i][0])
					+ d * (x_new - point2D[i][0]) * (x_new - point2D[i][0]) * (x_new - point2D[i][0]);
				vector<double> p_new_ij;
				p_new_ij.push_back(x_new);
				p_new_ij.push_back(y_new);
				insertList.push_back(p_new_ij);
			}
		}

		insertList.push_back(point2D[point2D.size() - 1]);
		return insertList;

	}

};

调用的方法如下:

//存储已知的点序列
vector<vector<double>> pointList;

//初始化
CubicSpline cs;
cs.CubicSpline_init(pointList);

//输出样条插值后的结果,参数表示每一对点间要插入几个值
vector<vector<double>> pointInsert = cs.CubicSpline_Insert(10);

三. 可视化

三次样条R语言 简述三次样条函数_三次样条R语言_05

上面的可视化是使用matplotlib-cpp生成。如果你希望使用该功能,请参考我之前的博客:

.

通过程序实现,我们得到了插入新点之后的点序列。然后使用matplotlib-cpp的绘制功能,就能够得到一个最终的三次样条曲线拟合图,具体代码如下:

void PlotDraw_int(vector<vector<double>> plistOriginal, vector<vector<double>> plistInsert){

        vector<double> xO;
        vector<double> yO;

        vector<double> xI;
        vector<double> yI;

        for (int i = 0; i < plistOriginal.size(); i++) {
            xO.push_back(plistOriginal[i][0]);
            yO.push_back(plistOriginal[i][1]);                    
        }

        for (int i = 0; i < plistInsert.size(); i++) {
            xI.push_back(plistInsert[i][0]);
            yI.push_back(plistInsert[i][1]);
        }      

        // Plot line from given x and y data. Color is selected automatically.
        plt::plot(xO, yO);

        // Plot a red dashed line from given x and y data.
        plt::plot(xI, yI, "r--");

        // Add graph title
        plt::title("Cubic Curves");

        // Enable legend.
        plt::legend();

        // save figure
        const char* filename = "basic.png";
        std::cout << "Saving result to " << filename << std::endl;;
        plt::save(filename);    
    
    }

为了能够清楚的显示出插值前和插值后的区别,我将两个点序列的可视化都展现在图片中,其中plistOriginal表示插值前(对应上图蓝色折线),plistInsert表示插值后(对应上图红色虚线)。可以清楚的看到,插值后,曲线变得平滑。

总结

三次样条曲线具有非常重要的工程价值。我们在解很多拟合优化问题的时候,需要这种能够在有固定约束点的条件约束下,提供连续拟合的工具。最近一直在做的颜色迁移,就有这方面的应用。如果未来有机会的话,我会分享三次样条在颜色迁移方面的工程应用。如果您看到这里,希望可以点赞,分享我的文章,这对我的创作将大有助益。