计算几何03_三次参数样条曲线与Cardinal曲线

在这里插入图片描述

一、三次参数样条曲线

三次样条曲线的唯一缺点就是缺乏几何不变形。即当型值点发生几何变换时不能保证参数递增。因此提出了以弦长为参数的三次参数样条曲线。

1.1 定义

已知n个型值点Pi(xi, yi), i = 1, 2,…, n且相邻型值点不重合;若 p(t) 满足下列条件:
(1)型值点Pi在函数 p(t) 上。
(2)p(t) 在整个区间[P1, Pn]上二阶连续可导。
(3)在每个子区间[Pi, Pi+1], i = 1, 2,…, n-1上,分段函数 p(t) 都是参数t的三次多项式。则称函数 是过型值点的三次参数样条函数,由三次参数样条函数构成的曲线称为三次参数样条曲线。

其中,
在这里插入图片描述
子区间的弦长Li为:
在这里插入图片描述

1.2 表达式

第i段的 pi(t) 表示为:
在这里插入图片描述

总的来说,三次参数样条曲线就是在三次样条曲线基础上对xyz各分量分别进行参数化的曲线表达,如下:

  • 3个坐标分量x,y,z分别是参数 t 的三次样条函数
  • 对型值点做参数化
  • 对3个坐标分量分别处理

1.3 算法

(1)读入n个型值点坐标。
(2)根据实际情况,确定三次参数样条曲线的边界条件。
(3)计算曲线的系数,将其表达为型值点二阶导数的函数。
(4)用追赶法分别沿x方向与y方向求解三弯矩方程。
(5)将求解出的系数代入三次参数样条函数的x分量与y分量表达式中,构造三次参数样条曲线。
(6)循环访问每个节点。在每个子区间内,参数t按照弦长计算增量,根据每组(x,y)坐标,绘制三次参数样条曲线。

1.4 代码实现

1.4.1 读取型值点

首先我们需要来扩充以下二维点类p2.h与p2.cpp(主要如下):

class CP2  
{
public:
	CP2();
	virtual ~CP2();
	CP2(double x, double y);
	friend CP2 operator +(const CP2 &p0, const CP2 &p1);//运算符重载
	friend CP2 operator -(const CP2 &p0, const CP2 &p1);
	friend CP2 operator -(double scalar, const CP2 &p);
	friend CP2 operator -(const CP2 &p, double scalar);
	friend CP2 operator *(const CP2 &p, double scalar);
	friend CP2 operator *(double scalar, const CP2 &p);
	friend CP2 operator /(const CP2 &p0, CP2 &p1);
	friend CP2 operator /(const CP2 &p, double scalar);
public:
	double x;//直线段端点x坐标
	double y;//直线段端点y坐标
};
CP2::CP2()
{

}

CP2::~CP2()
{

}

CP2::CP2(double x, double y)
{
	this->x = x;
	this->y = y;
}

CP2 operator +(const CP2 &p0, const CP2 &p1)//对象的和
{
	CP2 result;
	result.x = p0.x + p1.x;
	result.y = p0.y + p1.y;
	return result;
}

CP2 operator -(const CP2 &p0, const CP2 &p1)//对象的差
{
	CP2 result;
	result.x = p0.x - p1.x;
	result.y = p0.y - p1.y;
	return result;
}
CP2 operator -(double scalar, const CP2 &p)//常量和对象的差
{
	CP2 result;
	result.x = scalar - p.x;
	result.y = scalar - p.y;
	return result;
}
CP2 operator -(const CP2 &p, double scalar)//对象和常量的差
{
	CP2 result;
	result.x = p.x - scalar;
	result.y = p.y - scalar;
	return result;
}

CP2 operator *(const CP2 &p, double scalar)//对象和常量的积
{
	return CP2(p.x * scalar, p.y * scalar);
}

CP2 operator *(double scalar, const CP2 &p)//常量和对象的积
{	
	return CP2(p.x * scalar, p.y * scalar);
}

CP2 operator /(const CP2 &p0, CP2 &p1)//对象的商
{
	if(fabs(p1.x)<1e-6)
		p1.x = 1.0;
	if(fabs(p1.y)<1e-6)
		p1.y = 1.0;
	CP2 result;
	result.x = p0.x / p1.x;
	result.y = p0.y / p1.y;
	return result;
}

CP2 operator /(const CP2 &p, double scalar)//对象数除
{
	if(fabs(scalar)<1e-6)
		scalar = 1.0;
	CP2 result;
	result.x = p.x / scalar;
	result.y = p.y / scalar;
	return result;
}

之后在主函数中定义读取函数:

// CGeometricfiguretestViewmessage handlers
void CGeometricfiguretestView::ReadPoint()
{
	P[1].x = -400, P[1].y = -150;
	P[2].x = -100, P[2].y =  0;
	P[3].x = -300, P[3].y =  100;
	P[4].x =  0,   P[4].y =  200;
	P[5].x =  300, P[5].y =  100;
	P[6].x =  100, P[6].y =  0;
	P[7].x =  400, P[7].y =  -150;
}

1.4.2 绘制型值点

//绘制型值点
void CGeometricfiguretestView::DrawDataPoint(CDC* pDC)
{
	CBrush NewBrush, *OldBrush;
	NewBrush.CreateSolidBrush(RGB(0, 0, 0));
	OldBrush = pDC->SelectObject(&NewBrush);
	for(int i = 1; i < 8; i++)
		pDC->Ellipse(ROUND(P[i].x - 5), ROUND(P[i].y - 5), ROUND(P[i].x + 5), ROUND(P[i].y + 5));
	pDC->SelectObject(OldBrush);
}

1.4.3 三次参数样条曲线绘制

三次参数样条曲线计算与绘制,具体过程见注释(计算与推导和三次样条曲线致):

//三次参数样条曲线
void CGeometricfiguretestView::DrawCubicSpline(CDC* pDC)
{
	int n = 7;
	const int dim = 8;//二维数组维数
	double b1 = 0, bn = 0;//起点和终点的一阶导数
	double L[dim];//参数样条曲线的弦长
	double lambda[dim], mu[dim];
	double l[dim], m[dim], u[dim];
	CP2 c1, cn;//起点和终点的方向余弦
	CP2	D[dim];
    CP2 M[dim], K[dim];//追赶法过渡矩阵
    CP2 B1[dim], B2[dim], B3[dim], B4[dim];//函数的系数
	CP2 delt[dim];
	for(int i=1; i<n;i++)//计算弦长
    {
		delt[i]=P[i+1]-P[i];
		L[i]=sqrt(delt[i].x*delt[i].x+delt[i].y*delt[i].y);
	}
	//边界条件的投影
    c1.x = b1*cos(delt[1].x/L[1]);//起点
	c1.y = b1*cos(delt[1].y/L[1]);
   	cn.x = bn*cos(delt[n-1].x/L[n-1]);//终点
	cn.y = bn*cos(delt[n-1].y/L[n-1]);
	for(int i = 2; i < n; i++)
	{
		lambda[i] = L[i-1]/(L[i-1]+L[i]);//计算λ
		mu[i]=L[i]/(L[i-1]+L[i]);//计算μ
		D[i]=6/(L[i-1]+L[i])*((P[i+1]-P[i])/L[i]-(P[i]-P[i-1])/L[i-1]);//计算D
    }
	D[1]=6*((P[2]-P[1])/L[1]-b1)/L[1];//夹持端的D[1]
    D[n]=6*(bn-(P[n]-P[n-1])/L[n-1])/L[n-1];//夹持端的D[n]
    mu[1]=1;//夹持端的μ[1],
	lambda[n]=1;//夹持端的λ[n]
	//追赶法求解三弯矩方程
    l[1]=2;
	u[1]=mu[1]/l[1];
    for(int i=2; i <= n; i++)
    {
		m[i]=lambda[i];
        l[i]=2-m[i]*u[i-1];
        u[i]=mu[i]/l[i];
    }
    K[1] = D[1]/l[1];//解LK=D
    for(int i = 2; i <= n;i++)
    {
		K[i]=(D[i]-m[i]*K[i-1])/l[i];
	}
	M[n] = K[n];//解UM=K
	for(int i = n-1; i >= 1;i--)
	{
		M[i]=K[i]-u[i]*M[i+1];
	}
	//计算三次样条函数的系数
	for(int i = 1; i < n; i++)
    {
		B1[i]=P[i];
        B2[i]=(P[i+1]-P[i])/L[i]-L[i]*(M[i]/3+M[i+1]/6);
        B3[i]=M[i]/2;
        B4[i]=(M[i+1]-M[i])/(6*L[i]);
    }
	CPen pen(PS_SOLID,3,RGB(0,0,0));
	pDC->SelectObject(&pen);
	pDC->MoveTo(ROUND(P[1].x), ROUND(P[1].y));
	double tStep = 0.5;//步长
	CP2 p;
	for(int i = 1; i < n; i++)//循环访问每个节点
    {
		for(double t=0; t <= L[i]; t += tStep)
		{
			p =B1[i]+B2[i]*t+B3[i]*t*t+B4[i]*t*t*t;
			pDC->LineTo(ROUND(p.x), ROUND(p.y));//绘制参数样条曲线
        }
    }

1.4.4 主函数调用

void CGeometricfiguretestView::OnDraw(CDC* pDC)
{
	CTestDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
	// TODO: add draw code for native data here
	CRect rect;//定义客户区矩形
	GetClientRect(&rect);//获得客户区矩形的信息
	pDC->SetMapMode(MM_ANISOTROPIC);//自定义二维坐标系
	pDC->SetWindowExt(rect.Width(), rect.Height());//设置窗口范围
	pDC->SetViewportExt(rect.Width(), -rect.Height());//设置视区范围,且x轴水平向右为正,y轴垂直向上为正
	pDC->SetViewportOrg(rect.Width() / 2, rect.Height() / 2);//设置客户区中心为二维坐标系原点
	rect.OffsetRect(-rect.Width() / 2, -rect.Height() / 2);//rect矩形与客户区重合
	ReadPoint();
	DrawDataPoint(pDC);
	DrawCubicParameterSpline(pDC);
}

编译运行,可见如下:

在这里插入图片描述
我们也可以改变一下边界约束条件:

double b1 = 1, bn = 1;//起点和终点的一阶导数

在这里插入图片描述

二、Cardinal曲线

2.1 Hermite基矩阵

Hermite的每个曲线两个端点的坐标和导数来定义。
在这里插入图片描述
假设Hermite样条曲线的方程可以写成矩阵形式,如下:
在这里插入图片描述
将边界条件p(0)=yi和p(1)=yi+1代入上式,得:

yi ﹦d,yi+1﹦a﹢b﹢c﹢d

且上式的一阶导数为:

在这里插入图片描述
将边界条件p’(0)=Ri和p’(1)=Ri+1带入上式,得:

Ri ﹦c,Ri+1﹦3a﹢2b﹢c

Hermite边界条件的矩阵表示为:
在这里插入图片描述

该方程对多项式系数求解,有:
在这里插入图片描述
式中,Mh称为Hermite基矩阵,是边界约束矩阵的逆矩阵。需要说明的是,弗格森于1963年用于飞机设计的均匀参数插值三次样条方程就是Mh。
在这里插入图片描述
因此,可得:
在这里插入图片描述
最终可得到:
在这里插入图片描述

2.2 Cardinal曲线

Cardinal样条是Hermite样条的变形,仅由相邻型值点的坐标就可以计算出导数。

设相邻的四个型值点分别记为Pi-1, Pi, Pi+1,Pi+2。Cardinal样条插值方法规定Pi, Pi+1两型值点间插值多项式的边界条件为:
在这里插入图片描述

式中, u为可调参数,称为张力参数,可以控制Cardinal样条曲线型值点间的松紧程度。
记s=(1-u)/2,用类似Hermite曲线样条中的方法,将Cardinal边界条件代入下边的参数方程,
在这里插入图片描述

可以得到矩阵表达式:
在这里插入图片描述

式中,Mc称为Cardinal矩阵。

一条Cardinal样条曲线完全由四个连续的型值点给出,中间两个型值点是曲线段端点,另外两个型值点用来辅助计算端点导数。只要给出一组型值点的坐标值,就可以分段绘制出Cardinal样条曲线。

2.3 Cardinal 算法

(1)读入n个型值点坐标Pi,i=1,2,…n。
(2)在两端各加入一个虚拟点P0和Pn+1。
(3)设置张力参数u值,计算Cardinal基矩阵Mc。
(4)计算Mc矩阵与边界条件矩阵的算法,每4个顶点构成一个条件矩阵。
(5)根据型值点个数计算每段曲线中插值参数t的值,使用直线段连接对应的点p(x(t),y(t))绘制曲线。

2.4 代码实现

1.4.1 读取型值点

void CGeometricfiguretestView::ReadPoint()
{
	P[0].x =-500, P[0].y =0;
	P[1].x =-450, P[1].y =-100;
	P[2].x =-350, P[2].y =-250;
	P[3].x =-250, P[3].y = 200;
	P[4].x =-150, P[4].y = 100;
	P[5].x =-50,  P[5].y = 150;
	P[6].x = 40, P[6].y = 10;
	P[7].x = 100, P[7].y = -60;
	P[8].x = 150, P[8].y = 80;
	P[9].x = 200, P[9].y = 70;
	P[10].x =230,P[10].y =-10;
	P[11].x =300,P[11].y =-200;
	P[12].x =400,P[12].y = 150;
	P[13].x =520,P[13].y = 250;
	n = 13;
}

1.4.2 绘制型值点

void CGeometricfiguretestView::DrawDataPoint(CDC* pDC)//绘制型值点
{
	CBrush NewBrush, *pOldBrush;
	NewBrush.CreateSolidBrush(RGB(0, 0, 0));
	pOldBrush = pDC->SelectObject(&NewBrush);
	for( int i = 1; i < 13; i++)
		pDC->Ellipse(ROUND(P[i].x - 5), ROUND(P[i].y - 5), ROUND(P[i].x + 5), ROUND(P[i].y + 5));
	pDC->SelectObject(pOldBrush);
	pDC->Ellipse(ROUND(P[0].x - 5), ROUND(P[0].y - 5), ROUND(P[0].x + 5), ROUND(P[0].y + 5));
	pDC->Ellipse(ROUND(P[13].x - 5), ROUND(P[13].y - 5), ROUND(P[13].x + 5), ROUND(P[13].y + 5));
}

1.4.3 计算与绘制

void CGeometricfiguretestView::DrawCardinalCurve(CDC* pDC)//Cardinal曲线
{
	double M[4][4];
	double u=0.2;
	double s=(1-u)/2;
	CP2 p;
	M[0][0]=-s ,M[0][1]=2-s,M[0][2]= s-2 ,M[0][3]= s;//Mc矩阵
	M[1][0]=2*s,M[1][1]=s-3,M[1][2]=3-2*s,M[1][3]=-s;
	M[2][0]=-s ,M[2][1]= 0 ,M[2][2]=  s  ,M[2][3]= 0;
	M[3][0]= 0 ,M[3][1]= 1 ,M[3][2]=  0  ,M[3][3]= 0;
	double t3,t2,t1,t0;
	CPen pen(PS_SOLID,2,RGB(0,0,0));
	CPen* pOldPen=pDC->SelectObject(&pen);
	pDC->MoveTo(ROUND(P[1].x),ROUND(P[1].y));
	for(int i=0; i< n-2; i++)
	{
		Point[0]=P[i],Point[1]=P[i+1],Point[2]=P[i+2],Point[3]=P[i+3];
		MultiplyMatrix(M, P, i);
		double tStep = 0.001;
		for(double t=0.0; t<1; t += tStep)
		{
			t3 = t*t*t; t2 = t*t; t1 = t; t0 = 1;
			p = t3*Point[0] + t2*Point[1] + t1*Point[2] + t0*Point[3];
			pDC->LineTo(ROUND(p.x),ROUND(p.y));
		}
	}
	pDC->SelectObject(pOldPen);
}

void CGeometricfiguretestView::MultiplyMatrix(double M0[][4], CP2 P0[4], int n)
{
	for(int i=0;i<4;i++)
		Point[i] = M0[i][0]*P0[n] + M0[i][1]*P0[n+1] + M0[i][2]*P0[n+2] + M0[i][3]*P0[n+3];
}

其中定义MultiplyMatrix函数为了实现4×4的Mc矩阵与1×4的边界约束矩阵的乘法计算。

编译运行,如下:
在这里插入图片描述

我们也可以改变一下张力参数来试一下:

double u=1.0;

在这里插入图片描述

© 版权声明
THE END
喜欢就支持一下吧
点赞286 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容