[Windows图形编程]种子填充算法

        PaintBoardDemo工程到了实现填充这一步,实现算法基本选定为种子填充算法。经过较长一段时间的反复修正算法及调试(╮(╯▽╰)╭),现在基本已经定型了。是时候来记录一下这个过程中的“坎坷经历”了。

        首先大致介绍一下种子填充算法,如下图,基本可以概括为选定屏幕中任意一点,由此为种子,向其四周(即上下左右)“开花”(即填充),并且将上下左右各点当成种子点,直至满足If表达式的点个数为0。

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需要从内存中读取某一点像素值,进行计算,写入新的像素值到内存中(个人理解,若存在不严谨或错误之处望指出)。

       因此如果能够直接在内存中修改像素值,即将位图数据锁定到内存,通过指针直接访问及修改各点的像素值,那势必将显著提升整个填充算法的运行效率。实现这一想法主要通过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之间,嗯,效率还是让人满意的。那么最后贴一张程序运行结果图好啦。

10232401_GpAl.png

转载于:https://my.oschina.net/u/1017232/blog/121919

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值