1.实验目的和内容
1.1实验目的
通过C/C++语言实现三种直线段的生成算法(DDA,中点画线,改进的Bresenham画线算法),深入理解三种算法的核心思想,并比较三种算法产生的直线段和它们的执行效率。同时,学习使用easyX绘图库,掌握简单的图形绘制函数的使用方法为以后的实验课程打下坚实的基础。
1.2实验内容
用三种直线段的生成算法(DDA,中点画线,改进的Bresenham画线算法),解决给定任意直线段两端点P1(x1,y1)和P2(x2,y2),画出该直线的问题,并比较三种算法产生的直线段和它们的执行效率。
2.算法原理
2.1 DDA(数值微分算法)基本原理
选定x2-x1和y2- y1的绝对值较大者作为步进方向(假定x2-x1较大),取该方向上的△x为一个像素单位长,即x每次递增一个像素,然后利用式yi+1=yi+(y2-y1)/(x2-x1)*△x计算相应的y值,把每次计算出的(xi+1, yi+1)经取整后顺序输出到显示器,则得光栅化后的直线。之所以取x2-x1和y2-y1绝对值较大者为步进方向,是因为如果取较小的为步进方向则会出现直线不连续的情况。
2.2中点画线基本原理
采用F(x,y)=y-kx-b=0,其中k=(y2-y1)/(x2-x1)。(假定0≤k≤1,x是最大位移方向)。当前象素点为(xi, yi) 。下一个象素点为Pd (右方)或Pu(右上) 。设M=(xi+1, yi+0.5),为Pu与Pd的中点,Q为理想直线与x=xi+1垂线的交点。将Q与M的y坐标进行比较。当Q在M的上方,则Pu 应为下一个象素点;Q在M的下方,应取Pd为下一点。Q与M的位置关系通过将M点的坐标代入直线方程来判断,即通过判断d= F(xm,ym)=F(xi+1, yi+0.5)=yi+0.5-k(xi+1)-b的符合来确定。当d<0,M在L(Q点)下方,取右上方Pu为下一个象素;当d>0,M在L(Q点)上方,取右方Pd为下一个象素;当d=0,选Pd或Pu均可,约定取Pd为下一个象素(注意:所有的点都采取相同规则)。但是这样做,每一个象素的计算量包括加法,减法,乘法,除法等计算,更加增加了计算量。为了提高算法效率同样采用增量算法的思想改进算法。 经过推导d>=0时,要判下一个象素位置,应计算d=F(xi+2,y+0.5)=d-k;增量为-k。d<0时,要判下一个象素位置,应计算d=F(xi+2,y+1.5)=d+1+k。增量为1-k。初始值d可以写为0.5-k。改进通过2d△x代替d,这样d的计算变为d=△x-2△y。d的更新改为d=d+2△x-2△y或者d=d-2△y。
以上仅是0<=k<=1的情况,对-1<=k<=0可以推出初始d=-2 △y - △x,d更新为d=d-2△x-2△y或者d=d-2△y。对k>1可以推出初始d=-△y+2△x,d更新为d=d+2△x-2△y或者d=d+2△x。对k<-1可以推出初始d=-△y-2△x,d更新为d=d-2△x或者d=d-2△x-2△y。
在根据k做出判断时并不计算出真实的k,而是先将(x1,y1)设置为x1始终较小的点,定义dx = x2 - x1; dy = y2 - y1; s=dx-abs(dy);根据s的符号和dy的符号可以判断k属于那种情况。
2.3改进的Bresenham画线算法基本原理
对任意斜率的实现请参见实现步骤,这里仅解释斜率0~1之间的直线。通过设计使得每次只要检查误差项的符号就可以决定实际的增量值,以斜率0<=k<=1的直线为例,如图1所示。若通过(0,0)的直线的斜率大于1/2,即e>1/2(如图1所示的e2),它与x=1直线的交点离y=1直线较y=0直线近,因此取像素点(1,1),如果斜率小于1/2,即e<1/2(如图1所示的e1),则应取像素点(1,0)。当斜率等于1/2时,即e=1/2,可以选择任一个,只有保证整个算法选择一致,该算法选择(1,1)像素点。为了简化判断可设e’=e- 1/2(第一步优化),这样只要判断e’的符号即可。首先令误差项的初值为-1/2,如果e’= △y/△x-1/2大于或等于零,则x加1,y加1;如果小于零,x加1, y值不变。对于下一步误差项的计算,一般分两种情况,如图2所示,对e’≥0, y增加一步(如图2所示e2的情况),新的误差项e"=e’+ △y/△x-1 (此时超过1个像素点距离了,需要减少1,在图2中实际上e"=2e’-3/2,这时小于零了,应该取(2, 1)像素点);对e’<0, y没有走步(如图2所示e1的情况),e"=e’+△y/△x (如图2所示,实际上e"=2e1- 1/2,这时大于零了,也应该取(2,1)像素点)。实际上,误差项e’的数值大小与算法的执行没有什么关系,只与 e’的符号。所以进行第二次优化。在e’= △y/△x-1/2两边同乘以2△x,可消除除法运算:令初始e’=2*△y-△x;如果e≥0则下一步e’=e+ 2(△y - △x);如果e<0则下一步e’=e+ 2△y其中e=2△xe’。
图1 Bresenham思路
图2 误差项计算
2.4增量算法
在一个迭代算法中,如果每一步的x,y值是前一步的值加上一个增量来获得的,则称为增量算法。该算法对提高算法效率和编程实现均有很好的作用。
3.算法实现过程及结果
说明:使用函数initgraph(640, 480)初始化一个640x480分辨率的窗口,再使用函数setorigin(320, 240)重新设置其坐标原点。值得注意的是此屏幕坐标系的y轴正方向是垂直向下的,为简便起见,按照正常的笛卡尔坐标系设置算法,仅在点亮坐标点时将y取反,得到正常的显示效果。
3.1 DDA(数值微分算法)实现过程
首先分别定义在x轴上的增量increx和在y轴上的增量increy。然后判断(x2-x1)与(y2-y1)绝对值的大小。如果(x2-x1)的绝对值较大则令increx=1,increy=(y2-y1)/(x2-x1)即斜率k。如果(y2-y1)的绝对值较大则令increx=(x2-x1)/(y2-y1)即斜率的倒数1/k, increy=1。同时令length=max(| x2-x1|,| y2-y1|)。这么做的主要目的是:根据斜率的不同选取不同的每次加一个像素点的坐标轴。若|k|>1,应当以y轴为加一坐标轴。若|k|<=1,应当以x轴为加一坐标轴。之后,让x,y从起始点(x1,y1)开始点亮该坐标点,每次更新x=x+increx,y=y+increy。直到点亮坐标的个数大于length为止。函数putpixel(x,y,color)可以将坐标系上点x,y处的像素打上color颜色。代码如图3所示,效果如图4所示:
对图3做以下说明:为验证算法正确性选取|k|>1的直线两条,|k|<1的直线两条,还有两条与x轴,y轴重合的直线。直线分别是(x1,y1,x2,y2)=(-200, 0, 200, 0); (x1,y1,x2,y2)= (0,200,0,-200); (x1,y1,x2,y2)= (-200, 50, 200, -50); (x1,y1,x2,y2)= (-50, 200, 50, -200); (x1,y1,x2,y2)= (200, 50, -200, -50); (x1,y1,x2,y2)= (50, 200, -50, -200)。以下算法均选取这六条有代表性的直线段,不再赘述。
3.2中点画线实现过程
首先比较横坐标x1和x2的大小,使得(x1,y1)始终是横坐标较小的点作为起始点。定义变量dx=x2 - x1,dy = y2 - y1。之后,根据dx是否等于零,将斜率k赋以不同的值。如果dx==0,则k = dy100;如果dx!=0,则k = -(double)dy / (x1 - x2)。这样做的主要目的是:将垂直于x轴的直线段分离出来,其中100可以是任意一个较大的正整数。定义变量x,y始终等于当前要点亮的坐标值,初值赋为x1,y1。然后根据k的大小将直线段的绘制分为四种情况:情况a:(k>=0&&k<=1); 情况b:(k<=0&&k>=-1); 情况c:(k>1); 情况d:(k<-1)。下面对四种情况分别进行说明。
情况1:k>=0&&k<=1时如图5黄色部分所示。在这种情况下x轴为主动坐标轴每次增加一,令d = dx - 2 * dy;d1 = -2 * dy;d2 = -2 * dy + 2 * dx。d开始赋初值dx-2dy,每次根据d的符合确定是否将变量y加一,即在y轴上增加一个像素值。d1,d2分别表示每次迭代时,d>=0时的增量和d<0时的增量。根据d的符号不断更新y和d的值,直到x>x2循环结束。直线绘制完毕。
情况2:k<=0&&k>=-1时如图5蓝色部分所示。在这种情况下x轴仍为主动坐标轴每次增加一,令d = -2 * dy - dx;d1 = -2 * dy - 2 * dx;d2 = -2 * dy。d开始赋初值-2 * dy - dx,每次根据d的符合确定是否将变量y减一,即在y轴上减少一个像素值。d1,d2分别表示每次迭代时,d>=0时的增量和d<0时的增量。根据d的符号不断更新y和d的值,直到x>x2循环结束。直线绘制完毕。值得注意的是,与情况1不同此时是d>=0时,每次对y减一。这是因为在迭代开始时,首先让(x1,y1)始终等于x较大的一点,因此当k<=0&&k>=-1 时,y1始终大于y2,所以要在更新y时减一。
情况3:k>1时如图5绿色部分所示。在这种情况下y轴为主动坐标轴每次增加一,令d = -dy + 2 * dx;d1 = 2 * dx - 2 * dy;d2 = 2 * dx。d开始赋初值-dy + 2 * dx,每次根据d的符合确定是否将变量x加一,即在x轴上增加一个像素值。d1,d2分别表示每次迭代时,d>=0时的增量和d<0时的增量。根据d的符号不断更新y和d的值,直到y>y2循环结束。直线绘制完毕。这次与情况1和情况2均不相同,情况3(k>1)时是将y轴作为主动坐标轴,然后根据d的符号对x进行更新。
情况4: k<-1时如图5红色部分所示。在这种情况下y轴为主动坐标轴每次增加一,令d = -dy - 2 * dx;d1 = -2dx;d2 = -2dx-2*dy;。d开始赋初值-dy - 2 * dx,每次根据d的符合确定是否将变量x加一,即在x轴上增加一个像素值。d1,d2分别表示每次迭代时,d>=0时的增量和d<0时的增量。根据d的符号不断更新x和d的值,直到y>y2循环结束。直线绘制完毕。
代码部分展示如图6,7,8,9,10所示,执行效果如图11所示。
3.3改进的Bresenham画线算法实现过程
首先定义一个符号函数可以根据参数的不同返回不同的值。如果输入大于0返回1,如果等于0返回0,如果小于0返回-1。定义变量x,y始终等于当前要点亮的坐标值,初值赋为x1,y1。定义s1 = sign(x2 - x1);s2 = sign(y2 - y1)。X轴上的增量定义为increx等于(x2 - x1)的绝对值,y轴上的增量定义为increy 等于(y2 - y1)的绝对值。然后通过比较increx和increy的大小使得increx始终是两者中较大的一个。并设置一个交换标记interchange,若increx和increy发生了交换,则置interchange = 1,默认为0。定义增量e=2 * increy – increx。然后进入循环,循环increx次。每次循环如果interchange == 1,则y =y+ s2。否则x =x+ s1。如果e>=0,则e =e- 2 * increx。并且在e>=0的情况下如果interchange == 1,则x =x+ s1。否则y =y+ s2。
代码部分展示如图12,13所示,执行效果如图14所示
3.4三种算法效率对比
说明:将每种算法画100次(200, 50),(-200, -50)输出执行开始时间,执行结束时间和总执行时间。为避免特殊性每种算法执行6次。
1
DDA算法如图15;中点画线如图16;改进的Bresenham如图17;统计时间的代码如图18。
有几张执行时间的截图不能看出在实际运行的过程中,并没有像课上所讲的那样,执行时间会有较大的差别。而是它们的执行时间基本没有差别。对上述现象进行探索,首先分析这三种算法之所以在理论上会产生执行效率差距较大的原因是:DDA算法涉及到浮点数的运算而其他两种算法是整数算法。在此基础上对C/C++执行浮点数乘法和整数乘法运算做以下实验。
定义double型数据a=4.7,b=9.3,c=0;int 型数据a1=4,b1=0。分别进行浮点类型乘法c=ab和整数类型乘法b1=a12。具体代码如图19,分别统计执行109次的用时,结果如图20,21。
由上图可见,在本人所使用的电脑配置(CPU i5-8300H)和C/C++(编译器 GUU GCC Compiler)编译环境下,int型数据的乘法运算甚至慢于double型数据乘法运算。这极有可能是三种算法效率比较无法得到预期效果的原因。至于为什么会发生这种不符合常理的现象,目前本人通过查找资料,询问老师还没有得到确切的答案。
4.心得体会
基本掌握esayX绘图工具。通过自己用代码实现三种画直线算法对它们的算法原理有了更进一步的理解。特别是在编程的过程中更加体会到了增量算法的用处,它不仅可以提高算法的效率同时也为编程实现提供了极大的便利。在实现任意斜率的中点画线算法时,起初是采用浮点除法计算斜率判断直线是属于那种情况的,但是后来经过检查发现,这样做会降低算法的效率,经过思考便改为通过判断dy和s=dx-abs(dy)的符号来确定直线斜率的情况。这样可以减少一次浮点数除法运算。在实现算法的编程过程中,发现常常可以通过一些简单操作减少算法实现的复杂程度。例如取反操作。
在对三种算法进行效率比较时,得到了不符合预期的情况,虽然目前还没有找出原因,但是我明白了理论和实践是不同的。理论再严密的证明,没有实践的检验也是空谈。并且不能迷信课本上所讲的,要自己主动思考,小心求证,大胆质疑,哪怕是错的也要知道为什么错。毕竟尽信书不如无书。
总之,这次实验课收获颇丰。不仅学习到了三种直线段绘制算法的实现,而且也培养了自己大胆质疑的精神。
最后,感谢老师为我解答的问题还有其他为我解决过问题的同学们。