本学期算法课上我们学习了计算几何的基础内容,在课后的深入了解学习中我发现,计算几何仅仅是算法世界一个重要分支——计算机图形学的基础部分之一,计算机图形学还有很多其他非常有趣的算法,例如直线生成、圆生成、椭圆生成。而在本学期进行java项目实践的过程中,我也遇到了一个和计算机图形学息息相关的问题,那就是如何实现windows自带画图软件中的工具油漆桶?网上的开源画图代码基本上均只实现了其他简单的绘制工具。为此,在查阅大量相关资料后,我学习到,种子填充算法可以很好地实现多边形区域填充,并用其中效果最好的基于栈的扫描线种子填充算法实现了画板中的油漆桶工具。找到特定的算法,搞懂原理并写出算法的程序代码,在这个过程中,我深刻地认识到了算法的无处不在,也深切地感受到算法的乐趣,感受到用算法解决问题后的成就感。
简要介绍下算法的原理,实现非矢量图形区域填充常用的种子填充算法 根据对图像区域边界定义方式以及对点的颜色修改方式不同 可分为注入填充算法(Flood Fill Algorithm)和边界填充算法(Boundary Fill Algorithm)。两者的核心都是递归加搜索,即从指定的种子点开始,向上、下、左、右、左上、左下、右上和右下全部八个方向上搜索,逐个像素进行处理,直到遇到边界。两者的区别仅在于Flood Fill Algorithm不强调区域的边界,它只是从指定位置开始,将所有联通区域内某种指定颜色的点都替换成另一种颜色,即实现颜色替换的功能;而边界填充算法与注入填充算法递归的结束条件不一样,Boundary Fill Algorithm强调边界的存在,只要是边界内的点,无论是什么颜色,都替换成指定的颜色。
但是在实际项目中,使用递归算法效率太低,为了消除递归,有一种更为常用的改进算法,即扫描线种子填充算法。它通过沿竖直扫描线填充像素段,一段一段地来处理8-联通的相邻点,这样算法处理过程中就只需要将每个竖直像素段的起始点位置压入一个特殊的栈,而不需要像递归算法那样将当前位置周围尚未处理的所有相邻点都压入堆栈,从而节省了堆栈空间,本实例采用的就是结合泛洪填充算法(或者说注入填充算法)的扫描线种子填充算法。算法具体步骤为:
(1) 初始化一个空的栈用于存放种子点,将种子点(x, y)入栈;
(2) 判断栈是否为空,如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),x是当前的扫描线;
(3) 从种子点(x, y)出发,沿当前扫描线向上、下两个方向填充,直到边界。分别标记区段的上、下端点坐标为yUp和yDown;
(4) 分别检查与当前扫描线相邻的x - 1和x + 1两条扫描线(即与这一区段相连通的左、右两条扫描线)在区间[yUp, yDown]中的像素,从yUp开始向yDown方向搜索,若存在非边界且未填充的像素点,则找出这些相邻的像素点中最下边的一个,并将其作为种子点压入栈中,然后返回第(2)步。
步骤(4)中有一个较难理解的问题:对当前区段相连通的左、右两条扫描线进行检查时,为什么只是检查区间[yUp, yDown]中的像素?如果新扫描线的实际范围比这个区间大甚至不连续该怎么处理?我没有查到严谨的证明过程,不过可以通过查到的一个相关例子来理解:
注意该例子是边界填充的,采用水平扫描像素点的方法,且当前扫描结束后,扫描与其相邻的上、下两条扫描线,其余原理完全相同。假设当前算法已在第3步处理完黄色点所在的第5行,确定了区间[7, 9]。
相邻的第4行虽然实际范围比区间[7, 9]大,但是由于被(4, 6)边界点阻碍,在确定种子点(4, 9)后(注意上面的算法步骤是取最右边的点),向左填充只能填充右边的第7列到第10列之间的区域,左边的第3列到第5列之间的区域没有填充。然而如果对第3行处理完后,第4行的左边部分作为第3行下边的相邻行,再次得到扫描的机会。第3行的区间是[3, 9],向左跨过了第6列这个障碍点,第2次扫描第4行的时候就从第3列开始,向右找可以确定种子点(4, 5)。这样第4行就有了两个种子点可以被完整地填充。由此可见,对于有障碍点的行,通过相邻边的关系,可以跨越障碍点,通过多次扫描得到完整的填充。
源代码:
程序的大部分类为实现画板用户界面和其他基本工具的代码,实现区域填充的扫描线种子算法只有SeedFillAlgorithm一个类:
//算法核心部分
public void seedFillScanLineWithStack(int x, int y, int newColor, int oldColor) { if(oldColor == newColor) { return; } emptyStack(); int y1; boolean spanLeft, spanRight; push(x, y);//种子点入栈 while(true) { //取当前种子点 x = popx(); if(x == -1) return; y = popy(); y1 = y; while(y1 >= 0 && getColor(x, y1) == oldColor) y1--; //找到待填充区域顶端 y1++; //从起始像素点开始填充 spanLeft = spanRight = false; while(y1 < height && getColor(x, y1) == oldColor) { setColor(x, y1, newColor); //检查相邻左扫描线 if(!spanLeft && x > 0 && getColor(x - 1, y1) == oldColor) { push(x - 1, y1); spanLeft = true; } else if(spanLeft && x > 0 && getColor(x - 1, y1) != oldColor) { spanLeft = false; } //检查相邻右扫描线 if(!spanRight && x < width - 1 && getColor(x + 1, y1) == oldColor) { push(x + 1, y1); spanRight = true; } else if(spanRight && x < width - 1 && getColor(x + 1, y1) != oldColor) { spanRight = false; } y1++; } } }
//在绘制类中调用种子填充算法的方法即可实现油漆桶工具 if (Painter.drawMethod==11) { SeedFillAlgorithm ffa; ffa = new SeedFillAlgorithm(bufImg); ffa.seedFillScanLineWithStack(x1,y1,new Color(ColorPanel.iiR,ColorPanel.iiG,ColorPanel.iiB).getRGB(), ffa.getColor(x1, y1)); ffa.updateResult(); repaint(); }
package java_final_work; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ColorModel; //基于栈的扫描种子线算法 public class SeedFillAlgorithm implements BufferedImageOp { private BufferedImage inputImage; private int[] inPixels; private int width; private int height; private int maxStackSize = 500; private int[] xstack = new int[maxStackSize]; private int[] ystack = new int[maxStackSize]; private int stackSize; public SeedFillAlgorithm(BufferedImage rawImage) { this.inputImage = rawImage; width = rawImage.getWidth(); height = rawImage.getHeight(); inPixels = new int[width*height]; getRGB( rawImage,0, 0, width, height, inPixels ); } public BufferedImage getInputImage() { return inputImage; } public void setInputImage(BufferedImage inputImage) { this.inputImage = inputImage; } public int getColor(int x, int y) { int index = y * width + x; return inPixels[index]; } public void setColor(int x, int y, int newColor) { int index = y * width + x; inPixels[index] = newColor; } public void updateResult() { setRGB( inputImage, 0, 0, width, height, inPixels ); } //算法核心部分 public void seedFillScanLineWithStack(int x, int y, int newColor, int oldColor) { if(oldColor == newColor) { return; } emptyStack(); int y1; boolean spanLeft, spanRight; push(x, y); while(true) { x = popx(); if(x == -1) return; y = popy(); y1 = y; while(y1 >= 0 && getColor(x, y1) == oldColor) y1--; y1++; spanLeft = spanRight = false; while(y1 < height && getColor(x, y1) == oldColor) { setColor(x, y1, newColor); if(!spanLeft && x > 0 && getColor(x - 1, y1) == oldColor) { push(x - 1, y1); spanLeft = true; } else if(spanLeft && x > 0 && getColor(x - 1, y1) != oldColor) { spanLeft = false; } if(!spanRight && x < width - 1 && getColor(x + 1, y1) == oldColor) { push(x + 1, y1); spanRight = true; } else if(spanRight && x < width - 1 && getColor(x + 1, y1) != oldColor) { spanRight = false; } y1++; } } } private void emptyStack() { while(popx() != - 1) { popy(); } stackSize = 0; } final void push(int x, int y) { stackSize++; if (stackSize==maxStackSize) { int[] newXStack = new int[maxStackSize*2]; int[] newYStack = new int[maxStackSize*2]; System.arraycopy(xstack, 0, newXStack, 0, maxStackSize); System.arraycopy(ystack, 0, newYStack, 0, maxStackSize); xstack = newXStack; ystack = newYStack; maxStackSize *= 2; } xstack[stackSize-1] = x; ystack[stackSize-1] = y; } final int popx() { if (stackSize==0) return -1; else return xstack[stackSize-1]; } final int popy() { int value = ystack[stackSize-1]; stackSize--; return value; } //以下实现BufferedImageOp接口中的抽象方法 @Override public BufferedImage filter(BufferedImage src, BufferedImage dest) { return null; } public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { if ( dstCM == null ) dstCM = src.getColorModel(); return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); } public Rectangle2D getBounds2D( BufferedImage src ) { return new Rectangle(0, 0, src.getWidth(), src.getHeight()); } public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { if ( dstPt == null ) dstPt = new Point2D.Double(); dstPt.setLocation( srcPt.getX(), srcPt.getY() ); return dstPt; } public RenderingHints getRenderingHints() { return null; } //去掉了BufferedImage.getRGB()方法中的惩罚措施 public int[] getRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { int type = image.getType(); if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) return (int [])image.getRaster().getDataElements( x, y, width, height, pixels ); return image.getRGB( x, y, width, height, pixels, 0, width ); } //去掉了BufferedImage.setRGB()方法中的惩罚措施 public void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { int type = image.getType(); if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) image.getRaster().setDataElements( x, y, width, height, pixels ); else image.setRGB( x, y, width, height, pixels, 0, width ); } }