欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~
这篇文章讲述贝塞尔曲线的原理及代码实现,非常简单轻松的一篇。
背景
几何建模与仿真是图形学里很重要的模块,实际的应用场景中,模型的外观往往不规则,不能由简单的点、线、面生成。
贝塞尔曲线就是简单而实用的几何模型。 【掘金上不能放视频,请在“sumsmile”公众号内预览】图形渲染(4)贝赛尔曲线
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝兹(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。维基百科-贝赛尔曲线
可见工业推动科学文明
贝塞尔曲线原理
曲线原理推导
原理非常简单,跟着我的思路一步步推导。
- 按比例t在直线P0 P1上可以取一个点
- 在P0P1和P1P2上分别按比例t取两个点,连成一条线(绿色),绿线上再按比例t取一个点(黑色),之后不断更改t的取值[0~1],黑点连续移动形成的红色线条就是二次贝赛尔曲线。
你应该有感觉了,这是个典型的递归算法。
明白了二次贝塞尔曲线,再感受更高次的演化。
- 3次贝赛尔曲线
- 4次贝赛尔曲线
- 5次贝赛尔曲线
数学公式推导
上文提到,贝塞尔曲线由多个点控制。依次按比例t取线段上的点,取得的点组成线条再继续取点,最后只剩下一个点,即贝赛尔曲线的轨迹。
以二次曲线为例:
看起来就是对b0 b1 b2的坐标按权重取值。
如果是多次曲线呢?其实每个点的系数符合二次多项式的规则。
假设有n个点,那么由下面这个代数式决定曲线的轨迹,不详细讲了,耐心看看就明白了。
当然了,计算机程序不用推导这么复杂的代数,用迭代的方式计算即可。
贝塞尔曲线代码实现
实现代码、
基于openCV,屏幕上选择4个点,生成一条贝赛尔曲线。
代码有详细的注释,对照注释看代码。详细代码附录在文末。
/**
* 贝塞尔曲线递归算法,计算每个t对一个的曲线轨迹点
*
* control_points:控制贝塞尔曲线的点
* t:插值比例/权重
*/
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
if (control_points.size() == 1) {
return control_points[0];
}
std::vector<cv::Point2f> lerp_points;
for (size_t i = 1; i < control_points.size(); i++)
{
lerp_points.push_back(lerp_v2f(control_points[i - 1], control_points[i], t));
}
return recursive_bezier(lerp_points, t);
}
/**
* 触发贝塞尔曲线插值算法,获取所有曲线点
*
* control_points:控制贝塞尔曲线的点
* window:绘制曲线/点的窗口
*/
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// 每次迭代的幅度为0.001,设计足够小的步幅,使曲线更连贯,否则可能会有断点
for (double i = 0.0; i < 1.0; i+=0.001)
{
// 每一次for循环迭代,都计算出曲线上的一个轨迹点
auto point = recursive_bezier(control_points, i);
// 点的颜色为绿色,注意通道为BGR
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
实现效果
完整代码及工程
完整工程:
main.cpp完整代码:
#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>
std::vector<cv::Point2f> control_points;
int pointsize = 4;
void mouse_handler(int event, int x, int y, int flags, void *userdata)
{
if (event == cv::EVENT_LBUTTONDOWN && control_points.size() < pointsize)
{
std::cout << "Left button of the mouse is clicked - position (" << x << ", "
<< y << ")" << '\n';
control_points.emplace_back(x, y);
}
}
void naive_bezier(const std::vector<cv::Point2f> &points, cv::Mat &window)
{
auto &p_0 = points[0];
auto &p_1 = points[1];
auto &p_2 = points[2];
auto &p_3 = points[3];
for (double t = 0.0; t <= 1.0; t += 0.001)
{
auto point = std::pow(1 - t, 3) * p_0 + 3 * t * std::pow(1 - t, 2) * p_1 +
3 * std::pow(t, 2) * (1 - t) * p_2 + std::pow(t, 3) * p_3;
window.at<cv::Vec3b>(point.y, point.x)[2] = 255;
}
}
cv::Point2f lerp_v2f(const cv::Point2f& a, const cv::Point2f& b, float t)
{
return a + (b - a) * t;
}
/**
* 贝塞尔曲线递归算法,计算每个t对一个的曲线轨迹点
*
* control_points:控制贝塞尔曲线的点
* t:插值比例/权重
*/
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
if (control_points.size() == 1) {
return control_points[0];
}
std::vector<cv::Point2f> lerp_points;
for (size_t i = 1; i < control_points.size(); i++)
{
lerp_points.push_back(lerp_v2f(control_points[i - 1], control_points[i], t));
}
return recursive_bezier(lerp_points, t);
}
/**
* 触发贝塞尔曲线插值算法,获取所有曲线点
*
* control_points:控制贝塞尔曲线的点
* window:绘制曲线/点的窗口
*/
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// 每次迭代的幅度为0.001,设计足够小的步幅,使曲线更连贯,否则可能会有断点
for (double i = 0.0; i < 1.0; i+=0.001)
{
// 每一次for循环迭代,都计算出曲线上的一个轨迹点
auto point = recursive_bezier(control_points, i);
// 点的颜色为绿色,注意通道为BGR
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
int main()
{
cv::Mat window = cv::Mat(700, 700, CV_8UC3, cv::Scalar(0));
cv::cvtColor(window, window, cv::COLOR_BGR2RGB);
cv::namedWindow("Bezier Curve", cv::WINDOW_AUTOSIZE);
cv::setMouseCallback("Bezier Curve", mouse_handler, nullptr);
int key = -1;
while (key != 27)
{
for (auto &point : control_points)
{
cv::circle(window, point, 3, {255, 255, 255}, 3);
}
if (control_points.size() == pointsize)
{
naive_bezier(control_points, window);
bezier(control_points, window);
cv::imshow("Bezier Curve", window);
cv::imwrite("my_bezier_curve.png", window);
key = cv::waitKey(0);
return 0;
}
cv::imshow("Bezier Curve", window);
key = cv::waitKey(20);
}
return 0;
}
欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵~~