區域填充算法和多邊形填充的掃描線算法
本文主要介紹幾種區域填充算法,重點解釋多邊形的掃描線填充算法,最后實現了多邊形填充算法,包括在附錄文件中。在參考【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
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);
}
}
|
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所示:
圖1. 四相鄰
(2) 八相鄰(8-Adjacent):兩個像素是八相鄰的,則它們在彼此水平、垂直或者是斜方向上相鄰的位置,如圖2所示:
圖2. 八相鄰
如果兩個像素是四連通(4-connected),指它們之間有一條四相鄰像素組成的連續路徑;如果兩個像素是八連通(8-connected),指它們之間有一條四相鄰像素組成的連續路徑。舉個例子,如下圖3所示,是由黑色、灰色和白色組成的像素圖,給定一個像素點S,則由它定義的四連通區域共有20像素,由它定義的八連通區域共有28個像素。
圖3 像素區域
這里介紹兩種簡單的填充算法:
(1) 一種稱為洪水填充法。規定采用4連通的區域,下面用幻燈片演示了這個過程,下面的代碼片段實現了該算法。由於沒有使用到區域間的相關性,很多像素點可能會重復被填充。
- 從內部一個像素點開始,並用新的顏色替換它
- 填充四連通或者八連通區域,直到所有的內部點被替換了
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)如果算法多次測試一個像素,會導致占用的內存擴大。
(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 符號定義的區域(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
