贝塞尔曲线拟合

Haoxiang Li bio photo By Haoxiang Li

接上一篇Blog,这里用贝塞尔曲线来平滑多个点。

和样条插值不同,在多个点上作贝塞尔曲线的时候,曲线只穿过首尾两个点,中间的点都是作为控制点。

移动控制点,曲线也随之形变,可以造成一种拉扯的效果。在各种作图工具中,经常使用贝塞尔曲线来画曲线。一般的操作都是先画一条线段,然后可以通过拖动一个控制点来调整线段的弯曲程度。

作多点贝塞尔曲线只需要一个公式。所有的点的X值,被归一化到[0,1]区间内。 具体理论,可以参考这个页面Bézier curves。89年创建的,可有年头了。

这里还是贴代码吧。

首先需要得到X区间的总长度。

CGPoint startPt = [[_points objectAtIndex:0] CGPointValue];
CGPoint endPt = [[_points objectAtIndex:(self.pointCount - 1)] CGPointValue];
float amount = endPt.x - startPt.x;

然后就是曲线方程了,这个比样条插值要简单不少。 rank是指总的阶数,也就是实际的点数。这个函数表示n个点的贝塞尔曲线在x处的值。 这里的ux属于区间[0,1]

float (^bezierSpline)(int rank, float ux) = ^(int rank, float ux) {
        
    float p = 0.0f;
        
    for (int i = 0; i < rank; i++)
    {
        CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            
        p += pt.y * powf((1 - ux), (rank - i - 1)) * powf(ux, i) * (factorial(rank - 1) 
                  / (factorial(i) * factorial(rank - i - 1)));
    }
    
    return p;
};

下面很容易了,画图的时候步长为1,求得ux之后,带入方程得到点的y值,画曲线。

    [path moveToPoint:startPt];
    
    for (float curX = startPt.x; (curX - endPt.x) < 1e-5; curX += 1.0f)
    {
        float u = (curX - startPt.x) / amount;
        [path addLineToPoint:
            CGPointMake(curX, bezierSpline(self.pointCount, u))];
    }
    
    CGContextSetLineWidth(context, 1.0f);
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    CGContextAddPath(context, path.CGPath);
    CGContextStrokePath(context);
}

和三次样条相比,由于不经过中间点,丢失细节比较多,平滑度更好。

平滑前 no bezier

三次样条 no bezier

贝塞尔曲线 no bezier

示例代码: Sample-CurveFit 和上个例子放在一个repo里。

参考: Wiki