区域填充算法


 

填色算法分为两大类:

  • 扫描线填色 (Scan-Line Filling) 算法。这类算法建立在多边形边界的矢量形式数据之上,可用于程序填色,也可用于交互填色
  • 种子填色 (Seed Filling) 算法。这类算法建立在多边形边界的图像形式数据之上,并还需提供多边形界内一点的坐标。所以,它一般只能用于人机交互填色,而难以用于程序填色

 

多边形填充的扫描线算法

扫描线算法是按照扫描线顺序,计算扫描线与多边形的相交区间,再用要求的颜色显示这些区间的像素,完成转换工作。

对于一条扫描线,有如下四个步骤:

  1. 计算扫描线与多边形各边的交点
  2. 把所有交点按 \(x\) 值递增排序
  3. 把扫描线交点两两配对得到填色区间
  4. 把相交区间中的像素置为多边形颜色,相交区间外的像素置为背景色

所谓扫描线,其实就是一系列平行于 \(x\) 轴的直线,例如

图中就是一条扫描线,其纵坐标为 \(y\) ,与多边形有两个交点。

 

一般来说,在处理一条扫描线时,仅对与它相交的多边形的边进行求交处理。我们把相交的边称为活性边,并把它们按照交点 \(x\) 坐标递增的顺序存放在一个链表中,称为活性边表 (AET) 。为了方便起见,我们只存放边与交点的 “特征“ :

  • 交点横坐标 \(x\)
  • 与相邻扫描线交点横坐标的差 \(\Delta x\)
  • 边的最大纵坐标 \(y_{\max}\)

不必存放交点纵坐标,因为扫描线本身就提供这一信息。例如

图中有两条相邻的扫描线,活性边为 \(P_1P_2\) ,可以看出,我们可以通过当前交点推算出下一个交点的位置,并且可以通过当前 \(y\) 值与 \(y_{\max}\) 进行比较来判断这条边是否绘制完毕。

根据直线方程,我们容易得到

\[\Delta x = \dfrac{x_{P_2}-x_{P_1}}{y_{P_2}-y_{P_1}} \]

也就是斜率的倒数。对于水平线,我们之后进行考虑。

 

注意到我们总是需要先得到初始的顶点才能开始上面的操作,因此为了方便活性边表的建立与更新,可以这样考虑:在一开始,对于每一条扫描线,都去寻找在扫描线上出现的多边形顶点,将带有这些顶点的边存放起来,当处理扫描线时,就将存放的边拿出来处理。

于是我们为每一条扫描线建立一个新边表 (NET) ,存放在该扫描线上第一次出现的边。具体来说,如果有一条边最低的端点为 \(y_{\min}\) ,就将它添加到扫描线 \(y_{min}\) 的新边表中。考虑到效率问题,我们选择遍历多边形的每一条边,记录边的最低端点进行添加。

 

基本的算法流程如下:

  1. 初始化一个边表的数组 NET ,用于存放每个扫描线初始边的信息
  2. 初始化活性边表 AET
  3. 对于 NET 中的每个扫描线,将它们的初始边按照 \(x\) 坐标的插入排序放进 AET 中
  4. 将 AET 中的点两两配对,填充区间
  5. 遍历 AET ,将其中 \(y\) 达到 \(y_{\max}\) 的节点删掉,将剩余节点的 \(x\) 增加 \(\Delta x\)
  6. 特别地,如果允许多边形的边相交,还需要对 AET 重新排序

我们暂时不考虑相交情形,那么如图所示

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

如图所示是一个 AET ,我们每次从 NET 的一个扫描线中取出从它开始的所有边插入到 AET 中。

 

最后得到扫描线算法的程序,有两个注意点:

  1. 对于水平边,这里选择直接抛弃,因为对于多边形填充,即使抛弃水平边,内部的填充线也足够显示图像;处理水平边比较麻烦,所以在这里暂时略去了
  2. 这里会对调上面的 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 个方向组合。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM