牛人请进,贝塞尔曲线生成算法

tuxw 2003-12-16 03:31:02
转了好几个论坛,找不到答案

给定一条贝塞尔曲线的四(一个起点,两个控制点,一个终点)个关键点,在VC下可以用 PolyBezier 生成这条曲线,请问 PolyBezier 是如何实现的?就是怎样用(只能用) moveto、lineto 来生成这条曲线?
...全文
941 22 打赏 收藏 转发到动态 举报
写回复
用AI写文章
22 条回复
切换为时间正序
请发表友善的回复…
发表回复
flying520520 2003-12-29
  • 打赏
  • 举报
回复
好!
tuxw 2003-12-19
  • 打赏
  • 举报
回复
谢谢,我忽略了这个问题,最小精度实际应该是1个逻辑单位,比1小后是没意义了

下面是我现在的可以控制递归深度的测试函数

int myPolyBezierToEx( CDC *pDC, const POINT p0, const POINT p1,
const POINT p2, const POINT p3,
int depth )
{
long double d1 = PointToLine2( p1, p0, p3 );
long double d2 = PointToLine2( p2, p0, p3 );
if( d1 < d2 ) d1 = d2;

if( d1 < 1.0001 || depth <= 0 ) {
pDC->LineTo( p3 );
return depth;
}

// 折分曲线
...

myPolyBezierToEx( pDC, p0, p11, p22, p33, depth-1 );
myPolyBezierToEx( pDC, p33, p32, p31, p3, depth-1 );
}

理论上来,递归越深,生成的曲线越平滑,这一点在画椭圆、圆等比较规则的曲线时是符合的,我将那个判断点距的条件去掉,递归到10层以上时,速度的优势就基本表现不出来了。

但在画字体轮廓时(由多段短曲线构成),一般递归4、5层就比较好了,加上点距条件,观察返回结果,发现递归的层恰好也在4、5层左右,但如果去掉点距条件,强行递归更深层次,效果反而越来越差,对此很是不解????

开始想可能是坐标不能为小数,导致递归一定数后,那个取中点的算法已经在原地踏步了,但这样应该只是牺牲一时间,不应该带来曲线质量变化?还是因为我的逻辑精度远高于设备精度,导致显示器无法表现?我要拿这算法来做控制的,现在只是在显示器上模拟,具体效果还有待考证……


谢谢大家的指点,本帖结了,请继续关注我的下一个问题:
http://expert.csdn.net/Expert/topic/2569/2569601.xml?temp=.2605249
(不规则区域填充算法)
tuxw 2003-12-18
  • 打赏
  • 举报
回复
我测试了,前面说的内凹点确实由连p1p2引起的,直接连p0p3后好了很多(个别地方还是有),但生成的曲线圆滑度明显不够,这就迫使我给定更小的误差值,这又取决求点到直线距离的精度,用我上面帖出的函数,那个比较误差最小为0.9,再小程序一运行就退出了,估计进入了无限递归。

你上面给的求点到直线的方法,因为要求 ∠PtP1P2,可能仍然要用 atan (目前我只想到这种方法,就是求tp1,p1p2与X轴夹角之差:P)

下面是我改求距离平方的函数
long double DistancePointToLine2( const POINT pt, const POINT p1, const POINT p2 )
{

long double a1 = atan( long double(p2.y-p1.y)/(p2.x-p1.x) );
long double a2 = atan( long double(pt.y-p1.y)/(pt.x-p1.x) );

// sin(∠PtP1P2)
long double t = sin( fabs( a1 - a2 ) );

// DistanceOfTwoPoints2 是点距平方
// [ |PtP1| * sin(∠PtP1P2) ] ^ 2
return DistanceOfTwoPoints2( pt, p1 ) * t * t;

}

现在误差可以小到0.8
可是效果还是差强人意,总是没有连p1p2那么平滑

另外,E 比1小的话,求距离平方比直接求距离趋向于 E 的速度要快,从而减小递归次,是否会因此导致误差精度上的损失呢?
saint001 2003-12-18
  • 打赏
  • 举报
回复
对于每一个t,都可以
Pi(j) = pi(j-1)*t + pi-1(j-1)*(1-t)=t*(pi(j-1)-pi-1(j-1))+pi-1(j-1)
递推得到p(t),应该是递推
就是对每个t,应用我最开始的那个回复进行递推计算
不过这样还是逐点运算,但CAGD强调线性运算和稳定性更重些,用到PC机上不知道他们是不是用这个算法

你的那个递归,实际上是用Decast算法算出t=1/2的值,然后再分段,每次只计算t=1/2的值
优点是可能有的地方t要算得深一些(密),有的地方浅一些,但这样又增加了判断递归结束的花销。
如果采用同等深度的计算,可以加上递归层数限制,比如超过10层就结束,这实际上精度就固定为1/2^10,跟逐点运算就是一样了。
dengsf 2003-12-18
  • 打赏
  • 举报
回复
我对计算几何的东西不熟悉,上面的建议都是乱来的,有更好的方法请指点。

而那个生成曲线的方法是在某段语言晦涩的书里找出来的,可能领略不了某些精妙的思想,从而导致表述的时候有所遗漏……不清楚啊不清楚!

TO saint001(saint001) :
请问“德卡算法”是什么?
“……对所有的t进行递归……”,我转过来的“只对t=1/2递归”是什么回事?
请问能否介绍一下德卡算法,并结合该例子说明一下。可以吗?
dengsf 2003-12-18
  • 打赏
  • 举报
回复
TO tuxw(醉书生):
嗯,用到中点距离代替到线段距离确实不行,前者趋向0的速度可能会比后者的慢很多。我想得不全面……

个人感觉,当误差比较小的时候,直接连p0p3,应该会比 p0p1p2p3的折线段要好一些。
因为前者明显地要比后者要光滑很多;
而且,P1,P2本来只是控制点,实际生成的曲线到这两点还是有一定差距的;既然两者都有差距,那还不如用形式较简单的POP3来代替~~不过具体怎样就不清楚了。
你说的图形放大和圆弧上看到的内凹现象,可能是因为P0P1P2P3的折线段造成的,不知直接用POP3线段代替的效果如何?

上面应该还可以再做一些优化,我说说我的建议:
1、输入的 E 是允许误差的平方,这样求距离的时候,可以直接用距离的平方跟 E 进行比较,免去 sqrt 的运算;
2、求点到直线的距离,用atan好像太浪费时间了~~我说说我的想法:
设直线上的两点是 p1,p2,想求线段外的点 pt 到该直线的距离的“平方”(跟你程序的命名方式一样),可以这样:
(pt.x - p1.x)^2 + (pt.y - p1.y)^2
- [(pt.x-p1.x)(p2.x-p1.x) + (pt.y-p1.y)(p2.y-p1.y)]^2 / [(p1.x - p2.x)^2 + (p1.y - p2.y)^2]
第一行是 pt到p1 的距离的平方。
第二行是求线段在 p1p2 上投影的线段的长度的平方。分子等于 [|p1p2|*|p1pt|*cos(<ptp1p2)]^2(<表示角度符号^ ^),分母是 |p1p2|^2。相除就得到投影距离的平方了~~
整个式子用勾股定理求出距离的“平方”,跟第一个建议结合起来,应该可以免去了sqrt,atan,cos等神奇的运算了。
txw2 2003-12-18
  • 打赏
  • 举报
回复
上面打错了,我的意思是没有就结帖了

连续回复不能超过3次,只好再注册个ID :P
tuxw 2003-12-18
  • 打赏
  • 举报
回复
进一步测试发现,在几何图形(特别是在较大的圆和椭圆)的表现上,前面那个函数效果明显要好,用递归的方法无法完全消除凸圆弧上某些点内凹的现象


公式法
x = (1-t)^3 *x0 + 3*t*(1-t)^2 *x1 + 3*t^2*(1-t) *x2 + t^3 *x3
y = (1-t)^3 *y0 + 3*t*(1-t)^2 *y1 + 3*t^2*(1-t) *y2 + t^3 *y3

递归法
Pi(j) = [Pi(j-1) + P(i-1)(j-1)] / 2

我认为公式法应该是比较通用的方法,效果应该最接近的,特别是在给定曲线两端点距离较大的弧线表现很好,递归法的优点是速度快,但只适合比较短或较长但弯曲得不厉害的曲线

再等一等,看还有没有更好的方法,如果有,就结贴了
dengsf 2003-12-18
  • 打赏
  • 举报
回复
to saint001(saint001):
原来那就是Decast算法,你上面已经说了的~~~谢谢……
根据你给的那个递推式子应该可以证明那些结论了。

to tuxw(醉书生) :
我的意思是这样来算 点pt 到 直线 p1p2 的距离的 “平方”:
long double DistancePointToLine2( const POINT pt, const POINT p1, const POINT p2 )
{
/*
d(pt, p1p2)^2 =
(pt.x - p1.x)^2 + (pt.y - p1.y)^2
- [(pt.x-p1.x)(p2.x-p1.x) + (pt.y-p1.y)(p2.y-p1.y)]^2 / [(p1.x - p2.x)^2 + (p1.y - p2.y)^2]
*/
int ptx_p1x = pt.x - p1.x;
int pty_p1y = pt.y - p1.y;
int p2x_p1x = p2.x - p1.x;
int p2y_p1y = p2.y - p1.y;
//p1->pt距离平方
double d_p1pt_2 = ptx_p1x * ptx_p1x + pty_p1y * pty_p1y;
//p1pt 跟 p1p2 的点积的平方
double dj_p1pt_p1p2_2 = (ptx_p1x*p2x_p1x + pty_p1y*p2y_p1y) * (ptx_p1x*p2x_p1x + pty_p1y*p2y_p1y);
//p1->p2的距离平方
double d_p1p2_2 = p1x_p2x*p1x_p2x + p1y_p2y*p1y_p2y;
return d_p1pt_2 - dj_p1pt_p1p2_2 / d_p1p2_2;
}

用平方来比较误差就是,假设你原来希望这样来判断误差:
d(pt,p0p3) < E
现在改为判断 d(pt,p0p3)^2 < E^2.
这跟原来的效果基本相同吧。
d(pt,p0p3)^2 在上面的求距离函数中已给出了,
E^2,可以在运行时先算一次,又或者将指定的值认为就是 误差的平方。

至于误差不能太小,我想因为"点的坐标是整数"而引起的,也可能是精度丢失造成的,我没有深入钻研过你的程序,所以上面是胡乱猜测而已。
实际上,误差E表示的是 以象素为单位 的误差,设得太小好像意义不大:)
tuxw 2003-12-17
  • 打赏
  • 举报
回复
To dengsf(杀神大菜鸟):

/*
不过德卡算法对每个t都进行递推运算,而不是只用t=1/2再加上递归
Pi(j) = pi(j-1)*t + pi-1(j-1)*(1-t)
*/

请问 t 的递归是怎样的,谢谢
tuxw 2003-12-17
  • 打赏
  • 举报
回复
TO : dengsf(杀神大菜鸟)
我试了你的方法,发现以到中点的距离平方来判断不行,因为可能一条曲本身很接近直线,但端点距离很远,会增加加递归的次数,且对往回绕的曲线误差较大,有些地方会出现平滑的圆弧上某些点往内凹。
另外很意外的是按上面的方法不递归,直接对后面的三点 LineTo,用来描述字体时已经比较近了,我又试着手工递归了三层,效果基本上很好了,只是放大后在很圆的地方会看到明显的折线段。

下面是我现在采用的版本,还是求了点到直线的距离,速度还是很快,我的视图是要用鼠标滚轮来缩放的,用视的 OnDraw 函数中用上面贴的那个函数时,每滚一下滚轮,需要1、2秒来刷新,放大倍数高时耗时更长,而下面这个版本鼠标可以连续滚动,仅仅是视图有些闪烁,
我试着用VC提供的 PolyBezierTo 生成曲线跟它重叠显示,放大到一千倍左右,基本上还能重合,只有少数总分有少量出入,所以我觉得这个算法还是可行的。

POINT CenterOfTwoPoints( POINT p1, POINT p2)
{
POINT center;
center.x = (p1.x + p2.x) >> 1;
center.y = (p1.y + p2.y) >> 1;

return center;
}

double DistanceOfTwoPoints( const POINT p1, const POINT p2 )
{
return sqrt( (p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y) );
}

double DistancePointToLine( const POINT pt, const POINT p1, const POINT p2 )
{
double a1 = atan( double(p2.y-p1.y)/(p2.x-p1.x) );
double a2 = atan( double(pt.y-p1.y)/(pt.x-p1.x) );

a1 = 3.14159265/2 - fabs( a1 - a2 );

return fabs( DistanceOfTwoPoints( pt, p1 ) * cos(a1) );
}

void myPolyBezierTo( CDC *pDC, const POINT p0, const POINT p1,
const POINT p2, const POINT p3 )
{
double d1 = DistancePointToLine( p1, p0, p3 );
double d2 = DistancePointToLine( p2, p0, p3 );
if( d1 < d2 ) d1 = d2;

if( d1 < 1.00002 ) {
pDC->LineTo( p1 );
pDC->LineTo( p2 );
pDC->LineTo( p3 ); // 本来只要这句就可以了,但加上前两句精度更好一些

return;
}

// 使用公式 P(i)(j) = [p(i)(j-1) + p(i-1)(j-1)] / 2
// 将原曲线分成两条曲线:( p00, p11, p22, p33 ) , ( p33, p32, p31, p30 )
// P00
// P10 P11
// P20 P21 P22
// P30 P31 P32 P33
POINT p11, p21, p22, p31, p32, p33;

p11 = CenterOfTwoPoints(p1, p0);

p21 = CenterOfTwoPoints(p2, p1);
p22 = CenterOfTwoPoints(p21, p11);

p31 = CenterOfTwoPoints(p3, p2);
p32 = CenterOfTwoPoints(p31, p21);
p33 = CenterOfTwoPoints(p32, p22);

myPolyBezierTo( pDC, p0, p11, p22, p33 );
myPolyBezierTo( pDC, p33, p32, p31, p3 );
}
dengsf 2003-12-17
  • 打赏
  • 举报
回复
唉,老是这样,一不小心就按错了。继续:

对 P0,P1,P2,P3 4个点,先作如下判断:
如果 max( d(p1, p0p3), d(p2, p0p3) ) < E (d()是点到直线距离,E是给定的误差范围,表示p0,p2,p3,p3的凸包的边跟生成曲线的最大差距。凸包我不知道是什么,可能是这四个点所能组成的最大的凸四边形吧。)
如果 满足 条件,则简单用线段 P0P3 代替生成的曲线(因为P0P3 也在其生成的凸包里面,所以其误差也一定小于E)。退出。
如果不满足,则作如下运算:

P0(0)
P1(0) P1(1)
P2(0) P2(1) P2(2)
P3(0) P3(1) P3(2) P3(3)
其中 Pi(0) = Pi
Pi(j) = [pi(j-1) + pi-1(j-1)] / 2

可以证明,
p3(3)在原来的曲线上面,
以 p0(0), p1(1), p2(2), p3(3) 生成的BEZIER曲线跟 原来生成的曲线的P0 - P3(3)那部分完全重合;
P3(3),p3(2),P3(1),P3(0) 生成的BEZIER曲线跟 原来生成的曲线的 P3(3) - P3 那部分完全重合。

这样,用这 8 个点生成两段曲线 来代替原来的做法,并对 每 4 个点也重复上面的过程~~~~


感觉这样的空间利用比较多,应该还可以再进行一些代码级的优化。如果用汇编来编写,则效率可能更高。
dengsf 2003-12-17
  • 打赏
  • 举报
回复
我找了一下,发现实际应用时是通过递归逼近来实现的,我说说我看到的大概做法:

对 P0,P1,P2,P3
saint001 2003-12-17
  • 打赏
  • 举报
回复
dengsf(杀神大菜鸟):
你说的类似那个德卡斯特里奥算法
不过德卡算法对每个t都进行递推运算,而不是只用t=1/2再加上递归
Pi(j) = pi(j-1)*t + pi-1(j-1)*(1-t)
dengsf 2003-12-17
  • 打赏
  • 举报
回复
是笔误,不好意思。
就是以类似于 p2(1) = [ p2(0) + p1(0) ] /2 的方式进行。


个人认为,既然 d() 本身也只是起上界的作用,不是求出精确的误差,那我们也可以在此基础上取一个更高的上界。
因为:
d( p1, p0p3 ) <= d( p1, p') (p'是 p0p3 的中点)
用这个来代替 点到直线的距离 ,程序会更简单。

再进一步,假设给定的误差 是距离的平方,即判断条件改为 d(p1, p')^2 < E,求距离 d() 的时候就不必开方了~~~~

不过,过分将上界扩大,可能在曲线实际上已经达到要求,但我们的判断条件还没有达到要求时,继续进行运算,这可能反而增加了时间。具体效果如何应该值得斟酌……
tuxw 2003-12-17
  • 打赏
  • 举报
回复
TO : dengsf(杀神大菜鸟)

上面的这算法最大的好处是去掉了浮点乘法运行算(但求点到直线的距离还是要的),估计速度会快很多,递归的浓度可以由设定的误差值控制,但下面的递推公式有点不理解,能否进一步解释一下,谢谢:
Pi(j) = [pi(j-1) + pi-1(j-1)] / 2

另外,上面的公式是不是有笔误?会不会是这样的:
Pi(j) = [Pi(j-1) + P(i-1)(j-1)] / 2
LeeMaRS 2003-12-16
  • 打赏
  • 举报
回复
M$的算法都是层层优化的。。。岂非我们可以相比
tuxw 2003-12-16
  • 打赏
  • 举报
回复
谢谢大家,下面是我按上面方法写的函数,是可以生成曲线的,我用它来画字体的轮廓,
但是速度很慢, 跟VC提供的 PolyBezierTo 简直不可同日而语, 请问下面的代码应该怎么优化一下,或有什么更简单的算法?
还有一点就是 t 循环的步长不好确定,太细则花的时间更多,且精度无法与 VC 提供的
PolyBezierTo 相比,放大后可看出明显的区别, 很想知道 VC 是怎么做到的

void myPolyBezierTo( CDC *pDC, const POINT* lpPoints )
{
// 根据起点到终点的x、y变量化量,最较大的一个用来确定步长
// 曲线不能往回绕,以保证起起到终点能表最大变化量
int dx = lpPoints[0].x - lpPoints[3].x;
if( dx < 0 ) dx *= -1;

int dy = lpPoints[0].y - lpPoints[3].y;
if( dy < 0 ) dy *= -1;

dy = dx > dy ? dx : dy;
double step = 1.0 / dy; // 步长

pDC->MoveTo( lpPoints[0] );

double x, y, t3, t2, t11, t12, t13;
for( double t = 0.0; t <= 1.0; t += step ) {
t11 = 1-t;
t12 = t11 * t11;
t13 = t12 * t11; // (1-t)^3

t2 = t * t;
t3 = t2 * t; // t^3

// x = (1-t)^3 *x0 + 3*t*(1-t)^2 *x1 + 3*t^2*(1-t) *x2 + t^3 *x3
// y = (1-t)^3 *y0 + 3*t*(1-t)^2 *y1 + 3*t^2*(1-t) *y2 + t^3 *y3
// 0 <= t <= 1
x = t13 * lpPoints[0].x
+ 3 * t * t12 * lpPoints[1].x
+ 3 * t2 * t11 * lpPoints[2].x
+ t3 * lpPoints[3].x;

y = t13 * lpPoints[0].y
+ 3 * t * t12 * lpPoints[1].y
+ 3 * t2 * t11 * lpPoints[2].y
+ t3 * lpPoints[3].y;

pDC->LineTo( x, y );
}
}
saint001 2003-12-16
  • 打赏
  • 举报
回复
给定p0,p1,p2,p3
p00=p0
p01=p1
p02=p2
p03=p3

p10=p00*(1-t)+p01*t
p11=p01*(1-t)+p02*t
p12=p02*(1-t)+p03*t

p20=p10*(1-t)+p11*t
p21=p11*(1-t)+p12*t

p30=p20*(1-t)+p21*t

p(t)=p30, 0<=t<=1
saint001 2003-12-16
  • 打赏
  • 举报
回复
德卡斯特里奥算法应用到n=3的时候
四个控制顶点p0,p1,p2,p3
p00=p0,p10=p1,p20=p2,p30=p3

p01=p00*(1-t)+p10*t
p11=p10*(1-t)+p20*t
p21=p20*(1-t)*p30*t

p02=p01*(1-t)+p11*t
p12=p11*(1-t)+p21*t

p03=p02*(1-t)+p12*t

p(t)=p03 0<=t<=1

优点:线性运算,并且中间过程生成的变量有几何意义,便于分析几何特征
加载更多回复(2)

33,007

社区成员

发帖
与我相关
我的任务
社区描述
数据结构与算法相关内容讨论专区
社区管理员
  • 数据结构与算法社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧