點和多邊形關系的算法實現
好了,現在我們已經了解了矢量叉積的意義,以及判斷直線段是否有交點的算法,現在回過頭看看文章開始部分的討論的問題:如何判斷一個點是否在多邊形內部? 根據射線法的描述,其核心是求解從P點發出的射線與多邊形的邊是否有交點。注意,這里說的是射線,而我們前面討論的都是線段,好像不適用吧?沒錯,確實是 不適用,但是我要介紹一種用計算機解決問題時常用的建模思想,應用了這種思想之后,我們前面討論的方法就適用了。什么思想呢?就是根據問題域的規模和性質 抽象和簡化模型的思想,這可不是故弄玄虛,說說具體的思路吧。
計算機是不能表示無窮大和無窮小,計算機處理的每一個數都有確定的值,而且必須有確定的值。我們面臨的問題域是整個實數空間的坐標系,在每個維度上都是 從負無窮到正無窮,比如射線,就是從坐標系中一個明確的點到無窮遠處的連線。這就有點為難計算機了,為此我們需要簡化問題的規模。假設問題中多邊形的每個 點的坐標都不會超過(-10000.0, +10000.0)區間(比如我們常見的圖形輸出設備都有大小的限制),我們就可以將問題域簡化為(-10000.0, +10000.0)區間內的一小塊區域,對於這塊區域來說,>= 10000.0就意味着無窮遠。你肯定已經明白了,數學模型經過簡化后,算法中提到的射線就可以理解為從模型邊界到內部點P之間的線段,前面討論的關於線 段的算法就可以使用了。
射線法的基本原理是判斷由P點發出的射線與多邊形的交點個數,交點個數是奇數表示P點在多邊形內(在多邊形的邊上也視為在多邊形內部的特殊情 況),正常情況下經過點P的射線應該如圖6(a)所示那樣,但是也可能碰到多種非正常情況,比如剛好經過多邊形一個定點的情況,如圖6 (b),這會被誤認為和兩條邊都有交點,還可能與某一條邊共線如圖6 (c)和(d),共線就有無窮多的交點,導致判斷規則失效。還要考慮凹多邊形的情況,如圖6(e)。
圖6 射線法可能遇到的各種交點情況
針對這些特殊情況,在對多邊形的每條邊進行判斷時,要考慮以下這些特殊情況,假設當前處理的邊是P1P2,則有以下原則:
1)如果點P在邊P1P2上,則直接判定點P在多邊形內;
2)如果從P發出的射線正好穿過P1或者P2,那么這個交點會被算作2次(因為在處理以P1或P2為端點的其它邊時可能已經計算過這個點了),對這種情況的處理原則是:如果P的y坐標與P1、P2中較小的y坐標相同,則忽略這個交點;
3)如果從P發出的射線與P1P2平行,則忽略這條邊;
對於第三個原則,需要判斷兩條直線是否平行,通常的方法是計算兩條直線的斜率,但是本算法因為只涉及到直線段(射線也被模型簡化為長線段了),就簡化了 很多,判斷直線是否水平,只要比較一下線段起始點的y坐標是否相等就行了,而判斷直線是否垂直,也只要比較一下線段起始點的x坐標是否相等就行了。
應用以上原則后,掃描線法判斷點是否在多邊形內的算法流程就完整了,圖7就是算法的流程圖:
最終掃描線法判斷點是否在多邊形內的算法實現如下:
228 bool IsPointInPolygon(const Polygon& py, const Point& pt) 229 { 230 assert(py.IsValid()); /*只考慮正常的多邊形,邊數>=3*/ 231 232 int count = 0; 233 LineSeg ll = LineSeg(pt, Point(-INFINITE, pt.y)); /*射線L*/ 234 for(int i = 0; i < py.GetPolyCount(); i++) 235 { 236 /*當前點和下一個點組成線段P1P2*/ 237 LineSeg pp = LineSeg(py.pts[i], py.pts[(i + 1) % py.GetPolyCount()]); 238 if(IsPointOnLineSegment(pp, pt)) 239 { 240 return true; 241 } 242 243 if(!pp.IsHorizontal()) 244 { 245 if((IsSameFloatValue(pp.ps.y, pt.y)) && (pp.ps.y > pp.pe.y)) 246 { 247 count++; 248 } 249 else if((IsSameFloatValue(pp.pe.y, pt.y)) && (pp.pe.y > pp.ps.y)) 250 { 251 count++; 252 } 253 else 254 { 255 if(IsLineSegmentIntersect(pp, ll)) 256 { 257 count++; 258 } 259 } 260 } 261 } 262 263 return ((count % 2) == 1); 264 }
在圖形學領域實施的真正工程代碼,通常還會增加一個多邊形的外包矩形快速判斷,對點根本就不在多邊形周圍的情況做快速排除, 提高算法效率。這又涉及到求多邊形外包矩形的算法,這個算法也很簡單,就是遍歷多邊形的所有節點,找出各個坐標方向上的最大最小值。以下就是求多邊形外包 矩形的算法:
266 void GetPolygonEnvelopRect(const Polygon& py, Rect& rc) 267 { 268 assert(py.IsValid()); /*只考慮正常的多邊形,邊數>=3*/ 269 270 double minx = py.pts[0].x; 271 double maxx = py.pts[0].x; 272 double miny = py.pts[0].y; 273 double maxy = py.pts[0].y; 274 for(int i = 1; i < py.GetPolyCount(); i++) 275 { 276 if(py.pts[i].x < minx) 277 minx = py.pts[i].x; 278 if(py.pts[i].x > maxx) 279 maxx = py.pts[i].x; 280 if(py.pts[i].y < miny) 281 miny = py.pts[i].y; 282 if(py.pts[i].y > maxy) 283 maxy = py.pts[i].y; 284 } 285 286 rc = Rect(minx, miny, maxx, maxy); 287 }
除了掃描線法,還可以通過多邊形邊的法矢量方向、多邊形面積以及角度和等方法判斷點與多邊形的關系。但是這些算法要么只支持 凸多邊形,要么需要復雜的三角函數運算(多邊形邊數小於44時,可采用近似公式計算夾角和,避免三角函數運算),使用的范圍有限,只有掃描線法被廣泛應 用。
至此,本文的內容已經完結,以上通過對點與矩形、點與圓、點與直線以及點與多邊形位置關系判斷算法的講解,向大家展示了幾種常見的計算幾何算法實現,用簡短而易懂的代碼剖析了這些算法的實質。下一篇將介紹計算機圖形學中最基本的直線生成算法。
參考資料:
【1】計算幾何:算法設計與分析 周培德 清華大學出版社 2005年
【2】計算幾何:算法與應用 德貝爾赫(鄧俊輝譯) 清華大學出版社 2005年
【3】算法導論 Thomas H.Cormen等(潘金貴等譯) 機械工業出版社 2006年
【4】計算機圖形學 孫家廣、楊常貴 清華大學出版社 1995年