判斷點在多邊形內的算法(Winding Number詳解)

 

在計算幾何中,判定點是否在多邊形內,是個非常有趣的問題。通常有兩種方法:

1.Crossing Number(交叉數)

它計算從點P開始的射線穿過多邊形邊界的次數。當“交叉數”是偶數時,點在外面;當它是奇數時,點在裏面。這種方法有時被稱爲“奇-偶”檢驗。

2.Winding Number(環繞數)

它計算多邊形繞着點P旋轉的次數。只有當“圈數”wn = 0時,點纔在外面; 否則,點在裏面。

如果一個多邊形是不自交的(稱爲“簡單多邊形”),那麼這兩種方法對任意點都給出相同的結果。但對於非簡單多邊形,這兩種方法在某些情況下會給出不同的答案。如下圖所示,當一個多邊形與自身重疊時,對於重疊區域內的點,如果使用交叉數判斷,它在外面;而使用環繞數判斷則在裏面。

 

         

                                     頂點按次序編號: 0 1 2 3 4 5 6 7 8 9

在上圖中,綠色區域中的點,wn = 2,表示在多邊形中重疊了2次。相比於Crossing number,winding number給出了更內蘊性的答案。

儘管如此,早些時候,crossing number方法應用的更廣泛,因爲最初計算幾何專家們錯誤地認爲crossing number比winding number計算起來更加高效。但事實並非如此,兩者的時間複雜度完全一樣。Franklin在2000年給出一個計算winding number的非常快的實現。因此,爲了幾何正確性和效率的原因,在確定一個多邊形中的一個點時,wn算法應該總是首選的。

The Crossing Number

該方法計算從點P開始的射線穿過多邊形邊界的次數(不管穿過的方向)。如果這個數是偶數,那麼點在外面;否則,當交叉數爲奇數時,點在多邊形內。其正確性很容易理解,因爲每次射線穿過多邊形邊緣時,它的內外奇偶性都會發生變化(因爲邊界總是分隔內外)。最終,任何射線都在邊界多邊形之外結束。所以,如果點在多邊形內,那麼對邊界的穿過次序一定是:out>...>in>out,因此交叉數一定是奇數;同樣地,如果點在多邊形外,那麼對邊界的穿過次序一定是in > out ... > in > out,因此交叉數必是偶數。

在實現crossing number的算法時,必須確保只計算改變奇偶性的交叉位置。特別是,對於射線穿過頂點的情況需要適當的處理。下圖列舉了射線與多邊形可能的相交情況:

                                                                        射線與多邊形的邊可能的相交情況  

此外,必須確定多邊形邊界上的點P是在內部還是外部。一般約定:如果點在邊的左側,那麼認爲點P在內部;如果點在邊的右側,那麼認爲點P在外部。如果兩個不同的多邊形共享一個共同的邊界線段,那麼該線段上的一點將會在一個多邊形或另一個多邊形中,而不是同時在兩個多邊形中。這避免了許多可能發生的問題,特別是在計算機圖形顯示中。

一個簡單的做法是選擇一條x軸正方向的水平射線,對於這樣一條射線,很容易計算多邊形的邊與它的交點。而且,很容易確定交點是否存在。算法只需沿着多邊形的每一條邊,依次計算交點,當相交時,cn增加1,從而計算出最終的總交叉數。

此外,相交測試必須遵循如下的規則,處理一些特殊情況(如上圖):

  1. 向上的邊,包含起點,但不包含終點;
  2. 向下的邊,包含終點,但不包含起點;
  3. 水平的邊,不包含起點和終點;
  4. 邊與射線的交點必須嚴格在點P的右側

按照上述規則,處理特殊的相交情況,就能得到正確的交叉數。其中,規則#4將導致在邊界右側的點在多邊形外部,在左側的點將會被判定爲在內部。

Crossing Number Pseudo-Code:

對於n個點組成的多邊形V={V[0], V[1], ......,V[n]},其中V[n]=V[0], 計算幾何大牛Franklin給出了一個非常有名的實現:

typedef struct {int x, y;} Point;

cn_PnPoly( Point P, Point V[], int n )
{
    int    cn = 0;    // the  crossing number counter

    // loop through all edges of the polygon
    for (each edge E[i]:V[i]V[i+1] of the polygon) {
        if (E[i] crosses upward ala Rule #1
         || E[i] crosses downward ala  Rule #2) {
            if (P.x <  x_intersect of E[i] with y=P.y)   // Rule #4
                 ++cn;   // a valid crossing to the right of P.x
        }
    }
    return (cn&1);    // 0 if even (out), and 1 if  odd (in)

}

注意,對於滿足規則#1和#2的向上和向下交叉的測試也排除了水平邊緣(規則#3)。總而言之,很多工作是通過幾個測試完成的,這使得這個算法很優雅。

然而,交叉數方法的有效性是基於“約當曲線定理”(Jordan Curve Theorem),該定理表明,一條簡單的閉合曲線將二維平面分成兩個完全連通的分量:一個有界的“內”分量和一個無界的“外”分量。需要注意的是,曲線必須是簡單的(沒有自身交叉),否則可能有兩個以上的組成部分,然後就不能保證跨越邊界改變進出奇偶性。因此,該方法不適用於自相交的多邊形。

The Winding Number

另一方面,winding number方法能準確判定一個點是否在自交的封閉曲線內。該方法通過計算多邊形有多少次環繞點P來實現。只有當多邊形不環繞該點,也就是環繞數wn = 0時,一個點纔在外面。

不妨定義:平面上的點P相對於任意連續封閉曲線的環繞數爲\mathbf{wn}(P,C)。對於一條水平向右的射線R,我們每一條與R相交的邊需要判斷其終點在R上面還是下面。如果邊從下往上穿過R,wn+1;否則wn-1。所有邊遍歷一遍,最終得到總的\mathbf{wn}(P,C),如下圖所示:

此外,我們沒必要計算實際的交點,只需要使用如下方法判斷當前穿過的邊的環繞數應該+1還是-1:

如下圖所示,如果一條邊向上穿過射線R,那麼P點在邊ViVi+1的左側;而對於一條向下的邊,P點在邊ViVi+1的右側。

 

Winding Number Pseudo-Code:

通過以上分析,容易給出如下的wn計算僞代碼(和cn的計算一樣使用相同的邊相交規則):


typedef struct {int x, y;} Point;

wn_PnPoly( Point P, Point V[], int n )
{
    int    wn = 0;    // the  winding number counter

    // loop through all edges of the polygon
    for (each edge E[i]:V[i]V[i+1] of the polygon) {
        if (E[i] crosses upward ala Rule #1)  {
            if (P is  strictly left of E[i])    // Rule #4
                 ++wn;   // a valid up intersect right of P.x
        }
        else
        if (E[i] crosses downward ala Rule  #2) {
            if (P is  strictly right of E[i])   // Rule #4
                 --wn;   // a valid down intersect right of P.x
        }
    }
    return wn;    // =0 <=> P is outside the polygon

}

顯然,環繞數方法與交叉數方法有着相同的計算效率。但由於該方法更加具有普遍性,因此,在確定一個點是否在任意多邊形內時,推薦使用Winding Number方法。

通過一些技巧可以進一步提高wn算法的效率,在下面給出的wn_PnPoly() 的實現中,我們可以看到這一點。在該代碼中,所有完全在P以上或完全在P以下的邊只經過兩次不等式檢驗就被拒絕(沒有交點)。然而,在目前流行的cn算法的實現中,需要3次不等式檢驗才能做到這一點。由於在實際應用中,大多數邊都會被拒絕,因此進行比較的次數減少了大約33%(或更多)。在使用非常大的(1,000,000邊)隨機多邊形(邊長<多邊形直徑的1/10)和1000個隨機測試點(在多邊形的邊界內)進行運行時測試時,測試結果表明wn算法的平均效率提高了20%。

 

Winding Number算法的實現

// isLeft(): tests if a point is Left|On|Right of an infinite line.
//    Input:  three points P0, P1, and P2
//    Return: >0 for P2 left of the line through P0 and P1
//            =0 for P2  on the line
//            <0 for P2  right of the line

inline int
isLeft( Point P0, Point P1, Point P2 )
{
    return ( (P1.x - P0.x) * (P2.y - P0.y)
            - (P2.x -  P0.x) * (P1.y - P0.y) );
}
//===================================================================

// cn_PnPoly(): crossing number test for a point in a polygon
//      Input:   P = a point,
//               V[] = vertex points of a polygon V[n+1] with V[n]=V[0]
//      Return:  0 = outside, 1 = inside
// This code is patterned after [Franklin, 2000]
int
cn_PnPoly( Point P, Point* V, int n )
{
    int    cn = 0;    // the  crossing number counter

    // loop through all edges of the polygon
    for (int i=0; i<n; i++) {    // edge from V[i]  to V[i+1]
       if (((V[i].y <= P.y) && (V[i+1].y > P.y))     // an upward crossing
        || ((V[i].y > P.y) && (V[i+1].y <=  P.y))) { // a downward crossing
            // compute  the actual edge-ray intersect x-coordinate
            float vt = (float)(P.y  - V[i].y) / (V[i+1].y - V[i].y);
            if (P.x <  V[i].x + vt * (V[i+1].x - V[i].x)) // P.x < intersect
                 ++cn;   // a valid crossing of y=P.y right of P.x
        }
    }
    return (cn&1);    // 0 if even (out), and 1 if  odd (in)

}
//===================================================================


// wn_PnPoly(): winding number test for a point in a polygon
//      Input:   P = a point,
//               V[] = vertex points of a polygon V[n+1] with V[n]=V[0]
//      Return:  wn = the winding number (=0 only when P is outside)
int
wn_PnPoly( Point P, Point* V, int n )
{
    int    wn = 0;    // the  winding number counter

    // loop through all edges of the polygon
    for (int i=0; i<n; i++) {   // edge from V[i] to  V[i+1]
        if (V[i].y <= P.y) {          // start y <= P.y
            if (V[i+1].y  > P.y)      // an upward crossing
                 if (isLeft( V[i], V[i+1], P) > 0)  // P left of  edge
                     ++wn;            // have  a valid up intersect
        }
        else {                        // start y > P.y (no test needed)
            if (V[i+1].y  <= P.y)     // a downward crossing
                 if (isLeft( V[i], V[i+1], P) < 0)  // P right of  edge
                     --wn;            // have  a valid down intersect
        }
    }
    return wn;
}
//===================================================================

 

References

Wm. Randolph Franklin, "PNPOLY  - Point Inclusion in Polygon Test" Web Page (2000)

Tomas Moller & Eric Haines, "Ray/Polygon Intersection" in Real-Time Rendering (3rd Edition) (2008)

Joseph O'Rourke, "Point in  Polygon" in Computational Geometry in C (2nd Edition) (1998)

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