关于性能:如何确定2D点是否在多边形内?

How can I determine whether a 2D Point is within a Polygon?

我试图创建一个快速的2D点内多边形算法,用于命中测试(例如EDCOX1,0)。对有效技术的建议将不胜感激。


对于图形,我不喜欢整数。许多系统使用整数进行用户界面绘制(像素毕竟是整数),但MacOS例如对所有内容都使用浮点。MacOS只知道点,一个点可以转换成一个像素,但取决于监视器的分辨率,它可能会转换成其他东西。在视网膜屏幕上,半个点(0.5/0.5)是像素。不过,我从未注意到MacOS用户界面比其他用户界面慢得多。毕竟,所有3D API(OpenGL或Direct3D)也可以与浮动和现代图形库一起使用,它们常常利用GPU加速。好的。

现在你说速度是你最关心的问题,好吧,我们来看看速度。在运行任何复杂的算法之前,首先做一个简单的测试。围绕多边形创建一个轴对齐的边界框。这是非常容易,快速,可以安全的你很多计算。这是怎么回事?迭代多边形的所有点,并找到x和y的最小/最大值。好的。

例如,您有点(9/1), (4/3), (2/7), (8/2), (3/6)。这意味着xmin是2,xmax是9,ymin是1,ymax是7。具有两条边(2/1)和(9/7)的矩形外的点不能在多边形内。好的。

1
2
3
4
// p is your point, p.x is the x coord, p.y is the y coord
if (p.x < Xmin || p.x > Xmax || p.y < Ymin || p.y > Ymax) {
    // Definitely not within the polygon!
}

这是针对任何点运行的第一个测试。正如你所看到的,这个测试非常快,但也非常粗糙。要处理边界矩形内的点,我们需要更复杂的算法。有几种计算方法。哪种方法有效还取决于多边形是否可以有洞,或者始终是实心的。下面是实体的例子(一个凸面,一个凹面):好的。

Polygon without hole好的。

这里有一个带洞的:好的。

Polygon with hole好的。

绿色的那个中间有个洞!好的。

最简单的算法,可以处理上述三种情况,而且速度仍然相当快,被称为光线投射。该算法的思想非常简单:从多边形以外的任何地方绘制一条虚拟光线到您的点,并计算它击中多边形一侧的频率。如果击中的数量是偶数,它就在多边形外面,如果它是奇怪的,它在里面。好的。

Demonstrating how the ray cuts through a polygon好的。

卷绕数算法将是另一种选择,它对于点非常接近多边形线更精确,但也要慢得多。由于浮点精度和舍入问题的限制,光线投射可能会对离多边形边太近的点失败,但实际上这几乎不是问题,就像一个点位于离边太近的地方一样,观察者在视觉上甚至无法识别它是已经在里面还是还在外面。好的。

你还有上面的边界框,记得吗?只需在边界框外选择一个点,并将其用作光线的起点。例如,点(Xmin - e/p.y)肯定在多边形之外。好的。

但什么是e?好吧,EDOCX1(实际上是epsilon)给了边界框一些填充。如我所说,如果我们开始太靠近多边形线,光线跟踪就会失败。因为边界框可能等于多边形(如果多边形是轴对齐的矩形,则边界框等于多边形本身!)我们需要一些填充物来保证安全,仅此而已。你应该选择多大的EDOCX1?0?不要太大。这取决于绘制的坐标系比例。如果你的像素步长为1,那么只需选择1(但0.1也会有效)好的。

既然光线有了它的起点和终点坐标,问题就从"多边形内的点"转移到"光线与多边形边相交的频率"。因此我们不能像以前那样只处理多边形点,现在我们需要实际的边。边总是由两点定义的。好的。

1
2
3
4
side 1: (X1/Y1)-(X2/Y2)
side 2: (X2/Y2)-(X3/Y3)
side 3: (X3/Y3)-(X4/Y4)
:

你需要测试光线的各个方面。把光线看作是一个矢量,每边都是一个矢量。射线必须精确地击中每一侧一次,或者根本不击中每一侧一次。它不能两次击中同一侧。二维空间中的两条直线总是恰好相交一次,除非它们是平行的,在这种情况下,它们永远不会相交。然而,由于向量的长度有限,两个向量可能不平行,而且永远也不会相交,因为它们太短,无法彼此相交。好的。

1
2
3
4
5
6
7
8
9
10
11
// Test the ray against all sides
int intersections = 0;
for (side = 0; side < numberOfSides; side++) {
    // Test if current side intersects with ray.
    // If yes, intersections++;
}
if ((intersections & 1) == 1) {
    // Inside of polygon
} else {
    // Outside of polygon
}

到目前为止还不错,但是如何测试两个向量是否相交?下面是一些C代码(未测试),应该可以做到这一点:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#define NO 0
#define YES 1
#define COLLINEAR 2

int areIntersecting(
    float v1x1, float v1y1, float v1x2, float v1y2,
    float v2x1, float v2y1, float v2x2, float v2y2
) {
    float d1, d2;
    float a1, a2, b1, b2, c1, c2;

    // Convert vector 1 to a line (line 1) of infinite length.
    // We want the line in linear equation standard form: A*x + B*y + C = 0
    // See: http://en.wikipedia.org/wiki/Linear_equation
    a1 = v1y2 - v1y1;
    b1 = v1x1 - v1x2;
    c1 = (v1x2 * v1y1) - (v1x1 * v1y2);

    // Every point (x,y), that solves the equation above, is on the line,
    // every point that does not solve it, is not. The equation will have a
    // positive result if it is on one side of the line and a negative one
    // if is on the other side of it. We insert (x1,y1) and (x2,y2) of vector
    // 2 into the equation above.
    d1 = (a1 * v2x1) + (b1 * v2y1) + c1;
    d2 = (a1 * v2x2) + (b1 * v2y2) + c1;

    // If d1 and d2 both have the same sign, they are both on the same side
    // of our line 1 and in that case no intersection is possible. Careful,
    // 0 is a special case, that's why we don't test">=" and"<=",
    // but"<" and">".
    if (d1 > 0 && d2 > 0) return NO;
    if (d1 < 0 && d2 < 0) return NO;

    // The fact that vector 2 intersected the infinite line 1 above doesn't
    // mean it also intersects the vector 1. Vector 1 is only a subset of that
    // infinite line 1, so it may have intersected that line before the vector
    // started or after it ended. To know for sure, we have to repeat the
    // the same test the other way round. We start by calculating the
    // infinite line 2 in linear equation standard form.
    a2 = v2y2 - v2y1;
    b2 = v2x1 - v2x2;
    c2 = (v2x2 * v2y1) - (v2x1 * v2y2);

    // Calculate d1 and d2 again, this time using points of vector 1.
    d1 = (a2 * v1x1) + (b2 * v1y1) + c2;
    d2 = (a2 * v1x2) + (b2 * v1y2) + c2;

    // Again, if both have the same sign (and neither one is 0),
    // no intersection is possible.
    if (d1 > 0 && d2 > 0) return NO;
    if (d1 < 0 && d2 < 0) return NO;

    // If we get here, only two possibilities are left. Either the two
    // vectors intersect in exactly one point or they are collinear, which
    // means they intersect in any number of points from zero to infinite.
    if ((a1 * b2) - (a2 * b1) == 0.0f) return COLLINEAR;

    // If they are not collinear, they must intersect in exactly one point.
    return YES;
}

输入值是向量1(v1x1/v1y1v1x2/v1y2和向量2(v2x1/v2y1v2x2/v2y2的两个端点。所以你有2个向量,4个点,8个坐标。YESNO很清楚。YES增加交叉口,NO不起作用。好的。

共线怎么样?这意味着两个向量都位于同一条无限线上,取决于位置和长度,它们根本不相交,或者它们相交于无限多的点上。我不太清楚该怎么处理这个案子,我也不会把它算作交叉路口。不管怎样,这种情况在实践中很少见,因为浮点舍入错误;更好的代码可能不会测试== 0.0f,而是像< epsilon,其中epsilon是一个相当小的数字。好的。

如果你需要测试更多的点,你当然可以通过将多边形边的线性方程标准形式保存在内存中来加快整个过程,因此你不必每次都重新计算这些点。这将在每次测试中为您保存两个浮点乘法和三个浮点减法,以换取在内存中为每个多边形存储三个浮点值。这是典型的内存与计算时间权衡。好的。

最后但同样重要的是:如果您可以使用3D硬件来解决这个问题,那么还有一个有趣的选择。让GPU为你做所有的工作。创建屏幕外的绘画表面。完全用黑色填充。现在让OpenGL或Direct3D绘制您的多边形(甚至所有多边形,如果您只想测试点是否在其中任何一个多边形内,但不关心是哪个多边形),并用不同的颜色(例如白色)填充多边形。要检查点是否在多边形中,请从绘图表面获取该点的颜色。这只是一个O(1)内存提取。好的。

当然,只有在绘图面不一定很大的情况下,此方法才可用。如果它不能装入GPU内存,这个方法比在CPU上执行慢。如果它必须是巨大的,并且您的GPU支持现代明暗器,那么您仍然可以通过实现上面所示的光线投射来使用GPU明暗器,这是绝对可能的。对于更多的多边形或大量要测试的点,这将得到回报,考虑到一些GPU将能够并行测试64到256个点。但是注意,将数据从CPU传输到GPU和后端总是很昂贵的,所以只要测试一对点对几个简单多边形,其中点或多边形是动态的并且将频繁改变,GPU方法将很少得到回报。好的。好啊。


我认为下面这段代码是最好的解决方案(从这里开始):

1
2
3
4
5
6
7
8
9
10
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;
}

争论

  • nvert:多边形中的顶点数。上面提到的文章已经讨论了是否在末尾重复第一个顶点。
  • 顶点,顶点:包含多边形顶点的x和y坐标的数组。
  • test x,test y:测试点的x和y坐标。

它既短又有效,适用于凸凹多边形。如前所述,应首先检查边界矩形,并分别处理多边形孔。

这背后的想法很简单。作者描述如下:

I run a semi-infinite ray horizontally (increasing x, fixed y) out from the test point, and count how many edges it crosses. At each crossing, the ray switches between inside and outside. This is called the Jordan curve theorem.

每次水平光线穿过任何边缘时,变量C都会从0切换到1,从1切换到0。所以基本上,它会跟踪交叉的边数是偶数还是奇数。0表示偶数,1表示奇数。


这是NIRG给出的答案的C版本,来自这个RPI教授。请注意,使用来自该RPI源的代码需要属性。

在顶部添加了一个边界框复选框。然而,正如James Brown指出的那样,主代码几乎和边界框检查本身一样快,因此边界框检查实际上可以减慢整个操作,以防您检查的大多数点都在边界框内。因此,可以不选中边界框,或者如果多边形的边界框不经常更改形状,则可以预先计算它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public bool IsPointInPolygon( Point p, Point[] polygon )
{
    double minX = polygon[ 0 ].X;
    double maxX = polygon[ 0 ].X;
    double minY = polygon[ 0 ].Y;
    double maxY = polygon[ 0 ].Y;
    for ( int i = 1 ; i < polygon.Length ; i++ )
    {
        Point q = polygon[ i ];
        minX = Math.Min( q.X, minX );
        maxX = Math.Max( q.X, maxX );
        minY = Math.Min( q.Y, minY );
        maxY = Math.Max( q.Y, maxY );
    }

    if ( p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY )
    {
        return false;
    }

    // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
    bool inside = false;
    for ( int i = 0, j = polygon.Length - 1 ; i < polygon.Length ; j = i++ )
    {
        if ( ( polygon[ i ].Y > p.Y ) != ( polygon[ j ].Y > p.Y ) &&
             p.X < ( polygon[ j ].X - polygon[ i ].X ) * ( p.Y - polygon[ i ].Y ) / ( polygon[ j ].Y - polygon[ i ].Y ) + polygon[ i ].X )
        {
            inside = !inside;
        }
    }

    return inside;
}


下面是M.Katz基于NIRG方法的答案的javascript变体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function pointIsInPoly(p, polygon) {
    var isInside = false;
    var minX = polygon[0].x, maxX = polygon[0].x;
    var minY = polygon[0].y, maxY = polygon[0].y;
    for (var n = 1; n < polygon.length; n++) {
        var q = polygon[n];
        minX = Math.min(q.x, minX);
        maxX = Math.max(q.x, maxX);
        minY = Math.min(q.y, minY);
        maxY = Math.max(q.y, maxY);
    }

    if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
        return false;
    }

    var i = 0, j = polygon.length - 1;
    for (i, j; i < polygon.length; j = i++) {
        if ( (polygon[i].y > p.y) != (polygon[j].y > p.y) &&
                p.x < (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x ) {
            isInside = !isInside;
        }
    }

    return isInside;
}


计算点P和每个多边形顶点之间的角的定向和。如果总定向角度为360度,则该点位于内部。如果总数为0,则该点位于外部。

我更喜欢这种方法,因为它更稳健,对数值精度的依赖性更小。

计算交集数均匀性的方法是有限的,因为在计算交集数的过程中可以"击中"顶点。

编辑:顺便说一下,这种方法适用于凹凸多边形。

编辑:我最近找到了一篇关于这个话题的维基百科文章。


波波波波波引用的埃里克·海恩斯的文章真的很棒。特别有趣的是比较算法性能的表格;与其他方法相比,角度求和方法确实很糟糕。还有一个有趣的问题是,像使用查找网格将多边形进一步细分为"in"和"out"扇区这样的优化,即使在边长大于1000的多边形上,也能使测试速度极快。

不管怎样,现在还很早,但是我的投票采用了"交叉点"的方式,这正是麦基所描述的我的想法。然而,我发现大卫·伯克最简洁地描述和编纂了它。我喜欢没有真正的三角学要求,它适用于凸面和凹面,它的表现相当好,边的数量增加。

顺便说一句,这里是EricHaines文章中的性能表之一,它是为感兴趣的,对随机多边形进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
                       number of edges per polygon
                         3       4      10      100    1000
MacMartin               2.9     3.2     5.9     50.6    485
Crossings               3.1     3.4     6.8     60.0    624
Triangle Fan+edge sort  1.1     1.8     6.5     77.6    787
Triangle Fan            1.2     2.1     7.3     85.4    865
Barycentric             2.1     3.8    13.8    160.7   1665
Angle Summation        56.2    70.4   153.6   1403.8  14693

Grid (100x100)          1.5     1.5     1.6      2.1      9.8
Grid (20x20)            1.7     1.7     1.9      5.7     42.2
Bins (100)              1.8     1.9     2.7     15.1    117
Bins (20)               2.1     2.2     3.7     26.3    278

这个问题很有趣。我有另一个可行的想法,不同于本帖的其他答案。

其思想是利用角度之和来决定目标是在内部还是外部。如果目标在一个区域内,目标和每两个边界点的角度形式之和将为360。如果目标在外部,总和将不为360。角度有方向。如果角度向后,则角度为负。这就像计算绕组数一样。

请参考此图片,以获得对该想法的基本了解:enter image description here

我的算法假设顺时针方向是正方向。这里有一个潜在的输入:

1
[[-122.402015, 48.225216], [-117.032049, 48.999931], [-116.919132, 45.995175], [-124.079107, 46.267259], [-124.717175, 48.377557], [-122.92315, 47.047963], [-122.402015, 48.225216]]

下面是实现该思想的Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def isInside(self, border, target):
degree = 0
for i in range(len(border) - 1):
    a = border[i]
    b = border[i + 1]

    # calculate distance of vector
    A = getDistance(a[0], a[1], b[0], b[1]);
    B = getDistance(target[0], target[1], a[0], a[1])
    C = getDistance(target[0], target[1], b[0], b[1])

    # calculate direction of vector
    ta_x = a[0] - target[0]
    ta_y = a[1] - target[1]
    tb_x = b[0] - target[0]
    tb_y = b[1] - target[1]

    cross = tb_y * ta_x - tb_x * ta_y
    clockwise = cross < 0

    # calculate sum of angles
    if(clockwise):
        degree = degree + math.degrees(math.acos((B * B + C * C - A * A) / (2.0 * B * C)))
    else:
        degree = degree - math.degrees(math.acos((B * B + C * C - A * A) / (2.0 * B * C)))

if(abs(round(degree) - 360) <= 3):
    return True
return False

NIRG的快速答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension CGPoint {
    func isInsidePolygon(vertices: [CGPoint]) -> Bool {
        guard !vertices.isEmpty else { return false }
        var j = vertices.last!, c = false
        for i in vertices {
            let a = (i.y > y) != (j.y > y)
            let b = (x < (j.x - i.x) * (y - i.y) / (j.y - i.y) + i.x)
            if a && b { c = !c }
            j = i
        }
        return c
    }
}

当我是迈克尔·史通布拉克手下的一名研究人员时,我做了一些关于背部的研究——你知道,这位教授提出了英格尔斯、PostgreSQL等等。

我们意识到最快的方法是首先做一个边界框,因为它是超快速的。如果它在边界框外,它在外部。否则,你会更努力地工作…

如果你想要一个好的算法,请看开源项目PostgreSQL的地理工作源代码…

我想指出的是,我们从未对左右手习惯有过任何了解(也可以表示为"内部"与"外部"问题)。

更新

BKB的链接提供了大量合理的算法。我在研究地球科学问题,因此需要一个在纬度/经度上有效的解决方案,它有一个特殊的用手习惯的问题——是小区域内的区域还是大区域内的区域?答案是垂直线的"方向"很重要——要么是左手的,要么是右手的,这样你就可以把任意一个区域表示为任何给定多边形的"内部"。因此,我的工作使用了该页上列举的解决方案3。

此外,我的工作还为"在线"测试使用了单独的函数。

…因为有人问:我们发现,当垂直线的数量超过某个数字时,边界框测试是最好的-如果必要,在进行更长的测试之前,做一个非常快速的测试…一个边界框是通过简单地取最大的X、最小的X、最大的Y和最小的Y并将它们组合在一起构成一个框的四个点…

另一个建议是:我们在一个网格空间中进行了所有更复杂和"光线变暗"的计算,所有这些计算都是在平面上的正点上进行的,然后重新投影回"真实"的经度/纬度,从而避免了当一个跨越经度180线和处理极地区域时可能出现的绕包错误。工作很棒!


真的很喜欢NIRG发布的和Bobobobo编辑的解决方案。我只是让它对javascript更友好,并且对我的使用更加清晰:

1
2
3
4
5
6
7
8
function insidePoly(poly, pointx, pointy) {
    var i, j;
    var inside = false;
    for (i = 0, j = poly.length - 1; i < poly.length; j = i++) {
        if(((poly[i].y > pointy) != (poly[j].y > pointy)) && (pointx < (poly[j].x-poly[i].x) * (pointy-poly[i].y) / (poly[j].y-poly[i].y) + poly[i].x) ) inside = !inside;
    }
    return inside;
}


DavidSegond的答案基本上是标准的一般答案,RichardT是最常见的优化方法,尽管其他的方法也有。其他强大的优化基于不太一般的解决方案。例如,如果要用大量的点检查同一个多边形,三角形化多边形可以极大地加快速度,因为有许多非常快速的tin搜索算法。另一种方法是,如果多边形和点位于低分辨率的有限平面上,例如屏幕显示,则可以将多边形绘制到具有给定颜色的内存映射显示缓冲区上,并检查给定像素的颜色,以查看它是否位于多边形中。

像许多优化一样,这些优化是基于特定的而不是一般的情况,并且收益是基于摊余时间而不是单一使用。

在这个领域工作时,我发现C'isbn 0-521-44034-3中的乔瑟夫·奥洛克(Joeseph O'Rourkes)的计算几何是一个很大的帮助。


Obj-C版本NIGG的答案与采样方法的测试点。Nirg的回答对我很有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (BOOL)isPointInPolygon:(NSArray *)vertices point:(CGPoint)test {
    NSUInteger nvert = [vertices count];
    NSInteger i, j, c = 0;
    CGPoint verti, vertj;

    for (i = 0, j = nvert-1; i < nvert; j = i++) {
        verti = [(NSValue *)[vertices objectAtIndex:i] CGPointValue];
        vertj = [(NSValue *)[vertices objectAtIndex:j] CGPointValue];
        if (( (verti.y > test.y) != (vertj.y > test.y) ) &&
        ( test.x < ( vertj.x - verti.x ) * ( test.y - verti.y ) / ( vertj.y - verti.y ) + verti.x) )
            c = !c;
    }

    return (c ? YES : NO);
}

- (void)testPoint {

    NSArray *polygonVertices = [NSArray arrayWithObjects:
        [NSValue valueWithCGPoint:CGPointMake(13.5, 41.5)],
        [NSValue valueWithCGPoint:CGPointMake(42.5, 56.5)],
        [NSValue valueWithCGPoint:CGPointMake(39.5, 69.5)],
        [NSValue valueWithCGPoint:CGPointMake(42.5, 84.5)],
        [NSValue valueWithCGPoint:CGPointMake(13.5, 100.0)],
        [NSValue valueWithCGPoint:CGPointMake(6.0, 70.5)],
        nil
    ];

    CGPoint tappedPoint = CGPointMake(23.0, 70.0);

    if ([self isPointInPolygon:polygonVertices point:tappedPoint]) {
        NSLog(@"YES");
    } else {
        NSLog(@"NO");
    }
}

sample polygon


我也认为360求和只适用于凸多边形,但这不是真的。

这个站点有一个很好的图表显示了这一点,并且对点击测试有一个很好的解释:Gamasutra-闯入新年:碰撞检测


我知道这很古老,但这里有一个在Cocoa中实现的光线投射算法,以防有人感兴趣。不确定这是最有效的方法做事情,但它可以帮助某人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- (BOOL)shape:(NSBezierPath *)path containsPoint:(NSPoint)point
{
    NSBezierPath *currentPath = [path bezierPathByFlatteningPath];
    BOOL result;
    float aggregateX = 0; //I use these to calculate the centroid of the shape
    float aggregateY = 0;
    NSPoint firstPoint[1];
    [currentPath elementAtIndex:0 associatedPoints:firstPoint];
    float olderX = firstPoint[0].x;
    float olderY = firstPoint[0].y;
    NSPoint interPoint;
    int noOfIntersections = 0;

    for (int n = 0; n < [currentPath elementCount]; n++) {
        NSPoint points[1];
        [currentPath elementAtIndex:n associatedPoints:points];
        aggregateX += points[0].x;
        aggregateY += points[0].y;
    }

    for (int n = 0; n < [currentPath elementCount]; n++) {
        NSPoint points[1];

        [currentPath elementAtIndex:n associatedPoints:points];
        //line equations in Ax + By = C form
        float _A_FOO = (aggregateY/[currentPath elementCount]) - point.y;  
        float _B_FOO = point.x - (aggregateX/[currentPath elementCount]);
        float _C_FOO = (_A_FOO * point.x) + (_B_FOO * point.y);

        float _A_BAR = olderY - points[0].y;
        float _B_BAR = points[0].x - olderX;
        float _C_BAR = (_A_BAR * olderX) + (_B_BAR * olderY);

        float det = (_A_FOO * _B_BAR) - (_A_BAR * _B_FOO);
        if (det != 0) {
            //intersection points with the edges
            float xIntersectionPoint = ((_B_BAR * _C_FOO) - (_B_FOO * _C_BAR)) / det;
            float yIntersectionPoint = ((_A_FOO * _C_BAR) - (_A_BAR * _C_FOO)) / det;
            interPoint = NSMakePoint(xIntersectionPoint, yIntersectionPoint);
            if (olderX <= points[0].x) {
                //doesn't matter in which direction the ray goes, so I send it right-ward.
                if ((interPoint.x >= olderX && interPoint.x <= points[0].x) && (interPoint.x > point.x)) {  
                    noOfIntersections++;
                }
            } else {
                if ((interPoint.x >= points[0].x && interPoint.x <= olderX) && (interPoint.x > point.x)) {
                     noOfIntersections++;
                }
            }
        }
        olderX = points[0].x;
        olderY = points[0].y;
    }
    if (noOfIntersections % 2 == 0) {
        result = FALSE;
    } else {
        result = TRUE;
    }
    return result;
}


一般的解决方案是将多边形分割成三角形,然后点击测试三角形,如这里所述。

如果你的多边形是凸形的,那么可能会有更好的方法。把多边形看作是无限线的集合。每一条线把空间分成两部分。对于每一点,很容易说出它是在这条线上的一边还是另一边。如果一个点在所有直线的同一侧,那么它在多边形内。


Java版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class Geocode {
    private float latitude;
    private float longitude;

    public Geocode() {
    }

    public Geocode(float latitude, float longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public float getLatitude() {
        return latitude;
    }

    public void setLatitude(float latitude) {
        this.latitude = latitude;
    }

    public float getLongitude() {
        return longitude;
    }

    public void setLongitude(float longitude) {
        this.longitude = longitude;
    }
}

public class GeoPolygon {
    private ArrayList<Geocode> points;

    public GeoPolygon() {
        this.points = new ArrayList<Geocode>();
    }

    public GeoPolygon(ArrayList<Geocode> points) {
        this.points = points;
    }

    public GeoPolygon add(Geocode geo) {
        points.add(geo);
        return this;
    }

    public boolean inside(Geocode geo) {
        int i, j;
        boolean c = false;
        for (i = 0, j = points.size() - 1; i < points.size(); j = i++) {
            if (((points.get(i).getLongitude() > geo.getLongitude()) != (points.get(j).getLongitude() > geo.getLongitude())) &&
                    (geo.getLatitude() < (points.get(j).getLatitude() - points.get(i).getLatitude()) * (geo.getLongitude() - points.get(i).getLongitude()) / (points.get(j).getLongitude() - points.get(i).getLongitude()) + points.get(i).getLatitude()))
                c = !c;
        }
        return c;
    }

}


C版本的NIRG答案在这里:我只分享代码。它可以节省一些时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static bool IsPointInPolygon(IList<Point> polygon, Point testPoint) {
            bool result = false;
            int j = polygon.Count() - 1;
            for (int i = 0; i < polygon.Count(); i++) {
                if (polygon[i].Y < testPoint.Y && polygon[j].Y >= testPoint.Y || polygon[j].Y < testPoint.Y && polygon[i].Y >= testPoint.Y) {
                    if (polygon[i].X + (testPoint.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) * (polygon[j].X - polygon[i].X) < testPoint.X) {
                        result = !result;
                    }
                }
                j = i;
            }
            return result;
        }


没有什么比问题的归纳定义更有益的了。为了完整起见,这里您在Prolog中有一个版本,它还可以澄清光线投射背后的思想:

基于http://www.ecse.rpi.edu/homepages/wrf/research/short_notes/pnpoly.html中简单算法的模拟

一些辅助谓词:

1
2
3
4
5
6
7
8
9
exor(A,B):- \+A,B;A,\+B.
in_range(Coordinate,CA,CB) :- exor((CA>Coordinate),(CB>Coordinate)).

inside(false).
inside(_,[_|[]]).
inside(X:Y, [X1:Y1,X2:Y2|R]) :- in_range(Y,Y1,Y2), X > ( ((X2-X1)*(Y-Y1))/(Y2-Y1) +      X1),toggle_ray, inside(X:Y, [X2:Y2|R]); inside(X:Y, [X2:Y2|R]).

get_line(_,_,[]).
get_line([XA:YA,XB:YB],[X1:Y1,X2:Y2|R]):- [XA:YA,XB:YB]=[X1:Y1,X2:Y2]; get_line([XA:YA,XB:YB],[X2:Y2|R]).

给定2点a和b(线(a,b))的直线方程为:

1
2
3
                    (YB-YA)
           Y - YA = ------- * (X - XA)
                    (XB-YB)

线的旋转方向必须是设置为时钟方向的边界和反时钟方向的洞。我们要检查点(x,y),即被测点是否在左边。我们线的半平面(这是一个品味问题,也可能是右侧,但也必须改变边界线的方向这种情况下,这是将光线从点投射到右边(或左边)。并确认与线的交叉点。我们选择了项目在水平方向上的光线(又是味觉的问题)它也可以在垂直方向上进行类似的限制,所以我们有:

1
2
3
               (XB-XA)
           X < ------- * (Y - YA) + XA
               (YB-YA)

现在我们需要知道这个点是在左边还是右边。只是线段,不是整个平面,所以我们需要只将搜索限制在该段,但这很容易。要在段内,线中只有一个点可以更高而不是纵轴上的y。因为这是一个更大的限制需要先检查,所以我们只先检查那些线路满足此要求,然后检查其能力。约旦曲线定理任何投射到多边形的光线必须在偶数行。所以我们完成了,我们会把射线扔到然后每当它与一条线相交时,切换它的状态。但是在我们的实现中,我们将检查满足给定限制并决定它的内部。对于多边形中的每一条线,必须这样做。

1
2
3
4
5
6
7
8
9
10
11
is_left_half_plane(_,[],[],_).
is_left_half_plane(X:Y,[XA:YA,XB:YB], [[X1:Y1,X2:Y2]|R], Test) :- [XA:YA, XB:YB] =  [X1:Y1, X2:Y2], call(Test, X , (((XB - XA) * (Y - YA)) / (YB - YA) + XA));
                                                        is_left_half_plane(X:Y, [XA:YA, XB:YB], R, Test).

in_y_range_at_poly(Y,[XA:YA,XB:YB],Polygon) :- get_line([XA:YA,XB:YB],Polygon),  in_range(Y,YA,YB).
all_in_range(Coordinate,Polygon,Lines) :- aggregate(bag(Line),    in_y_range_at_poly(Coordinate,Line,Polygon), Lines).

traverses_ray(X:Y, Lines, Count) :- aggregate(bag(Line), is_left_half_plane(X:Y, Line, Lines, <), IntersectingLines), length(IntersectingLines, Count).

% This is the entry point predicate
inside_poly(X:Y,Polygon,Answer) :- all_in_range(Y,Polygon,Lines), traverses_ray(X:Y, Lines, Count), (1 is mod(Count,2)->Answer=inside;Answer=outside).

VBA版本:

注意:请记住,如果您的多边形是地图中的一个区域,那么纬度/经度是y/x值,而不是x/y(纬度=y,经度=x),这是因为我理解经度不是测量值时的历史含义。

类模块:cpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Private pXValue As Double
Private pYValue As Double

'''''X Value Property'''''

Public Property Get X() As Double
    X = pXValue
End Property

Public Property Let X(Value As Double)
    pXValue = Value
End Property

'''''Y Value Property'''''

Public Property Get Y() As Double
    Y = pYValue
End Property

Public Property Let Y(Value As Double)
    pYValue = Value
End Property

模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
Public Function isPointInPolygon(p As CPoint, polygon() As CPoint) As Boolean

    Dim i As Integer
    Dim j As Integer
    Dim q As Object
    Dim minX As Double
    Dim maxX As Double
    Dim minY As Double
    Dim maxY As Double
    minX = polygon(0).X
    maxX = polygon(0).X
    minY = polygon(0).Y
    maxY = polygon(0).Y

    For i = 1 To UBound(polygon)
        Set q = polygon(i)
        minX = vbMin(q.X, minX)
        maxX = vbMax(q.X, maxX)
        minY = vbMin(q.Y, minY)
        maxY = vbMax(q.Y, maxY)
    Next i

    If p.X < minX Or p.X > maxX Or p.Y < minY Or p.Y > maxY Then
        isPointInPolygon = False
        Exit Function
    End If


    ' SOURCE: http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html

    isPointInPolygon = False
    i = 0
    j = UBound(polygon)

    Do While i < UBound(polygon) + 1
        If (polygon(i).Y > p.Y) Then
            If (polygon(j).Y < p.Y) Then
                If p.X < (polygon(j).X - polygon(i).X) * (p.Y - polygon(i).Y) / (polygon(j).Y - polygon(i).Y) + polygon(i).X Then
                    isPointInPolygon = True
                    Exit Function
                End If
            End If
        ElseIf (polygon(i).Y < p.Y) Then
            If (polygon(j).Y > p.Y) Then
                If p.X < (polygon(j).X - polygon(i).X) * (p.Y - polygon(i).Y) / (polygon(j).Y - polygon(i).Y) + polygon(i).X Then
                    isPointInPolygon = True
                    Exit Function
                End If
            End If
        End If
        j = i
        i = i + 1
    Loop  
End Function

Function vbMax(n1, n2) As Double
    vbMax = IIf(n1 > n2, n1, n2)
End Function

Function vbMin(n1, n2) As Double
    vbMin = IIf(n1 > n2, n2, n1)
End Function


Sub TestPointInPolygon()

    Dim i As Integer
    Dim InPolygon As Boolean

'   MARKER Object
    Dim p As CPoint
    Set p = New CPoint
    p.X = <ENTER X VALUE HERE>
    p.Y = <ENTER Y VALUE HERE>

'   POLYGON OBJECT
    Dim polygon() As CPoint
    ReDim polygon(<ENTER VALUE HERE>) 'Amount of vertices in polygon - 1
    For i = 0 To <ENTER VALUE HERE> 'Same value as above
       Set polygon(i) = New CPoint
       polygon(i).X = <ASSIGN X VALUE HERE> 'Source a list of values that can be looped through
       polgyon(i).Y = <ASSIGN Y VALUE HERE> 'Source a list of values that can be looped through
    Next i

    InPolygon = isPointInPolygon(p, polygon)
    MsgBox InPolygon

End Sub


我已经做了一个Nyrg的C++代码的Python实现:

输入

  • 边界点:构成多边形的节点。
  • 边界框位置:要过滤的候选点。(在我从边界框创建的实现中。

    (输入是元组列表,格式为:[(xcord, ycord), ...])

退换商品

  • 多边形内的所有点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def polygon_ray_casting(self, bounding_points, bounding_box_positions):
    # Arrays containing the x- and y-coordinates of the polygon's vertices.
    vertx = [point[0] for point in bounding_points]
    verty = [point[1] for point in bounding_points]
    # Number of vertices in the polygon
    nvert = len(bounding_points)
    # Points that are inside
    points_inside = []

    # For every candidate position within the bounding box
    for idx, pos in enumerate(bounding_box_positions):
        testx, testy = (pos[0], pos[1])
        c = 0
        for i in range(0, nvert):
            j = i - 1 if i != 0 else nvert - 1
            if( ((verty[i] > testy ) != (verty[j] > testy))   and
                    (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]) ):
                c += 1
        # If odd, that means that we are inside the polygon
        if c % 2 == 1:
            points_inside.append(pos)


    return points_inside

同样,这个想法是从这里提出的


.NET端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    static void Main(string[] args)
    {

        Console.Write("Hola");
        List<double> vertx = new List<double>();
        List<double> verty = new List<double>();

        int i, j, c = 0;

        vertx.Add(1);
        vertx.Add(2);
        vertx.Add(1);
        vertx.Add(4);
        vertx.Add(4);
        vertx.Add(1);

        verty.Add(1);
        verty.Add(2);
        verty.Add(4);
        verty.Add(4);
        verty.Add(1);
        verty.Add(1);

        int nvert = 6;  //Vértices del poligono

        double testx = 2;
        double testy = 5;


        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 = 1;
        }
    }


使用qt(qt 4.3+)时,可以使用qpolygon的函数containsPoint


如果你正在寻找一个Java脚本库,那么有一个JavaScript谷歌地图V3扩展用于多边形类来检测一个点是否驻留在它里面。

1
2
var polygon = new google.maps.Polygon([],"#000000", 1, 1,"#336699", 0.3);
var isWithinPolygon = polygon.containsLatLng(40, -90);

谷歌扩展Github


在光线投射算法中处理以下特殊情况:

  • 光线与多边形的一侧重叠。
  • 该点位于多边形的内部,光线通过多边形的顶点。
  • 该点位于多边形的外部,光线刚好接触到多边形的一个角度。
  • 检查确定点是否在复杂多边形内。本文提供了一种简单的方法来解决这些问题,因此对于上述情况不需要特殊处理。


    令人惊讶的是,没有人提前提出这个问题,但是对于需要数据库的实用主义者来说:MongoDB对包括这个查询在内的地理查询有极好的支持。

    你要找的是:

    db.neighborhoods.findOne({ geometry: { $geoIntersects: { $geometry: {
    type:"Point", coordinates: ["longitude","latitude" ] } } }
    })

    Neighborhoods是以标准geojson格式存储一个或多个多边形的集合。如果查询返回空值,则不相交,否则为空。

    这里有很好的记录:https://docs.mongodb.com/manual/tutorial/geospatial-tutorial/

    在330个不规则多边形网格中分类的6000多个点的性能不到一分钟,根本没有优化,包括用它们各自的多边形更新文档的时间。


    这里是C中多边形测试中不使用光线投射的点。它可以用于重叠区域(自交叉),参见use_holes参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    /* math lib (defined below) */
    static float dot_v2v2(const float a[2], const float b[2]);
    static float angle_signed_v2v2(const float v1[2], const float v2[2]);
    static void copy_v2_v2(float r[2], const float a[2]);

    /* intersection function */
    bool isect_point_poly_v2(const float pt[2], const float verts[][2], const unsigned int nr,
                             const bool use_holes)
    {
        /* we do the angle rule, define that all added angles should be about zero or (2 * PI) */
        float angletot = 0.0;
        float fp1[2], fp2[2];
        unsigned int i;
        const float *p1, *p2;

        p1 = verts[nr - 1];

        /* first vector */
        fp1[0] = p1[0] - pt[0];
        fp1[1] = p1[1] - pt[1];

        for (i = 0; i < nr; i++) {
            p2 = verts[i];

            /* second vector */
            fp2[0] = p2[0] - pt[0];
            fp2[1] = p2[1] - pt[1];

            /* dot and angle and cross */
            angletot += angle_signed_v2v2(fp1, fp2);

            /* circulate */
            copy_v2_v2(fp1, fp2);
            p1 = p2;
        }

        angletot = fabsf(angletot);
        if (use_holes) {
            const float nested = floorf((angletot / (float)(M_PI * 2.0)) + 0.00001f);
            angletot -= nested * (float)(M_PI * 2.0);
            return (angletot > 4.0f) != ((int)nested % 2);
        }
        else {
            return (angletot > 4.0f);
        }
    }

    /* math lib */

    static float dot_v2v2(const float a[2], const float b[2])
    {
        return a[0] * b[0] + a[1] * b[1];
    }

    static float angle_signed_v2v2(const float v1[2], const float v2[2])
    {
        const float perp_dot = (v1[1] * v2[0]) - (v1[0] * v2[1]);
        return atan2f(perp_dot, dot_v2v2(v1, v2));
    }

    static void copy_v2_v2(float r[2], const float a[2])
    {
        r[0] = a[0];
        r[1] = a[1];
    }

    注意:这是一个不太理想的方法,因为它包含了很多对atan2f的调用,但是它可能对读取该线程的开发人员有好处(在我的测试中,它比使用线交集方法慢了约23x)。


    答案取决于你是否有简单或复杂的多边形。简单多边形不能有任何线段交点。所以它们可以有洞,但线条不能互相交叉。复杂区域可以有直线交叉点,因此它们可以有重叠区域,或者只通过一个点相互接触的区域。

    对于简单多边形,最佳算法是光线投射(交叉数)算法。对于复杂多边形,该算法不检测重叠区域内的点。所以对于复杂的多边形,你必须使用缠绕数算法。

    这是一篇关于这两种算法的C实现的优秀文章。我试过了,效果很好。

    http://geomalgorithms.com/a03---u inclusion.html


    您可以通过检查将所需点连接到多边形顶点形成的区域是否与多边形本身的区域匹配来完成此操作。

    或者你可以检查从你的点到每对连续两个多边形顶点到你的检查点的内角的总和是否为360,但是我感觉第一个选项更快,因为它不涉及三角函数的逆运算和计算。

    我不知道如果你的多边形有一个洞里面会发生什么,但在我看来,主要的想法可以适应这种情况。

    你也可以在数学社区发表这个问题。我敢打赌他们有一百万种方法


    Scala版本的解决方案由NIGG(假定边界矩形预检查单独完成):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def inside(p: Point, polygon: Array[Point], bounds: Bounds): Boolean = {

      val length = polygon.length

      @tailrec
      def oddIntersections(i: Int, j: Int, tracker: Boolean): Boolean = {
        if (i == length)
          tracker
        else {
          val intersects = (polygon(i).y > p.y) != (polygon(j).y > p.y) && p.x < (polygon(j).x - polygon(i).x) * (p.y - polygon(i).y) / (polygon(j).y - polygon(i).y) + polygon(i).x
          oddIntersections(i + 1, i, if (intersects) !tracker else tracker)
        }
      }

      oddIntersections(0, length - 1, tracker = false)
    }

    为了检测对多边形的撞击,我们需要测试两件事:

  • 如果点在多边形区域内。(可通过光线投射算法完成)
  • 如果点位于多边形边界上(可通过用于多段线(直线)上点检测的相同算法完成)。

  • 这是@nirg answer的golang版本(灵感来自于@m-katz的C代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    func isPointInPolygon(polygon []point, testp point) bool {
        minX := polygon[0].X
        maxX := polygon[0].X
        minY := polygon[0].Y
        maxY := polygon[0].Y

        for _, p := range polygon {
            minX = min(p.X, minX)
            maxX = max(p.X, maxX)
            minY = min(p.Y, minY)
            maxY = max(p.Y, maxY)
        }

        if testp.X < minX || testp.X > maxX || testp.Y < minY || testp.Y > maxY {
            return false
        }

        inside := false
        j := len(polygon) - 1
        for i := 0; i < len(polygon); i++ {
            if (polygon[i].Y > testp.Y) != (polygon[j].Y > testp.Y) && testp.X < (polygon[j].X-polygon[i].X)*(testp.Y-polygon[i].Y)/(polygon[j].Y-polygon[i].Y)+polygon[i].X {
                inside = !inside
            }
            j = i
        }

        return inside
    }

    这只适用于凸面形状,但Minkowski门户优化和GJK也是测试点是否在多边形中的最佳选项。使用minkowski减法从多边形中减去点,然后运行这些算法查看多边形是否包含原点。

    另外,有趣的是,您可以使用支持函数更含蓄地描述您的形状,支持函数将方向向量作为输入,并沿该向量吐出最远的点。这可以让你描述任何凸形。弯曲的,由多边形制成的,或混合的。您还可以执行操作来组合简单支持函数的结果,以生成更复杂的形状。

    更多信息:http://xenocollide.snethen.com/mpr2d.html

    另外,Game Programming gems 7还讨论了如何在3D中实现这一点(: