计算机图形学算法-光栅图形学

直线段的扫描转换算法(一)

1.1 直线段扫描转换算法概述:

  • 光栅显示器屏幕上的直线

    • 核心方法:用离散像素点逼近直线


  • 直线绘制的三个著名算法

    • 数值微分法 (DDA)
    • 中点画线法
    • Bresenham 算法

1.2 数值微分法 (DDA)

  • 数值微分法 (DDA - Digital Different Analyzer)

    • 核心思想:增量思想,也基于直线方程 Ax + Bx+ C = 0,y = kx + b
    • y_i = kx_i + b, y_(i+1) = kx_(i+1) + b
    • y_(i+1) = kx_(i+1) + b = kx_i + k + b = y_i + k, 即 y_(i+1) = y_i + k 即为增量。
    • 因此即可将原式中的乘法换成加法。
    • k为直线斜率 k = y/x
  • 算法过程

    • |k| < 1时,x均匀增加,每次求出x对应的y值,再将 y'=(int)(y+0.5)下取整 y',
      得到(x,y') 像素点坐标,再将该像素点涂色即可。
    • 当斜率|k|<1时每次x加1,斜率|k|>1每次y加1


  • 算法总结

    • |k| < 1时,x_(i+1) = x_i + 1, y_(i+1) = y_i + k, 每次令y'=(int)(y_i+0.5),将像素点(x,y')涂色。
    • |k| > 1时,x、y互换,y_i+1 = y_i + 1,因为这时y的座标范围更加广,因此令y递增。


1.3 中点画线法

  • 中点画线算法

    • 直线一般式方程
    • F(x,y)=0, Ax + Bx + C = 0
    • 直线上的点:F(x,y) = 0
    • 直线上方的点:F(x,y) > 0
    • 直线下方的点:F(x,y) < 0
  • 核心思想

    • 每次在最大位移方向走一步,而另一个方向是走还是不走取决于中点误差项的判断。

    • 假定:0 < |k| < 1。* 因此,每次在x方向上加1,y方向上加1或不变需要判断。


    • 当 M 在 Q 的下方,即取 P_u为下一个像素点,否则取P_d。

    • 算法推导过程

      • 将 M 带入直线方程,F(x_m, y_m) = Ax_m + By_m + C
      • 当d_i < 0 时,M在Q的下方,取P_u
      • 当d_i > 0 时,M在Q的上方,取P_d
      • 当d_i = 0 时,M在直线上,取P_d或均可。
      • y = y + 1(d<0)或y(y>0),d_i = A(x_i + 1) + B(y_i + 0.5) + C
  • 总结

    • 中点画线法将算法提高到整数加法,优于 DDA 算法,但此算法还是存在浮点数加法运算 d_i = A(x_i + 1) + B(y_i + 0.5) + C


1.4 Bresenham 算法

  • 算法特点

    • 提供了一个更一般的算法。该算法不仅有好的效率,而且有更广泛的适用范围。
  • Bresenham 算法基本思想

    • 算法思想是通过各行、各列像素中心构造一组虚拟网格线,按照直线起点到终点的顺序,计算直线与各垂直网格线的交点,然后根据误差项的符号确定该列像素中与此交点最近的像素。


    • 假设每次x+1,y的递增(减)量为0或1,它取决于实际直线与最近光栅网络点的距离,这个距离的最大误差为0.5。

    • 误差项d初始值d_0 = 0, d = d + k。一旦d > 1,就把它减去1,以保证d的相对性,且在0、1之间。

    • x_(x+1) = x_i + 1,y_(i+1) = y_i + 1 (d> 0.5) 或 y_i (d < 0.5)

    • 改进 1:令 e = d - 0.5

      • x_(x+1) = x_i + 1,y_(i+1) = y_i + 1 (e > 0) 或 y_i (e < 0)
      • 每走一步有 e' = e + k
    • 改进 2:用 e*2△x来替换 e

      • e_0 = -△x,每走一步有e=e+2△y
      • if (e>0) then e = e - 2△x
    • 算法步骤

      1. 输入直线的两端点 P_0(x_0, y_0)和P_1(x_1, y_1)。
      2. 计算初始值 △x、△y、e = -△x、x = x_0、y= y_0。
      3. 绘制点(x,y)。
      4. e 更新为 e+x△y,判断e的符号。若e>0则(x,y)更新为(x+1,y+1),同时将e更新为 e-2△x。否则(x,y)更新为(x+1,y)。
      5. 当直线没有画完时,重复步骤 3、4。否则结束。


多边形的扫描转换(二)

多边形表示方法:

  • 顶点表示

    • 定义:顶点表示是用多边形的顶点序列来表示多边形。这种表示直观、几何意义强、占内存少,易于进行几何变换。
    • 缺点:由于没有明确指出哪些像素在多边形内,故不能直接用于面着色。


  • 点阵表示

    • 定义:点阵表示是用位于多边形内的像素集合来刻画多边形。这种表示丢失了许多几何信息 (如边界、顶点等),但它却是光栅显示系统显示时所需的表示形式。


  • 多边形的扫描转换

    • 把多边形的顶点表示转换为点阵表示,即为多边形的扫描转换。
  • 多边形分类

    • 凸多边形,任意两顶点间的连接均在多边形内


    • 凹多边形,任意两顶点间的连线有不在多边形内的


    • 含内环的多边形,多边形内包含多边形



X-扫描线算法

  • 基本思想

    • X-扫描线算法填充多边形的基本思想是按扫描线顺序,计算扫描线与多边形的相交区间,再用要求的颜色显示这些区间的像素,即完成填充工作。
    • 区间的端点可以通过计算扫描线与多边形边界线的交点获得。
  • 算法过程

    • 算法核心:按 X递增顺序排列交点的Y坐标序列。
    • ① 确定多边形所占有的最大扫描线数,得到多边形顶点的最小和最大 y值(Y_max和 Y_min)
    • ② 从Y_min 到 Y_max 每次用一条扫描线进行填充。
    • ③ 对一条扫描线填充的过程分为以下四步:
      a. 求交:计算扫描线与多边形各边的交点
      b. 排序:把所有交点按递增顺序进行排序
      c. 交点配对:第一个与第二个,第三个与第四个
      d. 区间填色:把这些相交区间内的像素置成不同于背景色的填充色
  • 扫描线与多边形顶点相交时,交点的取舍问题 (交点的个数应保证为偶数个)
    a. 若共享顶点的两条边分别落在扫描线的两边,交点只算 1个
    b. 若共享顶点的两条边在扫描线的同一边,交点个数为 0 个或2 个。需要检查两条边另外两个端点的y 值来判断。

  • 两个重要思想
    • 扫描线:当处理图形图像时按一条条扫描线处理
    • 增量的思想


  • 扫描线中的数据结构
    • 活性边表 (AET)
      • AET:把与当前扫描线相交的边称为活性边,并把它们按与扫描线交点 x 坐标递增的顺序存放在一个链表中。
      • 节点内容
        • x:当前扫描线与边的交点坐标
        • △x:从当前扫描线到下一条扫描线间 x 的增量,△x = 1/k
        • Y_max: 该边所交的最高扫描线的坐标值 Y_max,可以判断何时 "抛弃" 该边



  • 新边表 (NET)
    • 首先构造一个纵向链表,链表的长度为多边形所占有的最大扫描线数,链表的每个节点,称为一个吊桶,对应多边形覆盖的每一条扫描线。
    • NET挂在与该边低端y值相同的扫描线桶中,即存放在该扫描线第一次出现的边。
    • NET 节点内容
      • Y_max:该边的 Y_max
      • X_min:该边较低点的x 坐标值X_min
      • 1/k:该边的斜率 1/k




  • 每做一次新的扫描线时,要对已有的边进行三个处理:

    • 是否被去除掉
    • 如果不被去除,第二就要对它的数据进行更新。所谓更新数据就是要更新它的x值,即:x = x + (1/k)
    • 看有没有新的边进来,新的边在 NET 里,可以插入排序插进来。
  • 优点:避免求交运算。

  • 小结

    • 扫描线法可以实现已知任意多边形域边界的填充。该填充算法是按扫描线的顺序,计算扫描线与待填充区域的相交区间,再用要求的颜色显示这些区间的像素,即完成填充工作。
    • 提高算法效率的方法
      • 增量的思想
      • 连贯性思想
      • 构建了一套特殊的数据结构
  • 缺点:带填充区域的边界线必须事先知道,因此它的缺点是无法实现对未知边界的区域填充。

边缘填充算法

  • 基本思想

    • 按任意顺序处理多边形的每条边。在处理每条边时,首先求出该边与扫描线的交点,然后将每一条扫描线上交点右方的所有像素取补。多边形的所有边处理完毕之后,填充即完成。
  • 算法过程

    • 对于每条边,将其上下 Y_max 、Y_min所构成的矩形区域划分开,取矩形区域的右半部分像素取补。
  • 特点
    • 算法简单,但对于复杂图形,每个像素可能被访问多次。输入和输出量比有效边算法大得多。

栅栏填充算法

  • 算法原理
    • 栅栏指的是一条过多边形顶点且与扫描线垂直的直线。它把多边形分成两半。在处理每条边与扫描线的交点时,将交点与栅栏之间的像素取补。
    • 此算法与 边缘填充算法类似

边界标志算法

  • 算法原理
    • 帧缓冲器中对多边形的每条边进行直线扫描转换,亦即对多边形边界所经过的像素打上标志。
    • 然后再采用和扫描线算法类似的方法将位于多边形内的各个区段着上所需颜色。
    • 由于边界标志算法不必建立维护边表以及对它进行排序,所以边界标志算法更适合硬件实现,这时它的执行速度比有序边表算法快一至两个数量级。


区域填充

填充,是绘图软件极为重要的一个功能。用户通过点击某空白区域内任一点,即可为该区域着色,系统能自动识别边界线,最后停止填充。本章我们就讲解下填充算法的实现思路。

区域填充基础概念:

  • 区域,指已经表示成点阵形式的填充图形,是像素的集合。

  • 区域填充,指将区域内的一点 (常称种子点) 赋予给定颜色,然后将这种颜色扩展到整个区域内的过程。

  • 区域可采用内点表示和边界表示两种表示形式。

    • 内点表示:
      枚举出区域内部的所有像素,内部的所有像素着同一个颜色,边界像素着与内部像素不同的颜色。
    • 边界表示:
      枚举出边界上的所有像素,边界上的所有像素着同一个颜色,内部像素着与边界像素不同的颜色。
  • 常用填充算法
    填充算法有很多种,他们适合不同的场景,效率也有所不同,有逐点判断算法、种子填充算法、扫描线种子填充算法和活性边表算法,其中扫描线种子填充算法是绘图软件常采用的,也是本文要介绍的。下面先简要介绍下其它3种算法:

    • 逐点判断算法:该算法对绘图窗口内每一像素点进行射线环绕探测来实现内点判定,孤立地考虑像素点与区域间的关系,计算量较大,一般不采用。

    • 种子填充算法:该算法事先给定一像素点作为种子点,再以该种子点为起点朝四连通或八连通方向递归填充,如下图所示,左边为四连通填充效果,右边为八连通填充效果。但这种算法它仍基于像素,且区域内每一像素点均需入栈,易堆栈溢出,所以一般也不采用。


    • 活性边表算法:该算法充分挖掘了边的连贯性和顶点间的约束关系,通过扫描线自下而上扫描图形,并利用相交边的斜率关系快速定位下次扫描的边界点,进而实现快速填充,如下图所示。该算法在速度上比其它算法都快,但弊端是需要事先将图形近似化成多边形,并存储关键点的坐标数据,且不能由用户指定填充内点,自由度很低。故绘图软件中往往也不采用。


    • 扫描线种子填充算法
      先大致说下这个算法的主要思想,好有一个直观的感受:该算法需事先指定一个种子点,然后分别水平向右和向左地探测得到图形边界点,填充两端点之间的线段(边界为[xl,xr]),并让该线段之上和之下的不超过xr的最右侧内点入栈,继而栈顶像素点出栈作为新的种子点,重复上述操作至栈空。


重点讲扫描线种子填充算法

现在,假设我们要在下面的绘图空间内,填充边界颜色为X的区域,红色是用户点击的填充起点,阴影是填充后的颜色。那么按照上面所述思路,第一个种子点分别向左、向右找到边界,然后填充该区段后的效果就应该如下图所示。不仅填充了该区段,还把与自己向上紧邻的1像素、向下紧邻的1像素所在区段不超过xr的最右侧内点坐标压入了栈中。


继续,下一步就应该是像素点3出栈,搜索边界[xl,xr],填充区段,向上,向下,其中向上的时候发现整行都被填充过了,所以该行没有新种子点入栈,而向下的时候发现4正好属于内点、在最右侧、不超过xr,因此4入栈。下一步又是4出栈,同3的情况一样,最后进行到下图所示情况。


此时像素点5出栈,也和上面一样,执行完成后像素点6入栈。而6的这次就稍微有些不同了,因为6所在区段[xl,xr]比较长,往往上下就会有多于1个像素点入栈),可以看到这次有7、8、9入栈。


  • 这里想提几点注意事项:
    • 像素点入栈次序:应该是先向上探索,还是先向下探索,新种子点入栈有没有先后顺序要求,答案是没有,随意。
    • 搜索时遇到内部边界:在向上、向下探索时,很容易遇到内部边界,比如上图中的像素点1在向下探索时,生成了像素点2和3,而他俩中间其实是隔了3个颜色为X的边界像素点的,这个时候直接跳过它们即可。
    • 搜索时遇到内部孔洞:有人这时可能要质疑了,上面那种情况太特殊了,是满满的边界颜色,要是遇到“空心”的情况怎么办呢?比如2下面那一行?其实仔细想想,这种情况根本不存在,因为任何一个孔洞,它必然会有一个临界,这个临界就是扫描线在初遇到它的时候,本身完整的一个区段会被划分为左右区段(比如上图红色起点所在行的一整条扫描线,在向下探索时被划分成了2、3所在的两个区段),或左中右区段,甚至更多子区段的那个边界点,或边界线,上面那种情况就是边界线。而一旦被划分成多个区段后,[xl,xr]自然都会缩小,而算法对新种子点又有<=xr的限制,因此根本不用去考虑。
    • 填充起点不同:经过测试,只要按照算法的限定和要求来执行,用户指定的填充起点不同对算法正确性毫无影响,只是扫描的次序会有不同。
      不难看出,扫描线填充算法中种子点入栈次数与扫描线条数相同,较种子填充算法大大减少了堆栈操作,时空开销均有降低,效率都较高。所以该算法是绘图软件中常常采用的,非常高效。
  • 算法描述
    为了实现,我们进一步整理一下,把扫描线填充算法归纳为以下4个步骤实现:

    1. 初始化: 堆栈置空。将种子点(x, y) 入栈;
    2. 出栈: 若栈空则结束。否则取栈顶元素(x, y) , 以y 作为当前扫描线;
    3. 填充并确定种子点所在区段: 从种子点(x, y) 出发, 沿当前扫描线向左、右两个方向填充, 直到边界。分别标记区段的左、右端点坐标为xl和xr;
    4. 确定新的种子点: 在区间[xl,xr]中检查与当前扫描线y上、下相邻的两条扫描线上的像素。若存在非边界、未填充的像素, 则把每一区间的最右像素作为种子点压入堆栈, 返回2。



反走样

  • 走样定义:
    "锯齿" 是 "走样" 的一种形式。而走样是光栅显示的一种固有性质。产生走样现象的原因是像素本质上的离散性。

  • 走样现象

    • ① 光栅图形产生的阶梯形 (锯齿形)


    • ② 小物体由于 "走样" 而消失。图形中包含相对微小的物体时,这些物体在静态图形中容易被丢弃或忽略。
      • 动画序列中时隐时现,产生闪烁。
      • 矩形从左向右移动,当其覆盖某些像素中心时,矩形被显示出来,当没有覆盖像素中心时,矩形不被显示。


    • 简单地说,如果对一个快速变化的信号采样频率过低,所得样本表示的会是低频变化的信号:原始信号的频率看起来被较低的 "走样" 频率所代替。


反走样技术

  • 反走样定义

    • 反走样技术,即减少或消除走样效果的技术。
    • 采用分辨率更高的显示设备,对解决走样现象有所帮助,因为可以使锯齿相对物体更小一些。
  • 非加权区域采样方法

    • 原理
      根据物体的覆盖率计算像素的颜色。覆盖率是指某个像素区域被物体覆盖的比例。

    • 方法

      • 被多边形覆盖了一半的像素的亮度赋为 1/2,覆盖三分之一的像素亮度赋值为 1/3,以此类推。
    • 缺点

      1. 像素的亮度与相交区域的面积成正比,而与相交区域落在像素内的位置无关,这仍然会导致锯齿效应。
      2. 直线条上沿理想直线方向的相邻两个像素有时会有较大的灰度差。
  • 加权区域采样方法,

    • 原理

      • 将直线段看作是具有一定宽度的狭长矩形。当直线段与像素有交时,根据相交区域与像素中心距离来决定其对像素亮度的贡献。
      • 直线段对一个像素的亮度的贡献正比于相交区域与像素中心的距离。设置相交区域面积与像素中心距离的权函数 (高斯函数) 反映相交面积对整个像素亮度的贡献大小。
    • 离散计算方法

      • 将一个像素划分为n = 3 * 3个子像素,加权表可以取为:


  • 加权方案:中心子像素的加权是角子像素的 4 倍,是其他像素的 2 倍。对九个子像素的每个网格所计算出的亮度进行平均,然后求出所有中心落于直线段内的子像素。最后计算所有这些子像素对原像素亮度贡献之和。

  • 反走样是图形学中的一个根本问题,不可能避免;是图形学中的一个永恒问题。


推荐阅读更多精彩内容