本學期算法課上我們學習了計算幾何的基礎內容,在課后的深入了解學習中我發現,計算幾何僅僅是算法世界一個重要分支——計算機圖形學的基礎部分之一,計算機圖形學還有很多其他非常有趣的算法,例如直線生成、圓生成、橢圓生成。而在本學期進行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 ); } }