參考:https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
在GIS(地理信息管理系統)中,判斷一個坐標是否在多邊形內部是個經常要遇到的問題。乍聽起來還挺復雜。根據W. Randolph Franklin 提出的PNPoly算法,只需區區幾行代碼就解決了這個問題。
假設多邊形的坐標存放在一個數組里,首先我們需要取得該數組在橫坐標和縱坐標的最大值和最小值,根據這四個點算出一個四邊型,首先判斷目標坐標點是否在這個四邊型之內,如果在這個四邊型之外,那可以跳過后面較為復雜的計算,直接返回false。
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
// 這個測試都過不了。。。直接返回false;
}
接下來是核心算法部分:
int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy)
{
int i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++) {
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
| Argument | Meaning |
|---|---|
| nvert | Number of vertices in the polygon. Whether to repeat the first vertex at the end is discussed below. |
| vertx, verty | Arrays containing the x- and y-coordinates of the polygon's vertices. |
| testx, testy | X- and y-coordinate of the test point. |
額,代碼就這么簡單,但到底啥意思呢:
首先,參數nvert 代表多邊形有幾個點。浮點數testx, testy代表待測試點的橫坐標和縱坐標,*vertx,*verty分別指向儲存多邊形橫縱坐標數組的首地址。
我們注意到,每次計算都涉及到相鄰的兩個點和待測試點,然后考慮兩個問題:
1. 被測試點的縱坐標testy是否在本次循環所測試的兩個相鄰點縱坐標范圍之內?即
verty[i] <testy < verty[j]
或者
verty[j] <testy < verty[i]
2. 待測點test是否在i,j兩點之間的連線之下?看不懂后半短if statement的朋友請自行在紙上寫下i,j兩點間的斜率公式,要用到一點初中解析幾何和不等式的知識范疇,對廣大碼農來說小菜一碟。
然后每次這兩個條件同時滿足的時候我們把返回的布爾量取反。
可這到底是啥意思啊?
這個表達式的意思是說,隨便畫個多邊形,隨便定一個點,然后通過這個點水平划一條線,先數數看這條橫線和多邊形的邊相交幾次,(或者說先排除那些不相交的邊,第一個判斷條件),然后再數這條橫線穿越多邊形的次數是否為奇數,如果是奇數,那么該點在多邊形內,如果是偶數,則在多邊形外。詳細的數學證明這里就不做了,不過讀者可以自行畫多邊形進行驗證。
判斷一個點是否在多邊形內部 - 射線法思路
比如說,我就隨便塗了一個多邊形和一個點,現在我要給出一種通用的方法來判斷這個點是不是在多邊形內部(別告訴我用肉眼觀察……)。

首先想到的一個解法是從這個點做一條射線,計算它跟多邊形邊界的交點個數,如果交點個數為奇數,那么點在多邊形內部,否則點在多邊形外。

這個結論很簡單,那它是怎么來的?下面就簡單講解一下。
首先,對於平面內任意閉合曲線,我們都可以直觀地認為,曲線把平面分割成了內、外兩部分,其中“內”就是我們所謂的多邊形區域。

基於這一認識,對於平面內任意一條直線,我們可以得出下面這些結論:
直線穿越多邊形邊界時,有且只有兩種情況:進入多邊形或穿出多邊形。
在不考慮非歐空間的情況下,直線不可能從內部再次進入多邊形,或從外部再次穿出多邊形,即連續兩次穿越邊界的情況必然成對。
直線可以無限延伸,而閉合曲線包圍的區域是有限的,因此最后一次穿越多邊形邊界,一定是穿出多邊形,到達外部。 
現在回到我們最初的題目。假如我們從一個給定的點做射線,還可以得出下面兩條結論:
如果點在多邊形內部,射線第一次穿越邊界一定是穿出多邊形。
如果點在多邊形外部,射線第一次穿越邊界一定是進入多邊形。 
把上面這些結論綜合起來,我們可以歸納出:
當射線穿越多邊形邊界的次數為偶數時,所有第偶數次(包括最后一次)穿越都是穿出,因此所有第奇數次(包括第一次)穿越為穿入,由此可推斷點在多邊形外部。
當射線穿越多邊形邊界的次數為奇數時,所有第奇數次(包括第一次和最后一次)穿越都是穿出,由此可推斷點在多邊形內部。
到這里,我們已經了解了這個解法的思路,大家可以試着自己寫一個實現出來。
不知道大家思考得怎么樣,有沒有遇到一些不好處理的特殊情況。今天就來講講射線法在實際應用中的一些問題和解決方案。
1點在多邊形的邊上
前面我們講到,射線法的主要思路就是計算射線穿越多邊形邊界的次數。那么對於點在多邊形的邊上這種特殊情況,射線出發的這一次,是否應該算作穿越呢?

看了上面的圖就會發現,不管算不算穿越,都會陷入兩難的境地——同樣落在多邊形邊上的點,可能會得到相反的結果。這顯然是不正確的,因此對這種特殊情況需要特殊處理。
2點和多邊形的頂點重合

這其實是第一種情況的一個特例。
3射線經過多邊形頂點
射線剛好經過多邊形頂點的時候,應該算一次還是兩次穿越?這種情況比前兩種復雜,也是實現中的難點,后面會講解它的解決方案。

4射線剛好經過多邊形的一條邊
這是上一種情況的特例,也就是說,射線連續經過了多邊形的兩個相鄰頂點。

解決方案:
1判斷點是否在線上的方法有很多,比較簡單直接的就是計算點與兩個多邊形頂點的連線斜率是否相等,中學數學都學過。
2點和多邊形頂點重合的情況更簡單,直接比較點的坐標就行了。
3頂點穿越看似棘手,其實我們換一個角度,思路會大不相同。先來回答一個問題,射線穿越一條線段需要什么前提條件?沒錯,就是線段兩個端點分別在射線兩側。只要想通這一點,頂點穿越就迎刃而解了。這樣一來,我們只需要規定被射線穿越的點都算作其中一側。

如上圖,假如我們規定射線經過的點都屬於射線以上的一側,顯然點D和發生頂點穿越的點C都位於射線Y的同一側,所以射線Y其實並沒有穿越CD這條邊。而點C和點B則分別位於射線Y的兩側,所以射線Y和BC發生了穿越,由此我們可以斷定點Y在多邊形內。同理,射線X分別與AD和CD都發生了穿越,因此點X在多邊形外,而射線Z沒有和多邊形發生穿越,點Z位於多邊形外。
解決了第三點,這一點就毫無難度了。根據上面的假設,射線連續經過的兩個頂點顯然都位於射線以上的一側,因此這種情況看作沒有發生穿越就可以了。由於第三點的解決方案實際上已經覆蓋到這種特例,因此不需要再做特別的處理。
