计算机图形学|多边形填充算法
多边形填充是计算机图形学中的基础问题,涉及将多边形内部区域用特定颜色或图案填充。本次实验实现了两种经典的多边形填充算法:扫描线填充算法和种子填充算法
一、扫描线填充算法
算法原理
扫描线填充算法基于以下核心思想:从多边形的最小y坐标到最大y坐标,逐条水平扫描线与多边形相交,计算交点并配对,在配对的交点之间进行填充。
算法步骤:
- 确定多边形的y坐标范围
- 对每条扫描线,计算与多边形各边的交点
- 将交点按x坐标排序
- 将交点两两配对,在每对交点之间填充像素
关键实现
def scanline_fill(buf, poly, spacing=1):
h, w, _ = buf.shape
pts = [(int(round(x)), int(round(y))) for (x, y) in poly]
n = len(pts)
for y in range(0, h, spacing): # 按指定间距遍历扫描线
inters = [] # 存储交点列表
for i in range(n): # 遍历多边形的每条边
x0, y0 = pts[i]
x1, y1 = pts[(i+1) % n]
if y0 == y1: # 水平边,跳过
continue
# 确保扫描线在边的有效范围内
ymin = min(y0, y1)
ymax = max(y0, y1)
if y < ymin or y >= ymax:
continue
# 使用线性插值计算交点x坐标
x = x0 + (y - y0) * (x1 - x0) / (y1 - y0)
inters.append(x)
if not inters:
continue
inters.sort() # 对交点按x坐标排序
# 成对填充
for k in range(0, len(inters), 2):
if k+1 >= len(inters):
break
xstart = int(round(np.ceil(inters[k])))
xend = int(round(np.floor(inters[k+1])))
if xend < xstart:
continue
xstart = clamp(xstart, 0, w-1)
xend = clamp(xend, 0, w-1)
# 在扫描线上填充像素
buf[y, xstart:xend+1] = POLY_COLOR
算法特点
- 通过扫描线的方式减少了填充区域的计算复杂度
- 使用线性插值确保边界像素的精确计算
- 可通过
spacing参数控制扫描线密度,灵活实现不同填充效果
二、种子填充算法
算法原理
种子填充算法是一种基于区域生长的填充方法。从给定的种子点开始,向四个方向(上下左右)递归或迭代地填充像素,直到遇到边界为止。
算法步骤:
- 将种子点压入栈中
- 当栈非空时,弹出栈顶像素
- 如果该像素是目标颜色(未填充),则填充并检查四个相邻像素
- 如果相邻像素在边界内且未填充,则将其压入栈中
- 重复直到栈为空
关键实现
def seed_fill(buf, seed):
h, w, _ = buf.shape
sx, sy = seed
# 坐标转换:从窗口坐标转换到缓冲区坐标
sy_buf = h - 1 - sy
target = tuple(buf[sy_buf, sx]) # 获取种子点颜色
fillc = tuple(POLY_COLOR.tolist()) # 填充颜色
if target == fillc:
return # 已经填充过,直接返回
stack = [(sx, sy_buf)] # 使用栈而非递归,避免深度过大
while stack:
x, y = stack.pop()
# 边界检查
if x < 0 or x >= w or y < 0 or y >= h:
continue
# 检查是否需要填充
if tuple(buf[y, x]) != target:
continue
# 填充当前像素
buf[y, x] = POLY_COLOR
# 将四个相邻像素压入栈中
stack.append((x+1, y))
stack.append((x-1, y))
stack.append((x, y+1))
stack.append((x, y-1))
算法特点
- 区域连续性:适用于任意形状的闭合区域
- 递归深度过大会导致的栈溢出
- 边界敏感,精确识别边界像素,确保填充不越界
三、算法比较与应用场景
扫描线填充算法
- 优点:
- 对于规则多边形填充效率高
- 内存占用小,适合硬件实现
- 填充结果均匀一致
- 缺点:
- 对于复杂边界的多边形,交点计算可能复杂
- 需要预处理确定多边形的边
种子填充算法
- 优点:
- 适用于任意形状的闭合区域
- 算法简单直观,易于实现
- 适用于交互式填充
- 缺点:
- 需要指定种子点
- 可能产生递归深度过大的问题
- 对于大区域填充可能较慢
四、实验实现中的关键技术
1. 坐标系统转换
实验中存在两个坐标系统:窗口坐标(左下角为原点)和缓冲区坐标(左上角为原点)。通过np.flipud()函数进行转换,确保显示正确:
# 将缓冲区坐标转换为窗口坐标
img = np.flipud(buffer_img)
glDrawPixels(WIN_W, WIN_H, GL_RGB, GL_UNSIGNED_BYTE, img)
2. 边界处理
使用clamp()函数确保像素坐标在有效范围内:
def clamp(v, a, b):
return max(a, min(b, v))
3. 多边形边绘制
实现Bresenham画线算法绘制多边形边界:
def draw_polygon_edges(buf, poly):
五、总结
本次实验深入实现了两种经典的多边形填充算法。扫描线填充算法通过计算扫描线与多边形的交点并进行配对填充,适合规则多边形的快速填充。种子填充算法基于区域生长原理,从种子点开始向四周扩散填充,适合任意形状的闭合区域。
两种算法各有优缺点,在实际应用中应根据具体需求选择。扫描线填充更适合批量处理和硬件加速,而种子填充更适合交互式应用和复杂区域的填充。
通过本实验的实现,我们不仅掌握了多边形填充的基本原理,还学习了图形编程中的坐标转换、边界处理等关键技术
代码
填充算法.py
# 使用 PyOpenGL + GLUT 实现扫描线填充和种子填充多边形
# 在终端输入多边形顶点(像素坐标),选择算法并输入对应参数
import sys
import os
import json
from OpenGL.GL import *
from OpenGL.GLUT import *
import numpy as np
import tkinter as tk
from tkinter import ttk
WIN_W = 800
WIN_H = 600
BG_COLOR = np.array([255, 255, 255], dtype=np.uint8)
POLY_COLOR = np.array([200, 40, 40], dtype=np.uint8)
EDGE_COLOR = np.array([0, 0, 0], dtype=np.uint8)
buffer_img = None
polygon = []
algorithm = 'scanline' # or 'seed'
scan_spacing = 1
seed_point = (0, 0)
def clamp(v, a, b):
return max(a, min(b, v))
def parse_input():
global polygon, algorithm, scan_spacing, seed_point
print("实验二多边形填充")
mode = input("输入 'T' 使用内置测试用例,按回车手动输入顶点: ").strip().lower()
if mode == 't':
tests = load_tests()
if not tests:
print("未找到测试用例文件,进入手动输入模式。")
else:
print("可用测试用例:")
for i, t in enumerate(tests):
print(f" {i}: {t.get('name','case'+str(i))} - alg={t.get('algorithm')}")
while True:
sel = input("选择测试用例编号 (回车取消): ").strip()
if sel == '':
break
try:
idx = int(sel)
if 0 <= idx < len(tests):
case = tests[idx]
polygon = [(int(x), int(y)) for x, y in case['polygon']]
algorithm = case.get('algorithm', 'scanline')
if algorithm == 'seed':
seed_point = tuple(case.get('seed', (0, 0)))
else:
scan_spacing = int(case.get('spacing', 1))
print(f"已加载测试用例: {case.get('name','#'+str(idx))}")
return
except Exception:
pass
print("请输入有效的编号")
# 手动输入模式
try:
n = int(input("输入多边形顶点数 (整数): "))
except Exception:
print("输入不合法")
sys.exit(1)
polygon = []
print("请输入每个顶点的 x y(像素坐标,相对于窗口左下角,范围 0..{} 0..{})".format(WIN_W, WIN_H))
for i in range(n):
while True:
s = input(f"顶点 {i+1} (x y): ").strip()
parts = s.split()
if len(parts) >= 2:
try:
x = float(parts[0]); y = float(parts[1])
# clamp
x = clamp(int(round(x)), 0, WIN_W-1)
y = clamp(int(round(y)), 0, WIN_H-1)
polygon.append((x, y))
break
except Exception:
pass
print("输入格式: x y ")
alg = input("选择算法: 输入 's' 表示扫描线(fill by scanline),'f' 表示种子填充(flood fill). (默认 s): ").strip().lower()
if alg == 'f' or alg == 'seed':
algorithm = 'seed'
while True:
s = input("输入种子像素坐标 x y: ").strip().split()
if len(s) >= 2:
try:
sx = clamp(int(round(float(s[0]))), 0, WIN_W-1)
sy = clamp(int(round(float(s[1]))), 0, WIN_H-1)
seed_point = (sx, sy)
break
except Exception:
pass
print("示例输入: 120 130")
else:
algorithm = 'scanline'
s = input("输入扫描线间距(整数,默认 1): ").strip()
try:
scan_spacing = max(1, int(s))
except Exception:
scan_spacing = 1
def create_empty_buffer():
global buffer_img
buffer_img = np.full((WIN_H, WIN_W, 3), BG_COLOR, dtype=np.uint8)
def load_tests():
"""polygon_tests.json 加载测试用例列表"""
try:
base = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(base, 'polygon_tests.json')
if not os.path.exists(path):
return []
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
return data
return []
except Exception:
return []
def draw_polygon_edges(buf, poly):
# 简单的 Bresenham 画线到 buffer 中,用黑色画边
h, w, _ = buf.shape
def plot(x, y, color):
if 0 <= x < w and 0 <= y < h:
buf[y, x] = color
for i in range(len(poly)):
x0, y0 = poly[i]
x1, y1 = poly[(i+1) % len(poly)]
x0 = int(round(x0)); y0 = int(round(y0))
x1 = int(round(x1)); y1 = int(round(y1))
dx = abs(x1 - x0); sx = 1 if x0 < x1 else -1
dy = -abs(y1 - y0); sy = 1 if y0 < y1 else -1
err = dx + dy
while True:
plot(x0, y0, EDGE_COLOR)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 >= dy:
err += dy
x0 += sx
if e2 <= dx:
err += dx
y0 += sy
def scanline_fill(buf, poly, spacing=1):
h, w, _ = buf.shape
# polygon: list of (x,y)整数坐标
# 对每个 y (扫描线) 计算交点
pts = [(int(round(x)), int(round(y))) for (x, y) in poly]
n = len(pts)
for y in range(0, h, spacing):
inters = []
for i in range(n):
x0, y0 = pts[i]
x1, y1 = pts[(i+1) % n]
if y0 == y1:
continue
# 确保 y 在半开区间 [min(y0,y1), max(y0,y1)) 来避免重复
ymin = min(y0, y1)
ymax = max(y0, y1)
if y < ymin or y >= ymax:
continue
# 线性插值求 x
x = x0 + (y - y0) * (x1 - x0) / (y1 - y0)
inters.append(x)
if not inters:
continue
inters.sort()
# 成对填充
for k in range(0, len(inters), 2):
if k+1 >= len(inters):
break
xstart = int(round(np.ceil(inters[k])))
xend = int(round(np.floor(inters[k+1])))
if xend < xstart:
continue
xstart = clamp(xstart, 0, w-1)
xend = clamp(xend, 0, w-1)
# 注意 buffer 的 y 轴 0 在顶部,所以我们直接使用 y 作为行索引
buf[y, xstart:xend+1] = POLY_COLOR
def seed_fill(buf, seed):
h, w, _ = buf.shape
sx, sy = seed
# buffer 的行索引 0 在顶部,但我们在输入时约定 y 从底部,之前输入是相对于左下角
# parse_input 时我们已经使用了左下角坐标, 但 buffer 的原点是顶部(0) -> 转换:
sy_buf = sy # since we consistently used y as row index (y from 0 to h-1) assuming bottom-left
# 为了安全,翻转一下:在本程序我们要求输入坐标相对于窗口左下, 但 buffer 行0为顶部, 所以映射:
sy_buf = h - 1 - sy
target = tuple(buf[sy_buf, sx])
fillc = tuple(POLY_COLOR.tolist())
if target == fillc:
return
stack = [(sx, sy_buf)]
while stack:
x, y = stack.pop()
if x < 0 or x >= w or y < 0 or y >= h:
continue
if tuple(buf[y, x]) != target:
continue
buf[y, x] = POLY_COLOR
stack.append((x+1, y))
stack.append((x-1, y))
stack.append((x, y+1))
stack.append((x, y-1))
def prepare_image():
create_empty_buffer()
# draw polygon edges
draw_polygon_edges(buffer_img, polygon)
if algorithm == 'scanline':
# scanline algorithm expects y as row index with 0 at bottom, but our buffer row 0 is top
# So we'll adapt: create a temporary copy where row0 is bottom
temp = np.flipud(buffer_img.copy())
scanline_fill(temp, polygon, spacing=scan_spacing)
# flip back
np.copyto(buffer_img, np.flipud(temp))
else:
# seed fill: do mapping inside function
seed_fill(buffer_img, seed_point)
def display():
glClearColor(1.0, 1.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glPixelZoom(1, 1)
# glDrawPixels expects data with origin at bottom-left; our buffer is top-down, flip vertically
img = np.flipud(buffer_img)
glDrawPixels(WIN_W, WIN_H, GL_RGB, GL_UNSIGNED_BYTE, img)
glutSwapBuffers()
def keyboard(key, x, y):
k = key.decode() if isinstance(key, bytes) else key
if k == '\x1b' or k == 'q':
print("退出")
sys.exit(0)
if k == 'r':
print("重新渲染")
prepare_image()
glutPostRedisplay()
def start_glut_display_from_gui():
"""在 GUI 按下 Render 后调用:启动 GLUT 窗口显示已经准备好的 buffer_img"""
# 初始化 GLUT 并进入主循环
glutInit(sys.argv)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
glutInitWindowSize(WIN_W, WIN_H)
glutInitWindowPosition(100, 100)
glutCreateWindow(b"Polygon Fill - Preview")
glutDisplayFunc(display)
glutKeyboardFunc(keyboard)
glutReshapeFunc(reshape)
print("按 q 或 ESC 退出, 按 r 重新渲染")
glutMainLoop()
def gui_main():
"""简单的 Tkinter GUI:选择测试用例、手动输入顶点、选择算法和参数,然后渲染"""
if tk is None:
print("Tkinter 不可用,无法启动 GUI。请安装或使用命令行模式。")
return
root = tk.Tk()
root.title("Polygon Fill - Simple GUI")
tests = load_tests()
# left: test list
lf = ttk.Frame(root)
lf.grid(row=0, column=0, sticky='ns', padx=6, pady=6)
ttk.Label(lf, text='Tests').pack()
listbox = tk.Listbox(lf, height=8)
for i, t in enumerate(tests):
listbox.insert(tk.END, f"{i}: {t.get('name','case'+str(i))} ({t.get('algorithm')})")
listbox.pack()
def load_selected():
sel = listbox.curselection()
if not sel:
return
case = tests[sel[0]]
# fill fields
alg_var.set(case.get('algorithm','scanline'))
verts = '\n'.join(f"{x} {y}" for x, y in case['polygon'])
vert_text.delete('1.0', tk.END)
vert_text.insert(tk.END, verts)
if case.get('algorithm') == 'seed':
sx, sy = case.get('seed', (0,0))
seed_entry.delete(0, tk.END)
seed_entry.insert(0, f"{sx} {sy}")
else:
spacing_entry.delete(0, tk.END)
spacing_entry.insert(0, str(case.get('spacing', 1)))
ttk.Button(lf, text='Load Selected', command=load_selected).pack(pady=4)
# right: controls and vertex editor
rf = ttk.Frame(root)
rf.grid(row=0, column=1, sticky='nsew', padx=6, pady=6)
ttk.Label(rf, text='Vertices (x y per line)').grid(row=0, column=0, sticky='w')
vert_text = tk.Text(rf, width=40, height=12)
vert_text.grid(row=1, column=0, columnspan=2)
alg_var = tk.StringVar(value='scanline')
ttk.Label(rf, text='Algorithm:').grid(row=2, column=0, sticky='w')
ttk.Radiobutton(rf, text='Scanline', variable=alg_var, value='scanline').grid(row=3, column=0, sticky='w')
ttk.Radiobutton(rf, text='Seed Fill', variable=alg_var, value='seed').grid(row=3, column=1, sticky='w')
ttk.Label(rf, text='Scan spacing:').grid(row=4, column=0, sticky='w')
spacing_entry = ttk.Entry(rf)
spacing_entry.insert(0, '1')
spacing_entry.grid(row=4, column=1, sticky='w')
ttk.Label(rf, text='Seed x y:').grid(row=5, column=0, sticky='w')
seed_entry = ttk.Entry(rf)
seed_entry.insert(0, '400 300')
seed_entry.grid(row=5, column=1, sticky='w')
status = ttk.Label(rf, text='')
status.grid(row=6, column=0, columnspan=2, sticky='w')
def on_render():
global polygon, algorithm, scan_spacing, seed_point
# parse vertices
txt = vert_text.get('1.0', tk.END).strip()
lines = [l.strip() for l in txt.splitlines() if l.strip()]
poly = []
try:
for ln in lines:
a = ln.split()
if len(a) < 2:
raise ValueError('格式错误')
x = clamp(int(round(float(a[0]))), 0, WIN_W-1)
y = clamp(int(round(float(a[1]))), 0, WIN_H-1)
poly.append((x,y))
if len(poly) < 3:
status.config(text='至少需要3个顶点')
return
except Exception:
status.config(text='顶点格式错误: 每行 "x y"')
return
polygon = poly
algorithm = alg_var.get()
try:
scan_spacing = max(1, int(spacing_entry.get()))
except Exception:
scan_spacing = 1
try:
svals = seed_entry.get().split()
if len(svals) >= 2:
sx = clamp(int(round(float(svals[0]))), 0, WIN_W-1)
sy = clamp(int(round(float(svals[1]))), 0, WIN_H-1)
seed_point = (sx, sy)
except Exception:
pass
status.config(text='准备渲染... 窗口打开后按 q 退出')
# prepare image
prepare_image()
root.destroy()
start_glut_display_from_gui()
ttk.Button(rf, text='Render', command=on_render).grid(row=7, column=0, pady=8)
ttk.Button(rf, text='Quit', command=root.destroy).grid(row=7, column=1, pady=8)
root.mainloop()
def reshape(w, h):
global WIN_W, WIN_H, buffer_img
WIN_W = w; WIN_H = h
glViewport(0, 0, w, h)
def main():
parse_input()
prepare_image()
# 初始化 GLUT
glutInit(sys.argv)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
glutInitWindowSize(WIN_W, WIN_H)
glutInitWindowPosition(100, 100)
glutCreateWindow(b"Polygon Fill - Scanline and Seed Fill")
glutDisplayFunc(display)
glutKeyboardFunc(keyboard)
glutReshapeFunc(reshape)
print("按 q 或 ESC 退出, 按 r 重新渲染")
glutMainLoop()
if __name__ == '__main__':
main()
样例:polygon_tests.json
[
{
"name": "Triangle (scanline)",
"algorithm": "scanline",
"spacing": 1,
"polygon": [[100,100],[700,150],[400,500]]
},
{
"name": "Square (scanline coarse)",
"algorithm": "scanline",
"spacing": 4,
"polygon": [[200,150],[600,150],[600,450],[200,450]]
},
{
"name": "Concave Arrow (scanline)",
"algorithm": "scanline",
"spacing": 1,
"polygon": [[150,300],[350,500],[450,400],[350,300],[450,200],[350,150],[150,300]]
},
{
"name": "Star (seed fill)",
"algorithm": "seed",
"seed": [400,300],
"polygon": [[400,520],[440,360],[580,360],[460,280],[520,140],[400,220],[280,140],[340,280],[220,360],[360,360]]
},
{
"name": "Complex Convex (seed fill)",
"algorithm": "seed",
"seed": [450,350],
"polygon": [[300,200],[600,220],[700,350],[600,480],[300,500],[220,350]]
}
]

浙公网安备 33010602011771号