目录
作业要求:
1、总览
2、算法
具体代码实现:
本次作业其实比较简单,如果课上已经听明白了贝塞尔曲线的实现方法或者看懂了上述算法的描述,再阅读一下代码框架即可写出正确的代码。
其中 naive_bezier 是利用递推公式得到的曲线上点的坐标的计算方式,在运行自己所写代码时记得注释掉对它的调用,后续如果想要验证写的代码运行是否正确,可以取消注释。如果你写的曲线赋为绿色,则同时调用会出现黄色的曲线,即证明代码正确。
因为代码选择了四个点生成贝塞尔曲线,我就直接暴力的先连成三条线、选择三个点,再连成两条线、选择两个点,连成一条线,最终得到曲线上的点。如果想要写的具有普适性、以便增多处理点,可以采用栈、队列等配合循环完成递归操作。
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &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<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(float t=0; t<=1; t+=0.0001){
auto point = recursive_bezier(control_points, t);
//设置颜色为绿色
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
结果如下图所示:
提高部分:
本博客重点在于讲解提高部分:
实现对 Bézier 曲线的反走样。 ( 对于一个曲线上的点,不只把它对应于一个像素,你需要根据到像素中心的距离来考虑与它相邻的像素的颜色。 )
如何对周围像素进行处理呢?我最初浅薄的想,只需要同时处理目标像素的相邻像素即可,对邻域像素的颜色赋值,在原本的255基础上乘以其像素位置到目标像素位置的距离平方倒数,即:,并根据此分别处理了4-邻域像素和8-邻域像素。
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(float t=0; t<=1; t+=0.0001){
auto point = recursive_bezier(control_points, t);
window.at<cv::Vec3b>(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<cv::Vec3b>(y_neibor, x_neibor)[1] = 255 * w;
}
}
}
}
这样处理的问题是,对于4邻域,各个邻域像素到目标像素之间的距离基本是一致的,所以对邻域的赋值结果基本一致,也就是只是在着色目标像素的同时,给其周围4个像素了一个较暗的颜色,且因为距离平方都约为1,所以颜色基本没变化,视觉上只是加粗了曲线,锯齿效果解决的并不好。
而对于8邻域,邻域像素分成了两类,一类是在4邻域中处理的4个像素块,一类是新处理的在对角线的4个像素块,因为这两类中的各个像素块距离目标像素的距离基本一致,所以邻域分成两类赋予了两个等级的较暗、暗的颜色,但因为第一类的颜色与原本的颜色没太大差别,所以只能看到新加的一类的四个像素的颜色很暗,视觉上,曲线更不平整了。
以上做法问题有两个:
①不应该单纯取当前像素的标准邻域,而应该根据距离,选择距离当前像素最近的几个像素,这里依旧推荐选择4个,因为8个会导致线有些太粗了
②比重应具备更多区分性,这里借鉴了如下链接中@xuyonglai的回答,如下图所示。
作业四得到这样的结果是否满足要求? – 计算机图形学与混合现实在线平台
如此便可得到如下代码:
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(float t=0; t<=1; t+=0.0001){
auto point = recursive_bezier(control_points, t);
window.at<cv::Vec3b>(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<cv::Vec3b>(y_neibor, x_neibor)[1] = std::max(float(window.at<cv::Vec3b>(y_neibor, x_neibor)[1]), 255 * w);
}
}
}
}
代码里有一个点还没有解释,那就是为什么要使用max函数呢?因为当你处理下一个像素时,可能得到其最近邻域有一个是上一个已经处理的像素,如果直接对其赋值,会导致曲线有些像素出现不正常的暗,看起来断断续续的。