种子填充算法:
种子填充算法的基本思想是:从多边形区域的一个内点开始,由内向外用给定的颜色画点直到边界为止。
区域 可以由内部点或边界来定义,一般都采用边界定义,即区域边界上所有像素被置为特定值,而区域内部所有的像素均不取这个值。
区域可以分为四连接或八连接两种:
四连接区域:区域内每一个像素可以通过四个方向(上、下、左、右)组合到达。
八连接区域:区域内每一个像素可以通过四个方向(上、下、左、右)以及四个对角线方向的组合到达。
四连接算法:种子填充算法中,允许从四个方向寻找下一像素者,为四连接算法。
八连接算法:种子填充算法中,允许从八个方向寻找下一像素者,为八连接算法。
一般来说,八连接算法可以填充四连接区域,而四连接算法有时不能填充八连接区域。如图,八连接算法能够正确填充如图 a 所示的区域的内部,而四连接算法只能完成如图 b 的部分填充。
种子填充算法:
- 将种子像素压入栈中。
- 如果栈为空,则转 5),否则转 3)
- 弹出一个像素,并将该像素置成填充色,并判断该像素相邻的四连接像素是否为边界色或已经置为多边形的填充色,若不是,则将该像素压入栈。
- 转 2)
- 结束。
如何判断种子是否在多边形内:
方法之一:引射线法:从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果有奇数个交点,则说明在内部,如果有偶数个交点,则说明在外部。
这里由种子店引一条水平向右的射线。(sx < (jx - ix) * (sy - iy) / (jy - iy) + ix)用来保证边在种子点的右方,((iy > sy) != (jy > sy))用来判断边与水平射线相交。
((iy > sy) != (jy > sy)) and (sx < (jx - ix) * (sy - iy) / (jy - iy) + ix)
在判断边与水平射线相交时有些细节值得考虑
1.射线与多边形的顶点相交,这时交点只能计算一个。
2.射线与多边形顶点的交点不应该被计算,或算成两个交点
3.射线与多边形的一条边重合,这条边应该被忽略,总的只能算成一个交点
而使用 ((iy > sy) != (jy > sy)) 能同时考虑到以上情形。即 (iy > sy >= jy ) 或者 (jy > sy >= iy ) , 判断时,对于边的两端点,只取其中较低端点,而舍去较高端点。
核心代码:
(算法思路:首先,判断输入点是否不小于3,种子是否在多边形内部。声明矩阵记录像素点是否被填充。利用DDA算法计算出多边形边上的像素点,并标记。使用种子填充算法填充多边形内部。)
注意:种子填充算法可以采用递归实现,也可以如下采用栈实现。
from PySide2.QtCore import *
from copy import deepcopy
class Polygon:
def __init__(self, vertics, seed):
""" p:多边形顶点坐标 seeds:种子坐标"""
self.vertics = vertics
self.seed = seed
def seed_is_in_polygon(self):
"""判断种子是否位于多边形内部"""
i = 0
j = len(self.vertics) - 1
c = False
sx = self.seed.x()
sy = self.seed.y()
while i < len(self.vertics):
ix = self.vertics[i].x()
iy = self.vertics[i].y()
jx = self.vertics[j].x()
jy = self.vertics[j].y()
if ((iy > sy) != (jy > sy)) and (sx < (jx - ix) * (sy - iy) / (jy - iy) + ix):
c = not c
j = i
i = i + 1
return c
def max_x(self):
max1 = 0
for p in self.vertics:
if p.x() > max1:
max1 = p.x()
return max1
def min_x(self):
min1 = self.max_x()
for p in self.vertics:
if p.x() < min1:
min1 = p.x()
return min1
def max_y(self):
max1 = 0
for p in self.vertics:
if p.y() > max1:
max1 = p.y()
return max1
def min_y(self):
min1 = self.max_y()
for p in self.vertics:
if p.y() < min1:
min1 = p.y()
return min1
def get_points_set(self):
"""返回需要画的像素点集"""
points_set = []
if (not self.seed_is_in_polygon()) or (len(self.vertics) < 3):
return points_set
m = self.max_y() - self.min_y() + 1
n = self.max_x() - self.min_x() + 1
# 需要声明二维矩阵的大小
MF = [[False] * n for i in range(m)] # MF[m][n]
v = len(self.vertics)
p0 = self.vertics[0]
p1 = self.vertics[1]
for k in range(0, v):
if k < v - 1:
p0 = self.vertics[k]
p1 = self.vertics[k + 1]
else:
p0 = self.vertics[k]
p1 = self.vertics[0]
x0 = p0.x()
y0 = p0.y()
x1 = p1.x()
y1 = p1.y()
if y0 == y1:
y = y0
if x0 > x1:
temp = x0
x0 = x1
x1 = temp
for x in range(x0, x1):
MF[y - self.min_y()][x - self.min_x()] = True
elif x0 == x1:
x = x0
if y0 > y1:
temp = y0
y0 = y1
y1 = temp
for y in range(y0, y1):
MF[y - self.min_y()][x - self.min_x()] = True
else:
dy = float(y1 - y0) / float(x1 - x0)
dx = 1 / dy
if dy > -1 and dy < 1:
if x0 > x1:
temp = x0
x0 = x1
x1 = temp
temp = y0
y0 = y1
y1 = temp
ty = y0
for x in range(x0, x1 + 1):
y = (int)(ty + 0.5)
MF[y - self.min_y()][x - self.min_x()] = True
ty += dy
else:
if y0 > y1:
temp = y0
y0 = y1
y1 = temp
temp = x0
x0 = x1
x1 = temp
tx = x0
for y in range(y0, y1 + 1):
x = int(tx + 0.5)
MF[y - self.min_y()][x - self.min_x()] = True
tx += dx
seed_stack = []
seed_stack.append(self.seed)
MF[self.seed.y() - self.min_y()][self.seed.x() - self.min_x()] = True
while len(seed_stack) > 0:
ps = seed_stack.pop()
p0.setX(ps.x() - 1)
p0.setY(ps.y())
if not MF[p0.y() - self.min_y()][p0.x() - self.min_x()]:
p = deepcopy(p0) # 深拷贝与浅拷贝
seed_stack.append(p)
MF[p0.y() - self.min_y()][p0.x() - self.min_x()] = True
p0.setX(ps.x() + 1)
p0.setY(ps.y())
if not MF[p0.y() - self.min_y()][p0.x() - self.min_x()]:
p = deepcopy(p0) # 深拷贝与浅拷贝
seed_stack.append(p)
MF[p0.y() - self.min_y()][p0.x() - self.min_x()] = True
p0.setX(ps.x())
p0.setY(ps.y() - 1)
if not MF[p0.y() - self.min_y()][p0.x() - self.min_x()]:
p = deepcopy(p0) # 深拷贝与浅拷贝
seed_stack.append(p)
MF[p0.y() - self.min_y()][p0.x() - self.min_x()] = True
p0.setX(ps.x())
p0.setY(ps.y() + 1)
if not MF[p0.y() - self.min_y()][p0.x() - self.min_x()]:
p = deepcopy(p0) # 深拷贝与浅拷贝
seed_stack.append(p)
MF[p0.y() - self.min_y()][p0.x() - self.min_x()] = True
for j in range(0, m):
for i in range(0, n):
if MF[j][i] == True:
points_set.append(QPoint(i + self.min_x(), j + self.min_y()))
return points_set
加上UI界面实现效果:
扫描线种子填充算法
种子填充算法存在时、空浪费的缺点,因为在判断待填充点入栈时,只要被检查的四方向上的连续点为非填充色,便将其入栈。所以,在连续填充的过程中出现了许多点被重复入栈的情况,在填充点出栈的过程中,重复的出栈点对填充结果不造成任何影响,只是造成了时间和空间上的浪费。
扫描线种子填充算法就克服了这种缺陷:在任意不间断扫描线区段中只取一个种子像素。不间断区段的意思就是指一条扫描线上的一组相邻像素。算法的大概过程是:
- 从包含种子像素的堆栈中弹出区段的种子像素。
- 沿着扫描线对包含种子像素的区段左右像素进行填充,直至遇到边界像素为止,这样就将种子所在地扫描线上的像素进行了填充。
- 每条扫描线上区段内最左的和最右的像素记为Xleft和Xright。在Xleft<=x<=Xright范围内,检查与当前扫描线相邻的上下两条扫描线是否全为边界像素或者前面已经填充过的像素。若不是,那么在Xleft<=x<=Xright中,把与当前扫描线相邻的上、下两条扫描线中每一区间的最右像素作为种子压入栈中。
- 若栈为空,则结束。若栈非空,则转 1)。
这个算法中最关键的是第 3 步,就是从当前扫描线的上一条扫描线和下一条扫描线中寻找新的种子点。这里比较难理解的一点就是为什么只是检查新扫描线上区间[xLeft, xRight]中的像素?如果新扫描线的实际范围比这个区间大(而且不连续)怎么处理?
如果新扫描线上实际点的区间比当前扫描线的[xLeft, xRight]区间大,而且是连续的情况下,算法的第 2 步就处理了这种情况。如图所示:
假设当前处理的扫描线是黄色点所在的第7行,则经过第2步处理后可以得到一个区间[6,10]。然后第3步操作,从相邻的第6行和第8行两条扫描线的第6列开始向右搜索,确定红色的两个点分别是第6行和第8行的种子点,于是按照顺序将(6, 10)和(8, 10)两个种子点入栈。接下来的循环会处理(8, 10)这个种子点,根据算法第2步说明,会从(8, 10)开始向左和向右填充,由于中间没有边界点,因此填充会直到遇到边界为止,所以尽管第8行实际区域比第7行的区间[6,10]大,但是仍然得到了正确的填充。
如果新扫描线上实际点的区间比当前扫描线的[xLeft, xRight]区间大,而且中间有边界点的情况,算法又是怎么处理呢?算法描述中虽然没有明确对这种情况的处理方法,但是第 3 步确定上、下相邻扫描线的种子点的方法,以及靠右取点的原则,实际上暗含了从相邻扫描线绕过障碍点的方法。下面以图为例说明:
算法第 2 步处理完第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行就有了两个种子点,就可以被完整地填充了。
由此可见,对于有障碍点的行,通过相邻边的关系,可以跨越障碍点,通过多次扫描得到完整的填充,算法已经隐含了对这种情况的处理。
PS: 如需参考完整代码,请移步: 进行下载