多边形区域填充算法--扫描线填充算法(有序边表法) 有代码

2、扫描线算法(Scan-Line Filling)    转载  https://blog.csdn.net/u013044116/article/details/49737585  算法

        扫描线算法适合对矢量图形进行区域填充,只须要直到多边形区域的几何位置,不须要指定种子点,适合计算机自动进行图形处理的场合使用,好比电脑游戏和三维CAD软件的渲染等等。windows

        对矢量多边形区域填充,算法核心仍是求交。《计算几何与图形学有关的几种经常使用算法》一文给出了判断点与多边形关系的算法――扫描交点的奇偶数判断算法,利用此算法能够判断一个点是否在多边形内,也就是是否须要填充,可是实际工程中使用的填充算法都是只使用求交的思想,并不直接使用这种求交算法。究其缘由,除了算法效率问题以外,还存在一个光栅图形设备和矢量之间的转换问题。好比某个点位于很是靠近边界的临界位置,用矢量算法判断这个点应该是在多边形内,可是光栅化后,这个点在光栅图形设备上看就有多是在多边形外边(矢量点没有大小概念,光栅图形设备的点有大小概念),所以,适用于矢量图形的填充算法必须适应光栅图形设备。数组

 

2.1扫描线算法的基本思想缓存

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

 

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

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

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

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

 

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

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

图(6) 多边形与扫描线示意图

 

观察多边形与扫描线的交点状况,能够获得如下两个特色:

 

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

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

 

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

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

        假设多边形某条边所在的直线方程是:ax + by + c = 0,扫描线yi和下一条扫描线yi+1与该边的两个交点分别是(xi,yi)和(xi+1,yi+1),则可获得如下两个等式:

 

axi + byi + c = 0                        (等式 1)

axi+1 + byi+1 + c = 0                     (等式 2)

 

由等式1能够获得等式3:

 

xi = -(byi + c) / a                           (等式 3)

 

一样,由等式2能够获得等式4:

 

xi+1 = -(byi+1 + c) / a                      (等式 4)

 

由等式 4 – 等式3可获得

 

xi+1 – xi = -b (yi+1 - yi) / a

 

因为扫描线存在yi+1 = yi + 1的关系,将代入上式便可获得:

 

xi+1 – xi = -b / a

 

即△x = -b / a,是个常量(直线斜率的倒数)。

 

        “活动边表”是扫描线填充算法的核心,整个算法都是围绕者这张表进行处理的。要完整的定义“活动边表”,须要先定义边的数据结构。每条边都和扫描线有个交点,扫描线填充算法只关注交点的x坐标。每当处理下一条扫描线时,根据△x直接计算出新扫描线与边的交点x坐标,能够避免复杂的求交计算。一条边不会一直待在“活动边表”中,当扫描线与之没有交点时,要将其从“活动边表”中删除,判断是否有交点的依据就是看扫描线y是否大于这条边两个端点的y坐标值,为此,须要记录边的y坐标的最大值。根据以上分析,边的数据结构能够定义以下:

65 typedef struct tagEDGE

66 {

67     double xi;

68     double dx;

69     int ymax;

74 }EDGE;

 根据EDGE的定义,扫描线4和扫描线7的“活动边表”就分别如图(7)和图(8)所示:

 

 

 图(7) 扫描线4的活动边表

 

 

 图(8) 扫描线7的活动边表

 

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

 

 

 

图(9) 各扫描线的新边表

 

        讨论完“活动边表(AET)”和“新边表(NET)”,就能够开始算法的具体实现了,可是在进一步详细介绍实现算法以前,还有如下几个关键的细节问题须要明确:

(1)      多边形顶点处理

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

        假设多边形按照顶点P一、P2和P3的顺序产生两条相邻的边,P2就是所说的顶点。多边形的顶点通常有四种状况,如图(10)所展现的那样,分别被称为左顶点、右顶点、上顶点和下顶点:

图(10) 多边形顶点的四种类型

 

左顶点――P一、P2和P3的y坐标知足条件 :y1 < y2 < y3;

右顶点――P一、P2和P3的y坐标知足条件 :y1 > y2 > y3;

上顶点――P一、P2和P3的y坐标知足条件 :y2 > y1 && y2 > y3;

下顶点――P一、P2和P3的y坐标知足条件 :y2 < y1 && y2 < y3;

 

        对于左顶点和右顶点的状况,若是不作特殊处理会致使奇偶奇数错误,常采用的修正方法是修改以顶点为终点的那条边的区间,将顶点排除在区间以外,也就是删除这条边的终点,这样在计算交点时,就能够少计算一个交点,平衡和交点奇偶个数。结合前文定义的“边”数据结构:EDGE,只要将该边的ymax修改成ymax – 1就能够了。

        对于上顶点和下顶点,一种处理方法是将交点计算作0个,也就是修正两条边的区间,将交点从两条边中排除;另外一种处理方法是不作特殊处理,就计算2个交点,这样也能保证交点奇偶个数平衡。

(2)      水平边的处理

    水平边与扫描线重合,会产生不少交点,一般的作法是将水平边直接画出(填充),而后在后面的处理中就忽略水平边,不对其进行求交计算。

(3)      如何避免填充越过边界线

        边界像素的取舍问题也须要特别注意。多边形的边界与扫描线会产生两个交点,填充时若是对两个交点以及之间的区域都填充,容易形成填充范围扩大,影响最终光栅图形化显示的填充效果。为此,人们提出了“左闭右开”的原则,简单解释就是,若是扫描线交点是1和9,则实际填充的区间是[1,9),即不包括x坐标是9的那个点。

 

2.2扫描线算法实现

 

        扫描线算法的整个过程都是围绕“活动边表(AET)”展开的,为了正确初始化“活动边表”,须要初始化每条扫描线的“新边表(NET)”,首先定义“新边表”的数据结构。定义“新边表”为一个数组,数组的每一个元素存放对应扫描线的全部“新边”。所以定义“新边表”以下:

510     std::vector< std::list<EDGE> > slNet(ymax - ymin + 1);

ymax和ymin是多边形全部顶点中y坐标的最大值和最小值,用于界定扫描线的范围。slNet 中的第一个元素对应的是ymin所在的扫描线,以此类推,最后一个元素是ymax所在的扫描线。在开始对每条扫描线处理以前,须要先计算出多边形的ymax和ymin并初始化“新边表”:

503 void ScanLinePolygonFill(const Polygon& py, int color)

504 {

505     assert(py.IsValid());

506 

507     int ymin = 0;

508     int ymax = 0;

509     GetPolygonMinMax(py, ymin, ymax);

510     std::vector< std::list<EDGE> > slNet(ymax - ymin + 1);

511     InitScanLineNewEdgeTable(slNet, py, ymin, ymax);

512     //PrintNewEdgeTable(slNet);

513     HorizonEdgeFill(py, color); //水平边直接画线填充

514     ProcessScanLineFill(slNet, ymin, ymax, color);

515 }

        InitScanLineNewEdgeTable()函数根据多边形的顶点和边的状况初始化“新边表”,实现过程当中体现了对左顶点和右顶点的区间修正原则:

315 void InitScanLineNewEdgeTable(std::vector< std::list<EDGE> >& slNet,

316                               const Polygon& py, int ymin, int ymax)

317 {

318     EDGE e;

319     for(int i = 0; i < py.GetPolyCount(); i++)

320     {

321         const Point& ps = py.pts[i];

322         const Point& pe = py.pts[(i + 1) % py.GetPolyCount()];

323         const Point& pss = py.pts[(i - 1 + py.GetPolyCount()) %py.GetPolyCount()];

324         const Point& pee = py.pts[(i + 2) % py.GetPolyCount()];

325 

332         if(pe.y != ps.y) //不处理水平线

333         {

334             e.dx = double(pe.x - ps.x) / double(pe.y - ps.y);

335             if(pe.y > ps.y)

336             {

337                 e.xi = ps.x;

338                 if(pee.y >= pe.y)

339                     e.ymax = pe.y - 1;

340                 else

341                     e.ymax = pe.y;

342 

343                 slNet[ps.y - ymin].push_front(e);

344             }

345             else

346             {

347                 e.xi = pe.x;

348                 if(pss.y >= ps.y)

349                     e.ymax = ps.y - 1;

350                 else

351                     e.ymax = ps.y;

352                 slNet[pe.y - ymin].push_front(e);

353             }

354         }

355     }

356 }

多边形的定义Polygon和本系列第一篇《计算几何与图形学有关的几种经常使用算法》一文中的定义一致,此处就再也不重复说明。算法经过遍历全部的顶点得到边的信息,而后根据与此边有关的先后两个顶点的状况肯定此边的ymax是否须要-1修正。ps和pe分别是当前处理边的起点和终点,pss是起点的前一个相邻点,pee是终点的后一个相邻点,pss和pee用于辅助判断ps和pe两个点是不是左顶点或右顶点,而后根据判断结果对此边的ymax进行-1修正,算法实现很是简单,注意与扫描线平行的边是不处理的,由于水平边直接在HorizonEdgeFill()函数中填充了。

         ProcessScanLineFill()函数开始对每条扫描线进行处理,对每条扫描线的处理有四个操做,以下代码所示,四个操做分别被封装到四个函数中:

467 void ProcessScanLineFill(std::vector< std::list<EDGE> >& slNet,

468                          int ymin, int ymax, int color)

469 {

470     std::list<EDGE> aet;

471 

472     for(int y = ymin; y <= ymax; y++)

473     {

474         InsertNetListToAet(slNet[y - ymin], aet);

475         FillAetScanLine(aet, y, color);

476         //删除非活动边

477         RemoveNonActiveEdgeFromAet(aet, y);

478         //更新活动边表中每项的xi值,并根据xi从新排序

479         UpdateAndResortAet(aet);

480     }

481 }

InsertNetListToAet()函数负责将扫描线对应的全部新边插入到aet中,插入操做到保证aet仍是有序表,应用了插入排序的思想,实现简单,此处很少解释。FillAetScanLine()函数执行具体的填充动做,它将aet中的边交点成对取出组成填充区间,而后根据“左闭右开”的原则对每一个区间填充,实现也很简单,此处很少解释。RemoveNonActiveEdgeFromAet()函数负责将对下一条扫描线来讲已经不是“活动边”的边从aet中删除,删除的条件就是当前扫描线y与边的ymax相等,若是有多条边知足这个条件,则一并所有删除:

439 bool IsEdgeOutOfActive(EDGE e, int y)

440 {

441     return (e.ymax == y);

442 }

443 

444 void RemoveNonActiveEdgeFromAet(std::list<EDGE>& aet, int y)

445 {

446     aet.remove_if(std::bind2nd(std::ptr_fun(IsEdgeOutOfActive), y));

447 }

UpdateAndResortAet()函数更新边表中每项的xi值,就是根据扫描线的连贯性用dx对其进行修正,而且根据xi从小到大的原则对更新后的aet表从新排序:

 

449 void UpdateAetEdgeInfo(EDGE& e)

450 {

451     e.xi += e.dx;

452 }

453 

454 bool EdgeXiComparator(EDGE& e1, EDGE& e2)

455 {

456     return (e1.xi <= e2.xi);

457 }

458 

459 void UpdateAndResortAet(std::list<EDGE>& aet)

460 {

461     //更新xi

462     for_each(aet.begin(), aet.end(), UpdateAetEdgeInfo);

463     //根据xi从小到大从新排序

464     aet.sort(EdgeXiComparator);

465 }

 

        其实更新完xi后对aet表的从新排序是能够避免的,只要在维护aet时,除了保证xi从小到大的排序外,在xi相同的状况下若是能保证修正量dx也是从小到大有序,就能够避免每次对aet进行从新排序。算法实现也很简单,只须要对InsertNetListToAet()函数稍做修改便可,有兴趣的朋友能够自行修改。

        至此,扫描线算法就介绍完了,算法的思想看似复杂,实际上并不难,从具体算法的实现就能够看出来,整个算法实现不足百行代码。

 

#include<windows.h>
#include<GL/glut.h>
const int POINTNUM=7;      //多边形点数.
 
/******定义结构体用于活性边表AET和新边表NET***********************************/
typedef struct XET
{
    float x;
    float dx,ymax;
    XET* next;
} AET,NET;
 
/******定义点结构体point******************************************************/
struct point
{
    float x;
    float y;
} polypoint[POINTNUM]= { {250,50},{550,150},{550,400},{250,250},{100,350},{100,100},{120,30} }; //多边形顶点
 
void PolyScan()
{
    /******计算最高点的y坐标(扫描到此结束)****************************************/
    int MaxY=0;
    int i;
    for(i=0; i<POINTNUM; i++)
        if(polypoint[i].y>MaxY)
            MaxY=polypoint[i].y;
 
    /*******初始化AET表***********************************************************/
    AET *pAET=new AET;
    pAET->next=NULL;
 
    /******初始化NET表************************************************************/
    NET *pNET[1024];
    for(i=0; i<=MaxY; i++)
    {
        pNET[i]=new NET;
        pNET[i]->next=NULL;
    }
    glClear(GL_COLOR_BUFFER_BIT);        //赋值的窗口显示.
    glColor3f(0.0,0.0,0.0);             //设置直线的颜色红色
    glBegin(GL_POINTS);
    /******扫描并创建NET表*********************************************************/
    for(i=0; i<=MaxY; i++)
    {
        for(int j=0; j<POINTNUM; j++)
            if(polypoint[j].y==i)
            {
                //一个点跟前面的一个点造成一条线段,跟后面的点也造成线段
                if(polypoint[(j-1+POINTNUM)%POINTNUM].y>polypoint[j].y)
                {
                    NET *p=new NET;
                    p->x=polypoint[j].x;
                    p->ymax=polypoint[(j-1+POINTNUM)%POINTNUM].y;
                    p->dx=(polypoint[(j-1+POINTNUM)%POINTNUM].x-polypoint[j].x)/(polypoint[(j-1+POINTNUM)%POINTNUM].y-polypoint[j].y);
                    p->next=pNET[i]->next;
                    pNET[i]->next=p;
                }
                if(polypoint[(j+1+POINTNUM)%POINTNUM].y>polypoint[j].y)
                {
                    NET *p=new NET;
                    p->x=polypoint[j].x;
                    p->ymax=polypoint[(j+1+POINTNUM)%POINTNUM].y;
                    p->dx=(polypoint[(j+1+POINTNUM)%POINTNUM].x-polypoint[j].x)/(polypoint[(j+1+POINTNUM)%POINTNUM].y-polypoint[j].y);
                    p->next=pNET[i]->next;
                    pNET[i]->next=p;
                }
            }
    }
    /******创建并更新活性边表AET*****************************************************/
    for(i=0; i<=MaxY; i++)
    {
        //计算新的交点x,更新AET
        NET *p=pAET->next;
        while(p)
        {
            p->x=p->x + p->dx;
            p=p->next;
        }
        //更新后新AET先排序*************************************************************/
        //断表排序,再也不开辟空间
        AET *tq=pAET;
        p=pAET->next;
        tq->next=NULL;
        while(p)
        {
            while(tq->next && p->x >= tq->next->x)
                tq=tq->next;
            NET *s=p->next;
            p->next=tq->next;
            tq->next=p;
            p=s;
            tq=pAET;
        }
        //(改进算法)先从AET表中删除ymax==i的结点****************************************/
        AET *q=pAET;
        p=q->next;
        while(p)
        {
            if(p->ymax==i)
            {
                q->next=p->next;
                delete p;
                p=q->next;
            }
            else
            {
                q=q->next;
                p=q->next;
            }
        }
        //将NET中的新点加入AET,并用插入法按X值递增排序**********************************/
        p=pNET[i]->next;
        q=pAET;
        while(p)
        {
            while(q->next && p->x >= q->next->x)
                q=q->next;
            NET *s=p->next;
            p->next=q->next;
            q->next=p;
            p=s;
            q=pAET;
        }
        /******配对填充颜色***************************************************************/
 
        p=pAET->next;
        while(p && p->next)
        {
            for(float j=p->x; j<=p->next->x; j++)
                glVertex2i(static_cast<int>(j),i);
            p=p->next->next;//考虑端点状况
        }
    }
    glEnd();
    glFlush();
}
 
void init(int argc,char** argv)
{
    glutInit(&argc,argv);  //I初始化 GLUT.
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);  //设置显示模式:单个缓存和使用RGB模型
    glutInitWindowPosition(50,100);  //设置窗口的顶部和左边位置
    glutInitWindowSize(400,300);  //设置窗口的高度和宽度
    glutCreateWindow("Scan Program");
 
    glClearColor(1.0,1.0,1.0,0); //窗口背景颜色为白色
    glMatrixMode(GL_PROJECTION);
    gluOrtho2D(0,600,0,450);
}
 
void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(0.0,0.4,0.2);
    glPointSize(1);
    glBegin(GL_POINTS);
    PolyScan();
    glEnd();
    glFlush();
}
 
int main(int argc,char** argv)
{
    init(argc,argv);
    glutDisplayFunc(myDisplay);        //图形的定义传递给我window.
    glutMainLoop();
    return 0;
}
相关文章
相关标签/搜索