CGA填充算法之種子填充算法
平面區域填充算法是計算機圖形學領域的一個很重要的算法,區域填充即給出一個區域的邊界 (也可以是沒有邊界,只是給出指定顏色),要求將邊界范圍內的所有象素單元都修改成指定的顏色(也可能是圖案填充)。區域填充中最常用的是多邊形填色,本文討論種子填充算法(Seed Filling)
如果要填充的區域是以圖像元數據方式給出的,通常使用種子填充算法(Seed Filling)進行區域填充。種子填充算法需要給出圖像數據的區域,以及區域內的一個點,這種算法比較適合人機交互方式進行的圖像填充操作,不適合計算 機自動處理和判斷填色。根據對圖像區域邊界定義方式以及對點的顏色修改方式,種子填充又可細分為幾類:
比如:①注入填充算法(Flood Fill Algorithm)、
②邊界填充算法(Boundary Fill Algorithm)以及
③為減少遞歸和壓棧次數而改進的掃描線種子填充算法等等。
所有種子填充算法的核心其實就是一個遞歸算法,都是從指定的種子點開始,向各個方向上搜索,逐個像素進行處理,直到遇到邊界,各種種子填充算法只是在處理 顏色和邊界的方式上有所不同。
在開始介紹種子填充算法之前,首先也介紹兩個概念,就是“4-聯通算法”和“8-聯通算法”。
既然是搜索就涉及到搜索的方向 問題,從區域內任意一點出發,如果只是通過上、下、左、右四個方向搜索到達區域內的任意像素,則用這種方法填充的區域就稱為四連通域,這種填充方法就稱為 “4-聯通算法”。如果從區域內任意一點出發,通過上、下、左、右、左上、左下、右上和右下全部八個方向到達區域內的任意像素,則這種方法填充的區域就稱 為八連通域,這種填充方法就稱為“8-聯通算法”。



如圖1(a)所示,假設中心的藍色點是當前處理的點,如果是“4-聯通算法”,則只搜索處理周圍藍色標 識的四個點,如果是“8-聯通算法”則除了處理上、下、左、右四個藍色標識的點,還搜索處理四個紅色標識的點。兩種搜索算法的填充效果分別如如圖1(b) 和圖1(c)所示,假如都是從黃色點開始填充,則“4-聯通算法”如圖1(b)所示只搜索填充左下角的區域,而“8-聯通算法”則如圖1(c)所示,將左 下角和右上角的區域都填充了。
並不能僅僅因為圖1的填充效果就認為“8-聯通算法”一定比“4-聯通算法”好,應該根據應用環境和實際的需求選擇聯通搜索方式,在很多情況下,只有“4-聯通算法”才能得到正確的結果。
1. 注入填充算法(Flood Fill Algorithm)
邊界填充算法與注入填充算法的本質其實是一樣的,都是遞歸和搜索,區別只在於對邊界的確 認,也就是遞歸的結束條件不一樣。注入填充算法沒有邊界的概念,只是對聯通區域內指定的顏色進行替換,而邊界填充算法恰恰強調邊界的存在,只要是邊界內的 點無論是什么顏色,都替換成指定的顏色。邊界填充算法在應用上也非常的廣泛,畫圖軟件中的“油漆桶”功能就是邊界填充算法的例子。以下就是邊界填充算法的 一個實現:
void FloodSeedFill(int x, int y, int old_color, int new_color) { if(GetPixelColor(x, y) == old_color) { SetPixelColor(x, y, new_color); for(int i = 0; i < COUNT_OF(direction_8); i++) { FloodSeedFill(x + direction_8[i].x_offset, y + direction_8[i].y_offset, old_color, new_color); } } }
for循環實現了向8個聯通方向的遞歸搜索,秘密就在direction_8的定義:
typedef struct tagDIRECTION { int x_offset; int y_offset; }DIRECTION; DIRECTION direction_8[] = { {-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1} };
下圖就是應用本算法實現的“4-聯通”和“8-聯通”填充效果:
2. 邊界填充算法(Boundary Fill Algorithm)
邊界填充算法與注入填充算法的本質其實是一樣的,都是遞歸和搜索,區別只在於對邊界的確認,也就是遞歸的結束條件不一樣。注入填充算法沒有邊界的概念,只 是對聯通區域內指定的顏色進行替換,而邊界填充算法恰恰強調邊界的存在,只要是邊界內的點無論是什么顏色,都替換成指定的顏色。邊界填充算法在應用上也非 常的廣泛,畫圖軟件中的“油漆桶”功能就是邊界填充算法的例子。以下就是邊界填充算法的一個實現:
void BoundarySeedFill(int x, int y, int new_color, int boundary_color) { int curColor = GetPixelColor(x, y); if( (curColor != boundary_color)&& (curColor != new_color) ) { SetPixelColor(x, y, new_color); for(int i = 0; i < COUNT_OF(direction_8); i++) { BoundarySeedFill(x + direction_8[i].x_offset, y + direction_8[i].y_offset, new_color, boundary_color); } } }
關於direction_8的說明請參考上一節,圖3就是應用本算法實現的“4-聯通”和“8-聯通”填充效果(其中顏色值是1的點就是指定的邊界):

3. 掃描線種子填充算法(ScanLineSeed Fill Algorithm)
前面介紹的1與2,介紹的兩種種子填充算法的優點是非常簡單,缺點是使用了遞歸算法,這不但 需要大量棧空間來存儲相鄰的點,而且效率不高。為了減少算法中的遞歸調用,節省棧空間的使用,人們提出了很多改進算法,其中一種就是掃描線種子填充算法。
掃描線種子填充算法不再采用遞歸的方式處理“4-聯通”和“8-聯通”的相鄰點,而是通過沿水平掃描線填充像素段,一段一段地來處理“4-聯通”和“8- 聯通”的相鄰點。這樣算法處理過程中就只需要將每個水平像素段的起始點位置壓入一個特殊的棧,而不需要象遞歸算法那樣將當前位置周圍尚未處理的所有相鄰點 都壓入堆棧,從而可以節省堆棧空間。應該說,掃描線填充算法只是一種避免遞歸,提高效率的思想,前面提到的注入填充算法和邊界填充算法都可以改進成掃描線 填充算法,下面介紹的就是結合了邊界填充算法的掃描線種子填充算法。
掃描線種子填充算法的基本過程如下:當給定種子點(x, y)時,首先分別向左和向右兩個方向填充種子點所在掃描線上的位於給定區域的一個區段,同時記下這個區段的范圍[xLeft, xRight],然后確定與這一區段相連通的上、下兩條掃描線上位於給定區域內的區段,並依次保存下來。反復這個過程,直到填充結束。
掃描線種子填充算法可由下列四個步驟實現:
(1) 初始化一個空的棧用於存放種子點,將種子點(x, y)入棧;
(2)判斷棧是否為空,如果棧為空則結束算法,否則取出棧頂元素作為當前掃描線的種子點(x, y),y是當前的掃描線;
(3) 從種子點(x, y)出發,沿當前掃描線向左、右兩個方向填充,直到邊界。分別標記區段的左、右端點坐標為xLeft和xRight;
(4)分別檢查與當前掃描線相鄰的y - 1和y + 1兩條掃描線在區間[xLeft, xRight]中的像素,從xLeft開始向xRight方向搜索,若存在非邊界且未填充的像素點,則找出這些相鄰的像素點中最右邊的一個,並將其作為種 子點壓入棧中,然后返回第(2)步;
這個算法中最關鍵的是第(4)步,就是從當前掃描線的上一條掃描線和下一條掃描線中尋找新 的種子點。這里比較難理解的一點就是為什么只是檢查新掃描線上區間[xLeft, xRight]中的像素?如果新掃描線的實際范圍比這個區間大(而且不連續)怎么處理?我查了很多計算機圖形學的書籍和論文,好像都沒有對此做過特殊說 明,這使得很多人在學習這門課程時對此有揮之不去的疑惑。本着“毀人”不倦的思想,本文就羅嗦解釋一下,希望能解除大家的疑惑。
如果新掃描線上實際點的區間比當前掃描線的[xLeft, xRight]區間大,而且是連續的情況下,算法的第(3)步就處理了這種情況。如下圖所示:

假設當前處理的掃描線是黃色點所在的第7行,則經過第3步處理后可以得到一個區間 [6,10]。然后第4步操作,從相鄰的第6行和第8行兩條掃描線的第6列開始向右搜索,確定紅色的兩個點分別是第6行和第8行的種子點,於是按照順序將 (6, 10)和(8, 10)兩個種子點入棧。接下來的循環會處理(8, 10)這個種子點,根據算法第3步說明,會從(8, 10)開始向左和向右填充,由於中間沒有邊界點,因此填充會直到遇到邊界為止,所以盡管第8行實際區域比第7行的區間[6,10]大,但是仍然得到了正確 的填充。
如果新掃描線上實際點的區間比當前掃描線的[xLeft, xRight]區間大,而且中間有邊界點的情況,算法又是怎么處理呢?算法描述中雖然沒有明確對這種情況的處理方法,但是第4步確定上、下相鄰掃描線的種 子點的方法,以及靠右取點的原則,實際上暗含了從相鄰掃描線繞過障礙點的方法。下面以下圖為例說明:

算法第3步處理完第5行后,確定了區間[7, 9],相鄰的第4行雖然實際范圍比區間[7, 9]大,但是因為被(4, 6)這個邊界點阻礙,使得在確定種子點(4, 9)后向左填充只能填充右邊的第7列到第10列之間的區域,而左邊的第3列到第5列之間的區域沒有填充。雖然作為第5行的相鄰行,第一次對第4行的掃描根 據靠右原則只確定了(4, 9)一個種子點。但是對第3行處理完后,第4行的左邊部分作為第3行下邊的相鄰行,再次得到掃描的機會。第3行的區間是[3, 9],向左跨過了第6列這個障礙點,第2次掃描第4行的時候就從第3列開始,向右找,可以確定種子點(4, 5)。這樣第4行就有了兩個種子點,就可以被完整地填充了。
由此可見,對於有障礙點的行,通過相鄰邊的關系,可以跨越障礙點,通過多次掃描得到完整的填充,算法已經隱含了對這種情況的處理。
4. 根據本節總結的四個步驟實現並運行通過程序片段如下:
1 //掃描線繪制算法,提高填充效率,種子是基於鼠標點擊的像素位置 2 inline void CScanLineSeedFill::scanLineSeedFill(CPoint clickPos, GLfloat (&frameWork)[4][3] ) 3 { 4 //首先判定當前點是否有效,無效直接返回 5 assert(isInialDone); 6 //鼠標的點擊點必須在外圍框之內 7 GLdouble winFramePos[4][3]; 8 getViewPortCoordPos(frameWork, winFramePos); 9 int frameLeft = (int)winFramePos[0][0]; 10 int frameDown = (int)winFramePos[0][1]; 11 int frameRight = (int)winFramePos[2][0]; 12 int frameUp = (int)winFramePos[2][1]; 13 //對繪制的范圍進行限定:點擊框架之外,或者原點無效 14 //計算click位置在視口中的坐標 15 PointInt clickViewPos(clickPos.x, (this->winHigh) - (clickPos.y)); 16 if ((clickPos.x==0 && clickPos.y==0) ||!(clickViewPos.x>frameLeft && clickViewPos.x<frameRight && clickViewPos.y>frameDown && clickViewPos.y<frameUp)) 17 return ; 18 19 GLubyte readColor[3]; 20 glReadPixels(clickViewPos.x, clickViewPos.y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, readColor); 21 if (sameColor(readColor, boundryColor) || sameColor(readColor, fillColor))//如果種子是邊界點或填充顏色則直接退出 22 return; 23 24 glColor3ubv(fillColor); 25 //一開始非邊界的種子,必然可以繪制掃描描線 26 //PointInt startPoint; 27 //startPoint.x = clickViewPos.x; 28 //startPoint.y = clickViewPos.y; 29 seedsStack.push(clickViewPos); 30 //pixelStack.push(point); 31 float viewPosition[3]; 32 double worldPosition[3];//世界坐標系 33 34 int saveX; 35 int xRight,xLeft; 36 int x,y; 37 //如果棧不為空 38 while(!seedsStack.empty()) 39 { 40 //獲取最頂端的元素 41 PointInt tempPoint=seedsStack.top(); 42 //刪除最頂端的元素 43 seedsStack.pop(); 44 45 saveX = tempPoint.x;//繪制點的X坐標,從當前點向左向右掃描 46 x = tempPoint.x; 47 y = tempPoint.y; 48 glReadPixels(x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &readColor); 49 //向右:如果沒有到達右邊界,就填充 50 while(!sameColor(readColor, boundryColor)) 51 { 52 x=x+1; 53 glReadPixels(x, y, 1, 1,GL_RGB,GL_UNSIGNED_BYTE, &readColor); 54 } 55 xRight=x-1; 56 x=saveX-1; 57 glReadPixels(x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &readColor); 58 59 //向左:如果沒有到達左邊界,就填充 60 while(!sameColor(readColor, boundryColor)) 61 { 62 x=x-1; 63 glReadPixels(x, y, 1, 1, GL_RGB,GL_UNSIGNED_BYTE, &readColor); 64 } 65 //保存左端點 66 xLeft=x+1; 67 68 //把當前像素點還原為世界坐標系中的點然后再繪制 69 glBegin(GL_LINES); 70 viewPosition[0] = xLeft; 71 viewPosition[1] = y; 72 viewPosition[2] = 0; 73 getWorldCoordPos(viewPosition, worldPosition); 74 glVertex3f(worldPosition[0], worldPosition[1], worldPosition[2]); 75 viewPosition[0] = xRight; 76 getWorldCoordPos(viewPosition, worldPosition); 77 glVertex3f(worldPosition[0], worldPosition[1], worldPosition[2]); 78 glEnd(); 79 80 //從右邊的點開始 81 x=xRight; 82 //檢查上端的掃描線 83 searchNewLineSeed(seedsStack, xLeft, xRight, y+1); 84 searchNewLineSeed(seedsStack, xLeft, xRight, y-1); 85 }
1 //查找新掃描線的種子 2 inline void CScanLineSeedFill::searchNewLineSeed(stack<PointInt>& stk, int xLeft, int xRight, int y) 3 { 4 unsigned char readColor[3]; 5 int xt = xLeft; 6 bool findNewSeed = false; 7 while(xt <= xRight) 8 { 9 findNewSeed = false; 10 glReadPixels(xt, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, readColor); 11 while(!sameColor(readColor, boundryColor) && !sameColor(readColor, fillColor) && (xt < xRight)) 12 { 13 findNewSeed = true; 14 xt++; 15 glReadPixels(xt, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &readColor); 16 } 17 18 if(findNewSeed) 19 { 20 if(!sameColor(readColor, boundryColor) && !sameColor(readColor, fillColor) && (xt == xRight))//到達邊界 21 { 22 stk.push(PointInt(xt, y)); 23 } 24 else 25 stk.push(PointInt(xt - 1, y));//不到邊界 26 } 27 28 /*向右跳過內部的無效點(邊界點), 即與邊界相同顏色的點 29 (處理區間右端有障礙點的情況)*/ 30 //xt或者在障礙點上,或者為右邊端點 31 int tag = xt; 32 while((sameColor(readColor, boundryColor) || sameColor(readColor, fillColor)) && (xt < xRight)) 33 { 34 xt++; 35 glReadPixels(xt, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &readColor); 36 } 37 if (tag == xt)//如果xt>= xRight or 都不為邊界或已經填充則右移動 38 xt += 1; 39 40 } 41 }
程序運行結果如下:

算 法的實現其實就在ScanLineSeedFill()和SearchLineNewSeed()兩個函數中,神秘的掃描線種子填充算法也並不復雜,對 吧?至此,種子填充算法的幾種常見算法都已經介紹完畢,接下來將介紹兩種適合矢量圖形區域填充的填充算法,分別是掃描線算法和邊標志填充算法,注意適合矢量圖形的掃描線填充算法有時又被稱為“有序邊表法”,和掃描線種子填充算法是有區別的。
總結:
種子填充算法效率不是太高,及時是我實現的掃描線填充算法,可能我在判定當前像素信息是需要使用opengl獲取緩存棧中的內容,效果不是很理想。
