上篇博文讲到了填充算法的扫描线填充,这篇博文讲解另一大算法思路----------种子填充。
种子填充算法假设在多边形或区域内部至少有一个像素是已知的。然后设法找到区域内所有其他像素,并对它们进行填充。区域可以用内部定义或边界定义。
如果是内部定义,那么,区域内部所有像素具有同一种颜色或值,而区域外的所有像素具有另一种颜色或值,如下图:
如果是边界定义,那么区域边界上所有像素均具有特定的值或颜色。区域内部的所有像素均不取这一特定值。然而,边界外的像素则可具有与边界相同的值。如下图:
填充内部定义区域的算法成为泛填充算法(flood fill algorithm),填充边界定义区域的算法称为边界填充算法。
本篇blog着重讲解边界填充算法。
对于边界填充算法可以采用堆栈。采用堆栈的简单种子填充算法思路如下:
种子像素压入堆栈
当堆栈非空时
从堆栈中弹出一个像素
将该像素置成所要求的值
检查每个与当前像素邻接的4连通像素是否是边界像素或者是否已置成所要求的值。
若是上述两种情况之一, 则略而不计。否则把该像素压入堆栈。
常见的这种填充有四连通种子边界填充还有八连通种子边界填充,以四连通种子边界填充为例,写下伪代码:
#simple seed fill algorithm for 4-connected boundary-defined regions
#简单的四连通种子边界填充算法
#Seed(x,y) is the seed pixel:这是种子像素
#Push is a function for placing a pixel on the stack:Push是栈的push
#Pop is function for removing a pixel on the stack:Pop是栈的Pop
Pixel(x,y) = Seed(x,y)
#initialize stack
Push Pixel(x,y)
while(stack not empty)
#get a pixel from the stack:从栈中获取像素
Pop Pixel(x,y)
# <>:表示不具有
if Pixel(x,y) <> New value then
Pixel(x,y) = New value
end if
#examine the surrounding pixels to see if they should be placed onto the stack:判断是否具有边界值或新值
if(Pixel(x+1,y) <> New value and Pixel(x+1,y) <> Boundary value) then
Push Pixel(x+1,y)
end if
if(Pixel(x,y+1) <> New value and Pixel(x,y+1) <> Boundary value) then
Push Pixel(x,y+1)
end if
if(Pixel(x-1,y) <> New value and Pixel(x-1,y) <> Boundary value) then
Push Pixel(x-1,y)
end if
if(Pixel(x,y-1) <> New value and Pixel(x,y-1) <> Boundary value) then
Push Pixel(x,y-1)
end if
end while
可能有些人看完了伪代码之后还是懵逼状态,那么我们就举个例子来讲该算法的具体流程:
例1.填充由顶点(1,1),(8,1),(8,4),(6,6)以及(1,6)所决定的边界定义多边形区域,取种子像素是(4,3)。具体的填充流程从种子像素出发,跟着绿色箭头走:
(这图是我用windows自带的画图工具画的,各位看官还有没有更好的画这种图的工具啊,求告知,用mspaint一点一点的画真的一把辛酸泪啊o(╥﹏╥)o)
这个算法从种子像素开始,先检查种子像素右侧的像素是否是新值和边界值,发现都不是,就把(5,3)压入stack,然后把上侧(4,4),左侧(4,3),下侧(4,2)压入stack。也就是从把种子像素从右侧像素开始逆时针压入堆栈。压完了以后,开始检查stack不为空,于是pop,就pop出了(4,2)。于是以(4,2)为种子像素开始,又一轮压入循环,凡是被判为种子像素的像素都会用正红色填充。所以这幅图里的很多像素可能会被压入多次,当算法走到像素(5,5)的时候,由于(5,5)周围的像素具有边界值或新值,就没有像素压入堆栈了。于是Pop(7,4),接着往下填充。当达到像素(7,1)的时候,周围像素或已置成新值(正红色)或为边界像素,至此多边形已全部填充完毕,因此只从堆栈中弹出像素而不再填充新的像素,直到堆栈为空时算法停止。
代码如下:
void CCGPainterView::BoundaryFill_4Connection(CDC *pDC, CPoint startPoint, COLORREF fillCol, COLORREF boundaryCol)
{
//Write your boundary fill algorithm here.
CPoint fillPoint = startPoint;
COLORREF currentCol = pDC->GetPixel(fillPoint);
if(currentCol != boundaryCol&¤tCol != fillCol)
{
pDC->SetPixelV(fillPoint, fillCol);
fillPoint.y = startPoint.y+1; //(x, y+1)
BoundaryFill_4Connection(pDC, fillPoint, fillCol, boundaryCol);
fillPoint.y = startPoint.y-1; //(x, y-1)
BoundaryFill_4Connection(pDC, fillPoint, fillCol, boundaryCol);
fillPoint.x = startPoint.x-1;
fillPoint.y = startPoint.y; //(x-1, y)
BoundaryFill_4Connection(pDC, fillPoint, fillCol, boundaryCol);
fillPoint.x = startPoint.x+1; //(x+1, y)
BoundaryFill_4Connection(pDC, fillPoint, fillCol, boundaryCol);
}
}
我们可以看到刚刚的算法其实效率很慢,一来说,堆栈可能会很大占用内存,二来说,堆栈中还包含了很多重复和不必要的信息。
扫描线种子填充算法就克服了这种缺陷:在任意不间断扫描线区段中只取一个种子像素。不间断区段的意思就是指一条扫描线上的一组相邻像素。算法的大概过程是:
从包含种子像素的堆栈中弹出区段的种子像素
沿着扫描线对包含种子像素的区段左右像素进行填充,直至遇到边界像素为止,从而填满包含种子像素的区段。
区段内最左的和最右的像素记为Xleft和Xright。
在Xleft<=x<=Xright范围内,检查与当前扫描线相邻的上下两条扫描线是否全为边界像素或者前面已经填充过的像素。
如果这些扫描线既不包括边界元素,也不包括已填充的像素,那么在Xleft<=x<=Xright中把每一个区段的最右像素取做种子像素并压入堆栈
算法的伪代码如下:
#scan line seed fill algorithm:扫描线种子填充
#Seed(x,y) is the seed pixel:这是种子像素
#Pop is a function for removing a pixel from the stack:Pop是栈的Pop
#Push is a function for placing a pixel on the stack:Push是栈的push
#initialize stack:初始化栈
Push Seed(x,y)
while(stack not empty)
#get the seed pixel and set it to the new value:把种子像素弹出,填充种子像素
Pop Pixel(x,y)
Pixel(x,y) = Fill value
#save the x coordinate of the seed pixel:保存种子像素的x坐标
Savex = x
#fill the span to the right of the seed pixel:种子像素的x坐标加1,向右走一步
x = x+1
#从种子像素向右填充,直到遇到边界像素停止
while Pixel(x,y) <> Boundary value
Pixel(x,y) = Fill value
x = x+1
end while
#save the extreme right pixel:保存区段的右端点Xright
Xright = x-1
#reset the x coordinate to the value for the seed pixel:把x重置为种子像素的x坐标值
x = Savex
#fill the span to the left of the seed pixel:种子像素的x坐标减1,向左走一步
x = x-1
while Pixel(x,y) <> Boundary value:从种子像素向左填充,直到遇到边界像素停止
Pixel(x,y) = Fill vae
x = x-1
end while
#save the extreme left pixel:保存区段的左端点Xleft
Xleft = x+1
#reset the x coordinate to the value for the seed pixel:把x重置为种子像素的x坐标值
x = Savex
#check that the scan line above is neither a polygon boundary nor
#has been previously completely filled; if not,seed the scan line
#start at the left edge of the scan line subsan
#检查上面一条扫描线是不是边界线或者有没有被填充过,如果没有,从这条扫描线的左边界开始扫描
x = Xleft
y = y+1
while(x<=Xright)
#seed the scan line above:扫描上面的一条扫描线
Pflag = 0
while(Pixel(x,y) <> Boundary value and Pixel(x,y) <> Fill value and x < Xright)
if Pflag = 0 then Pflag = 1
x = x+1
end while
#push the extreme right pixel onto the stack:把该条扫描线最右端压入堆栈
if Pflag = 1 then
if(x = Xright and Pixel(x,y) <> Boundary value and Pixel(x,y) <> Fill value) then
Push Pixel(x,y)
else
Push Pixel(x-1,y)
end if
Pflag = 0
end if
#continue checking in case the span is interrupted:继续检查
Xenter = x
while((Pixel(x,y) = Boundary value or Pixel(x,y) = Fill value) and x
举个例子:算法填充该多边形,种子像素为(5,7)
多边形种子像素(5,7)压入堆栈。
算法开始:
将(5,7)像素作为区段种子从堆栈中退出。向右向左填充种子所在的区段
找到的端点是Xright=9,Xleft=1
检查上面一条扫描线,它不是边界线,也尚未填充,在1<=x<=9范围内最右的像素是(8,8)。这个像素就被标记为1
接着检查下面的一条扫描线,它既非边界线,也未被填充,在1<=x<=9这个范围内有两个子区段,
左边子区段取像素(3,6)为种子,用2来标记,然后把它压入堆栈
右边子区段取像素(9,6)为种子,用3来标记它,压入堆栈
算法一遍结束
算法继续从堆栈中推出顶部像素。这里,算法逐条往下填充多边形右边扫描线区域。依次压入的是(10,5),(10,4),(10,3)并填充
在(10,3)的时候,向左以及向右依次填充该区段得到Xleft=1和Xright=10,检查上面的扫描线,得到左边子区段的种子像素是(3,4)
把它压入堆栈。右边的子区段已填充。检查下面的扫描线,得到左边子区段的种子像素(3,2),以及右边子区段的种子像素(10,2),这两个像素也被压入堆栈。
然后多边形依次弹出种子点进行填充。最后弹出的是标记为1的像素种子点,并填充这条扫描线。这时候没有新像素压入堆栈,此时堆栈已空,多边形填满。算法结束
因此完成的程序代码应该是这样的:
void CCGPainterView::ScanLineFill(CDC *pDC, CPoint startPoint, COLORREF fillCol, COLORREF boundaryCol)
{
//Write your boundary fill algorithm here.
//定义堆栈
CArrayStack;
CPoint fillPoint = startPoint;
COLORREF currentCol = pDC->GetPixel(startPoint);
//最左点,最右点
int xl, xr;
//标志位
bool spanNeedFill;
//种子点
CPoint pt;
Stack.RemoveAll();//设置堆栈为空
pt.x = fillPoint.x;
pt.y = fillPoint.y;
Stack.Add(pt);//种子点压入堆栈
while (Stack.GetSize() != 0)
{
pt= Stack[Stack.GetSize() - 1];//取出种子点
Stack.RemoveAt(Stack.GetSize() - 1);//堆栈减短
fillPoint.y = pt.y;
fillPoint.x = pt.x;
//从种子开始向右填充
while (pDC->GetPixel(fillPoint.x, fillPoint.y) == currentCol)
{
pDC->SetPixelV(fillPoint, fillCol);
fillPoint.x++;
}
xr = fillPoint.x - 1;
fillPoint.x = pt.x - 1;
//从种子开始向左填充
while (pDC->GetPixel(fillPoint.x, fillPoint.y) == currentCol)
{
pDC->SetPixelV(fillPoint, fillCol);
fillPoint.x--;
}
xl = fillPoint.x + 1;
//处理上一条扫描线和下一条扫描线
for (int I = 0; I < 2; I++)
{
fillPoint.x = xl;
if (I == 0) fillPoint.y = fillPoint.y + 1;
else fillPoint.y = fillPoint.y - 2;
while (fillPoint.x < xr)
{
spanNeedFill = FALSE;
while (pDC->GetPixel(fillPoint.x, fillPoint.y) == currentCol)
{
spanNeedFill = TRUE;
fillPoint.x++;
}//待填充区搜索完毕
if (spanNeedFill)
{//右端点作为种子入栈
pt.x = fillPoint.x - 1;
pt.y = fillPoint.y;
Stack.Add(pt);
spanNeedFill = FALSE;
}
//继续向右检查以防遗漏
while (pDC->GetPixel(fillPoint.x, fillPoint.y) != currentCol && fillPoint.x < xr)
fillPoint.x++;
}//上一条扫描线上检查完毕
}
}
}
最后的实验效果是这样的:
代码如下:
void CCGPainterView::BoundaryFill_4Connection(CDC *pDC, CPoint startPoint, COLORREF fillCol, COLORREF backgroundCol)
{
//Write your boundary fill algorithm here.
CPoint fillPoint = startPoint; //设起始点为填充点
backgroundCol = RGB(255, 255, 255); //设置背景颜色为白色
COLORREF currentCol = pDC->GetPixel(fillPoint);
if(currentCol == backgroundCol) //判断当前像素点的颜色是否和背景颜色一致
{
pDC->SetPixelV(fillPoint, fillCol);
fillPoint.y = startPoint.y+1; //递归(x, y+1)点
BoundaryFill_4Connection(pDC, fillPoint, fillCol, backgroundCol);
fillPoint.y = startPoint.y-1; //递归(x, y-1)点
BoundaryFill_4Connection(pDC, fillPoint, fillCol, backgroundCol);
fillPoint.x = startPoint.x-1;
fillPoint.y = startPoint.y; //递归(x-1, y)点
BoundaryFill_4Connection(pDC, fillPoint, fillCol, backgroundCol);
fillPoint.x = startPoint.x+1; //递归(x+1, y)点
BoundaryFill_4Connection(pDC, fillPoint, fillCol, backgroundCol);
}
}