[計算機圖形學] 基於C#窗口的Bresenham直線掃描算法、種子填充法、掃描線填充法模擬軟件設計(二)


 

上一節鏈接:http://www.cnblogs.com/zjutlitao/p/4116783.html 

 

前言:

  在上一節中我們已經大致介紹了該軟件的是什么、可以干什么以及界面的大致樣子。此外還詳細地介紹了Bresenham直線掃描算法的核心思想及實現,並在最終在2-1小節引出工程中對於該算法具體的實現。本節將着手講解多邊形填充算法。

  


二、承接上篇 

  2-1、多邊形掃描轉換

  把頂點表示轉換為點陣表示:①從多邊形的給定邊界出發,求出其內部的各個像素;②並給幀緩沖器中各個對應元素設置相應灰度或顏色
   

 2-2、區域填充

  區域定義:已經表示成點陣的像素集合;
  區域的表示:①內部表示:把給定區域內的像素枚舉出來;②邊界表示:把區域邊界上的像素枚舉出來
    

 2-3、四連通填充和八連通填充

  根據區域的特性這里可以把區域分為四連通鄰域和八連通鄰域。①這里所謂四連通區域即:區域內任意兩個像素,從一個像素出發,可以通過上、下、左、右四種運動,到達另一個像素;②所謂的八連通區域即:區域內任意兩個像素,從一個像素出發,可以通過水平、垂直、正對角線、反對角線八種運動,到達另一個像素。  

      
  因此,我們可以利用四連通或八連通的可達的特性,設計出基於種子的洪泛填充方法。這里以四連通種子填充為例:
  ①找到一個區域內的點,進行填充
  ②將其上下左右非邊界且非已填充的點作為種子點繼續填充
  ③直至遞歸結束

 1 //4-connected boundary-fill
 2 //一種基於邊界返回的四連通填充法,比如繪圖顏料桶的作用
 3 void BoundaryFill4(int x,int y,int fill,int boundary)
 4 {
 5     int current;
 6     current = getpixel(x, y);
 7     if ((current != boundary) && (current != fill))
 8     {
 9         putpixel(x, y, fill);
10         BoundaryFill4(x+1, y, fill, boundary);
11         BoundaryFill4(x-1, y, fill, boundary);
12         BoundaryFill4(x, y+1, fill, boundary);
13         BoundaryFill4(x, y-1, fill, boundary);
14     }
15 }
16 //4-connected boundary-fill
17 //一種基於新顏色返回的四連通填充法,最終多把內部和邊界顏色塗成一樣
18 void FloodFill4(int x,int y,int fillColor,int oldColor)
19 {
20     int current;
21     current = getpixel(x, y);
22     if (current == oldColor)
23     {
24         putpixel(x, y, fillColor);
25         BoundaryFill4(x+1, y, fillColor, oldColor);
26         BoundaryFill4(x-1, y, fillColor, oldColor);
27         BoundaryFill4(x, y+1, fillColor, oldColor);
28         BoundaryFill4(x, y-1, fillColor, oldColor);
29     }
30 }

   其實基於八連通的填充算法也是類似,是一種遞歸算法!實踐證明:當圖過大進行遞歸時會出現爆棧的危險(本工程中如果選擇2x2像素的情況用種子填充法填充多邊形就會出現爆棧的情況)。下面是工程中用於種子填充的具體函數:該函數輸入為一個種子點(注意該種子點一定要確保在區域內!),第3行是為了程序安全考慮,防止繪制超出窗口VRAM;這里第9行是檢查checkBox的勾選來決定是用四連通填充還是用八連通填充。這里需要特別說明的有兩點

  ①Vram[a.X + a.Y * 600]是一個對窗口中各點的標記一維數組,能夠將二位坐標映射到該一維數組中。其值初始為false:表示未走過或不是邊界,當在畫點成線的過程中計算邊界的同時已經把邊界標記為true當做已走過的點,這樣就能為種子填充提供邊界條件,其實這里Vram可以用INT類型,1表示邊界、0表示未填充、2表示填充,就能在填充好之后區分邊界和內部了;
  ②direction_4[4]和direction_8[8]是從當前點按照四\八連通走法向其相鄰點移動的偏移量,用他們和當前點疊加就實現了當前點向其周圍點坐標的轉換。(這里direction_n[i]=pSearch_n[i]*XiangSu)

1 public Point[] pSearch_8 = 
2 { new Point(-1, 0), new Point(-1, 1), new Point(0, 1), new Point(1, 1),
3     new Point(1, 0), new Point(1, -1), new Point(0, -1), new Point(-1, -1) 
4 };                                      //八聯通填充八個方向偏移(原始偏移)
5 public Point[] pSearch_4 = 
6 { new Point(-1, 0), new Point(0, 1), new Point(1, 0), new Point(0, -1) 
7 };                                      //四聯通填充四個方向偏移
 1 void FloodSeedFill(Point a)
 2 {
 3     if (a.X < XiangSu / 2 || a.X > 508 || a.Y < XiangSu / 2 || a.Y > 440) return;//邊界情況
 4     if(Vram[a.X + a.Y*600]==false)
 5     {
 6         Rectangle rect = new Rectangle(a.X - XiangSu / 2, a.Y - XiangSu / 2, XiangSu, XiangSu);
 7         g.FillEllipse(red, rect);
 8         Vram[a.X + a.Y * 600] = true;//標記已經走過
 9         if (checkBox.Checked == true)
10             for (int i = 0; i < 4; i++)
11             {
12                 tempp.X = a.X + direction_4[i].X;
13                 tempp.Y = a.Y + direction_4[i].Y;
14                 FloodSeedFill(tempp);
15             }
16         else
17             for (int i = 0; i < 8; i++)
18             {
19                 tempp.X = a.X + direction_8[i].X;
20                 tempp.Y = a.Y + direction_8[i].Y;
21                 FloodSeedFill(tempp);
22             } 
23     }
24 }

   由上面的分析,我們已經知道了種子填充法會存在大圖爆棧的危險,此外對於種子填充法其初始種子點的選取也是比較費時的,因為我們事先並不知道哪個點是在區域內的(我這里投機取巧取了邊界點中第4個點的內側作為初始點,所以只有運氣好才會成功!)。而且,種子填充法最嚴重的問題是:無法處理交叉的圖形填充(因為帶有邊交叉的圖形或多個不連通的圖形不符合連通性的要求了)。 

  2-4、掃描線填充法

    既然上述種子填充算法存在這么多缺點,這里就介紹一種更加有效的方法。首先咱們還是先看幾個逐點判斷的例子:

       

           (a) 射線法                                      (b) 夾角法

                         圖:確定點在多邊形內還是多邊形外的兩種方法

  上面左圖是通過射線法與邊界交點數來判斷該點在內部還是在外部;上面右圖書利用夾角和是否為360°判斷該點是否在多邊形內部。但是可以想到這兩種甚至是所有的逐點判斷都不是太理想,對於百萬級像素的圖像生成就相當費時。那么就要介紹一種非逐點描繪的方法——掃描線算法。

  掃描線算法充分利用了相鄰像素之間的連貫性,避免了對像素的逐點判斷和求交運算,提高了算法效率。這里主要介紹三種連貫性:①區域連貫性;②掃描線連貫性;③邊的連貫性。

           
           (a) 區域連貫性                  (b) 掃描線連貫性             (c) 邊的連貫性
                                圖:三種主要的連貫性用於優化區域填充算法

  由上面的三種連貫性可以看出:在掃描線連貫性的基礎上應用邊的連貫性,就可以由一條掃描線快速地求出下一條掃描線然后整條線整條線地填充,這就是掃描線填充算法的核心思路!下面將結合本工程關於掃描線算法的實現講解掃描線算法(主要是復雜的數據結構了)的實現。

  2-4-1、掃描線填充算法中的數據結構介紹

    為了實現掃描線算法,我們需要專門設置一下邊的數據結構。如下:一個邊的類包括①xi,即邊的下端點x坐標,在活化邊鏈表中表示掃描線與邊的交點的x坐標;②dx,即邊的斜率的倒數;③ymax,即邊的上頂點的y值。此外這里對幾個比較等關系進行了重載(這里邊的小於關系見代碼)

 1 public class EDGE
 2 {
 3     public double xi;//邊的下端點x坐標,在活化鏈表(AET)中,表示掃描線與邊的交點x坐標
 4     public double dx;//是個常量(直線斜率的倒數)(x + dx, y + 1)
 5     public int ymax;//邊的上頂點的y值
 6     public static bool operator <(EDGE a, EDGE b)//重載排列關系
 7     {
 8         return (Math.Abs(a.xi - b.xi)<1 ? a.dx < b.dx : a.xi < b.xi);
 9     }
10     public static bool operator >(EDGE a, EDGE b)//重載排列關系
11     {
12         return (Math.Abs(a.xi - b.xi) < 1 ? a.dx > b.dx : a.xi > b.xi);
13     }
14     public static bool operator ==(EDGE a, EDGE b)//重載等於號
15     {
16         return (Math.Abs(a.xi - b.xi)<1  && a.dx == b.dx && a.ymax == b.ymax);
17     }
18     public static bool operator !=(EDGE a, EDGE b)//重載不等於號
19     {
20         return (Math.Abs(a.xi - b.xi)>1 || a.dx != b.dx || a.ymax != b.ymax);
21     }
22 }

  這樣對於一個多邊形,則有相應的一個描述它的新邊表。在本工程中新邊表用List<EDGE>[] NET = new List<EDGE>[500];定義,這樣NET[i]就表示掃描線y=i時剛要與之相交的邊的集合。特別提示下這里建立“新邊表”的規則就是:如果某條邊的較低端點(y坐標較小的那個點)的y坐標與掃描線y相等,則該邊就是掃描線y的新邊,應該加入掃描線y的“新邊表”。
        
  根據上面我們掃描線的連貫性的分析可知:如果能找到當前掃描線與多邊形的交點就能方便地繪制出該掃描線上的紅色(內部)線段了!比如這里y=7時我們如果能知道這樣一個結構就能輕易繪制出內部線段了。這里我們把這個數據結構定義為活動邊表:AET(這是個按照邊從小到大排序的鏈表)。

    

  這樣我們有了邊的數據結構、有了新邊表、有了活動邊表就能實施我們的掃描線算法了:AET是掃描線填充算法的核心,整個算法都是圍繞者這張表進行處理的。要完整的定義AET,需要先定義邊的數據結構。每條邊都和掃描線有個交點,掃描線填充算法只關注交點的x坐標。每當處理下一條掃描線時,根據△x直接計算出新掃描線與邊的交點x坐標,可以避免復雜的求交計算。一條邊不會一直待在AET中,當掃描線與之沒有交點時,要將其從AET中刪除,判斷是否有交點的依據就是看掃描線y是否大於這條邊兩個端點的y坐標值,為此,需要記錄邊的y坐標的最大值。

  2-4-2、掃描線填充算法細節介紹

  下面是整個算法的唯一外部需要調用的函數,該函數需要傳入多邊形頂點的集合Q,WinForm繪圖類實例的對象g,以及方便各種像素仿真用的XiangSu參數(因為FORM中使用的像素較高,繪制的點比較小,這里是按比例放大的)。從下面代碼來看可知:①定義新邊表然后實例化(一定要實例化,我當初沒實例化吃了不少虧!);②第11行根據多邊形的點求出y的最大和最小值(一定要用out關鍵字,C#和MFC不同,我沒找到同名引用,就用這個能實現函數內部修改,然后函數結束該值也變化的效果);③第12行函數是初始化新邊表;④第13行函數負責繪制所有的水平邊;⑤第14行函數負責掃描線填充實現。

 1 public void ScanLinePolygonFill(List<Point> Q, Graphics g, int XiangSu)
 2 {
 3     this.XiangSu = XiangSu;
 4     this.g = g;
 5 
 6     List<EDGE>[] NET = new List<EDGE>[500];//定義新邊表
 7     for (int i = 0; i < 500; i++) NET[i] = new List<EDGE>();//實例化
 8 
 9     int ymax=0, ymin=0;//多邊形y的最大值和最小值
10 
11     GetPolygonMinMax(Q, out ymax, out ymin);//計算更新ymax和ymin(ok)
12     InitScanLineNewEdgeTable(NET, Q, ymin, ymax);//初始化新邊表
13     HorizonEdgeFill(Q); //水平邊直接畫線填充
14     ProcessScanLineFill(NET, ymin, ymax);
15 }

   這里是初始化新邊表的函數。該函數通過遍歷所有頂點獲得邊的信息,對於一些特殊情況(< 左交點、> 右交點、V 下交點、^ 上交點)通過判斷與此邊有關的前后兩個頂點的情況,確定此邊的ymax是否需要做-1修正。這里ps和pe分別是當前處理邊的起點和終點,pss是起點的前一個相鄰點,pee是終點的后一個相鄰點,pss和pee用於輔助判斷ps和pe兩個點是否是左頂點或右頂點,然后根據判斷結果對此邊的ymax進行-1修正,算法實現非常簡單,注意與掃描線平行的邊是不處理的,因為水平邊直接在HorizonEdgeFill()處理了。最后還按照邊從小到大的順序給每個掃描線上的新邊集合排個序。

 1 /// <summary>
 2 /// 初始化新邊表
 3 /// 算法通過遍歷所有的頂點獲得邊的信息,然后根據與此邊有關的前后兩個頂點的情況
 4 /// 確定此邊的ymax是否需要-1修正。ps和pe分別是當前處理邊的起點和終點,pss是起
 5 /// 點的前一個相鄰點,pee是終點的后一個相鄰點,pss和pee用於輔助判斷ps和pe兩個
 6 /// 點是否是左頂點或右頂點,然后根據判斷結果對此邊的ymax進行-1修正,算法實現非
 7 /// 常簡單,注意與掃描線平行的邊是不處理的,因為水平邊直接在HorizonEdgeFill()
 8 /// 函數中填充了。
 9 /// </summary>
10 private void InitScanLineNewEdgeTable(List<EDGE>[] NET, List<Point> Q, int ymin, int ymax)
11 {
12     List<int> temp = new List<int>();
13     EDGE e;
14     for (int i = 0; i < Q.Count; i++)
15     {
16         Point ps = Q[i];
17         Point pe = Q[(i + 1) % Q.Count];
18         Point pss = Q[(i - 1 + Q.Count) % Q.Count];
19         Point pee = Q[(i + 2) % Q.Count];
20         if (pe.Y != ps.Y)//不處理平行線
21         {
22             e = new EDGE();
23             e.dx = (double)(pe.X - ps.X) / (double)(pe.Y - ps.Y) * XiangSu;
24             if (pe.Y > ps.Y)
25             {
26                 e.xi = ps.X;
27                 if (pee.Y >= pe.Y)
28                     e.ymax = pe.Y - XiangSu;
29                 else
30                     e.ymax = pe.Y;
31                 NET[ps.Y - ymin].Add(e);//加入對應的NET里
32                 temp.Add(ps.Y - ymin);
33             }
34             else
35             {
36                 e.xi = pe.X;
37                 if (pss.Y >= ps.Y)
38                     e.ymax = ps.Y - XiangSu;
39                 else
40                     e.ymax = ps.Y;
41                 NET[pe.Y - ymin].Add(e);//加入對應的NET里
42                 temp.Add(pe.Y - ymin);
43             }
44         }
45     }
46     for (int i = 0; i < temp.Count; i++)
47     {
48         My_Sort(ref NET[temp[i]]);
49     }
50 }

  對於掃描線填充子函數比較好理解:①第3行是實例化一個AET,用於動態記錄掃描線移動過程中掃描線與多邊形相交的邊的實時信息;②第9行函數負責將掃描線對應的所有新邊插入到AET中,插入操作到保證AET還是有序表(注意這里AET前面的關鍵字!);③第16行負責執行具體的填充動作,它將AET中的邊交點成對取出組成填充區間,然后根據“左閉右開”的原則對每個區間填充;④第17行函數負責將對下一條掃描線來說已經不是“活動邊”的邊從AET中刪除,刪除的條件就是當前掃描線y與邊的ymax相等,如果有多條邊滿足這個條件,則一並全部刪除;⑤第18行函數負責更新邊表中每項的xi值,就是根據掃描線的連貫性用dx對其進行修正,並且根據xi從小到大的原則對更新后的AET表重新排序。

 1 private void ProcessScanLineFill(List<EDGE>[] NET, int ymin, int ymax)
 2 {
 3     List<EDGE> AET=new List<EDGE>();//掃描線
 4     for (int y = ymin; y < ymax; y+=XiangSu)
 5     {
 6         #region 顯示運算信息
 7         g.DrawLine(new Pen(red),new Point(10,y),new Point(20,y));
 8         g.DrawString(AET.Count.ToString(), new Font("微軟雅黑", 6), blue, new Point(2, y));
 9         InsertNetListToAet(NET[y-ymin], ref AET);
10         g.DrawString(y + " -> " + NET[y - ymin].Count + " -> " + AET.Count.ToString(), new Font("微軟雅黑", 6), blue, new Point(25, y));
11         for (int i = 0; i < AET.Count; i++)
12         {
13             g.DrawString((((int)AET[i].xi) / XiangSu * XiangSu).ToString() + " ", new Font("微軟雅黑", 6), blue, new Point(400 + i * 24, y));
14         }
15         #endregion
16         FillAetScanLine(ref AET, y);
17         RemoveNonActiveEdgeFromAet(ref AET, y);//刪除非活動邊
18         UpdateAndResortAet(ref AET);//更新活動邊表中每項的xi值,並根據xi重新排序
19     }
20 }
 1 /// <summary>
 2 /// 負責將掃描線對應的所有新邊插入到aet中,插入操作到保證AET
 3 /// 還是有序表,插入排序的思想
 4 /// </summary>
 5 /// <param name="list"></param>
 6 /// <param name="AET"></param>
 7 private void InsertNetListToAet(List<EDGE> list, ref List<EDGE> AET)
 8 {
 9     if (list.Count == 0) return;
10     if (AET.Count == 0)
11     {
12         AET = list; 
13         return;
14     }//剛開始這里寫成if()AET=list;return;一直出錯!下次一定要規范!!!
15     List<EDGE> temp = new List<EDGE>();
16     int i = 0, j = 0;
17     while (i < list.Count && j < AET.Count)
18     {
19         if (list[i] == AET[j])
20         {
21             i++;
22             temp.Add(AET[j]);
23             j++;
24             continue;
25         }
26         if (list[i] < AET[j])
27         {
28             temp.Add(list[i]);
29             i++;
30             continue;
31         }
32         if (list[i] > AET[j])
33         {
34             temp.Add(AET[j]);
35             j++;
36             continue;
37         }
38     }
39     while (i < list.Count)
40     {
41         temp.Add(list[i]);
42         i++;
43     }
44     while (j < AET.Count)
45     {
46         temp.Add(AET[j]);
47         j++;
48     }
49     AET = temp;
50     //for (int i = 0; i < list.Count; i++)
51     //{
52     //    AET.Add(list[i]);
53     //}
54     //My_Sort(ref AET);
55 }
InsertNetListToAet(List list, ref List AET)
 1 /// <summary>
 2 /// FillAetScanLine()函數執行具體的填充動作,
 3 /// 它將aet中的邊交點成對取出組成填充區間,
 4 /// 然后根據“左閉右開”的原則對每個區間填充
 5 /// </summary>
 6 /// <param name="AET"></param>
 7 /// <param name="y"></param>
 8 private void FillAetScanLine(ref List<EDGE> AET, int y)
 9 {
10     if (AET.Count < 2) return;
11     y = y / XiangSu * XiangSu;
12     for (int i = 0; i < AET.Count; i += 2)
13     {
14         int from = ((int)AET[i].xi + XiangSu) / XiangSu * XiangSu;
15         int to = ((int)(AET[i + 1].xi + XiangSu / 2)) / XiangSu * XiangSu;
16         while (from < to)
17         {
18             Rectangle rect = new Rectangle(from - XiangSu / 2, y - XiangSu / 2, XiangSu, XiangSu);
19             g.FillEllipse(red, rect);
20             from += XiangSu;
21         }
22     }
23 }
private void FillAetScanLine(ref List AET, int y)
 1 /// <summary>
 2 /// 負責將對下一條掃描線來說已經不是“活動邊”的邊從aet中刪除,
 3 /// 刪除的條件就是當前掃描線y與邊的ymax相等,如果有多條邊滿
 4 /// 足這個條件,則一並全部刪除
 5 /// </summary>
 6 /// <param name="AET"></param>
 7 /// <param name="y"></param>
 8 private int line = 0;
 9 private void RemoveNonActiveEdgeFromAet(ref List<EDGE> AET, int y)
10 {
11     line = y;
12     AET.RemoveAll(IsEdgeOutOfActive);
13 }
14 private bool IsEdgeOutOfActive(EDGE obj)
15 {
16     return line == obj.ymax;
17 }
18 
19 /// <summary>
20 /// 更新邊表中每項的xi值,就是根據掃描線的連貫性用dx對其進行修正,
21 /// 並且根據xi從小到大的原則對更新后的aet表重新排序
22 /// </summary>
23 /// <param name="AET"></param>
24 private void UpdateAndResortAet(ref List<EDGE> AET)
25 {
26     AET.ForEach(UpdateAetEdgeInfo);//更新xi
27     My_Sort(ref AET);
28 }
29 private void UpdateAetEdgeInfo(EDGE e)
30 {
31     e.xi += e.dx;
32 }
更新和刪除的函數(這里面用了C#里List類的一個很好玩的方法,竟然能自定義刪除想刪的元素,而代碼就這么短!)

三、最終效果 

 如下圖最終實現了畫點成線,連線成圖,並用種子填充和掃描划線兩種方法實現了對多邊形的填充,有很好的展示效果,希望對想了解這幾個算法的朋友有用!(親-:)看完贊一下哦,讓更多的分享)

 

 

相關鏈接

  上一節鏈接: http://www.cnblogs.com/zjutlitao/p/4116783.html

  上述工程C#源碼:http://pan.baidu.com/s/1kTrAI5h 

  Bresenham算法講解pdf:http://pan.baidu.com/s/1sjM6Cax  

  連點成線課件pdf:http://pan.baidu.com/s/1GV9i2

  圖形填充課件pdf:http://pan.baidu.com/s/1kTJvfOr

  參考博客(good):http://blog.csdn.net/orbit/article/details/7368996

  C#List.Sort的用法:http://msdn.microsoft.com/zh-cn/library/b0zbh7b6.aspx

 C#參數傳遞百度文庫:鏈接太長啦♪(^∇^*)

  C#參數傳遞博客:http://www.cnblogs.com/qq731109249/archive/2012/11/01/2750401.html

  C#List.RemoveAll方法:http://technet.microsoft.com/zh-cn/library/wdka673a(it-it,VS.85).aspx

  最后還是打擊盜版LZ鏈接:http://www.cnblogs.com/zjutlitao/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM