繼續圖形學之旅,我們已經解決了如何畫線和畫圓的問題,接下來要解決的是,如何往一個區域內填充顏色?對一個像素填充顏色只需調用SetPixel之類的函數就行了,所以這個問題其實就是:如何找到一個區域內的所有像素?
區域的表示方法
定義一個區域可以有兩種方法,即內點表示法
和邊界表示法
,內點表示就是指用一種顏色表示區域內的點,只要當前像素是這種顏色就在區域內,邊界表示就是用一種顏色表示區域邊界,只要當前像素是這種顏色就表示到達了區域邊界。
簡單的種子填充算法
最簡單暴力的填充算法即是從區域內一點出發,向四周擴散填充,到達區域邊界時停止,常見的有四鄰法
和八鄰法
兩種,顧名思義,一個是向上下左右四個方向擴散填充,另一個是向周圍八個方向擴散,四鄰法可以確保不溢出區域邊界,但有可能出現一次填不滿區域的情況,八鄰法則相反,一定能填充滿當前區域,但有從對角線溢出邊界的危險。
邊界表示的四鄰法代碼實現:
void BoundaryFill4(HDC hdc, int x, int y, COLORREF boundaryColor, COLORREF newColor)
{
COLORREF c = GetPixel(hdc, x, y);
if (c != newColor && c != boundaryColor)
{
SetPixel(hdc, x, y, newColor);
BoundaryFill4(hdc, x + 1, y, boundaryColor, newColor);
BoundaryFill4(hdc, x - 1, y, boundaryColor, newColor);
BoundaryFill4(hdc, x, y + 1, boundaryColor, newColor);
BoundaryFill4(hdc, x, y - 1, boundaryColor, newColor);
}
}
我們用之前學習的Bresenham畫線算法畫一個矩形,然后用這個算法填充它。
Bresenham_Line(150, 150, 150, 200, hdc, RGB(0, 0, 0));
Bresenham_Line(150, 200, 200, 200, hdc, RGB(0, 0, 0));
Bresenham_Line(200, 200, 200, 150, hdc, RGB(0, 0, 0));
Bresenham_Line(200, 150, 150, 150, hdc, RGB(0, 0, 0));
BoundaryFill4(hdc, 175, 175, RGB(0, 0, 0), RGB(255, 0, 0));
運行效果:

很顯然,這種遞歸的填充算法簡單好理解,但效率是不可接受的,顯然我們需要提高算法效率,避免過多的遞歸調用。
掃描線種子填充算法
為了提高效率可以使用掃描線種子填充算法,這里的掃描線就是與x軸相平行的線,該算法可以由以下4個步驟實現:
- 初始化:堆棧置空,將初始種子點(x,y)入棧
- 出棧:若棧空則算法結束,否則取棧頂元素(x,y),以y作為當前掃描線
- 填充並確定種子點所在區段:從種子點(x,y)出發,向左右兩個方向填充,直到邊界。標記區段左右斷點為xl和xr。
- 確定新的種子點:在區間[xl,xr]中檢查與當前掃描線y上下相鄰的兩條掃描線上的像素。若存在非邊界、未填充像素,則把每一區間最右像素作為種子點壓入堆棧,返回第二步。
代碼實現:
void ScanLineFill4(HDC hdc, int x, int y, COLORREF oldColor, COLORREF newColor)
{
int xl, xr;
bool SpanNeedFill;
pair<int, int> seed;
stack<pair<int, int>> St;
seed.first = x; seed.second = y;
St.push(seed);
while (!St.empty())
{
seed = St.top();
St.pop();
y = seed.second;
x = seed.first;
while (GetPixel(hdc,x,y) == oldColor)//向右填充
{
SetPixel(hdc, x, y, newColor);
x++;
}
xr = x - 1;
x = seed.first - 1;
while (GetPixel(hdc, x, y) == oldColor)//向左填充
{
SetPixel(hdc, x, y, newColor);
x--;
}
xl = x + 1;
//處理上方的一條掃描線
x = xl;
y = y + 1;
while (x<xr)
{
SpanNeedFill = false;
while (GetPixel(hdc,x,y)==oldColor)
{
SpanNeedFill = true;
x++;
}
if (SpanNeedFill)
{
seed.first = x - 1; seed.second = y;
St.push(seed);
SpanNeedFill = false;
}
while (GetPixel(hdc, x, y) != oldColor && x < xr)x++;
}
//處理下方的一條掃描線
x = xl;
y = y - 2;
while (x < xr)
{
SpanNeedFill = false;
while (GetPixel(hdc, x, y) == oldColor)
{
SpanNeedFill = true;
x++;
}
if (SpanNeedFill)
{
seed.first = x - 1; seed.second = y;
St.push(seed);
SpanNeedFill = false;
}
while (GetPixel(hdc, x, y) != oldColor && x < xr)x++;
}
}
}
這次畫一個不太規則的圖形試試吧。
Bresenham_Line(100, 100, 150, 150, hdc, RGB(0, 0, 0));
Bresenham_Line(150, 150, 200, 100, hdc, RGB(0, 0, 0));
Bresenham_Line(200, 100, 200, 300, hdc, RGB(0, 0, 0));
Bresenham_Line(200, 300, 100, 300, hdc, RGB(0, 0, 0));
Bresenham_Line(100, 300, 100, 100, hdc, RGB(0, 0, 0));
ScanLineFill4(hdc, 150, 175, RGB(255, 255, 255), RGB(0, 255, 0));

這次由於對每一個待填充區段只需要壓棧一次,所以效率提高了,也沒有堆棧溢出的危險。
有序邊表的掃描線算法
接下來是最復雜的一種的掃描線算法,需要多邊形的所有邊信息,主要思想是求得每一條掃描線與多邊形的交點,從而兩兩配對得到處在多邊形內的區間,對這些區間進行上色,但要求掃描線與多邊形的交點,直接暴力地遍歷每條邊肯定是不可行的,我們需要引入活性邊表AET
和新邊表NET
來輔助計算。
NET中存放的是在該掃描線第一次出現的邊,也就是最低端點的y值等於當前掃描線位置的邊,對每一個結點,需要存儲當前x值、直線斜率倒數和直線最高點y值,如下圖所示:

通過NET就可以容易地得到AET,AET中存放的是掃描線與多邊形的交點。我們從下往上遍歷每條掃描線,對於掃描線i來說,將NET[i]中結點插入,將AET[i-1]中ymax=i的結點刪除,其余結點將x值加上斜率倒數之后插入,就得到了AET[i]。

有了AET之后,只需要配對每兩個交點,把區間內像素上色就可以了,但要注意,由於要從左到右配對,所以
AET表應時刻保持按x坐標遞增排序。
偽代碼:
void PolyFill(polygon,color)
{
初始化新邊表NET和活性邊表AET;
for(每條掃描線i)
{
把ymin=i的邊放進邊表NET[i];
}
for(每條掃描線i)
{
把新邊表NET[i]中結點插入AET[i](x坐標遞增有序排列);
AET[i-1]中ymax!=i的結點加入AET[i];
遍歷AET[i],把配對交點區間中像素上色;
}
}
邊界標志算法
還有一種基於掃描線思想的邊界標志算法,比較適合用硬件實現。基本思想是對多邊形每條邊進行掃描轉換,找到多邊形邊界的所有像素,對每條與多邊形相交的掃描線按從左到右的順序掃描每個像素,用一個布爾值inside表示當前點是否在多邊形內(初始為false),只要掃描到多邊形邊界像素,就把inside取反,若inside為真,則表示該點在多邊形內,則填充該像素。
偽代碼:
void edgemark_fill(polydef,color)
{
對多邊形每條邊掃描轉換;
inside=false;
for(每條掃描線)
{
for(掃描線上每個像素)
{
if(該像素是邊界像素)
inside=!inside;
else if(inside==true)
SetPixel(x,y,color);
}
}
}