一、问题总览
实现de Casteljau算法来绘制由4个控制点表示的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曲线上对应点的坐标。
二、参考答案
2.1 算法思想
De Casteljau 算法说明如下:
- 考虑一个p0, p1, … pn 为控制点序列的Bézier曲线。首先,将相邻的点连接
起来以形成线段。 - 用t : (1 − t) 的比例细分每个线段,并找到该分割点。
- 得到的分割点作为新的控制点序列,新序列的长度会减少一。
- 如果序列只包含一个点,则返回该点并终止。否则,使用新的控制点序列并转到步骤1。
使用[0,1] 中的多个不同的t来执行上述算法
-
例子如下
-
b
0
b_0
b0,
b
1
b_1
b1,
b
2
b_2
b2为三个参考点;
- 在 b 0 b 1 b_0b_1 b0b1 上找一点 b 0 1 b_0^1 b01,使得 b 0 b 0 1 b_0b_0^1 b0b01 : b 0 1 b 1 b_0^1b_1 b01b1 = t : (1 - t)
- b 1 1 b_1^1 b11 同理
- b 0 1 b_0^1 b01 与 b 1 1 b_1^1 b11 作为新的参考点,找点 b 0 2 b_0^2 b02,使比例关系满足t : (1 - t)
- b 0 2 b_0^2 b02 就是贝塞尔曲线上的一点,使用[0,1] 中的多个不同的t来执行上述算法
-
b
0
b_0
b0,
b
1
b_1
b1,
b
2
b_2
b2为三个参考点;
2.2 代码实现
2.2.1 Bezier函数的实现
- t=0 -> t=1, 调用de Casteljau算法
//cv::Mat &window:表示屏幕矩阵;矩阵内元素为CV_8UC3类型(无符号8位整数,RGB三通道,cv::Vec3b)
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.
for(double t = 0.0; t < 1.0; t += 0.001){
cv::Point2f point = recursive_bezier(control_points, t);
// 绘制坐标(point.y, point.x)的颜色为绿色,[1]表示RGB中的G
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
2.2.2 Recursive_bezier函数的实现
- 实现de Casteljau 算法来返回Bézier曲线上对应点的坐标
//cv::Point2f float类型的二维点坐标
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
// TODO: Implement de Casteljau's algorithm
if(control_points.size() == 1) return control_points[0];
std::vector<cv::Point2f> next_layer_control_points;
for(int i = 0; i < control_points.size() - 1; i++){
cv::Point2f p0 = control_points[i];
cv::Point2f p1 = control_points[i+1];
cv::Point2f p2 = p0 + t * (p1 - p0);
next_layer_control_points.push_back(p2);
}
return recursive_bezier(next_layer_control_points, t);
}
2.2.3 实现对贝塞尔曲线的反走样(奖励分数)
-
对于一个曲线上的点,不只把它对应于一个像
素,需要根据到像素中心的距离来考虑与它相邻的像素的颜色
- P是贝塞尔曲线上t对应的一点,P0是周围四个像素区域的交点,像素框的中心点为其余四个黑点
- 像素外框都是处于window矩阵整数部分(默认一个window单元格为一个像素),所以p所在的像素框的较近一竖边为min(floor(p.x), ceil(p.x)),横边min(floor(p.y), ceil(p.y))
- 计算出p0坐标( min(floor(p.x), ceil(p.x)),min(floor(p.y), ceil(p.y)) )
- 由于像素框大小为1 * 1,所以知道P0之后可以计算出周围四个像素中心点坐标
- 根据像素中心点到P点的距离来分配颜色
- 比如距离p点距离为dist,则该点所在像素的G通道 = 255 * (3 - dist)/3
- 使用的是哈夫曼距离,p离像素中心点最大哈夫曼距离为3
- 比如距离p点距离为dist,则该点所在像素的G通道 = 255 * (3 - dist)/3
double get_dist(cv::Point2f point1, cv::Point2f point2){//计算两点的哈夫曼距离
return fabs(point1.x - point2.x) + fabs(point1.y - point2.y);
}
//cv::Mat &window:表示屏幕矩阵;矩阵内元素为CV_8UC3类型(无符号8位整数,RGB三通道,cv::Vec3b)
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.
for(double t = 0.0; t < 1.0; t += 0.001){
cv::Point2f point = recursive_bezier(control_points, t);
cv::Point2f point0( std::min(floor(point.x), ceil(point.x)), std::min(floor(point.y), ceil(point.y)) );
double dist;
std::vector<double> bias{0.5, -0.5};
for(int i = 0; i < 4; i++){
cv::Point2f centerPoint(point0.x + bias[i % 2], point0.y + bias[i % 2]);//计算中心点
dist = get_dist(point, centerPoint);
window.at<cv::Vec3b>(centerPoint.y, centerPoint.x)[1] = 255 * (3 - dist) / 3;
}
}
}
三、编译
如往常一样
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./BezierCurve
通过点击屏幕来设置控制点
附件
作业4连接