填色算法分为两大类:
- 扫描线填色 (Scan-Line Filling) 算法。这类算法建立在多边形边界的矢量形式数据之上,可用于程序填色,也可用于交互填色
- 种子填色 (Seed Filling) 算法。这类算法建立在多边形边界的图像形式数据之上,并还需提供多边形界内一点的坐标。所以,它一般只能用于人机交互填色,而难以用于程序填色
多边形填充的扫描线算法
扫描线算法是按照扫描线顺序,计算扫描线与多边形的相交区间,再用要求的颜色显示这些区间的像素,完成转换工作。
对于一条扫描线,有如下四个步骤:
- 计算扫描线与多边形各边的交点
- 把所有交点按 \(x\) 值递增排序
- 把扫描线交点两两配对得到填色区间
- 把相交区间中的像素置为多边形颜色,相交区间外的像素置为背景色
所谓扫描线,其实就是一系列平行于 \(x\) 轴的直线,例如

图中就是一条扫描线,其纵坐标为 \(y\) ,与多边形有两个交点。
一般来说,在处理一条扫描线时,仅对与它相交的多边形的边进行求交处理。我们把相交的边称为活性边,并把它们按照交点 \(x\) 坐标递增的顺序存放在一个链表中,称为活性边表 (AET) 。为了方便起见,我们只存放边与交点的 “特征“ :
- 交点横坐标 \(x\)
- 与相邻扫描线交点横坐标的差 \(\Delta x\)
- 边的最大纵坐标 \(y_{\max}\)
不必存放交点纵坐标,因为扫描线本身就提供这一信息。例如

图中有两条相邻的扫描线,活性边为 \(P_1P_2\) ,可以看出,我们可以通过当前交点推算出下一个交点的位置,并且可以通过当前 \(y\) 值与 \(y_{\max}\) 进行比较来判断这条边是否绘制完毕。
根据直线方程,我们容易得到
也就是斜率的倒数。对于水平线,我们之后进行考虑。
注意到我们总是需要先得到初始的顶点才能开始上面的操作,因此为了方便活性边表的建立与更新,可以这样考虑:在一开始,对于每一条扫描线,都去寻找在扫描线上出现的多边形顶点,将带有这些顶点的边存放起来,当处理扫描线时,就将存放的边拿出来处理。
于是我们为每一条扫描线建立一个新边表 (NET) ,存放在该扫描线上第一次出现的边。具体来说,如果有一条边最低的端点为 \(y_{\min}\) ,就将它添加到扫描线 \(y_{min}\) 的新边表中。考虑到效率问题,我们选择遍历多边形的每一条边,记录边的最低端点进行添加。
基本的算法流程如下:
- 初始化一个边表的数组 NET ,用于存放每个扫描线初始边的信息
- 初始化活性边表 AET
- 对于 NET 中的每个扫描线,将它们的初始边按照 \(x\) 坐标的插入排序放进 AET 中
- 将 AET 中的点两两配对,填充区间
- 遍历 AET ,将其中 \(y\) 达到 \(y_{\max}\) 的节点删掉,将剩余节点的 \(x\) 增加 \(\Delta x\)
- 特别地,如果允许多边形的边相交,还需要对 AET 重新排序
我们暂时不考虑相交情形,那么如图所示

这就是 NET 的数组,每个元素都存放了从对应扫描线开始的边的信息。

如图所示是一个 AET ,我们每次从 NET 的一个扫描线中取出从它开始的所有边插入到 AET 中。
最后得到扫描线算法的程序,有两个注意点:
- 对于水平边,这里选择直接抛弃,因为对于多边形填充,即使抛弃水平边,内部的填充线也足够显示图像;处理水平边比较麻烦,所以在这里暂时略去了
- 这里会对调上面的 3 4 步骤,这是因为如果先插入新的边,然后配对填充,有可能在顶点处产生多条边的数据,造成配对错误

例如在上图中,添加新的边后会发现配对出现了问题:在第一个顶点位置的两条边配对了,导致 \(AB\) 段无法配对填充。
所以我们先配对填充,再插入新的边,然后立即更新边表去掉旧的边,在新的循环中配对填充。当然,这种做法会导致初始顶点无法绘制,不过对于多边形填充而言无伤大雅。
#define MAX_SIZE 600
// 边表结构
typedef struct AET
{
double x;
double dx;
int y_max;
AET* next;
} Aet;
// 清除数据
void clear(Aet* ap)
{
if (ap == NULL)
{
return;
}
clear(ap->next);
free(ap);
}
// 将 ymin = i 的边放入 net[i]
void putEdge(int n, Aet* net[], POINT points[])
{
// 将 ymin = i 的边放入 net[i]
for (int i = 0; i < n; i++)
{
// 边的端点
POINT p1 = points[i];
POINT p2 = points[(i + 1) % n];
// 如果是水平节点,直接跳过
if (p1.y == p2.y)
{
continue;
}
// 存放边的信息
Aet* aet = (Aet*)malloc(sizeof(Aet));
// 取 ymin ,添加边
int y_min;
if (p1.y > p2.y)
{
aet->x = p2.x;
aet->y_max = p1.y;
y_min = p2.y;
}
else
{
aet->x = p1.x;
aet->y_max = p2.y;
y_min = p1.y;
}
aet->dx = 1.0 * (p2.x - p1.x) / (p2.y - p1.y);
aet->next = NULL;
// 将边添加到 y_min 扫描线的边表末端
Aet* tmp = net[y_min];
while (tmp->next != NULL)
{
tmp = tmp->next;
}
tmp->next = aet;
}
}
// 插入边
void insertEdge(Aet* active, Aet* edge)
{
// 存放第一条边
Aet* ep = edge->next;
// 将 edge 中的边逐个插入到 active 中
while (ep != NULL)
{
Aet* ap = active;
// 移动直到 ap 下一个为空,或者下一个 x >= ep->x
while (ap->next != NULL && ep->x > ap->next->x)
{
ap = ap->next;
}
// 临时存放 ap->next
Aet* tmp = ap->next;
// 构造新的节点
ap->next = (Aet*)malloc(sizeof(Aet));
ap = ap->next;
// 重新赋值
ap->x = ep->x;
ap->dx = ep->dx;
ap->y_max = ep->y_max;
ap->next = tmp;
// 如果两个节点重合,应该把 dx 较小的那个放在前面
if (tmp != NULL && ap->x == tmp->x && ap->dx > tmp->dx)
{
// 交换它们的值
double t = ap->dx;
ap->dx = tmp->dx;
tmp->dx = t;
t = ap->y_max;
ap->y_max = tmp->y_max;
tmp->y_max = t;
}
// 移动到下一个边
ep = ep->next;
}
}
// 填充扫描线
void scanFill(HDC hdc, Aet* active, int y, COLORREF color)
{
Aet* ap = active;
// 要求两两非空,填充两点之间的点
while (ap->next != NULL && ap->next->next != NULL)
{
ap = ap->next;
for (int x = ap->x; x <= ap->next->x; x++)
{
SetPixel(hdc, x, y, color);
}
ap = ap->next;
}
}
// 更新边表
void updateAet(Aet* active, int scan)
{
Aet* ap = active;
// 由于下一个节点可能为空,又需要下一个节点的下一个节点,因此要做两次判断
while (ap != NULL && ap->next != NULL)
{
if (ap->next->y_max == scan)
{
// 删除节点,这里直接跳跃两个节点
Aet* tmp = ap->next;
ap->next = tmp->next;
tmp->next = NULL;
free(tmp);
}
else
{
// 普通节点增加 dx ,然后到下一个节点
ap->next->x += ap->next->dx;
ap = ap->next;
}
}
}
// 多边形扫描填充算法
void polyFill(HDC hdc, int n, POINT points[], COLORREF color)
{
// 扫描线标号,扫描线标号范围
int scan = 0, smin = MAX_SIZE, smax = 0;
// 确定扫描线范围
for (int i = 0; i < n; i++)
{
if (points[i].y >= smax)
{
smax = points[i].y;
}
if (points[i].y <= smin)
{
smin = points[i].y;
}
}
// 初始化新边表
Aet* net[MAX_SIZE];
for (scan = smin; scan <= smax; scan++)
{
// 边表头,不存放任何东西
net[scan] = (Aet*)malloc(sizeof(Aet));
net[scan]->next = NULL;
}
// 初始化活性边表,表头不存放任何东西
Aet* active = (Aet*)malloc(sizeof(Aet));
active->next = NULL;
// 将 ymin = i 的边放入 net[i]
putEdge(n, net, points);
// 开始填充
for (scan = smin; scan <= smax; scan++)
{
scanFill(hdc, active, scan, color); // 遍历边表进行填充
insertEdge(active, net[scan]); // 取边插入 AET 表
updateAet(active, scan); // 更新边表
clear(net[scan]); // 清除无用新边表
}
// 清除活性边表
clear(active);
}
绘图测试程序:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
POINT points[6] = {100, 300, 200, 100, 250, 200, 300, 100, 400, 400, 280, 450};
int n = sizeof(points) / sizeof(POINT);
polyFill(hdc, n, points, RGB(255, 0, 0));
EndPaint(hWnd, &ps);
}
绘图结果

最后,对于交点问题,如果不用上面的技巧,就需要考虑取舍问题。一般是通过扫描线与两条边的相对位置判断:
- 如果两条边在扫描线两侧,就记一个交点
- 如果都位于上方,则记 0 个交点;都位于下方,则记 2 个交点
- 如果重合,就记一个交点
具体实现时,只需判断端点和顶点 \(y\) 值的关系即可。
边界标志算法
边界标志算法的基本思想是对多边形的边界经过的像素打上标记,然后用扫描线对每条线上的像素标记区段进行着色。该算法把边经过的像素都标记为一种颜色,然后保有一个布尔值 inside ,如果遇到标记颜色,就将 inside 取反。如果 inside 为 true ,就说明在多边形中,需要填色;否则就不在多边形中。
用软件实现时,扫描线算法与边界标志算法的执行速度几乎相同,但由于边界标志算法不需要建立维护边表以及排序,所以边界标志算法更适合硬件实现,这时它的执行速度比有序边表算法快 1~2 个数量级。
种子填充算法
这里讨论的区域指已经表示成点阵形式的填充图案,它是像素的集合。区域可采用内点表示和边界表示两种形式,也就是将内点和边界点分别用同一种颜色表示。区域填充指先将区域的一点赋予指定的颜色,然后将该颜色扩展到整个区域的过程。
区域填充算法要求区域连通性,只有这样才有可能将种子点的颜色扩散到其它区域。区域可分为 4 向连通区域和 8 向连通区域。其中 4 向连通区域是指从区域上任一点出发,可通过上下左右 4 个方向组合,不越过边界到达区域中的任意点;而 8 向连通区域则需要上下左右、左上、右下、左下、右下 8 个方向组合。