快速分段3次樣條曲線擬合和折線重採樣算法實現
cheungmine
(保留所有權利。本文可以在互聯網上轉載,但不允許出版和印刷)
本文采用3次樣條函數,用分段插值的快速計算方法,實現了用鼠標在屏幕上繪製任意光滑的曲線,並同時使用折線重採樣的擬合方法,去除多餘的插值點。本文所敘述的算法,可以用來繪製等高線等光滑曲線,並且由於採用了折線的重採樣,以最小的數據量保證了繪圖的精確度。
本文以作者2002年根據《計算方法》(同濟大學出版社)一書所述的算法爲基礎修改而得,因爲時間久遠,這裏不再敘述什麼是張力樣條的具體公式。僅僅對張力樣條做簡單介紹。
1 問題的提出
在曲線數字化,或繪製曲線的應用中,通常要求曲線是光滑的,並且通過控制點。這在高等數學裏,有專門的術語描述--導數的連續性。我們這裏做個形象的比喻:拿一根彈性強的硬鋼絲,通過固定在木板上的一系列點,就得到一條滿足我們要求的光滑的曲線。當我們在鋼絲的2端,施加巨大的拉力,可以看到,鋼絲在固定點處的光滑程度越來越小,直到變爲折線段。這個力就是張力。我們用一個係數表示,就是張力系數f。張力系數是張力的倒數,當f=0時,張力最大,曲線退化爲折線段。當f=1,基本類似拋物線。比較合適的取值是:
f=0.1666667
2 樣條函數
關於樣條函數,我實在忘記原理了。請讀者自行學習原理的部分。實際當中,通常是用分段擬合的方法:
給定木板上(p0,p1,p2,p3)4個固定點,要求樣條鋼絲依次通過這4個點,我們就可以確定p1,p2之間的樣條函數公式。根據這個公式和一定的步長step,就可以插值出p1,p2之間的一系列點q1,q2,...,qn。依次用線段連接這些點,就得到用折線段按擬合出來的樣條曲線。步長越小,擬合出的折點越多,越接近曲線的真實值。然而數據量就越多。
3 分段擬合方法
給點一系列固定點p0,p1,p2,...,pn-3,pn-2,pn-1,依次採樣上面的分段擬合方法:
1) 擬合p0,p1,p2,p3得到p1,p2間的點:q11,q12,q13,...,q1m。
2) 擬合q1m,p2,p3,p4得到p2,p3間的點:q21,q22,q23,...,q2n。
3) 擬合q2n,p3,p4,p5得到p3,p4間的點:q31,q32,q33,...,q3w。
4) ...
按上面的方法,一直擬合出:pn-3,pn-2間的點。
上面這些擬合點和原來的控制點,按次序就構成了整條函數曲線的擬合折線。採樣這種擬合方法,計算量小,極易編制程序。
4 擬合的精度
但是,這種擬合得出的精度受步長的限制,我們一方面想提高精度Precision,只有把step設置的很小。又不想增加很大的數據量。這是矛盾的:
Precision <-> 1/step
這種等間距step的擬合方式,無論在曲線的平緩處,還是轉彎處,都採用了一樣的擬合步長。而通常我們只希望在曲線的平緩處只要少量的幾個點就可以達到我們的要求。而在轉彎處,用密集些的點擬合。
因此引入了重採樣的方法,去除一些不必要的點。重採樣的依據就是:
設折線段上一系列點p0,p1,p2,...,pm,pn。分別計算p1,p2,...,pm到直線p0,pn的距離。如果這些距離的最大值pk,小於某個限值dist,則曲線上的點p1,p2,...,pm都不是必要的,我們只要保留p0,pn即可。(式1)
如果pk大於我們規定的限值dist,則分別以(p0,p1,p2,...,pk-2,pk-1,pk)和(pk,pk+1,pk+2,...,pm,pn)作爲2條新的折線重新計算上面的(式1)。
在編制程序的時候,使用遞歸可以很容易解決這個問題。在我提供的測試程序中,可以看到2種情況(重採樣和不重採樣)下的點數是相差很大的。
5 結論
到此,我們近乎完美地解決問題:採樣分段樣條插值,並且採用重採樣,減小數據量,同時保證精度不受損失。
6 下面就是完整的代碼。我採樣C語言寫的:
/*==============================================================================
cheungmine - All rights reserved
April 5, 2009
==============================================================================*/
#include <stdio.h>
#include <float.h>
#include <assert.h>
#include <math.h>
#ifndef CG_BIPI
#define CG_BIPI 6.28318530717958647692
#endif
#ifndef IN
#define IN
#endif
#ifndef OUT
#define OUT
#endif
#ifndef INOUT
#define INOUT
#endif
/* Vertex structure */
typedef struct
{
double x, y;
} cg_vertex_t;
/**
* CG_vertex_get_dist_to_line
* 計算點到直線的距離和垂足ft
*/
double CG_vertex_get_dist_to_line (
IN const cg_vertex_t *pt,
IN const cg_vertex_t *start,
IN const cg_vertex_t *end,
OUT cg_vertex_t *ft)
{
double a1 = pt->x - start->x;
double a2 = pt->y - start->y;
double s = sqrt(a1*a1 + a2*a2);
a1 = atan2(a2, a1);
if(a1 < 0) a1 += CG_BIPI;
a2 = atan2((end->y-start->y), (end->x-start->x));
if(a2 < 0) a2 += CG_BIPI;
a1 = fabs(sin(a1-a2)) * s;
if (ft){
s *= cos(a1-a2);
ft->x = start->x + s*cos(a2);
ft->y = start->y + s*sin(a2);
}
return a1;
}
/*===========================================================================
Spline3 Functions
===========================================================================*/
/* 返回pl的點到直線[start,end]的最大距離點的索引imax和最大距離smax */
static int max_dist_at(
const cg_vertex_t* start,
const cg_vertex_t* end,
const cg_vertex_t* pl, int np,
double *smax)
{
int i, imax = 0;
double s;
*smax = 0;
// 計算全部點到直線的距離, 找出最大的點
for(i=0; i<np; i++){
s = CG_vertex_get_dist_to_line(&pl[i], start, end, 0);
if (s > *smax){
*smax = s;
imax = i;
}
}
return imax;
}
/* 遞歸重採樣方法*/
static void resample_recursive(
const cg_vertex_t* start,
const cg_vertex_t* end,
const cg_vertex_t* pl,
int np,
cg_vertex_t *pout,
int *nout,
const double *dist)
{
double smax;
int imax;
if (np==0) return;
imax = max_dist_at(start, end, pl, np, &smax);
if (imax==0||imax==np-1) return;
if (smax<*dist) return;
resample_recursive( start, &pl[imax], pl, imax, pout, nout, dist);
pout[(*nout)++] = pl[imax];
resample_recursive( &pl[imax], end, &pl[imax+1], np-imax-1, pout, nout, dist);
}
/**
* CG_vertices_fit_resample
* 曲線重採樣擬合, 返回擬合之後的pout數組的點數
*/
int CGAL_CALL CG_vertices_fit_resample (
IN const cg_vertex_t* start, /* 折線或多邊形的起點*/
IN const cg_vertex_t* end, /* 折線或多邊形的終點*/
IN const cg_vertex_t* mids, /* 折線或多邊形的中間點數組*/
IN int nmid, /* 折線或多邊形的中間點數組的點數*/
OUT cg_vertex_t* pout, /* 返回擬合後的中間點數組, 點數組由客戶分配,
至少與輸入的中間點數組的點數相同*/
IN double dist /* 擬合的最小距離, 必須指定>0的有效值*/
)
{
int nout = 0;
resample_recursive(start, end, mids, nmid, pout, &nout, &dist);
return nout;
}
/**
* CG_vertex_insert_pt_spline
* 計算張力樣條插值點: p1, p2之間的點, 返回插值點數目
* p0-----p1........p2------p3
* 本程序爲減少數據複製, 採用points數組返回插值點或擬合後的值,
* 因此調用者必須分配足夠的空間以存儲中間計算結果和返回的值
* 必須滿足:
* size > (|p1,p2|/step)*2
* 如果設置的step太小, 系統則自動根據size/2來調整step, 確保
* 計算能正確進行
*/
int CGAL_CALL CG_vertex_insert_pt_spline (
IN double step, /* 插值的步長, 必須指定有效的步長*/
IN cg_vertex_t p0, /* 起點*/
IN cg_vertex_t p1, /* 插值段的起點*/
IN cg_vertex_t p2, /* 插值段的終點*/
IN cg_vertex_t p3, /* 終點*/
IN OUT cg_vertex_t *points, /* 返回的插值點數組, 數組由調用者分配*/
IN int size, /* 插值點數組的最大尺寸, 此尺寸必須足夠容納2倍的插值點數*/
IN double ratio, /* 張力系數[0,1], 最佳0.1666667. -1 爲取默認.1666667 */
IN double dist /* 擬合的最小距離, 必須>0. <=0 不擬合*/
)
{
int i, count, at;
double x, y, ca, sa, s12, h1, h2, h3, u1, u2, v1, v3, d1, d2, D, M1, M2;
cg_vertex_t end = {p2.x, p2.y};
// 必須計算p1,p2距離, 確保不會溢出
d1 = p2.x-p1.x;
d2 = p2.y-p1.y;
s12 = sqrt(d1*d1 + d2*d2);
// p1,p2距離太小, 返回
if (dist>0 && s12 < dist)
return 0;
// 步距離太長, 計算p1與p2中點即可
if (step > s12/2 ){
points[0].x = (p2.x+p1.x)/2;
points[0].y = (p2.y+p1.y)/2;
return 1;
}
// 平移原點至p1
p0.x -= p1.x;
p0.y -= p1.y;
p2.x = d1;
p2.y = d2;
p3.x -= p1.x;
p3.y -= p1.y;
// 旋轉至p1->p2爲x軸方向
ca = p2.x / s12; // cos(a)
sa = p2.y / s12; // sin(a)
x = p0.x * ca + p0.y * sa;
p0.y = -p0.x * sa + p0.y * ca;
p0.x = x;
p2.x = s12;
p2.y = 0;
x = p3.x * ca + p3.y * sa;
p3.y = -p3.x * sa + p3.y * ca;
p3.x = x;
// 判斷是否是單值函數
if ( p0.x * p2.x >= 0 || p2.x * (p2.x-p3.x) >= 0 )
return 0;
// 計算係數, 這裏爲清晰起見, 沒有優化代碼
h1 = -p0.x;
h2 = p2.x;
h3 = p3.x - p2.x;
u1 = h2/(h1+h2); // u1=p2.x/(p2.x-p0.x);
u2 = h2/(h2+h3); // u2=p2.x/(p3.x-p1.x);
v1 = -p0.y;
v3 = p3.y;
d1 = -6.0*p0.y/((h1+h2)*p0.x);
d2 = 6.0*p3.y/(h3*p3.x);
D = 4.0-u1*u2;
if(D < 0.001)
return 0;
M1 = (2*d1-u1*d2)/D;
M2 = (2*d2-u2*d1)/D;
// 計算插值點數, 如果點數超過size/2, 則自動增大step
count = (int) floor((s12+step/2)/step);
if (count > size/2){
count = size/2;
step=s12/(count+1);
}
// 張力系數, 最佳=0.16666667
if(ratio<0) ratio = 0.16666667;
x = 0.0;
y = 0.0;
at = (dist>0 && count<=size/2)? count:0;
for(i=0; i < count; i++){
x += step;
y = ratio*x*(M1*(h2-x)*(x-2*h2)+M2*(x-h2)*(x+h2))/h2;
points[at+i].x = x*ca - y*sa + p1.x;
points[at+i].y = x*sa + y*ca + p1.y;
}
// 需要重採樣: p1 as start, p2 as end
if (dist>0)
count = CG_vertices_fit_resample(&p1, &end, points+at, count, points, dist);
return count;
}
7 我還提供了一個MFC的測試程序。你只要用鼠標在上面點即可。測試程序演示如何調用上面的函數,實現繪製連續樣條曲線。測試程序在下面的鏈接可以下載得到:
http://download.csdn.net/source/1177219
程序沒實現封閉的樣條曲線。在封閉部分有:
pn-3~~~~qw~pn-2=====pn-1=====p0=====p1~q1~~~~p2
其中爲做曲線計算的部分以=====表示。讀者可以多次調用CG_vertex_insert_pt_spline以分別計算擬合pn-2=====pn-1、pn-1=====p0和p0=====p1段的曲線。
2009-4-5 by win32 -cheungmine -sdk