[Windows圖形編程]種子填充算法

        PaintBoardDemo工程到了實現填充這一步,實現算法基本選定爲種子填充算法。通過較長一段時間的反覆修正算法及調試(╮(╯▽╰)╭),如今基本已經定型了。是時候來記錄一下這個過程當中的「坎坷經歷」了。css

        首先大體介紹一下種子填充算法,以下圖,基本能夠歸納爲選定屏幕中任意一點,由此爲種子,向其四周(即上下左右)「開花」(即填充),而且將上下左右各點當成種子點,直至知足If表達式的點個數爲0。html

image

       能夠看到圖中的算法用到了遞歸,這裏,博主就迎來了須要對算法進行改進的第一個問題,即消除遞歸。也許有人會問,爲何要消除遞歸呢?這裏就牽涉到一部分操做系統的知識了,通常編譯後的程序運行時佔用的內存分爲幾個部分,其中就包含棧,堆兩個區域。棧,由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。堆,通常由程序員分配釋放,若程序員不釋放,程序結束時可能由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

相關文章
相關標籤/搜索