三、改進的掃描線填充算法
掃描線填充算法的原理和實現都很簡單,但是因爲要同時維護“活動邊表(AET)”和“新邊表(NET)”,對存儲空間的要求比較高。這兩張表的部分內容是重複的,而且“新邊表”在很多情況下都是一張稀疏表,如果能對其進行改進,避免出現兩張表,就可以節省存儲空間,同時省去從“邊表”生成“新邊表”的開銷,同時也省去了用“新邊表”維護“活動邊表”的開銷,基於這個原則可以對原始掃描線算法進行改進。
3.1重新設計“活動邊表”
改進的算法仍然使用了“活動邊表”的概念,但是不再構造獨立的“活動邊表”,而是直接在“邊表”中劃定一部分區間作爲“活動邊區間”,也就是說,把多邊形的邊分成兩個子集,一個是與掃描線有交點的邊的集合,另一個是與掃描線沒有交點的邊的集合。要達到這個目的,只需要對“活動邊表”按照每條邊的頂點ymax座標排序即可。這個排序與原始掃描線算法中對“活動邊表”的維護原理是一樣的,因爲只有邊的ymax座標區間內與掃描線有交點的邊纔可能是“活動邊”。爲了避免重複掃描整個“活動邊表”,需要用一個first指針和一個last指針用於標識“活動邊區間”。first指針之前的邊都是已經處理過的邊,同樣,last指針之後的邊都是還沒有處理的邊。每處理完一條掃描線,都要更新first和last指針位置,調整last指針的位置將ymax大於當前掃描線的邊納入到“活動邊區間”,同時調整first指針將處理完成的邊排除在“活動邊區間”之外。
如果調整last指針的依據是邊的ymax是否大於當前掃描線,那麼調整first指針的依據是什麼?也就是如何判斷一條邊已經處理完了?方法是在邊(EDGE)定義中增加一個dy(Δy)屬性,這個屬性被初始化成這條邊在y方向上的長度,每處理完一條掃描線,dy都要做減一處理,當dy=0時,就說明這條邊已經不與掃描線相交了,可以被排除在活動邊區間之外。改進的掃描線算法的“邊”的完整定義如下:
7 typedef struct tagEDGE2 8 { 9 double xi; 10 double dx; 11 int ymax; 12 int dy; 17 }EDGE2; |
EDGE2定義中xi、dx和ymax的含義和原始算法中EDGE的定義相同,只是多了一個dy屬性。
每當處理一條掃描線時,除了“活動邊區間”的first指針和last指針需要調整之外,還要將first指針和last指針之間的“活動邊”按照xi從小到大的順序排序,以保證填充算法能夠用正確的交點線段序列畫線填充。因此,每次調整“活動邊區間”的first指針和last指針之後,都要對“活動邊區間”重新排序,也就是說“活動邊區間”內的各邊的位置並不固定,會隨着掃描線的變化而相應地變化。
仍以圖(6)所示的多邊形爲例,處理掃描線10時的“活動邊表”狀態如圖(11-a)所示,而處理掃描線8時的“活動邊表”狀態則如圖(11-b)所示。可以看出,當處理掃描線8時,“活動邊區間”內的邊的順序有了調整,因爲新加入的P6P1和P1P2兩條邊與掃描線的交點座標xi比P5P6與掃描線的交點座標xi小,因此排在P5P6前面。
圖(11)改進的活動邊表結構
3.2新“活動邊表”的構造與調整
改進的掃描線算法的重點是“活動邊表”的構造和調整。“活動邊表”的構造方法如下:
(1) 首先剔除多邊形各邊的水平邊,然後將剩下的邊按照ymax的值從大到小順序存入一個線性表中,表中第一個元素ymax值最大的表,最後一個元素是ymax值最小的邊。對於各邊中左、右頂點的情況需要和原始算法一樣做調整,以免出現交點個數不正確的異常。這裏對調整的策略再強調一下,調整都是針對邊的終點進行的,對於圖(10-a)所示的左頂點,需要先將P2點的座標調整爲(x2 – dx, y2 - 1),然後再求邊的ymax、xi和dy。對於圖(10-b)所示的右頂點,需要將P2點的座標調整爲(x2 + dx, y2 + 1),然後再求邊的ymax、xi和dy。
(2) 加入first指針和last指針,構成“活動邊區間”。first指針和last指針之間的邊都是和當前掃描線有交點的邊或已經處理過的邊,已經處理過的邊的dy是0,因此,對“活動邊”掃描時需要忽略其中dy已經是0的邊。這些已經處理過的邊會加載在正常的邊中,直到調整first指針時被剔除出“活動邊區間”。
“活動邊表”的調整指的是在處理完每根掃描線之後,更新“活動邊表”中“活動邊區間”內的各邊的相關屬性的值,比如遞減dy的值,調整交點xi座標的值等等。根據EDGE2的定義,每根掃描線處理完之後需要對“活動邊區間”內的邊做如下調整:
(1)調整“活動邊區間”中參與求交計算的各邊的屬性值,這些調整算法是:
dy = dy – 1;
xi = xi – dx;
(2)調整“活動邊區間”的first指針和last指針,使符合條件的新邊加入到“活動邊區間”,同時將處理完的邊從“活動邊區間”剔除。這些調整算法是:
if(first所指邊的Δy爲0)
first=first+1;
if(last所指的下一條邊的ymax大於下一掃描線的y值)
last=last+1
3.3改進的掃描線填充算法實現
首先定義“活動邊表”,這是一個線性表,每個元素是一條邊的全部屬性,同時還要包含first指針和last指針,其數據結構定義如下:
19 typedef struct tagSP_EDGES_TABLE 20 { 21 std::vector<EDGE2> slEdges; 22 int first; 23 int last; 24 }SP_EDGES_TABLE; |
改進的掃描線填充算法重點仍然是新“活動邊表”的構造,這裏給出構造新“活動邊表”的算法實現:
36 void InitScanLineEdgesTable(SP_EDGES_TABLE& spET, const Polygon& py) 37 { 38 EDGE2 e; 39 for(int i = 0; i < py.GetPolyCount(); i++) 40 { 41 const Point& ps = py.pts[i]; 42 const Point& pe = py.pts[(i + 1) % py.GetPolyCount()]; 43 const Point& pee = py.pts[(i + 2) % py.GetPolyCount()]; 44 51 if(pe.y != ps.y) //不處理水平線 52 { 53 e.dx = double(pe.x - ps.x) / double(pe.y - ps.y); 54 if(pe.y > ps.y) 55 { 56 if(pe.y < pee.y) //左頂點 57 { 58 e.xi = pe.x - e.dx; 59 e.ymax = pe.y - 1; 60 e.dy = e.ymax - ps.y + 1; 61 } 62 else 63 { 64 e.xi = pe.x; 65 e.ymax = pe.y; 66 e.dy = pe.y - ps.y + 1; 67 } 68 } 69 else //(pe.y < ps.y) 70 { 71 if(pe.y > pee.y) //右頂點 72 { 73 e.xi = ps.x; 74 e.ymax = ps.y; 75 e.dy = ps.y - (pe.y + 1) + 1; 76 } 77 else 78 { 79 e.xi = ps.x; 80 e.ymax = ps.y; 81 e.dy = ps.y - pe.y + 1; 82 } 83 } 84 85 InsertEdgeToEdgesTable(e, spET.slEdges); 86 } 87 } 88 spET.first = spET.last = 0; 89 } |
Polygon定義了一個多邊形,其pts數組按照順序存放了多邊形的各個頂點,InitScanLineEdgesTable()函數從Polygon中依次取出三個頂點,前兩個頂點構成當前處理的邊,後一個頂點用於輔助判斷是否是左、右頂點的情況,如果是左、右頂點的情況,就要對邊的終點的座標做調整(調整的方法在3.2小節已經描述)。調整完線段終點座標後構造邊e,然後由InsertEdgeToEdgesTable()函數將e插入到線性表中,插入操作滿足線性表按照ymax從大到小有序,這個是插入排序的基本算法,這裏就不再列出代碼。
算法的另一個終點就是處理每條掃描線和“活動邊表”的關係,計算出每條掃描線需要填充的區間。一下就是ProcessScanLineFill2()函數的實現:
189 void ScanLinePolygonFill2(const Polygon& py, int color) 190 { 191 assert(py.IsValid()); 192 193 int ymin = 0; 194 int ymax = 0; 195 GetPolygonMinMax(py, ymin, ymax); 196 SP_EDGES_TABLE spET; 197 InitScanLineEdgesTable(spET, py); 198 HorizonEdgeFill(py, color); //水'cb?平'c6?邊'b1?直'd6?接'bd?畫'bb?線'cf?填'cc?充'b3? 199 ProcessScanLineFill2(spET, ymin, ymax, color); 200 } |
ProcessScanLineFill2()函數依次處理每條掃描線,根據3.2節的算法描述,UpdateEdgesTableActiveRange()函數和SortActiveRangeByX()函數更新“活動邊區間”並對區間內的邊排序,FillActiveRangeScanLine函數從“活動邊區間”內依次取出兩個交點組成填充區間,調用前面介紹的DrawHorizontalLine()函數完成畫線填充,UpdateActiveRangeIntersection()函數則根據3.2節的算法描述更新參與求交計算的各邊的屬性值。這四個函數的實現都很簡單,結合3.2節的算法描述很容易理解,此處僅列出代碼,不做過多解釋。
91 void UpdateEdgesTableActiveRange(SP_EDGES_TABLE& spET, int yScan) 92 { 93 std::vector<EDGE2>& slET = spET.slEdges; 94 int edgesCount = static_cast<int>(slET.size()); 95 while((spET.last < (edgesCount - 1)) && (slET[spET.last + 1].ymax >= yScan)) 96 { 97 spET.last++; 98 } 99 100 while(slET[spET.first].dy == 0) 101 { 102 spET.first++; 103 } 104 } |
125 void FillActiveRangeScanLine(SP_EDGES_TABLE& spET, int yScan, int color) 126 { 127 std::vector<EDGE2>& slET = spET.slEdges; 128 int pos = spET.first; 129 130 do 131 { 132 pos = GetIntersectionInActiveRange(spET, pos); 133 if(pos != -1) 134 { 135 int x1 = ROUND_INT(slET[pos].xi); 136 pos = GetIntersectionInActiveRange(spET, pos + 1); 137 if(pos != -1) 138 { 139 int x2 = ROUND_INT(slET[pos].xi); 140 pos++; 141 DrawHorizontalLine(x1, x2, yScan, color); 142 } 143 else 144 { 145 assert(false); 146 } 147 } 148 }while(pos != -1); 149 } 150 151 bool EdgeXiComparator2(EDGE2& e1, EDGE2& e2) 152 { 153 return (e1.xi < e2.xi); 154 } 155 156 void SortActiveRangeByX(SP_EDGES_TABLE& spET) 157 { 158 std::vector<EDGE2>& slET = spET.slEdges; 159 160 sort(slET.begin() + spET.first, 161 slET.begin() + spET.last + 1, 162 EdgeXiComparator2); 163 } 164 165 void UpdateActiveRangeIntersection(SP_EDGES_TABLE& spET) 166 { 167 for(int pos = spET.first; pos <= spET.last; pos++) 168 { 169 if(spET.slEdges[pos].dy > 0) 170 { 171 spET.slEdges[pos].dy--; 172 spET.slEdges[pos].xi -= spET.slEdges[pos].dx; 173 } 174 } 175 } 176 177 void ProcessScanLineFill2(SP_EDGES_TABLE& spET, 178 int ymin, int ymax, int color) 179 { 180 for (int yScan = ymax; yScan >= ymin; yScan--) 181 { 182 UpdateEdgesTableActiveRange(spET, yScan); 183 SortActiveRangeByX(spET); 184 FillActiveRangeScanLine(spET, yScan, color); 185 UpdateActiveRangeIntersection(spET); 186 } 187 } |
<下一篇:邊標誌填充算法>