转自:https://zhuanlan.zhihu.com/p/344934774
引入
在画画的时候,你可能会遇到画曲线的情况。比如你想画一个肥宅的大肚子轮廓,此时你随手一画,发现不好看,感觉太鼓了,于是你只能重新画,再画一遍,发现太小了,于是只能再重新画,如此反复许多次之后,你终于画对了。
作为一个天才小画家,你心里想,如果有一个小滑块,可以在保证曲线平滑的情况下,通过拉动滑块实现曲线形状的调节,那不就不用来回画了吗!
嘿,您别说,还真有,这个东西就叫做贝塞尔曲线(Bézier curve),有了这个,你便可以像这样调节曲线:
是不是很熟悉?没错!贝塞尔曲线广泛应用于各种绘图相关的软件中,甚至计算机中的字体设计就全靠贝塞尔曲线来控制。
接下来,我们详细讲一讲贝塞尔曲线的原理。
一个简单的例子
讲之前,我们先看一张图:
这里的 P0、P1、P2 分别称之为控制点,贝塞尔曲线的产生完全与这三个点位置相关。
这也就意味着,我们可以通过调节控制点的位置,进而调整整个曲线。
贝塞尔曲线是一个对强迫症极其友好的曲线,看这个动图就让人很舒适,而它的构造方法也一样让人很舒适。
最开始,对于绿色线段的两头 Q0 和 Q1,将其分别放在 P0 和 P1 的位置,此时让它们运动,要求:Q0 往 P1 方向,Q1 往 P2 方向,分别匀速运动,并且同时到达线段的另一头。
转化成数学公式,即为
在绿色线段上再取一个点 B ,如果 B 在绿色线段上的运动也满足上述的规律,那岂不是很爽!所以不妨再规定:
令上述等式等于 t,t 肯定是 [0,1] 的,其意义是点在它所处线段的位置。那么随着 t 的增大,Q0、Q1、B 的位置也就随之确定了!最终 B 的轨迹,便构成了贝塞尔曲线。
递归性质
仔细观察一下上述的构造过程,我们可以观察到:
首先,有三个控制点;
三个控制点形成两个线段,每个线段上有一个点在运动,于是得到两个点;
两个点形成一个线段,这个线段上有一个点在运动,于是得到一个点;
最后一个点的运动轨迹便构成了贝塞尔曲线!
我们发现,实际上是每轮都是 n 个点,形成 n-1 条线段,每个线段上有一个点在运动,那么就只关注这 n-1 个点,循环往复。最终只剩一个点时,它的轨迹便是结果。
那么,似乎最开始的控制点,也不一定是三个?如果是四个、五个,甚至更多呢?
当然有——
如此一来,你会发现贝塞尔曲线内的递归结构。实际上,上述介绍的分别是三阶、四阶、五阶的贝塞尔曲线,贝塞尔曲线可以由阶数递归定义。
公式
到了最讨厌的公式环节。
点在线段上,可以用 来表示。如果你把整个递归的每一步都按这个展开,那么最终可以得到如下公式:
分段贝塞尔曲线
虽然贝塞尔曲线的阶数可以很高,但是如果曲线的阶数过高,调整控制点对曲线的影响就比较小,调整起来相当麻烦。
于是,我们常常使用分段的贝塞尔曲线,保证每一小段不会太复杂。这样每次只用调小段,还可以做到只调局部不影响大局,那就相当舒服了。
分段带来的唯一问题是,曲线在段与段的交界处,如何保证平滑?
所谓平滑,其实就是一阶导数连续,也就是左右导数的极限相同。
对两侧的贝塞尔曲线求导,分别代入 t=0 和 t=1 (即贝塞尔曲线的开始和结束时间),让二者相等。此时能发现,当两侧控制点与分段交接点共线且形成的线段长度相等时,满足曲线平滑性质。
再分享一个网站,可以在线玩贝塞尔曲线:链接
实验
内容:
- 完成贝塞尔曲线的递归定义函数。
- 对曲线进行反走样(Anti-Aliasing, AA)。
解析
简单的不得了,直接带入点在线段上的公式即可。
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{// TODO: Implement de Casteljau's algorithmif(control_points.size() == 1) return control_points[0];std::vector<cv::Point2f> a;for(int i = 0;i+1 < control_points.size();i ++) {auto p = control_points[i] + t * (control_points[i+1] - control_points[i]);a.push_back(p);}return recursive_bezier(a, t);
}
对于 AA ,只需要在曲线附近做点插值就行,满足离曲线越近的像素的像素值越高,越远的越低,即可。这里随便写了点:
- For 2*2 grids, for each .
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{// TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's // recursive Bezier algorithm.double delta = 0.001;for(double t = 0;t <= 1;t += delta) {auto point = recursive_bezier(control_points, t);int w = 1;for(int i = -w+1;i <= w;i ++) {for(int j = -w+1;j <= w;j ++) {int x = point.x + i, y = point.y + j;double dist = sqrt(pow(point.x-x,2) + pow(point.y-y,2));window.at<cv::Vec3b>(y, x)[1] = std::min(window.at<cv::Vec3b>(y, x)[1] + 255 * std::max(2-exp(dist),0.0), 255.0);// auto k = abs(((int)(point.x+1-i))-point.x) * abs(((int)(point.y+1-j))-point.y);// window.at<cv::Vec3b>(y, x)[1] = std::min(window.at<cv::Vec3b>(y, x)[1] + 255 * k, 255.0f);}}}}
注:图源网络、games101课件
本文首发于知乎专栏图形图像与机器学习,以后会常更新,欢迎大家的关注与催更。