作业要求
Bézier Curve(贝塞尔曲线)是一种用于计算机图形学的参数曲线。在本次作业中,你需要实 现 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 算法说明如下:
1. 考虑一个 p0 , p1 , ... pn 为控制点序列的 Bézier 曲线。首先,将相邻的点连接 起来以形成线段。
2. 用 t : (1 − t) 的比例细分每个线段,并找到该分割点。
3. 得到的分割点作为新的控制点序列,新序列的长度会减少一。
4. 如果序列只包含一个点,则返回该点并终止。否则,使用新的控制点序列并转到步骤 1。使用 [0,1] 中的多个不同的 t 来执行上述算法,你就能得到相应的 Bézier 曲线。
评分
• [5 分] 提交的格式正确,包含所有必须的文件。代码可以编译和运行。
• [20 分] De Casteljau 算法:对于给定的控制点,你的代码能够产生正确的 Bézier 曲线。
• [5 分] 奖励分数:实现对 Bézier 曲线的反走样。(对于一个曲线上的点,不只把它对应于一个像素,你需要根据到像素中心的距离来考虑与它相邻的像素的颜色。 )
1. 基于 naive_bezier 函数控制 Bézier 曲线
naive_bezier是框架自带的函数,通过使用多项式公式直接计算 Bézier 曲线上的点。naive_bezier是静态计算曲线点, 在绘制时,没有实时响应控制点的移动,而是基于初始的控制点集进行一次性绘制,当用户拖动控制点时,曲线不会更新,因为绘制的曲线是静态的,且并未每帧重算。
实现方式:
- 对 t ∈ [0,1] 进行小步长迭代。
- 对每个 t,用公式计算 Bézier 曲线上的一个点:
- 将计算出的点绘制到图像上。
代码实现:
// 使用多项式公式绘制 Bézier 曲线的函数(非递归方法)
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];
// 通过从 t=0 到 t=1 的小步长迭代计算 Bézier 曲线上的点
for (double t = 0.0; t <= 1.0; t += 0.001)
{
// 使用多项式公式计算 Bézier 曲线点
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;
}
}
运行结果:
2. 基于bezier 函数控制 Bézier 曲线
在确保代码框架一切正常后,就可以开始完成你自己的实现了。注释掉 main 函数中 while 循环内调用 naive_bezier 函数的行,并取消对 bezier 函数的注释。要求将 Bézier 曲线绘制为绿色。
bezier
函数使用 de Casteljau 算法 递归动态计算曲线点,每次绘制时都会重新计算 Bézier 曲线点。由于曲线点每帧都根据当前控制点实时计算,所以当用户拖动控制点时,曲线能即时更新。
实现方式:
- 对 t ∈ [0,1] 进行小步长迭代。
- 对每个 t,调用 recursive_bezier 递归计算 Bézier 曲线上的点。
- recursive_bezier 的逻辑是:
- 如果控制点数为 1,则返回该点。
- 否则,对控制点进行线性插值,生成下一组控制点,递归计算。
关于bezier曲线的定义:给定点 P0 ,P1 ,..., Pn ,则n次贝塞尔曲线由下式给出:
n次贝塞尔曲线可由如下递归表达:
代码实现:
// 使用递归的 de Casteljau 算法绘制 Bézier 曲线的函数
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// 使用递归算法计算 Bézier 曲线点
for(double t = 0.0; t <= 1.0; t += 0.001){
cv::Point2f point = recursive_bezier(control_points,t);
// 确保点在图像窗口范围内,然后着色
if(point.x >= 0 && point.x < window.cols && point.y >= 0 && point.y < window.rows){
// 将计算的点标记为绿色
//在 cv::Vec3b 中,像素的 R、G、B 通道分别存储在索引 [2]、[1] 和 [0] 中:
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
}
注意,cv::Vec3b 是一个包含 3 个分量的向量,分别对应 B(蓝色)、G(绿色)、R(红色)。
[1] 表示访问 cv::Vec3b 中的第二个分量,即绿色通道(G)。
在 cv::Vec3b 中:
-
[0]
对应蓝色通道(B)。 -
[1]
对应绿色通道(G)。 -
[2]
对应红色通道(R)。
// 使用 de Casteljau 算法递归计算 Bézier 曲线点的函数
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
// TODO: Implement de Casteljau's algorithm
// 递归的终止条件:当控制点数量减少到仅剩一个点时,这个点就是 Bézier 曲线在特定参数t下的结果点。
if(control_points.size() == 1){
return control_points[0];
}
// 用于存储中间点的向量
std::vector<cv::Point2f> next_points;
// 通过线性插值计算相邻控制点之间的新点
for(size_t i=0; i < control_points.size()-1; i++){
cv::Point2f point = (1-t) * control_points[i]+t * control_points[i+1];
next_points.push_back(point);
}
// 递归计算 Bézier 曲线点
return recursive_bezier(next_points, t);
}
运行结果:
通过拖动控制点可以控制曲线。
放大后可以看到明显的锯齿:
特性 | naive_bezier | bezier |
算法类型 | 静态公式计算 | 动态递归计算 |
计算复杂度 | O(n)O(n)O(n)(与步长 ttt 的取值相关) | O(n2)O(n^2)O(n2)(递归带来额外开销) |
实时性 | 直接公式计算效率高 | 递归开销较高,性能稍逊 |
动态控制点响应 | 需要额外调整主循环逻辑来响应控制点拖动 | 每帧动态重新计算,天然支持控制点变化 |
代码复杂度 | 简单清晰,便于理解 | 使用递归,逻辑较复杂 |
扩展性 | 仅适用于 4 个控制点的固定 Bézier 曲线 | 支持任意数量的控制点 |
3. Anti-aliasing (反走样)
核心问题
在渲染 Bézier 曲线时,如果只为曲线点赋予颜色,而忽略周围像素的影响,可能会产生锯齿现象,即曲线看起来不平滑。
Anti-aliasing 的目标
根据 Bézier 点与像素中心的距离,调整周围像素的颜色强度。让曲线的颜色在周围像素中逐渐过渡,从而减轻锯齿感。
Anti-aliasing 的思路
1.计算 Bézier 曲线点:
遍历曲线的参数 t,精确生成曲线上的点。
2.影响区域:
让 Bézier 点对其周围 3×3像素区域产生影响。
Bézier 点point到9个邻域像素中心的最大距离是 3/√2(point在中心像素的某个顶点),最小距离是0( point与像素中心重合) 。
3.距离权重:
根据 Bézier 点与像素中心的距离,分配颜色强度(距离越近,颜色越亮)。
4.归一化处理:
对距离进行归一化,将其映射到 [0, 1],确保颜色强度计算在固定范围内。
距离越小,强度越高。当 normalized_distance = 0(像素距离 Bézier 点最近),颜色强度为最大值 255。
距离越大,强度越低。当 normalized_distance = 1(像素距离 Bézier 点最远),颜色强度为最小值 0。
5.更新策略:
使用较高颜色强度更新像素,防止覆盖已有的较亮颜色。
代码实现:
// 使用递归的 de Casteljau 算法绘制 Bézier 曲线的函数
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// 遍历参数 t,从 0 到 1,以步长 0.001 逐步生成 Bézier 曲线上的点
for(float t = 0.0f; t <= 1.0f; t += 0.001f){
// 使用递归 de Casteljau 算法计算参数 t 对应的 Bézier 曲线点
cv::Point2f point = recursive_bezier(control_points, t);
// 遍历 Bézier 点周围的 3x3 区域像素
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
// 计算邻近像素的中心点坐标
// 使用 std::floor 取整,然后加 0.5 得到像素中心
float pixel_center_x = std::floor(point.x + i) + 0.5f;
float pixel_center_y = std::floor(point.y + j) + 0.5f;
// 计算 Bézier 点与当前像素中心的欧几里得距离
float distance = std::sqrt(std::pow(point.x - pixel_center_x, 2) + std::pow(point.y - pixel_center_y, 2));
// 最大距离为 3/√2(覆盖 3x3 像素区域的对角线距离)
float max_distance = 3.0f / std::sqrt(2.0f);
// 将距离归一化到 [0, 1] 的范围
float normal_distance = distance / max_distance;
// 计算颜色强度,使用距离归一化后的反比例关系
// 距离越近,强度越高,最大值为 255
float ratio = std::max(0.0f, 1.0f - normal_distance);
float color_intensity = 255.0f * ratio;
// 计算当前像素的整数坐标
int py = std::floor(point.y + j);
int px = std::floor(point.x + i);
// 检查像素坐标是否在图像窗口范围内,避免越界
if(px >= 0 && px < window.cols && py >= 0 && py < window.rows) {
// 如果当前像素的绿色通道值小于计算的颜色强度,则更新为更大的值
// 确保只更新更高的颜色值,减少不必要的操作。
window.at<cv::Vec3b>(py, px)[1] = std::fmax(color_intensity, window.at<cv::Vec3b>(py, px)[1]);
}
}
}
}
}
运行结果:
放大后也比较顺滑:
通过增加控制点可以尝试绘制更多的 Bézier 曲线。