區域填充算法和多邊形填充的掃描線算法[轉]


區域填充算法和多邊形填充的掃描線算法

本文主要介紹幾種區域填充算法,重點解釋多邊形的掃描線填充算法,最后實現了多邊形填充算法,包括在附錄文件中。在參考【5】中,作者詳細介紹了一系列區域填充算法,可以查看相應網頁。代碼的下載地址為:https://github.com/twinklingstar20/twinklingstar_cn_region_polygon_fill_scanline/

1. 1.      區域的定義和填充

1.1   像素定義的區域(Pixel-Defined Region)

1.1.1        邊界定義區域(boundary-defined)

定義某些像素是邊界,邊界包圍着一塊區域。填充所有在邊界內的相連通的像素,主要分下面幾個步驟:

  1. 從區域內部一個像素點開始
  2. 判斷這個像素是否是一個邊界像素點或者已經被填充了
  3. 如果都不是,就把它填充,然后開始設置鄰居像素點。

用圖片演示這個過程,如下面的幻燈片所示,代碼片段如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void boundaryFill4 ( int x, int y, int fill, int boundary)
{
    int current;
    current = getPixel (x,y);
    if (current != boundary && current !=fill)
    {
       setColor(fill);
       setPixel(x,y);
       boundaryFill4 (x+1, y, fill, boundary);
       boundaryFill4 (x−1, y, fill, boundary);
       boundaryFill4(x, y+1, fill, boundary);
       bonddaryFill4(x, y−1, fill, boundary);
    }
}

 

 
 
 
 
 
 
 
 
 
 
 
2013-11-10 15-41-57
  • 2013-11-10 15-41-57
  • 2013-11-10 15-30-21
  • 2013-11-10 15-30-35
  • 2013-11-10 15-35-48
  • 2013-11-10 15-31-05
1 2 3 4 5
 
 

 

1.1.2          內定義區域(interior-defined)

內定義區域的定義是:給定一個像素S,顏色是C,區域R指與S連通的且顏色都是C的像素集合(Region R is the set of all pixels having color C that are “connected” to a given pixel S)。

如果兩個像素連通,則它們之間有一條“相鄰(adjacent)”像素組成的連續路徑,所以連通的概念就依賴“相鄰”的定義。在圖形學中,相鄰通常有兩種定義:

(1)       四相鄰(4-Adjacent):兩個像素是四相鄰的,則它們在彼此水平或者垂直相鄰的位置上,如圖1所示:

2013-11-13 22-12-03

圖1. 四相鄰

(2)       八相鄰(8-Adjacent):兩個像素是八相鄰的,則它們在彼此水平、垂直或者是斜方向上相鄰的位置,如圖2所示:

2013-11-13 22-12-28

圖2. 八相鄰

如果兩個像素是四連通(4-connected),指它們之間有一條四相鄰像素組成的連續路徑;如果兩個像素是八連通(8-connected),指它們之間有一條四相鄰像素組成的連續路徑。舉個例子,如下圖3所示,是由黑色、灰色和白色組成的像素圖,給定一個像素點S,則由它定義的四連通區域共有20像素,由它定義的八連通區域共有28個像素。

 

 

圖3 像素區域

這里介紹兩種簡單的填充算法:

(1)       一種稱為洪水填充法。規定采用4連通的區域,下面用幻燈片演示了這個過程,下面的代碼片段實現了該算法。由於沒有使用到區域間的相關性,很多像素點可能會重復被填充。

  1. 從內部一個像素點開始,並用新的顏色替換它
  2. 填充四連通或者八連通區域,直到所有的內部點被替換了
1
2
3
4
5
6
7
8
9
10
11
12
void floodFill4 ( int x, int y, int fill, int oldColor)
{
      if (getPixel(x,y) == oldColor)
      {
         setColor(fill);
         setPixel(x,y);
         floodFill4 (x+ 1 , y, fill, oldColor);
         floodFill4 (x− 1 , y, fill, oldColor);
         floodFill4(x, y+ 1 , fill, oldColor);
         floodFill4(x, y− 1 , fill, oldColor);
      }
}

 

1 2 3 4 5
 
 

 

缺點是:1)大量的嵌套調用;2)很多像素點可能會被測試多次;3)難以清楚的掌控由於嵌套調用所占的內存大小;4)如果算法多次測試一個像素,會導致占用的內存擴大。

(2)利用像素間的相關性,可以提高算法的性能,並避免堆棧的溢出。每次填充在同一條掃描線上相鄰的一排像素,同時把與它相鄰的未填充的種子像素放在堆棧中,下面的幻燈片演示了這個過程。偽碼如下所示:

1
2
3
4
5
6
7
8
9
Push address of seed pixel on the stack;
while ( stack not empty)
{
     Pop the stack to provide the next seed;
     Fill the run defined by the seed;
     In the row above find interior runs reachable from this run;
     Push the addresses of the rightmost pixels of each such run;
     Do the same for the row below the current run;
}
1 2 3 4 5 6 7 8 9 10 11 12
 
 
1.2   符號定義的區域(Symbolically Defined Region)

這里簡單介紹下符號定義的區域的分類,詳細參見參考【4】,主要包括兩類:

(1)用一系列的矩形方塊表示的區域;

(2)通過一條表示一個區域邊界的路徑來界定一個區域:

1)用一個數學公式來定義邊界,例如采用(x-122)^2+(y-36)^2=25來定義一個圓的區域

2)通過一系列的多邊形頂點,像(x1,y1),(x2,y2),(x3,y3)…(xn,yn)來定義一個多邊形區域

3)通過一系列相鄰的像素來定義。

4)鏈碼(chain code),這是一個很經典的方法,在參考【3】和【4】中都有簡單的介紹,這里不詳細介紹這塊知識

5)其它

2.     多邊形填充算法

2.1   算法思想

參考【5】,掃描線填充算法的基本思想是:每條水平掃描線與多邊形的邊產生一系列交點,交點之間形成一條一條的線段,該線段上的像素就是需要被填充的像素。將這些交點按照x坐標排序,將排序后的交點兩兩成對,作為線段的兩個端點。水平掃描線從上到下(或從下到上)掃描由多條首尾相連的線段,使用要求的顏色填充該水平線段上的像素。多邊形掃描完成后,顏色填充也就完成了。掃描線填充算法可以歸納為以下4個步驟:

(1)       求交,計算掃描線與多邊形的交點;

(2)       交點排序,對第(1)步得到的交點按照x值從小到大進行排序;

(3)       顏色填充,對排序后的交點兩兩組成一個水平線段,以畫線段的方式進行顏色填充;

(4)       是否完成多邊形掃描?如果是就結束算法,如果不是就改變掃描線,然后轉第1步繼續處理;

整個算法的關鍵是第1步,需要用盡量少的計算量求出交點,還要考慮交點是線段端點的特殊情況,最后,交點的計算最好是整數,便於光柵設備輸出顯示。對於每一條掃描線,如果每次都按照正常的線段與直線相交算法進行計算,則計算量大,而且效率低下,如圖(4)所示:

 

 

圖4. 掃描線算法

2.2   存在的問題

利用上述算法還存在幾個問題,接下來分別討論:

(1)       如果多個多邊形相鄰的話,它們可能會共享一條邊,那么共享邊可能會被繪制兩次,圖5和圖6演示了該錯誤:

 

 

圖5. 相鄰的兩個三角形共享邊

 

 

 

圖6. 左圖是背景顏色與前景顏色混合的情況,右圖是不繪制共享邊的情況

對這個問題有一種很好的解決方案是:

原則1:每個多邊形只擁有它左邊的像素,即采用左閉右開的原則,如果邊是水平的話,則只擁有底邊。        

 

 

圖7. 共享邊的解決方案

(2)       如圖7所示,若采用Bresenham直線繪制算法,(參見文章,《布雷森漢姆直線算法》可能會出現一些像素超出邊所在的范圍:

原則2:在計算完掃描線與邊的交點后,會形成一條條的首尾相連的線段,用xLeft表示左端點,xRight表示右端點,xLeft和xRight是實數,取大於等於xLeft的最小整數xNewLeft,取小於等於xRight的最大整數xNewRight,則繪制像素范圍是[xNewLeft,xNewRight),如果 xNewRight<xRight,則右端也封閉。

(3)       水平掃描線與端點發生相交的情況。如圖8所示,穿過頂點H的掃描線,發生了兩次相交(一次是與邊GH,一次是與邊HI),所以2.1描述的算法思想的第1步結束后,H點會存儲兩次,排完序后H兩邊的奇偶性會相同,因此會錯誤的將H右邊的像素進行填充。(本圖是從參考【4】中獲取的,個人覺得該圖解釋這個問題,有點牽強。如果整個圖左右翻轉的話,解釋這個問題就特別清楚了,算法思想的第1步結束后,該掃描線上共有三個交點:(H,H,右交點),這樣問題就明顯了)。這里采用兩條原則,可以很簡單的解決這個問題:

 

 

圖8. 填充多邊形

原則3:忽略任何一條與水平邊的相交計算;

原則4:如果交點是邊的上端點,則把該端點忽略。

舉個遵守該該原則的例子,如圖9所示,得到每條邊端點相交的數量:

 

 

圖9. 一個多邊形的端點相交的數量

2.3   數據結構設計

2.3.1活動邊鏈表(Active-Edge List,AEL)

在計算相鄰兩條掃描線與一條邊的相交時,可以利用它們之間的相關性。假設,一條邊與的斜率是k,與掃描線y相交於x,則該邊與掃描線y+1相交於x+1/k點的位置,利用這個特性可以減少運算量。為了方便進行該運算,有人提出了一種數據結構,稱為活動邊鏈表,鏈表的每個節點存儲三個數據:(1)與當前水平掃描線的交點xint;(2)斜率m的倒數,1/m;(3)邊的上端點的yhigh。舉個例子來說該結構的存儲方式,如圖10所示,虛線代表了水平掃描線,在該水平掃描線上與多邊形共有4個交點,則在AEL中會存儲4個節點,4個結點按照xint從小到大排序。

 

 

圖10. 活動邊鏈表

2.3.2邊表(Edge Table,ET)

邊表存儲的是邊的信息,邊表中每個節點的數據結構與AEL中每個節點的相同,同樣存儲了三個信息:(1)邊下端點X坐標xbottom;(2)邊斜率的倒數;(3)邊上端點Y坐標yhigh。當AEL表進行更新時,邊表這種數據結構提供了快速索引的功能。如圖10中多邊形的邊信息,用邊表表示,如圖11所示,這里就記錄了其中四條邊的信息。首先用一個邊節點的數組,一條邊下端點的Y坐標值,表示該數組的索引。圖10中,Y=20掃描線上,共有兩條邊(原則3,忽略水平邊),所以把兩條邊的信息記錄節點,該節點存儲在數組索引號20的表項后面,注意:在我的實現中,要求這個鏈表中的節點也是按照xbottom從小到大的順序排列的;例如掃描線39所示,不存在邊的下端點在該掃描線上,則索引號39的表項后面為空。

 

 

圖11. 邊表

2.4   算法實現

如下面的代碼片段所示,主要有如下幾個步驟:

(1)       分配AEL的表頭g_ptrAELHead和邊表g_ptrEdgeTable[EDGE_TABLE_SIZE]的表頭內存空間,由於現在顯示器在垂直方向的分辨率一般不超過1024,所以這里不進行優化。

(2)       初始化邊表

(3)       在當前掃描線上,在ET中是否存在表項,如果存在,則插入到AEL表中。

(4)       填充該掃描線

(5)       更新AEL。AEL中是否有邊的y坐標值大於或者等於下一條掃描線(原則1:上邊不進行繪制),由於AEL結點中保存有yhigh,這一步很容易判斷,將相應的節點刪除。

(6)       判斷算法是否結束,否則的話重復(3)-(5)幾個步驟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _Edge
{
     double  dbX;
     double  dbDelta;
     int     inMaxY;
     _Edge*  ptrNext;
}Edge;
 
Edge*   g_ptrEdgeTable[EDGE_TABLE_SIZE];
Edge*   g_ptrAELHead;
void scanLineFill(Vector* ptrPolygon, int inNumPoly, DWORD inColor)
{
     allocEdges();
     initEdgeTable(ptrPolygon,inNumPoly);
     for ( int y=g_inMinY ; y<g_inMaxY ; y++ )
     {
         insertAEL(g_ptrAELHead,g_ptrEdgeTable[y]);
         fillAELScanLine(g_ptrAELHead,y,inColor);
         updateAEL(g_ptrAELHead,y+1);
     }
     deallocEdges();
}
2.5   算法結果演示

如圖12所示,實現該算法,並用glut庫實現了個簡單的Demo,按’U’,’L’可以放大或者縮小圖像,即放大縮小每個“像素”占據的像素大小。

 

 

圖12. 算法演示

3.      參考

【1】http://www.cs.ucdavis.edu/~ma/ECS175_S00/Notes/0411_a.pdf

【2】http://www.cs.tufts.edu/~sarasu/courses/comp175-2009fa/pdf/comp175-04-region-filling.pdf

【3】岡薩雷斯《數字圖像處理》

【4】F.S Hill, JR. 《Computer Graphics Using OpenGL, Second Edition》

【5】http://blog.csdn.net/orbit/article/details/7368996

 

spacer
More Articles


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM