填色算法分為兩大類:
- 掃描線填色 (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 個方向組合。