总览
Bézier 曲线是一种用于计算机图形学的参数曲线。在本次作业中,你需要实现 de Casteljau 算法来绘制由 4 个控制点表示的 Bézier 曲线 (当你正确实现该
算法时,你可以支持绘制由更多点来控制的 Bézier 曲线)。
你需要修改的函数在提供的 main.cpp 文件中。
• bezier
:该函数实现绘制 Bézier 曲线的功能。它使用一个控制点序列和一个 OpenCV::Mat
对象作为输入,没有返回值。它会使 t 在 0 到 1 的范围内进行迭代,并在每次迭代中使 t 增加一个微小值。对于每个需要计算的 t,将调用另一个函数recursive_bezier
,然后该函数将返回在 Bézier 曲线上 t 处的点。最后,将返回的点绘制在 OpenCV::Mat
对象上。
• recursive_bezier
:该函数使用一个控制点序列和一个浮点数 t 作为输入,实现 de Casteljau 算法来返回 Bézier 曲线上对应点的坐标。
算法
De Casteljau 算法说明如下:
- 考虑一个 p0, p1, … pn 为控制点序列的 Bézier 曲线。首先,将相邻的点连接
起来以形成线段。 - 用 t : (1 − t) 的比例细分每个线段,并找到该分割点。
- 得到的分割点作为新的控制点序列,新序列的长度会减少一。
- 如果序列只包含一个点,则返回该点并终止。否则,使用新的控制点序列并转到步骤 1。使用 [0,1] 中的多个不同的 t 来执行上述算法,你就能得到相应的 Bézier 曲线。
开始编写
在本次作业中,你会在一个新的代码框架上编写,它比以前的代码框架小很多。和之前作业相似的是,你可以选择在自己电脑的系统或者虚拟机上完成作业。
请下载项目的框架代码,并使用以下命令像以前一样构建项目:
$ mkdir build
$ cd build
$ cmake . .
$ make
之后,你可以通过使用以下命令运行给定代码 ./BezierCurve
。运行时,程序将打开一个黑色窗口。现在,你可以点击屏幕选择点来控制 Bézier 曲线。程
序将等待你在窗口中选择 4 个控制点,然后它将根据你选择的控制点来自动绘制 Bézier 曲线。代码框架中提供的实现通过使用多项式方程来计算 Bézier 曲线并绘制为红色。两张控制点对应的 Bézier 曲线如下所示:
在确保代码框架一切正常后,就可以开始完成你自己的实现了。注释掉 main
函数中 while
循环内调用 naive_bezier
函数的行,并取消对 bezier
函数的注释。要求你的实现将 Bézier 曲线绘制为绿色。
如果要确保实现正确,请同时调用 naive_bezier
和 bezier
函数,如果实现正确,则两者均应写入大致相同的像素,因此该曲线将表现为黄色。如果是这样,你可以确保实现正确。
你也可以尝试修改代码并使用不同数量的控制点,来查看不同的 Bézier 曲线。
作业代码
bezier
函数如下,主要从[0,1]迭代 t
,代入 recursive_bezier
求出当前 t
对应的贝塞尔曲线上的点。
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
float t = 0.0f;
for(int i=0; i<1000; ++i) {
t += 0.001f;
// recursive Bezier algorithm.
cv::Point2f point = recursive_bezier(control_points, t);
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
recursive_bezier
函数如下,一开始是四个点,三条边,根据 t
选择三条边上的三个点,然后生成两条边,再根据 t
选择两边上的两个点,然后生成一条边,再根据 t
选择一条边上的一个点作为最后的选定的贝塞尔曲线上的点。
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
// TODO: Implement de Casteljau's algorithm
std::vector<cv::Point2f> temp;
std::vector<cv::Point2f> c_p(control_points);
while(c_p.size() > 1) {
for(int i=0; i<c_p.size()-1; ++i) {
float distX = c_p[i+1].x - c_p[i].x;
float distY = c_p[i+1].y - c_p[i].y;
temp.push_back(cv::Point2f(c_p[i].x+t*distX, c_p[i].y+t*distY));
}
c_p.clear();
for(auto x:temp) {
c_p.push_back(x);
}
temp.clear();
}
return c_p[0];
}
结果如下
可以看出有明显走样。
提高作业
这里进行反走样操作,对于选中的点,判断其到周围像素中心点的哈夫曼距离,对于哈夫曼距离大于等于2的不产生影响,小于2的,其影响系数为 (2-hafuman_dist)/2。修改后的 bezier
函数如下
float get_hafuman_dist(int x, int y, cv::Point2f point)
{
float x_center = (float)x + 0.5f;
float y_center = (float)y + 0.5f;
return fabs(point.x - x_center) + fabs(point.y - y_center);
}
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
float t = 0.0f;
for(int i=0; i<1000; ++i) {
t += 0.001f;
// recursive Bezier algorithm.
cv::Point2f point = recursive_bezier(control_points, t);
int x = std::min(window.cols-1, std::max(0, (int)point.x));
int y = std::min(window.rows-1, std::max(0, (int)point.y));
int inc[3] = {0, -1, 1};
for(int j=0; j<3; ++j)
for(int k=0; k<3; ++k) {
int new_x = std::min(window.cols-1, std::max(0, x+inc[j]));
int new_y = std::min(window.rows-1, std::max(0, y+inc[k]));
float dist = get_hafuman_dist(new_x, new_y, point);
if(dist >= 2.0f)
continue;
window.at<cv::Vec3b>(new_y, new_x)[1] = std::max(window.at<cv::Vec3b>(new_y, new_x)[1], (u_char)((2.0f-dist)*255/2.0f));
}
}
}
其结果明显比之前要好