Android多邊形區(qū)域掃描線種子填充算法的示例
1.3掃描線種子填充算法
1.1和1.2節(jié)介紹的兩種種子填充算法的優(yōu)點(diǎn)是非常簡單,缺點(diǎn)是使用了遞歸算法,這不但需要大量??臻g來存儲相鄰的點(diǎn),而且效率不高。為了減少算法中的遞歸調(diào)用,節(jié)省??臻g的使用,人們提出了很多改進(jìn)算法,其中一種就是掃描線種子填充算法。掃描線種子填充算法不再采用遞歸的方式處理“4-聯(lián)通”和“8-聯(lián)通”的相鄰點(diǎn),而是通過沿水平掃描線填充像素段,一段一段地來處理“4-聯(lián)通”和“8-聯(lián)通”的相鄰點(diǎn)。這樣算法處理過程中就只需要將每個水平像素段的起始點(diǎn)位置壓入一個特殊的棧,而不需要象遞歸算法那樣將當(dāng)前位置周圍尚未處理的所有相鄰點(diǎn)都壓入堆棧,從而可以節(jié)省堆??臻g。應(yīng)該說,掃描線填充算法只是一種避免遞歸,提高效率的思想,前面提到的注入填充算法和邊界填充算法都可以改進(jìn)成掃描線填充算法,下面介紹的就是結(jié)合了邊界填充算法的掃描線種子填充算法。
掃描線種子填充算法的基本過程如下:當(dāng)給定種子點(diǎn)(x, y)時,首先分別向左和向右兩個方向填充種子點(diǎn)所在掃描線上的位于給定區(qū)域的一個區(qū)段,同時記下這個區(qū)段的范圍[xLeft, xRight],然后確定與這一區(qū)段相連通的上、下兩條掃描線上位于給定區(qū)域內(nèi)的區(qū)段,并依次保存下來。反復(fù)這個過程,直到填充結(jié)束。
掃描線種子填充算法可由下列四個步驟實(shí)現(xiàn):
(1) 初始化一個空的棧用于存放種子點(diǎn),將種子點(diǎn)(x, y)入棧;
(2) 判斷棧是否為空,如果棧為空則結(jié)束算法,否則取出棧頂元素作為當(dāng)前掃描線的種子點(diǎn)(x, y),y是當(dāng)前的掃描線;
(3) 從種子點(diǎn)(x, y)出發(fā),沿當(dāng)前掃描線向左、右兩個方向填充,直到邊界。分別標(biāo)記區(qū)段的左、右端點(diǎn)坐標(biāo)為xLeft和xRight;
(4) 分別檢查與當(dāng)前掃描線相鄰的y - 1和y + 1兩條掃描線在區(qū)間[xLeft, xRight]中的像素,從xLeft開始向xRight方向搜索,若存在非邊界且未填充的像素點(diǎn),則找出這些相鄰的像素點(diǎn)中最右邊的一個,并將其作為種子點(diǎn)壓入棧中,然后返回第(2)步;
這個算法中最關(guān)鍵的是第(4)步,就是從當(dāng)前掃描線的上一條掃描線和下一條掃描線中尋找新的種子點(diǎn)。這里比較難理解的一點(diǎn)就是為什么只是檢查新掃描線上區(qū)間[xLeft, xRight]中的像素?如果新掃描線的實(shí)際范圍比這個區(qū)間大(而且不連續(xù))怎么處理?我查了很多計(jì)算機(jī)圖形學(xué)的書籍和論文,好像都沒有對此做過特殊說明,這使得很多人在學(xué)習(xí)這門課程時對此有揮之不去的疑惑。本著“毀人”不倦的思想,本文就羅嗦解釋一下,希望能解除大家的疑惑。
如果新掃描線上實(shí)際點(diǎn)的區(qū)間比當(dāng)前掃描線的[xLeft, xRight]區(qū)間大,而且是連續(xù)的情況下,算法的第(3)步就處理了這種情況。如圖(4)所示:
圖(4) 新掃描線區(qū)間增大且連續(xù)的情況
假設(shè)當(dāng)前處理的掃描線是黃色點(diǎn)所在的第7行,則經(jīng)過第3步處理后可以得到一個區(qū)間[6,10]。然后第4步操作,從相鄰的第6行和第8行兩條掃描線的第6列開始向右搜索,確定紅色的兩個點(diǎn)分別是第6行和第8行的種子點(diǎn),于是按照順序?qū)ⅲ?, 10)和(8, 10)兩個種子點(diǎn)入棧。接下來的循環(huán)會處理(8, 10)這個種子點(diǎn),根據(jù)算法第3步說明,會從(8, 10)開始向左和向右填充,由于中間沒有邊界點(diǎn),因此填充會直到遇到邊界為止,所以盡管第8行實(shí)際區(qū)域比第7行的區(qū)間[6,10]大,但是仍然得到了正確的填充。
如果新掃描線上實(shí)際點(diǎn)的區(qū)間比當(dāng)前掃描線的[xLeft, xRight]區(qū)間大,而且中間有邊界點(diǎn)的情況,算法又是怎么處理呢?算法描述中雖然沒有明確對這種情況的處理方法,但是第4步確定上、下相鄰掃描線的種子點(diǎn)的方法,以及靠右取點(diǎn)的原則,實(shí)際上暗含了從相鄰掃描線繞過障礙點(diǎn)的方法。下面以圖(5)為例說明:
圖(5) 新掃描線區(qū)間增大且不連續(xù)的情況
算法第3步處理完第5行后,確定了區(qū)間[7, 9],相鄰的第4行雖然實(shí)際范圍比區(qū)間[7, 9]大,但是因?yàn)楸唬?, 6)這個邊界點(diǎn)阻礙,使得在確定種子點(diǎn)(4, 9)后向左填充只能填充右邊的第7列到第10列之間的區(qū)域,而左邊的第3列到第5列之間的區(qū)域沒有填充。雖然作為第5行的相鄰行,第一次對第4行的掃描根據(jù)靠右原則只確定了(4, 9)一個種子點(diǎn)。但是對第3行處理完后,第4行的左邊部分作為第3行下邊的相鄰行,再次得到掃描的機(jī)會。第3行的區(qū)間是[3, 9],向左跨過了第6列這個障礙點(diǎn),第2次掃描第4行的時候就從第3列開始,向右找,可以確定種子點(diǎn)(4, 5)。這樣第4行就有了兩個種子點(diǎn),就可以被完整地填充了。
由此可見,對于有障礙點(diǎn)的行,通過相鄰邊的關(guān)系,可以跨越障礙點(diǎn),通過多次掃描得到完整的填充,算法已經(jīng)隱含了對這種情況的處理。根據(jù)本節(jié)總結(jié)的四個步驟,掃描線種子填充算法的實(shí)現(xiàn)如下:
void SearchLineNewSeed(std::stack<Point>& stk, int xLeft, int xRight, int y, int new_color, int boundary_color) { int xt = xLeft; bool findNewSeed = false; while(xt <= xRight) { findNewSeed = false; while(IsPixelValid(xt, y, new_color, boundary_color) && (xt < xRight)) { findNewSeed = true; xt++; } if(findNewSeed) { if(IsPixelValid(xt, y, new_color, boundary_color) && (xt == xRight)) stk.push(Point(xt, y)); else stk.push(Point(xt - 1, y)); } /*向右跳過內(nèi)部的無效點(diǎn)(處理區(qū)間右端有障礙點(diǎn)的情況)*/ int xspan = SkipInvalidInLine(xt, y, xRight, new_color, boundary_color); xt += (xspan == 0) ? 1 : xspan; /*處理特殊情況,以退出while(x<=xright)循環(huán)*/ } }
FillLineRight()和FillLineLeft()兩個函數(shù)就是從種子點(diǎn)分別向右和向左填充顏色,直到遇到邊界點(diǎn),同時返回填充的點(diǎn)的個數(shù)。這兩個函數(shù)返回填充點(diǎn)的個數(shù)是為了正確調(diào)整當(dāng)前種子點(diǎn)所在的掃描線的區(qū)間[xLeft, xRight]。
SearchLineNewSeed()函數(shù)完成算法第4步所描述的操作,就是在新掃描線上尋找種子點(diǎn),并將種子點(diǎn)入棧,新掃描線的區(qū)間是xLeft和xRight參數(shù)確定的:
void SearchLineNewSeed(std::stack<Point>& stk, int xLeft, int xRight, int y, int new_color, int boundary_color) { int xt = xLeft; bool findNewSeed = false; while(xt <= xRight) { findNewSeed = false; while(IsPixelValid(xt, y, new_color, boundary_color) && (xt < xRight)) { findNewSeed = true; xt++; } if(findNewSeed) { if(IsPixelValid(xt, y, new_color, boundary_color) && (xt == xRight)) stk.push(Point(xt, y)); else stk.push(Point(xt - 1, y)); } /*向右跳過內(nèi)部的無效點(diǎn)(處理區(qū)間右端有障礙點(diǎn)的情況)*/ int xspan = SkipInvalidInLine(xt, y, xRight, new_color, boundary_color); xt += (xspan == 0) ? 1 : xspan; /*處理特殊情況,以退出while(x<=xright)循環(huán)*/ } }
最外層的while循環(huán)是為了保證區(qū)間[xLeft, xRight]右端被障礙點(diǎn)分隔成多段的情況能夠得到正確處理,通過外層while循環(huán),可以確保為每一段都找到一個種子點(diǎn)(對于障礙點(diǎn)在區(qū)間左端的情況,請參考圖(5)所示實(shí)例的解釋,是隱含在算法中完成的)。內(nèi)層的while循環(huán)只是為了找到每一段最右端的一個可填充點(diǎn)作為種子點(diǎn)。SkipInvalidInLine()函數(shù)的作用就是跳過區(qū)間內(nèi)的障礙點(diǎn),確定下一個分隔段的開始位置。循環(huán)內(nèi)的最后一行代碼有點(diǎn)奇怪,其實(shí)只是用了一個小“詭計(jì)”,確保在遇到真正的邊界點(diǎn)時循環(huán)能夠正確退出。這不是一個值得稱道的做法,實(shí)現(xiàn)此類軟件控制有更好的方法,本文這樣做的目的只是為了使代碼簡短一些,讓讀者把注意力集中在算法處理邏輯上,而不是冗雜難懂的循環(huán)控制條件上。
算法的實(shí)現(xiàn)其實(shí)就在ScanLineSeedFill()和SearchLineNewSeed()兩個函數(shù)中,神秘的掃描線種子填充算法也并不復(fù)雜,對吧?至此,種子填充算法的幾種常見算法都已經(jīng)介紹完畢,接下來將介紹兩種適合矢量圖形區(qū)域填充的填充算法,分別是掃描線算法和邊標(biāo)志填充算法,注意適合矢量圖形的掃描線填充算法有時又被稱為“有序邊表法”,和掃描線種子填充算法是有區(qū)別的。
相關(guān)文章
Android Color顏色過度計(jì)算實(shí)現(xiàn)代碼
這篇文章主要介紹了Android Color顏色過度計(jì)算實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-06-06Android使用Kotlin實(shí)現(xiàn)多節(jié)點(diǎn)進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android使用Kotlin實(shí)現(xiàn)多節(jié)點(diǎn)進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03Android實(shí)現(xiàn)將View轉(zhuǎn)化為圖片并保存到本地
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)將View轉(zhuǎn)化為圖片并保存到本地,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Android studio button 按鈕 四種綁定事件的方法【實(shí)例代碼】
這篇文章主要介紹了Android studio button 按鈕 四種綁定事件的方法,非常不錯,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08Flutter上線項(xiàng)目實(shí)戰(zhàn)記錄之路由篇
這篇文章主要給大家介紹了關(guān)于Flutter上線項(xiàng)目實(shí)戰(zhàn)記錄之路由篇的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09Android實(shí)現(xiàn)登陸頁logo隨鍵盤收放動態(tài)伸縮(完美解決鍵盤彈出遮擋控件的問題)
這篇文章主要介紹了Android實(shí)現(xiàn)登陸頁logo隨鍵盤收放動態(tài)伸縮(完美解決鍵盤彈出遮擋控件的問題)的相關(guān)資料,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09Android launcher中模擬按home鍵的實(shí)現(xiàn)
這篇文章主要介紹了Android launcher中模擬按home鍵的實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2017-05-05