區域填充算法


 

填色算法分為兩大類:

  • 掃描線填色 (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