圖形學入門(3)——區域填充算法(region filling)

繼續圖形學之旅,咱們已經解決了如何畫線和畫圓的問題,接下來要解決的是,如何往一個區域內填充顏色?對一個像素填充顏色只需調用SetPixel之類的函數就好了,因此這個問題其實就是:如何找到一個區域內的全部像素?c++

區域的表示方法

定義一個區域能夠有兩種方法,即內點表示法邊界表示法,內點表示就是指用一種顏色表示區域內的點,只要當前像素是這種顏色就在區域內,邊界表示就是用一種顏色表示區域邊界,只要當前像素是這種顏色就表示到達了區域邊界。算法

簡單的種子填充算法

最簡單暴力的填充算法便是從區域內一點出發,向四周擴散填充,到達區域邊界時中止,常見的有四鄰法八鄰法兩種,顧名思義,一個是向上下左右四個方向擴散填充,另外一個是向周圍八個方向擴散,四鄰法能夠確保不溢出區域邊界,但有可能出現一次填不滿區域的狀況,八鄰法則相反,必定能填充滿當前區域,但有從對角線溢出邊界的危險。ide

邊界表示的四鄰法代碼實現:函數

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));

運行效果:3d

很顯然,這種遞歸的填充算法簡單好理解,但效率是不可接受的,實際上我運行時填充100*100像素的區域就直接GG了(堆棧溢出),顯然咱們須要提升算法效率,避免過多的遞歸調用。code

掃描線種子填充算法

爲了提升效率可使用掃描線種子填充算法,這裏的掃描線就是與x軸相平行的線,該算法能夠由如下4個步驟實現:blog

  1. 初始化:堆棧置空,將初始種子點(x,y)入棧
  2. 出棧:若棧空則算法結束,不然取棧頂元素(x,y),以y做爲當前掃描線
  3. 填充並肯定種子點所在區段:從種子點(x,y)出發,向左右兩個方向填充,直到邊界。標記區段左右斷點爲xl和xr。
  4. 肯定新的種子點:在區間[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);
        }
    }
}
相關文章
相關標籤/搜索