Games101:作业4解析(含提高部分)

目录

作业要求:

1、总览

2、算法

具体代码实现:

提高部分:

参考链接:


作业要求:

1、总览

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 曲线上对应点的坐标。

2、算法

De Casteljau 算法说明如下:
1. 考虑一个 p 0 , p 1 , ... p n 为控制点序列的 Bézier 曲线。首先,将相邻的点连接起来以形成线段。
2. t : (1 t ) 的比例细分每个线段,并找到该分割点。
3. 得到的分割点作为新的控制点序列,新序列的长度会减少一。
4. 如果序列只包含一个点,则返回该点并终止。否则,使用新的控制点序列并转到步骤 1
使用 [0,1] 中的多个不同的 t 来执行上述算法,你就能得到相应的 Bézier 曲线。

具体代码实现:

        本次作业其实比较简单,如果课上已经听明白了贝塞尔曲线的实现方法或者看懂了上述算法的描述,再阅读一下代码框架即可写出正确的代码。

        其中 naive_bezier 是利用递推公式得到的曲线上点的坐标的计算方式,在运行自己所写代码时记得注释掉对它的调用,后续如果想要验证写的代码运行是否正确,可以取消注释。如果你写的曲线赋为绿色,则同时调用会出现黄色的曲线,即证明代码正确。

        因为代码选择了四个点生成贝塞尔曲线,我就直接暴力的先连成三条线、选择三个点,再连成两条线、选择两个点,连成一条线,最终得到曲线上的点。如果想要写的具有普适性、以便增多处理点,可以采用栈、队列等配合循环完成递归操作。

cv::Point2f recursive_bezier(const std::vector &control_points, float t) 
{
    // TODO: Implement de Casteljau's algorithm
    auto &p_0 = control_points[0];
    auto &p_1 = control_points[1];
    auto &p_2 = control_points[2];
    auto &p_3 = control_points[3];
    auto p_01 = (1-t)*p_0 + t * p_1;
    auto p_11 = (1-t)*p_1 + t * p_2;
    auto p_21 = (1-t)*p_2 + t * p_3;
    auto p_02 = (1-t)*p_01 + t * p_11;
    auto p_12 = (1-t)*p_11 + t * p_21;
    auto p_03 = (1-t)*p_02 + t * p_12;
	
    return cv::Point2f(p_03);
}

void bezier(const std::vector &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(float t=0; t<=1; t+=0.0001){
        auto point = recursive_bezier(control_points, t);
        //设置颜色为绿色
	    window.at(point.y, point.x)[1] = 255;
    }
}

        结果如下图所示:

Games101:作业4解析(含提高部分)_第1张图片

提高部分:

        本博客重点在于讲解提高部分:

实现对 Bézier 曲线的反走样。 ( 对于一个曲线上的点,不只把它对应于一个像
素,你需要根据到像素中心的距离来考虑与它相邻的像素的颜色。 )
        如果放大我们在前一步得到的图片,可以看到线的锯齿情况很严重,提高部分即在于解决这个问题。其实这里也已经说明了如何进行反走样,即对应获取得到的曲线上的点,不仅要使当前像素标出,还应对其周围的像素进行处理。

Games101:作业4解析(含提高部分)_第2张图片

        如何对周围像素进行处理呢?我最初浅薄的想,只需要同时处理目标像素的相邻像素即可,对邻域像素的颜色赋值,在原本的255基础上乘以其像素位置到目标像素位置的距离平方倒数,即:\large color = 255 \times \frac{1}{||P_{neighbour} - P_{goal}||^2},并根据此分别处理了4-邻域像素和8-邻域像素。

Games101:作业4解析(含提高部分)_第3张图片

void bezier(const std::vector &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(float t=0; t<=1; t+=0.0001){
        auto point = recursive_bezier(control_points, t);
	window.at(point.y, point.x)[1] = 255;
	const float x[4] = {0,0,1,-1};
	const float y[4] = {1,-1,0,0};
	//int xnow = round(point.x), ynow = round(point.y);
	//float d = std::sqrt(std::pow(point.x-xnow,2)+std::pow(point.y-ynow,2));
        for(int i=0;i<4;i++){
            float x_neibor = floor(point.x + x[i]);
            float y_neibor = floor(point.y + y[i]);
            if(x_neibor>=0 && x_neibor<700 && y_neibor>=0 && y_neibor<700){
                float w = 1/std::sqrt((std::pow(x_neibor-point.x,2)+std::pow(y_neibor-point.y,2)));
                window.at(y_neibor, x_neibor)[1] = 255 * w;
            } 
        }
    }
}

         这样处理的问题是,对于4邻域,各个邻域像素到目标像素之间的距离基本是一致的,所以对邻域的赋值结果基本一致,也就是只是在着色目标像素的同时,给其周围4个像素了一个较暗的颜色,且因为距离平方都约为1,所以颜色基本没变化,视觉上只是加粗了曲线,锯齿效果解决的并不好。

Games101:作业4解析(含提高部分)_第4张图片

        而对于8邻域,邻域像素分成了两类,一类是在4邻域中处理的4个像素块,一类是新处理的在对角线的4个像素块,因为这两类中的各个像素块距离目标像素的距离基本一致,所以邻域分成两类赋予了两个等级的较暗、暗的颜色,但因为第一类的颜色与原本的颜色没太大差别,所以只能看到新加的一类的四个像素的颜色很暗,视觉上,曲线更不平整了。

Games101:作业4解析(含提高部分)_第5张图片

         以上做法问题有两个:

①不应该单纯取当前像素的标准邻域,而应该根据距离,选择距离当前像素最近的几个像素,这里依旧推荐选择4个,因为8个会导致线有些太粗了

②比重应具备更多区分性,这里借鉴了如下链接中@xuyonglai的回答,如下图所示。

Games101:作业4解析(含提高部分)_第6张图片

作业四得到这样的结果是否满足要求? – 计算机图形学与混合现实在线平台

         如此便可得到如下代码: 

void bezier(const std::vector &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(float t=0; t<=1; t+=0.0001){
        auto point = recursive_bezier(control_points, t);
	window.at(point.y, point.x)[1] = 255;
	const float x[4] = {0,0,0.5,-0.5};
	const float y[4] = {+0.5,-0.5,0,0};
	int xnow = round(point.x), ynow = round(point.y);
	float d = std::sqrt(std::pow(point.x-xnow,2)+std::pow(point.y-ynow,2));
        for(int i=0;i<4;i++){
            float x_neibor = floor(point.x + x[i]);
            float y_neibor = floor(point.y + y[i]);
            if(x_neibor>=0 && x_neibor<700 && y_neibor>=0 && y_neibor<700){
            float w = d/std::sqrt((std::pow(x_neibor-point.x,2)+std::pow(y_neibor-point.y,2)));
            //float w = 1/std::sqrt((std::pow(x_neibor-point.x,2)+std::pow(y_neibor-point.y,2)));
            window.at(y_neibor, x_neibor)[1] = std::max(float(window.at(y_neibor, x_neibor)[1]), 255 * w);
            } 
        }
    }
}

         

Games101:作业4解析(含提高部分)_第7张图片

Games101:作业4解析(含提高部分)_第8张图片

        代码里有一个点还没有解释,那就是为什么要使用max函数呢?因为当你处理下一个像素时,可能得到其最近邻域有一个是上一个已经处理的像素,如果直接对其赋值,会导致曲线有些像素出现不正常的暗,看起来断断续续的。

Games101:作业4解析(含提高部分)_第9张图片

参考链接:

作业四得到这样的结果是否满足要求? – 计算机图形学与混合现实在线平台

你可能感兴趣的:(Games101,技术美术)