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