[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

相关文章
相关标签/搜索