有序边表算法----计算机图形学

有序边表算法:

有序边表算法的基本思想是:用水平扫描线从上到下(或从下到上)扫描由多条首尾相连的线段构成的多边形,每根扫描线与多边形的某些边产生一系列交点。将这些交点按照x坐标排序,将排序后的点两两成对,作为线段的两个端点,以所填的颜色画水平直线。多边形被扫描完毕后,颜色填充也就完成了。扫描线填充算法也可以归纳为以下4个步骤:

 

(1) 求交,计算扫描线与多边形的交点

(2) 交点排序,对第2步得到的交点按照x值从小到大进行排序;

(3) 颜色填充,对排序后的交点两两组成一个水平线段,以画线段的方式进行颜色填充;

(4) 是否完成多边形扫描?如果是就结束算法,如果不是就改变扫描线,然后转第1步继续处理;

 

整个算法的关键是第1步,需要用尽量少的计算量求出交点,还要考虑交点是线段端点的特殊情况,最后,交点的步进计算最好是整数,便于光栅设备输出显示。

对于每一条扫描线,如果每次都按照正常的线段求交算法进行计算,则计算量大,而且效率底下,如图所示:

有序边表算法----计算机图形学_第1张图片

 

观察多边形与扫描线的交点情况,可以得到以下两个特点:

 

(1) 每次只有相关的几条边可能与扫描线有交点,不必对所有的边进行求交计算;

(2) 相邻的扫描线与同一直线段的交点存在步进关系,这个关系与直线段所在直线的斜率有关;

 

第一个特点是显而易见的,为了减少计算量,有序边表算法需要维护一张由“活动边”组成的表,称为“活动边表(AET)”。例如扫描线4的“活动边表”由P1P2和P3P4两条边组成,而扫描线7的“活动边表”由P1P2、P6P1、P5P6和P4P5四条边组成。

第二个特点可以进一步证明,假设当前扫描线与多边形的某一条边的交点已经通过直线段求交算法计算出来,得到交点的坐标为(x, y),则下一条扫描线与这条边的交点不需要再求交计算,通过步进关系可以直接得到新交点坐标为(x + △x, y + 1)。前面提到过,步进关系△x是个常量,与直线的斜率有关,下面就来推导这个△x。

设边的直线方程为: a*x+b*y+c=0

若 y=y_{i} 时,x=x_{i} , 则当 y=y_{i+1}=y_{i}+1 时:

                                                          x_{i+1}=-\frac{b}{a}*y_{i+1}-\frac{c}{a}=-\frac{b}{a}*(y_{i}+1)-\frac{c}{a}

                                                                                                 =x_{i}-\frac{b}{a}

所以 \Delta x=-\frac{b}{a}

 

“活动边表”是扫描线填充算法的核心,整个算法都是围绕者这张表进行处理的。活动边表中每条边都和扫描线有个交点,有序边表算法只关注交点的x坐标。每当处理下一条扫描线时,根据△x直接计算出新扫描线与边的交点x坐标,可以避免复杂的求交计算。

一条边不会一直待在“活动边表”中,当扫描线与之没有交点时,要将其从“活动边表”中删除,判断是否有交点的依据就是看扫描线y是否大于这条边两个端点的y坐标值,为此,需要记录边的y坐标的最大值。

根据以上分析,活动边表的结点中至少应保存如下内容:

x: 当前扫描线与边的交点的x坐标值

\Delta x : 从当前扫描线到下一条扫描线之间的 x 增量

ymax : 与当前扫描线相交线段两端点的最大 y值

 

前面提到过,扫描线算法的核心就是围绕“活动边表(AET)”展开的,为了方便活性边表的建立与更新,我们为每一条扫描线建立一个“新边表(NET)”,存放该扫描线第一次出现的边。当算法处理到某条扫描线时,就将这条扫描线的“新边表”中的所有边逐一插入到“活动边表”中。“新边表”通常在算法开始时建立,建立“新边表”的规则就是:如果某条边的较低端点(y坐标较小的那个点)的y坐标与扫描线y相等,则该边就是扫描线y的新边,应该加入扫描线y的“新边表”。

新边表的每个结点存放对应边的初始信息,如该扫描线与该边的初始交点的 x (即较低端点的 x 值),x 的增量 \Delta x,以及该边的最大  y 值 ymax。

 

各扫描线的新边表

 

有序边表算法----计算机图形学_第2张图片

 

各扫描线的活动表

(图中每种颜色代表多边形的一条边)

有序边表算法----计算机图形学_第3张图片

在进一步详细介绍实现算法之前,还有以下几个关键的细节问题需要明确:

(1) 多边形边界上像素的取舍问题,用于避免填充扩大化

如图,对于左下角为(1,1),右上角为(3,3)的正方形,若对边界上的所有点进行填充,所填充像素面积将达到 3*3 ,而实际上该正方形应有的面积为 2*2.

有序边表算法----计算机图形学_第4张图片

为此,人们提出了“左闭右开”,“下闭上开”的原则,简单解释就是,如果扫描线交点是1和9,则实际填充的区间是[1,9),即不包括x坐标是9的那个点。

 

(2) 当扫描线与多边形顶点相交时,交点的取舍问题

在对多边形的边进行求交的过程中,在两条边相连的顶点处会出现一些特殊情况,因为此时两条边会和扫描线各求的一个交点,也就是说,在顶点位置会出现两个交点。当出现这种情况的时候,会对填充产生影响,因为填充的过程是成对选择交点的过程,错误的计算交点个数,会造成填充异常。

有序边表算法----计算机图形学_第5张图片

如图,扫描线 3 与 P2 相交,而 P2 是两条线段的共享顶点,求得交点序列为(1,3)(1,3)(9,3),扫描线 3 被交点 x 坐标划分的区间为 [ 0,1 ] , [ 1,1 ] , [ 1,9 ] ,这样将导致区间 [1,9] 的点被取为背景色。

所以对于 P2,P4 这种左顶点和有顶点的情况,需要把相同的交点只取一次值。常采用的方法是修改以顶点为终点的那条边的区间,将顶点排除在区间之外,也就是删除这条边的终点,这样在计算交点时,就可以少计算一个交点。即只要将该边的ymax修改为ymax – 1就可以了。

对于P1,P3 这种上顶点和下顶点,一种处理方法是将交点计算做0个,也就是修正两条边的区间,将交点从两条边中排除;另一种处理方法是不做特殊处理,就计算2个交点。(将边的ymax修改为ymax – 1同样可以解决这个问题。因为这样,对于上顶点,就取了 0 个交点,而对于下顶点,则取了 2 个交点)

 

(3)水平边的处理

当多边形的边与扫描线重合时,即水平边(水平边与扫描线重合,会产生很多交点),算法将忽略该条水平边。因为进入“活动边表” 的边都需要能计算出水平方向的步增,即 \Delta x=\frac{x_{1}-x_{0}}{y_{1}-y_{0}} , 而水平边的 y_{1}-y_{0} 结果为 0 . 

 

 

核心代码:

class Edge:
    def __init__(self, x, ymin, dx, ymax):
        self.x = x
        self.ymin = ymin
        self.dx = dx
        self.ymax = ymax
from PySide2.QtCore import *
from edge import *

class Polygon:
    def __init__(self, vs):
        self.vertex_pts = vs

    def pts_sets(self):
        pts = []
        if len(self.vertex_pts) < 3:
            return pts
        min1 = self.min_y()
        max1 = self.max_y()
        scan_lines = max1 - min1

        ET = []
        n = len(self.vertex_pts)
        i = 0
        while i < n:
            if i < n - 1:
                p0 = self.vertex_pts[i]
                p1 = self.vertex_pts[i + 1]
            else:
                p0 = self.vertex_pts[i]
                p1 = self.vertex_pts[0]
            if p0.y() > p1.y():
                temp = p0
                p0 = p1
                p1 = temp

            if p0.y() != p1.y():
                e = Edge(p0.x(), p0.y(), (p1.x() - p0.x())/(p1.y() - p0.y()), p1.y() - 1)
                ET.append(e)
            i += 1

        AET = []
        a = []
        i = 0
        while i < scan_lines:
            y = i + min1

            for e in ET[:]:
                if e.ymin == y:
                    AET.append(e)
                    ET.remove(e)

            for e in AET[::]:
                if e.ymax < y:
                    AET.remove(e)

            a.clear()
            for e in AET:
                a.append(e.x)
                e.x += e.dx

            a.sort()
            j = 0
            n = len(a)
            while j < n:
                tx = int(a[j])
                if a[j] > tx:
                    x0 = tx + 1
                else:
                    x0 = tx
                x1 = int(a[j+1])
                x = x0
                while x <= x1:
                    pts.append(QPoint(x, y))
                    x += 1
                j += 2
            i += 1

        return pts



    def max_y(self):
        max1 = 0
        for p in self.vertex_pts:
            if p.y() > max1:
                max1 = p.y()
        return max1

    def min_y(self):
        min1 = self.max_y()
        for p in self.vertex_pts:
            if p.y() < min1:
                min1 = p.y()
        return min1

 

加上UI界面实现效果:

有序边表算法----计算机图形学_第6张图片

 

PS: 如需参考完整代码,请移步:https://download.csdn.net/download/qq_42185999/11859774   进行下载

你可能感兴趣的:(计算机图形学)