[射线法]判断一个点是否在多边形内部

思路及实现转自“前端乱炖”,最后测试为自己修改后的代码。

一、思路

前言:本文源于前几天看到的一条微博 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

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