From http://blog.csdn.net/orbit/article/details/7343236算法
1.3扫描线种子填充算法函数
1.1和1.2节介绍的两种种子填充算法的优势是很是简单,缺点是使用了递归算法,这不但须要大量栈空间来存储相邻的点,并且效率不高。为了减小算法中的递归调用,节省栈空间的使用,人们提出了不少改进算法,其中一种就是扫描线种子填充算法。扫描线种子填充算法再也不采用递归的方式处理“4-联通”和“8-联通”的相邻点,而是经过沿水平扫描线填充像素段,一段一段地来处理“4-联通”和“8-联通”的相邻点。这样算法处理过程当中就只须要将每一个水平像素段的起始点位置压入一个特殊的栈,而不须要象递归算法那样将当前位置周围还没有处理的全部相邻点都压入堆栈,从而能够节省堆栈空间。应该说,扫描线填充算法只是一种避免递归,提升效率的思想,前面提到的注入填充算法和边界填充算法均可以改进成扫描线填充算法,下面介绍的就是结合了边界填充算法的扫描线种子填充算法。学习
扫描线种子填充算法的基本过程以下:当给定种子点(x, y)时,首先分别向左和向右两个方向填充种子点所在扫描线上的位于给定区域的一个区段,同时记下这个区段的范围[xLeft, xRight],而后肯定与这一区段相连通的上、下两条扫描线上位于给定区域内的区段,并依次保存下来。反复这个过程,直到填充结束。spa
扫描线种子填充算法可由下列四个步骤实现:.net
(1) 初始化一个空的栈用于存放种子点,将种子点(x, y)入栈;blog
(2) 判断栈是否为空,若是栈为空则结束算法,不然取出栈顶元素做为当前扫描线的种子点(x, y),y是当前的扫描线;递归
(3) 从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,直到边界。分别标记区段的左、右端点坐标为xLeft和xRight;ip
(4) 分别检查与当前扫描线相邻的y - 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,从xLeft开始向xRight方向搜索,若存在非边界且未填充的像素点,则找出这些相邻的像素点中最右边的一个,并将其做为种子点压入栈中,而后返回第(2)步;ci
这个算法中最关键的是第(4)步,就是从当前扫描线的上一条扫描线和下一条扫描线中寻找新的种子点。这里比较难理解的一点就是为何只是检查新扫描线上区间[xLeft, xRight]中的像素?若是新扫描线的实际范围比这个区间大(并且不连续)怎么处理?我查了不少计算机图形学的书籍和论文,好像都没有对此作过特殊说明,这使得不少人在学习这门课程时对此有挥之不去的疑惑。本着“毁人”不倦的思想,本文就罗嗦解释一下,但愿能解除你们的疑惑。get
若是新扫描线上实际点的区间比当前扫描线的[xLeft, xRight]区间大,并且是连续的状况下,算法的第(3)步就处理了这种状况。如图(4)所示:
图(4) 新扫描线区间增大且连续的状况
假设当前处理的扫描线是黄色点所在的第7行,则通过第3步处理后能够获得一个区间[6,10]。而后第4步操做,从相邻的第6行和第8行两条扫描线的第6列开始向右搜索,肯定红色的两个点分别是第6行和第8行的种子点,因而按照顺序将(6, 10)和(8, 10)两个种子点入栈。接下来的循环会处理(8, 10)这个种子点,根据算法第3步说明,会从(8, 10)开始向左和向右填充,因为中间没有边界点,所以填充会直到遇到边界为止,因此尽管第8行实际区域比第7行的区间[6,10]大,可是仍然获得了正确的填充。
若是新扫描线上实际点的区间比当前扫描线的[xLeft, xRight]区间大,并且中间有边界点的状况,算法又是怎么处理呢?算法描述中虽然没有明确对这种状况的处理方法,可是第4步肯定上、下相邻扫描线的种子点的方法,以及靠右取点的原则,实际上暗含了从相邻扫描线绕过障碍点的方法。下面以图(5)为例说明:
图(5) 新扫描线区间增大且不连续的状况
算法第3步处理完第5行后,肯定了区间[7, 9],相邻的第4行虽然实际范围比区间[7, 9]大,可是由于被(4, 6)这个边界点阻碍,使得在肯定种子点(4, 9)后向左填充只能填充右边的第7列到第10列之间的区域,而左边的第3列到第5列之间的区域没有填充。虽然做为第5行的相邻行,第一次对第4行的扫描根据靠右原则只肯定了(4, 9)一个种子点。可是对第3行处理完后,第4行的左边部分做为第3行下边的相邻行,再次获得扫描的机会。第3行的区间是[3, 9],向左跨过了第6列这个障碍点,第2次扫描第4行的时候就从第3列开始,向右找,能够肯定种子点(4, 5)。这样第4行就有了两个种子点,就能够被完整地填充了。
因而可知,对于有障碍点的行,经过相邻边的关系,能够跨越障碍点,经过屡次扫描获得完整的填充,算法已经隐含了对这种状况的处理。根据本节总结的四个步骤,扫描线种子填充算法的实现以下:
263 void ScanLineSeedFill(int x,int y, int new_color, int boundary_color) 264 { 265 std::stack<Point> stk; 266 267 stk.push(Point(x, y));//第1步,种子点入站 268 while(!stk.empty()) 269 { 270 Point seed = stk.top();//第2步,取当前种子点 271 stk.pop(); 272 273 //第3步,向左右填充 274 int count = FillLineRight(seed.x, seed.y, new_color, boundary_color);//向'cf?右'd3?填'cc?充'b3? 275 int xRight = seed.x+ count - 1; 276 count = FillLineLeft(seed.x- 1, seed.y, new_color, boundary_color);//向'cf?左'd7?填'cc?充'b3? 277 int xLeft = seed.x- count; 278 279 //第4步,处理相邻两条扫描线 280 SearchLineNewSeed(stk, xLeft, xRight, seed.y- 1, new_color, boundary_color); 281 SearchLineNewSeed(stk, xLeft, xRight, seed.y+ 1, new_color, boundary_color); 282 } 283 } |
FillLineRight()和FillLineLeft()两个函数就是从种子点分别向右和向左填充颜色,直到遇到边界点,同时返回填充的点的个数。这两个函数返回填充点的个数是为了正确调整当前种子点所在的扫描线的区间[xLeft, xRight]。SearchLineNewSeed()函数完成算法第4步所描述的操做,就是在新扫描线上寻找种子点,并将种子点入栈,新扫描线的区间是xLeft和xRight参数肯定的:
234 void SearchLineNewSeed(std::stack<Point>& stk, int xLeft,int xRight, 235 int y, int new_color, int boundary_color) 236 { 237 int xt = xLeft; 238 bool findNewSeed = false; 239 240 while(xt <= xRight) 241 { 242 findNewSeed = false; 243 while(IsPixelValid(xt, y, new_color, boundary_color)&& (xt < xRight)) 244 { 245 findNewSeed= true; 246 xt++; 247 } 248 if(findNewSeed) 249 { 250 if(IsPixelValid(xt, y, new_color, boundary_color)&& (xt == xRight)) 251 stk.push(Point(xt, y)); 252 else 253 stk.push(Point(xt- 1, y)); 254 } 255 256 /*向右跳过内部的无效点(处理区间右端有障碍点的状况)*/ 257 int xspan = SkipInvalidInLine(xt, y, xRight, new_color, boundary_color); 258 xt += (xspan ==0) ? 1 : xspan; 259 /*处理特殊状况,以退出while(x<=xright)循环*/ 260 } 261 } |
最外层的while循环是为了保证区间[xLeft, xRight]右端被障碍点分隔成多段的状况可以获得正确处理,经过外层while循环,能够确保为每一段都找到一个种子点(对于障碍点在区间左端的状况,请参考图(5)所示实例的解释,是隐含在算法中完成的)。内层的while循环只是为了找到每一段最右端的一个可填充点做为种子点。SkipInvalidInLine()函数的做用就是跳过区间内的障碍点,肯定下一个分隔段的开始位置。循环内的最后一行代码有点奇怪,其实只是用了一个小“诡计”,确保在遇到真正的边界点时循环可以正确退出。这不是一个值得称道的作法,实现此类软件控制有更好的方法,本文这样作的目的只是为了使代码简短一些,让读者把注意力集中在算法处理逻辑上,而不是冗杂难懂的循环控制条件上。
算法的实现其实就在ScanLineSeedFill()和SearchLineNewSeed()两个函数中,神秘的扫描线种子填充算法也并不复杂,对吧?至此,种子填充算法的几种常见算法都已经介绍完毕,接下来将介绍两种适合矢量图形区域填充的填充算法,分别是扫描线算法和边标志填充算法,注意适合矢量图形的扫描线填充算法有时又被称为“有序边表法”,和扫描线种子填充算法是有区别的。