PaintBoardDemo工程到了實現填充這一步,實現算法基本選定爲種子填充算法。通過較長一段時間的反覆修正算法及調試(╮(╯▽╰)╭),如今基本已經定型了。是時候來記錄一下這個過程當中的「坎坷經歷」了。css
首先大體介紹一下種子填充算法,以下圖,基本能夠歸納爲選定屏幕中任意一點,由此爲種子,向其四周(即上下左右)「開花」(即填充),而且將上下左右各點當成種子點,直至知足If表達式的點個數爲0。html
能夠看到圖中的算法用到了遞歸,這裏,博主就迎來了須要對算法進行改進的第一個問題,即消除遞歸。也許有人會問,爲何要消除遞歸呢?這裏就牽涉到一部分操做系統的知識了,通常編譯後的程序運行時佔用的內存分爲幾個部分,其中就包含棧,堆兩個區域。棧,由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。堆,通常由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。可是它和數據結構中的堆是兩回事,分配方式卻是相似於鏈表。在函數調用時,第一個進棧的是主函數中的下一條指令(函數調用語句的下一條可執行語句)的地址(以便在函數調用結束後,程序能從正確的地址繼續執行),而後是函數的各個參數,而後是函數的局部變量。當本次函數調用結束後,局部變量先出棧,而後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。而然在Windows操做系統下,內存中分配給棧的空間是有限制的,大小在1-2MB左右。由此可想而知,若是經過遞歸來實現算法,一旦填充區域較大,致使函數N次調用,很快系統分配給棧的內存空間就會耗盡,拋出stackoverflow異常。程序員
這裏,咱們考慮到經過隊列來消除遞歸,即將任意一點做爲起始種子入隊,出隊進行填充,檢測該點的上下左右四個方向,將符合填充條件的點繼續入隊,直至隊列爲空,那麼目標區域就被完整填充了。算法
1: //私有變量,用來保存起始點的顏色
2:
3: //將做爲判斷是否符合填充條件的重要依據(和起始點顏色一致即認爲須要填充)
4: private Color _sourcePointColor;
5:
6: private Bitmap _bitmapTemp;
7:
8: public void FloodFillMethod(Bitmap bitmap, Point seedPoint, Color fillColor)
9:
10: {
11: _bitmapTemp = bitmap;
12:
13: //記錄起始點顏色
14: _sourcePointColor = _bitmapTemp.GetPixel(seedPoint.X, seedPoint.Y);
15:
16: //應某人要求,不可直接使用MS的Queue<>類
17:
18: //所以ArrayQueue爲博主本身動手寫的隊列類,各位看官請直接無視
19: ArrayQueue myQueue = new ArrayQueue();
20: myQueue.Enqueue(seedPoint);
21:
22:
23: while (!myQueue.IsEmpty())
24: {
25:
26: Point seed = (Point)myQueue.Dequeue();
27: bitmap.SetPixel(seed.X, seed.Y, fillColor);
28:
29: //判斷右側點是否符合填充條件
30: if (IsValidPoint(new Point(seed.X + 1, seed.Y)))
31: myQueue.Enqueue(new Point(seed.X + 1, seed.Y));
32:
33: //判斷左側點是否符合填充條件
34: if (IsValidPoint(new Point(seed.X - 1, seed.Y)))
35: myQueue.Enqueue(new Point(seed.X - 1, seed.Y));
36:
37: //判斷下方點是否符合填充條件
38: if (IsValidPoint(new Point(seed.X, seed.Y + 1)))
39: myQueue.Enqueue(new Point(seed.X, seed.Y + 1));
40: //判斷上方點是否符合填充條件
41: if (IsValidPoint(new Point(seed.X, seed.Y - 1)))
42: myQueue.Enqueue(new Point(seed.X, seed.Y - 1));
43: }
44: }
45:
46: public bool IsValidPoint(Point seedPoint)
47: {
48: Color clr = _bitmapTemp.GetPixel(seedPoint.X, seedPoint.Y);
49: if (clr.ToArgb() == this._sourcePointColor.ToArgb()) return true;
50: else return false;
51: }
雖然以上代碼能夠實現填充,可是在運行中也暴露出一個很嚴重的問題,就是效率極其低下,分析代碼,不難看出存在極其嚴重的重複入隊現象。後來想到,符合條件的點入隊以前,就將其進行填充,那麼再下一次的遍歷到來時,調用IsValidPoint()方法,會發現該點的顏色已經和起始點的顏色不同了,那麼也就不會入隊,從而避免了重複入隊的現象。數組
1: //改進代碼以下
2: if(IsValidPoint(new Point(seed.X+1,seed.Y)))
3: {
4: _bitmapTemp.SetPixel(seed.X+1,seed.Y,fillColor);
5: myQueue.Enqueue(new Point(seed.X+1,seed.Y));
6: }
運行以後發現,效率確實有所提高,可是仍然達不到要求。致使這個因素最主要的緣由以下:數據結構
● GetPixel()和SetPixel()方法效率低下,每次僅完成一個像素的讀取和寫入。而且每一次填充的完成所需完成的操做很繁雜,如CPU須要從內存中讀取某一點像素值,進行計算,寫入新的像素值到內存中(我的理解,若存在不嚴謹或錯誤之處望指出)。ide
所以若是可以直接在內存中修改像素值,即將位圖數據鎖定到內存,經過指針直接訪問及修改各點的像素值,那勢必將顯著提高整個填充算法的運行效率。實現這一想法主要經過Bitmap類的LockBits方法。最終版本代碼以下:函數
1: private Bitmap _bitmapTemp; //位圖對象的引用
2: private Color _sourcePointColor; //保存起始點像素顏色,通常經過鼠標點擊得到起始點座標
3: private IntPtr _scan0; //指針,用以保存位圖鎖定到內存以後第一個像素的地址
4: private BitmapData _bitmapdata; //位圖在內存鎖定以後的數據,位圖圖像的特性
5: private int _stride; //位圖對象的跨距寬度(也稱爲掃描寬度)
6:
7: //Point數組,用來表示上下左右四個方向,經過For循環實現種子點四個方向的遍歷,從而減小重複代碼。
8: private Point[] _fill_direction = { new Point(1, 0), new Point(-1, 0), new Point(0, 1), new Point(0, -1) };
9:
10: public void FloodFillMethod(Bitmap bitmap, Point seedPoint, Color fillColor)
11: {
12: //初始化私有變量
13: this._bitmapTemp = bitmap;
14: this._sourcePointColor = _bitmapTemp.GetPixel(seedPoint.X, seedPoint.Y);
15:
16: //關於Scan0,Stride屬性不清楚的能夠查閱MSDN
17: this._bitmapdata = _bitmapTemp.LockBits(new Rectangle(0, 0, _bitmapTemp.Width, _bitmapTemp.Height), ImageLockMode.ReadWrite, _bitmapTemp.PixelFormat);
18: this._scan0 = _bitmapdata.Scan0;
19: this._stride = Math.Abs(_bitmapdata.Stride);
20:
21: //設置最大填充區間,不然超出範圍時將會拋出異常
22: int MIN_X = 1;
23: int MIN_Y = 1;
24: int MAX_X = _bitmapTemp.Width - 1;
25: int MAX_Y = _bitmapTemp.Height - 1;
26:
27: ArrayQueue myQueue = new ArrayQueue();
28: myQueue.Enqueue(seedPoint);
29:
30: while (!myQueue.IsEmpty())
31: {
32: Point seed = (Point)myQueue.Dequeue();
33:
34: for (int i = 0; i < 4; i++)
35: {
36: int new_point_x, new_point_y;
37: new_point_x = seed.X + _fill_direction[i].X;
38: new_point_y = seed.Y + _fill_direction[i].Y;
39:
40: if (new_point_x < MIN_X || new_point_x > MAX_X || new_point_y < MIN_Y || new_point_y > MAX_Y) continue;
41:
42: //C#含有指針操做的代碼,需放在unsafe{}塊中
43: unsafe
44: {
45: //計算新像素點在內存中的地址相對於第一個像素的偏移量
46: //Y軸座標*跨距寬度+X軸座標*4(1個int變量佔4個字節)
47: //能夠這樣理解,一個像素的顏色由四個份量組成,分別是Alpha透明度,Red紅色,Green綠色,Blue藍色。
48: //每個份量的範圍在[0,255],由8位二進制數表示,每一個份量各佔一個字節byte
49: //所以相鄰兩個像素,在內存中的地址相差4
50: int offset = new_point_y * _stride + new_point_x * 4;
51:
52: //得到新像素點在內存中的地址
53: IntPtr clr = _scan0 + offset;
54:
55: //比較新像素點和起始點顏色的值
56: if (*(int*)clr == _sourcePointColor.ToArgb())
57: {
58: //修改像素顏色值,即填充,而且入隊
59: *(int*)clr = fillColor.ToArgb();
60: myQueue.Enqueue(new Point(new_point_x, new_point_y));
61: }
62: }
63: }
64: }
65:
66: //完成填充以後,解鎖數據,釋放內存
67: _bitmapTemp.UnlockBits(_bitmapdata);
68: }
以上代碼運行效率爲:填充37W個像素點的時間在50~60ms之間,嗯,效率仍是讓人滿意的。那麼最後貼一張程序運行結果圖好啦。this