算法系列之十二:多邊形區域填充算法--改進的掃描線填充算法

三、改進的掃描線填充算法

        掃描線填充算法的原理和實現都很簡單,但是因爲要同時維護“活動邊表(AET)”和“新邊表(NET)”,對存儲空間的要求比較高。這兩張表的部分內容是重複的,而且“新邊表”在很多情況下都是一張稀疏表,如果能對其進行改進,避免出現兩張表,就可以節省存儲空間,同時省去從“邊表”生成“新邊表”的開銷,同時也省去了用“新邊表”維護“活動邊表”的開銷,基於這個原則可以對原始掃描線算法進行改進。

3.1重新設計“活動邊表”

        改進的算法仍然使用了“活動邊表”的概念,但是不再構造獨立的“活動邊表”,而是直接在“邊表”中劃定一部分區間作爲“活動邊區間”,也就是說,把多邊形的邊分成兩個子集,一個是與掃描線有交點的邊的集合,另一個是與掃描線沒有交點的邊的集合。要達到這個目的,只需要對“活動邊表”按照每條邊的頂點ymax座標排序即可。這個排序與原始掃描線算法中對“活動邊表”的維護原理是一樣的,因爲只有邊的ymax座標區間內與掃描線有交點的邊纔可能是“活動邊”。爲了避免重複掃描整個“活動邊表”,需要用一個first指針和一個last指針用於標識“活動邊區間”。first指針之前的邊都是已經處理過的邊,同樣,last指針之後的邊都是還沒有處理的邊。每處理完一條掃描線,都要更新firstlast指針位置,調整last指針的位置將ymax大於當前掃描線的邊納入到“活動邊區間”,同時調整first指針將處理完成的邊排除在“活動邊區間”之外。

        如果調整last指針的依據是邊的ymax是否大於當前掃描線,那麼調整first指針的依據是什麼?也就是如何判斷一條邊已經處理完了?方法是在邊(EDGE)定義中增加一個dy(Δy)屬性,這個屬性被初始化成這條邊在y方向上的長度,每處理完一條掃描線,dy都要做減一處理,當dy0時,就說明這條邊已經不與掃描線相交了,可以被排除在活動邊區間之外。改進的掃描線算法的“邊”的完整定義如下:

 7 typedef struct tagEDGE2

 8 {

 9     double xi;

10     double dx;

11     int ymax;

12     int dy;

17 }EDGE2;

 

EDGE2定義中xidxymax的含義和原始算法中EDGE的定義相同,只是多了一個dy屬性。

        每當處理一條掃描線時,除了“活動邊區間”的first指針和last指針需要調整之外,還要將first指針和last指針之間的“活動邊”按照xi從小到大的順序排序,以保證填充算法能夠用正確的交點線段序列畫線填充。因此,每次調整“活動邊區間”的first指針和last指針之後,都要對“活動邊區間”重新排序,也就是說“活動邊區間”內的各邊的位置並不固定,會隨着掃描線的變化而相應地變化。

        仍以圖(6)所示的多邊形爲例,處理掃描線10時的“活動邊表”狀態如圖(11-a)所示,而處理掃描線8時的“活動邊表”狀態則如圖(11-b)所示。可以看出,當處理掃描線8時,“活動邊區間”內的邊的順序有了調整,因爲新加入的P6P1P1P2兩條邊與掃描線的交點座標xiP5P6與掃描線的交點座標xi小,因此排在P5P6前面。

 

 

圖(11)改進的活動邊表結構

 

 3.2新“活動邊表”的構造與調整

         改進的掃描線算法的重點是“活動邊表”的構造和調整。“活動邊表”的構造方法如下:

 

(1)       首先剔除多邊形各邊的水平邊,然後將剩下的邊按照ymax的值從大到小順序存入一個線性表中,表中第一個元素ymax值最大的表,最後一個元素是ymax值最小的邊。對於各邊中左、右頂點的情況需要和原始算法一樣做調整,以免出現交點個數不正確的異常。這裏對調整的策略再強調一下,調整都是針對邊的終點進行的,對於圖(10-a)所示的左頂點,需要先將P2點的座標調整爲(x2 – dx, y2 - 1),然後再求邊的ymaxxidy。對於圖(10-b)所示的右頂點,需要將P2點的座標調整爲(x2 + dx, y2 + 1),然後再求邊的ymaxxidy

(2)       加入first指針和last指針,構成“活動邊區間”。first指針和last指針之間的邊都是和當前掃描線有交點的邊或已經處理過的邊,已經處理過的邊的dy0,因此,對“活動邊”掃描時需要忽略其中dy已經是0的邊。這些已經處理過的邊會加載在正常的邊中,直到調整first指針時被剔除出“活動邊區間”。

         “活動邊表”的調整指的是在處理完每根掃描線之後,更新“活動邊表”中“活動邊區間”內的各邊的相關屬性的值,比如遞減dy的值,調整交點xi座標的值等等。根據EDGE2的定義,每根掃描線處理完之後需要對“活動邊區間”內的邊做如下調整:

 1)調整“活動邊區間”中參與求交計算的各邊的屬性值,這些調整算法是:

      dy = dy – 1;

      xi = xi – dx;

 

2)調整“活動邊區間”的first指針和last指針,使符合條件的新邊加入到“活動邊區間”,同時將處理完的邊從“活動邊區間”剔除。這些調整算法是:

      if(first所指邊的Δy0)

          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 }

 

<下一篇:邊標誌填充算法>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章