[射線法]判斷一個點是否在多邊形內部

思路及實現轉自“前端亂燉”,最後測試爲自己修改後的代碼。

一、思路

前言:本文源於前幾天看到的一條微博 http://weibo.com/1057676857/Adpfwwfsv

這裏寫圖片描述

對於 po 主的言論我並不贊同。我是學化學的,沒有學過計算機專業的課程,但憑直覺我認爲這應該也是計算機圖形學裏面比較常見的一類問題。並且我認爲這並不需要多麼高端的計算機專業知識,只要中學數學沒有全還給老師,就應該能給出至少一種解法。下面就試着解一下這個題吧。

比如說,我就隨便塗了一個多邊形和一個點,現在我要給出一種通用的方法來判斷這個點是不是在多邊形內部(別告訴我用肉眼觀察……)。

這裏寫圖片描述

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

enter image description here

這個結論很簡單,那它是怎麼來的?下面就簡單講解一下。

首先,對於平面內任意閉合曲線,我們都可以直觀地認爲,曲線把平面分割成了內、外兩部分,其中“內”就是我們所謂的多邊形區域。

enter image description here

基於這一認識,對於平面內任意一條直線,我們可以得出下面這些結論:

  • 直線穿越多邊形邊界時,有且只有兩種情況:進入多邊形或穿出多邊形。
  • 在不考慮非歐空間的情況下,直線不可能從內部再次進入多邊形,或從外部再次穿出多邊形,即連續兩次穿越邊界的情況必然成對。
  • 直線可以無限延伸,而閉合曲線包圍的區域是有限的,因此最後一次穿越多邊形邊界,一定是穿出多邊形,到達外部。
    enter image description here

現在回到我們最初的題目。假如我們從一個給定的點做射線,還可以得出下面兩條結論:

  • 如果點在多邊形內部,射線第一次穿越邊界一定是穿出多邊形。
  • 如果點在多邊形外部,射線第一次穿越邊界一定是進入多邊形。
    enter image description here

把上面這些結論綜合起來,我們可以歸納出:

  • 當射線穿越多邊形邊界的次數爲偶數時,所有第偶數次(包括最後一次)穿越都是穿出,因此所有第奇數次(包括第一次)穿越爲穿入,由此可推斷點在多邊形外部。
    enter image description here
  • 當射線穿越多邊形邊界的次數爲奇數時,所有第奇數次(包括第一次和最後一次)穿越都是穿出,由此可推斷點在多邊形內部。
    enter image description here

    到這裏,我們已經瞭解了這個解法的思路,大家可以試着自己寫一個實現出來。關於算法實現中某些具體問題和邊界條件的處理,下次接着寫,這次畫圖已經畫夠了……

    後記:給出這個解法後,我簡單搜了一下,原來這種算法就叫做射線法(ray casting)或者奇偶規則法(even odd rule),是一種早已被廣泛應用的算法。後面還打算介紹另一種通過迴轉數(winding number,拓撲學的一個概念)解這個問題的思路。

本站專欄文章皆爲原創,轉載請註明出處(帶有 前端亂燉 字樣)和本文的顯式鏈接(http://www.html-js.com/article/1517),本站和作者保留隨時要求刪除文章的權利!



二、實現

看過上一次的思路講解後,不知道大家思考得怎麼樣,有沒有遇到一些不好處理的特殊情況。今天就來講講射線法在實際應用中的一些問題和解決方案。

  1. 點在多邊形的邊上

    前面我們講到,射線法的主要思路就是計算射線穿越多邊形邊界的次數。那麼對於點在多邊形的邊上這種特殊情況,射線出發的這一次,是否應該算作穿越呢?

    enter image description here

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

  2. 點和多邊形的頂點重合

    enter image description here

    這其實是第一種情況的一個特例。

  3. 射線經過多邊形頂點

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

    enter image description here

  4. 射線剛好經過多邊形的一條邊

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

    enter image description here

解決方案

1. 判斷點是否在線上的方法有很多,比較簡單直接的就是計算點與兩個多邊形頂點的連線斜率是否相等,中學數學都學過。

2. 點和多邊形頂點重合的情況更簡單,直接比較點的座標就行了。

3. 頂點穿越看似棘手,其實我們換一個角度,思路會大不相同。先來回答一個問題,射線穿越一條線段需要什麼前提條件?沒錯,就是線段兩個端點分別在射線兩側。只要想通這一點,頂點穿越就迎刃而解了。這樣一來,我們只需要規定被射線穿越的點都算作其中一側。

enter image description here

如上圖,假如我們規定射線經過的點都屬於射線以上的一側,顯然點D和發生頂點穿越的點C都位於射線Y的同一側,所以射線Y其實並沒有穿越CD這條邊。而點C和點B則分別位於射線Y的兩側,所以射線Y和BC發生了穿越,由此我們可以斷定點Y在多邊形內。同理,射線X分別與AD和CD都發生了穿越,因此點X在多邊形外,而射線Z沒有和多邊形發生穿越,點Z位於多邊形外。

4. 解決了第三點,這一點就毫無難度了。根據上面的假設,射線連續經過的兩個頂點顯然都位於射線以上的一側,因此這種情況看作沒有發生穿越就可以了。由於第三點的解決方案實際上已經覆蓋到這種特例,因此不需要再做特別的處理。

問題都解決了,其實並不複雜,不是嗎?啥也不說了,上代碼。

  /**
   * @description 射線法判斷點是否在多邊形內部
   * @param {Object} p 待判斷的點,格式:{ x: X座標, y: Y座標 }
   * @param {Array} poly 多邊形頂點,數組成員的格式同 p
   * @return {String} 點 p 和多邊形 poly 的幾何關係
   */
  function rayCasting(p, poly) {
    var px = p.x,
        py = p.y,
        flag = false

    for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
      var sx = poly[i].x,
          sy = poly[i].y,
          tx = poly[j].x,
          ty = poly[j].y

      // 點與多邊形頂點重合
      if((sx === px && sy === py) || (tx === px && ty === py)) {
        return 'on'
      }

      // 判斷線段兩端點是否在射線兩側
      if((sy < py && ty >= py) || (sy >= py && ty < py)) {
        // 線段上與射線 Y 座標相同的點的 X 座標
        var x = sx + (py - sy) * (tx - sx) / (ty - sy)

        // 點在多邊形的邊上
        if(x === px) {
          return 'on'
        }

        // 射線穿過多邊形的邊界
        if(x > px) {
          flag = !flag
        }
      }
    }

    // 射線穿過多邊形邊界的次數爲奇數時點在多邊形內
    return flag ? 'in' : 'out'
  }

本站專欄文章皆爲原創,轉載請註明出處(帶有 前端亂燉 字樣)和本文的顯式鏈接(http://www.html-js.com/article/1528),本站和作者保留隨時要求刪除文章的權利!



測試並用OpenCV顯示

Lib: OpenCV 2.4.9
Tools: Visual Studio 2013
Tips: 在測試前請先編譯並配置好OpenCV(項目屬性VC++目錄中的包含目錄、庫目錄,還有鏈接器中的輸入)

#include <iostream>
#include <vector>
#include <fstream>
#include "imgproc.hpp"  
#include "highgui.h"  
#include "opencv2/opencv.hpp"  

using namespace std;
using namespace cv;

struct Point2D{
    int x;
    int y;
};

/*
*   函數功能:判斷點是否在多邊形內
*   輸入參數: vector<Point2D> poly  構成多邊形的頂點座標
*             int x                 需要判斷點的橫座標
*             int y                 需要判斷點的縱座標
*   返 回 值: bool  true爲在多邊形內,false爲不在。也可修改爲返回string,用於區分:多邊形上、多邊形內、多邊形外
*/
bool PointInPolygon(vector<Point2D> poly, int x, int y)
{
    bool flag = false;
    int size = poly.size();
    for (int i = 0, j = size - 1; i < size; j = i, i++)
    {
        int x1 = poly[i].x;
        int y1 = poly[i].y;
        int x2 = poly[j].x;
        int y2 = poly[j].y;

        // 點與多邊形頂點重合
        if ((x1 == x && y1 == y) || (x2 == x && y2 == y))
        {
            //return 'on'; // 點在輪廓上
            return true;
        }

        // 判斷線段兩端點是否在射線兩側
        if ((y1 < y && y2 >= y) || (y1 >= y && y2 < y)) // 只有一邊取等號,當射線經過多邊形頂點時,只計一次
        {
            // 線段上與射線 Y 座標相同的點的 X 座標
            double crossX = (y - y1) * (x2 - x1) / (y2 - y1) + x1;  // y=kx+b變換成x=(y-b)/k 其中k=(y2-y1)/(x2-x1)

            // 點在多邊形的邊上
            if (crossX == x)
            {
                //return 'on'; // 點在輪廓上
                return true;
            }

            // 右射線穿過多邊形的邊界,每穿過一次flag的值變換一次
            if (crossX > x)
            {
                flag = !flag;  // 穿過奇數次爲true,偶數次爲false
            }
        }
    }

    // 射線穿過多邊形邊界的次數爲奇數時點在多邊形內
    //return flag ? 'in' : 'out'; // 點在輪廓內或外
    return flag ? true : false;
}

Mat DrowPolygon(vector<Point2D> rois)
{
    // 繪製一塊黑布
    Mat canvas = Mat::zeros(Size(512, 512), CV_8UC3);


    // 初始化默認第一個點爲最大最小XY
    int minX = rois[0].x;
    int maxX = rois[0].x;
    int minY = rois[0].y;
    int maxY = rois[0].y;
    int tempX = 0;
    int tempY = 0;

    // 尋找ROI邊界
    for (vector<Point2D>::const_iterator it = rois.cbegin(); it != rois.cend(); it++)
    {
        tempX = (*it).x;
        tempY = (*it).y;
        if (tempX > maxX)
        {
            maxX = tempX;
        }
        if (tempX < minX)
        {
            minX = tempX;
        }
        if (tempY > maxY)
        {
            maxY = tempY;
        }
        if (tempY < minY)
        {
            minY = tempY;
        }
    }

    // 藍色邊框
    rectangle(canvas, Rect(minX, minY, maxX - minX, maxY - minY), CV_RGB(0, 0, 255), 1);

    // 遍歷找到多邊形內部的點
    ofstream out("mask.txt");    // 將多邊形內的座標輸出到mask.txt
    int inPolygon = 0;
    int outPolygon = 0;
    int count = 0;
    for (int i = minY; i <= maxY; i++)
    {
        int rawNum = 0;
        for (int j = minX; j <= maxX; j++)
        {
            count++;
            if (PointInPolygon(rois, j, i))
            {
                out << j << " " << i << endl;
                inPolygon++;
                rawNum++;
                circle(canvas, Point(j, i), 1, CV_RGB(255, 0, 0));
            }
            else
            {
                outPolygon++;
            }
        }
        cout << "y = " << i << " PointNum:" << rawNum << endl;
    }
    out.close();

    // 最大最小邊界
    cout << "minX: " << minX << endl;
    cout << "maxX: " << maxX << endl;
    cout << "minY: " << minY << endl;
    cout << "maxY: " << maxY << endl;

    // 在多邊形內外點的個數及總數count
    cout << "count: " << count << endl;
    cout << "inPolygon: " << inPolygon << endl;
    cout << "outPolygon: " << outPolygon << endl;



    // 繪製綠色多邊形  
    int pointSize = rois.size();
    for (int i = 0; i < pointSize; i++)
    {
        if (i + 1 == pointSize)
        {
            line(canvas, Point(rois[i].x, rois[i].y), Point(rois[0].x, rois[0].y), CV_RGB(0, 255, 0));
        }
        else
        {
            line(canvas, Point(rois[i].x, rois[i].y), Point(rois[i + 1].x, rois[i + 1].y), CV_RGB(0, 255, 0));
        }
    }

    return canvas;
}

void main()
{
    vector<Point2D> polygon;
    Point2D p;

    // 從ori.txt中讀入多邊形的座標
    ifstream in("ori.txt");    
    while (in >> p.x >> p.y)
    {
        polygon.push_back(p);
    }
    in.close();

    imshow("Polygon", DrowPolygon(polygon));
    waitKey(0);
}

input: ori.txt
175 189
166 195
156 204
154 213
154 226
162 232
177 238
195 231
205 226
211 219
217 208
219 192
216 181
199 181
190 186

console output:
y = 181 PointNum:2
y = 182 PointNum:20
y = 183 PointNum:22
y = 184 PointNum:24
y = 185 PointNum:27
y = 186 PointNum:28
y = 187 PointNum:33
y = 188 PointNum:38
y = 189 PointNum:44
y = 190 PointNum:46
y = 191 PointNum:47
y = 192 PointNum:50
y = 193 PointNum:51
y = 194 PointNum:53
y = 195 PointNum:54
y = 196 PointNum:56
y = 197 PointNum:57
y = 198 PointNum:58
y = 199 PointNum:59
y = 200 PointNum:59
y = 201 PointNum:60
y = 202 PointNum:61
y = 203 PointNum:62
y = 204 PointNum:63
y = 205 PointNum:64
y = 206 PointNum:64
y = 207 PointNum:64
y = 208 PointNum:63
y = 209 PointNum:64
y = 210 PointNum:63
y = 211 PointNum:63
y = 212 PointNum:62
y = 213 PointNum:62
y = 214 PointNum:61
y = 215 PointNum:61
y = 216 PointNum:60
y = 217 PointNum:60
y = 218 PointNum:59
y = 219 PointNum:58
y = 220 PointNum:58
y = 221 PointNum:57
y = 222 PointNum:56
y = 223 PointNum:55
y = 224 PointNum:54
y = 225 PointNum:53
y = 226 PointNum:52
y = 227 PointNum:48
y = 228 PointNum:45
y = 229 PointNum:42
y = 230 PointNum:38
y = 231 PointNum:35
y = 232 PointNum:32
y = 233 PointNum:26
y = 234 PointNum:22
y = 235 PointNum:16
y = 236 PointNum:12
y = 237 PointNum:6
y = 238 PointNum:1
minX: 154
maxX: 219
minY: 181
maxY: 238
count: 3828
inPolygon: 2710
outPolygon: 1118

顯示結果

Show Polygon

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