多邊形區域填充算法--掃描線填充算法(有序邊表法) 有代碼

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;
}
相關文章
相關標籤/搜索