如何确定2D点是否在多边形内?


497

我正在尝试在多边形算法中创建一个快速的 2D点,以用于点击测试(例如Polygon.contains(p:Point))。对于有效技术的建议将不胜感激。


您忘了告诉我们您对右手或左手问题的看法-这也可以解释为“内部”与“外部” – RT
理查德·T

13
是的,我意识到现在这个问题还有很多未说明的问题,但是在这一点上,我有点想看到各种各样的回答。
Scott Evernden

4
一个90面的多边形称为enneacontagon,一个10,000面的多边形称为myriagon。

因为我在寻找“全部工作”算法上遇到了麻烦,所以“最优雅”是不可能的。我必须自己弄清楚:stackoverflow.com/questions/14818567/…–
davidkonrad

Answers:


731

对于图形,我宁愿不要整数。许多系统使用整数进行UI绘制(像素毕竟是ints),但是macOS例如对所有内容都使用float。macOS仅知道点,并且一个点可以转换为一个像素,但是根据显示器分辨率,它可能转换为其他像素。在视网膜屏幕上,半点(0.5 / 0.5)是像素。不过,我从未注意到macOS UI明显比其他UI慢。毕竟,所有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)的矩形外部的点不能在多边形内。

// 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

绿色的中间有一个洞!

可以处理上述所有三种情况并且仍然非常快的最简单算法称为ray cast。该算法的想法非常简单:从多边形外部的任意位置绘制一条虚拟射线到您的点,并计算其撞击多边形一侧的频率。如果命中数是偶数,则在多边形之外,如果是奇数,则在多边形内。

Demonstrating how the ray cuts through a polygon

绕数的算法将是一个另类,它是点非常接近多边形线条更准确,但它也慢得多。由于有限的浮点精度和舍入问题,射线投射可能会由于太靠近多边形边而失败,但实际上这并不是问题,好像点位于边附近,通常在视觉上甚至不可能查看器以识别它是否已经在内部或仍在外部。

您还有上方的边界框,记得吗?只需在边界框外选择一个点,然后将其用作射线的起点即可。例如,该点(Xmin - e/p.y)肯定在多边形之外。

但是什么e呢?好吧,e(实际上是epsilon)给边界框添加了一些填充。如我所说,如果我们开始太靠近多边形线,光线跟踪将失败。由于边界框可能等于多边形(如果多边形是轴对齐的矩形,则边界框等于多边形本身!),我们需要一些填充以使其安全,仅此而已。您应该选择e多少?不太大 这取决于您用于绘图的坐标系比例。如果您的像素步长为1.0,则只需选择1.0(但0.1也可以)

现在我们有了射线及其开始和结束坐标,问题从“ 多边形内的点 ”变为“ 射线与多边形边相交的频率 ”。因此,我们不能像以前那样仅使用多边形点,现在我们需要实际的边。边总是由两点定义。

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

您需要针对所有方面测试射线。认为射线是矢量,而每一边都是矢量。射线必须精确地击中每侧一次或根本不击中。它不能两次击中同一侧。二维空间中的两条线将始终恰好相交一次,除非它们是平行的,在这种情况下,它们永远不会相交。但是,由于向量的长度有限,因此两个向量可能不平行,也永远不会相交,因为它们太短而无法相遇。

// 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代码(未经测试),应该可以解决问题:

#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(和)和向量2(和)的两个端点。因此,您有2个向量,4个点,8个坐标。而且很清楚。增加十字路口,什么也不做。v1x1/v1y1v1x2/v1y2v2x1/v2y1v2x2/v2y2YESNOYESNO

COLLINEAR呢?这意味着两个向量都位于同一条无限线上,具体取决于位置和长度,它们根本不相交,或者它们以无数个点相交。我不太确定如何处理这种情况,无论如何我都不会将其视为交叉路口。嗯,由于浮点舍入错误,这种情况在实践中还是很少见的。更好的代码可能不会测试,== 0.0f而是会测试< epsilon,其中epsilon是一个很小的数字。

如果需要测试更多的点,则可以通过将多边形边的线性方程标准形式保留在内存中来一定程度地加快整个过程,因此不必每次都重新计算它们。这样可以在每个测试中节省两个浮点乘法和三个浮点减法,以换取在内存中每个多边形边存储三个浮点值。这是典型的内存与计算时间之间的折衷。

最后但并非最不重要的一点:如果您可以使用3D硬件解决问题,则有一个有趣的替代方法。只需让GPU为您完成所有工作即可。创建不在屏幕上的绘画表面。将其完全填充为黑色。现在,让OpenGL或Direct3D绘制多边形(如果您只是想测试点是否在其中任何一个,但您不在乎哪个,则可以绘制所有多边形),并用不同的多边形填充颜色,例如白色。要检查点是否在多边形内,请从绘图表面获取该点的颜色。这只是一个O(1)内存提取。

当然,仅当您的绘图表面不必很大时,此方法才可用。如果无法容纳到GPU内存中,则此方法比在CPU上执行该方法要慢。如果必须很大并且您的GPU支持现代着色器,您仍然可以通过将上面所示的光线投射实现为GPU着色器来使用GPU,这是绝对可能的。对于要测试的大量多边形或大量点,这会有所作为,考虑到某些GPU能够并行测试64至256点。但是请注意,将数据从CPU传输到GPU并回传总是很昂贵的,因此,仅针对几个简单的多边形测试几个点(点或多边形是动态的并且会经常变化),GPU的方法很少会付出代价关。


26
+1很棒的答案。关于使用硬件来完成此操作,我在其他地方读到它可能很慢,因为您必须从图形卡取回数据。但是我很喜欢减轻CPU负担的原则。有没有人对在OpenGL中如何做到这一点有很好的参考?
加文2009年

3
+1,因为这是如此简单!主要问题是,如果您的多边形和测试点排列在一个网格上(并非罕见),那么您必须处理“双重”交点,例如,直接穿过一个多边形点!(以2代替1)。进入这个奇怪的区域:stackoverflow.com/questions/2255842/…。计算机图形学充满了这些特殊情况:理论上简单,实践上多毛。
Jared Updike 2010年

7
@RMorrisey:你为什么这么认为?我看不到凹面多边形将如何失败。当多边形为凹面时,射线可能会多次离开并重新进入多边形,但是最后,对于凹面多边形,如果该点在内部,即使在外部,命中计数器也将是奇数。
梅基

6
“快缠绕数算法”,在描述softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm工作非常快...
SP

10
您使用/来分隔x和y坐标是令人困惑的,它表示为x除以y。总体而言,使用x,y(即x逗号y)更为清晰,这是一个有用的答案。
2013年

583

我认为以下代码是最好的解决方案(从此处获取):

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:多边形中的顶点数。在上面提到的文章中讨论了是否在末尾重复第一个顶点。
  • vertx,verty:包含多边形顶点的x坐标和y坐标的数组。
  • testx,testy:测试点的X坐标和y坐标。

它既短又有效,并且适用于凸多边形和凹多边形。如之前建议的那样,您应该先检查边界矩形,然后分别处理多边形孔。

这背后的想法很简单。作者对此进行了如下描述:

我从测试点水平发出半无限的光线(增加x,固定y),并计算它穿过多少条边。在每个交叉处,射线在内部和外部之间切换。这称为乔丹曲线定理。

每当水平射线穿过任何边缘时,变量c都会从0切换为1,从1切换为0。因此,基本上,它一直在跟踪交叉的边数是偶数还是奇数。0表示偶数,1表示奇数。


5
题。我传递的变量到底是什么?它们代表什么?
tekknolagi'2

9
@Mick它检查,verty[i]并且verty[j]位于的任一侧testy,因此它们永远不相等。
彼得·伍德

4
这段代码不够鲁棒,我不建议您使用它。这里是一个链接提供了一些详细的分析: www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf
米高拉

13
这种方法实际上有局限性(边缘情况):检查多边形[(10,10),(10,20),(20,20),(20,10)]中的点(15,20)将返回错误而不是真实。与(10,20)或(20,15)相同。在所有其他情况下,该算法都能正常工作,边缘情况下的假负数对我的应用程序来说也可以。
亚历山大·帕恰

10
@Alexander,这实际上是设计使然:通过以与上下边界相反的方式处理左边界和底边界,如果两个不同的多边形共享一条边,则沿该边的任何点将位于一个且仅一个多边形中。..有用的属性。
ward

69

这是nirg给出答案的C#版本,该答案来自该RPI教授。请注意,使用来自该RPI源的代码需要归因。

边框复选框已添加到顶部。但是,正如James Brown指出的那样,主代码几乎与边界框检查本身一样快,因此,如果您要检查的大多数点都在边界框内,边界框检查实际上会减慢整体操作的速度。 。因此,您可以将边界框检出,或者另一种方法是,如果多边形的边界框不太频繁地改变形状,则可以对其进行预先计算。

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

    // https://wrf.ecse.rpi.edu/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;
}

5
效果很好,谢谢,我转换成了JavaScript。stackoverflow.com/questions/217578/...
菲利普Lenssen

2
这比使用GraphicsPath快1000倍以上!边框复选框使功能慢了约70%。
詹姆斯·布朗

GraphicsPath.IsVisible()不仅速度较慢,而且对于边长在0.01f范围内的非常小的多边形也不起作用
NDepend团队的Patrick

50

这是M. Katz根据Nirg的方法得出的答案的JavaScript变体:

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

32

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

我更喜欢此方法,因为它更健壮且对数值精度的依赖性较小。

计算交叉点数量均匀度的方法是有限的,因为您可以在计算交叉点数量时“击中”顶点。

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

编辑:我最近发现了一个整体 有关该主题 Wikipedia文章


1
不,这不是真的。无论多边形的凸度如何,此方法都有效。
David Segonds

2
@DarenW:每个顶点只有一个acos;另一方面,该算法应该最简单地在SIMD中实现,因为它绝对没有分支。
贾斯珀·贝克斯

1
@emilio,如果该点远离三角形,我看不到该点和三角形的两个顶点形成的角度将如何为90度。
David Segonds,2009年

2
首先使用边界框检查来解决“点很远”的情况。对于trig,您可以使用预生成的表。
JOM

3
这是最佳解决方案,因为它是O(n),并且需要最少的计算。适用于所有多边形。30年前,我在IBM的第一份工作中就研究了该解决方案。他们签署了该协议,直到今天仍在其GIS技术中使用它。
Dominic Cerisano '16

24

这个问题真有趣。我还有另一个可行的想法,与本帖子的其他答案不同。这个想法是使用角度之和来确定目标是在内部还是外部。俗称绕组数

令x为目标点。令数组[0,1,.... n]为该区域的所有点。用一条线将目标点与每个边界点连接起来。如果目标点在此区域内。所有角度的总和将为360度。如果不是,则角度将小于360。

请参考此图像以基本了解该想法: 在此处输入图片说明

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

[[-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代码:

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

21

埃里克·海恩斯Eric Haines)的文章bobobobo引用确实很棒。表格比较算法性能特别有趣。角度求和方法与其他方法相比确实很差。同样有趣的是,使用查找网格将多边形进一步细分为“入”和“出”扇区的优化,即使在边数大于1000的多边形上,也可以使测试变得非常快。

无论如何,这是早期,但我的投票投给了“穿越”方法,这是梅基对我的描述。但是我发现它最简洁地由David Bourke描述和编纂。我喜欢不需要真正的三角函数,它适用于凸面和凹面,并且随着边数的增加,它的表现还算不错。

顺便说一下,这是埃里克·海恩斯(Eric Haines)感兴趣的文章中的性能表之一,它是对随机多边形进行测试。

                       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

11

Swift版本的nirg答案

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
    }
}

在计算b时,这有一个潜在的被零除的问题。如果对“ a”的计算表明存在相交的可能性,则只需计算“ b”,然后用“ c”下一行。如果两个点都在上面,或者两个点都在下面,则不可能-这由“ a”的计算来描述。
anorskdev

11

就像Nirg发布并由bobobobo编辑的解决方案一样。我只是使它对javascript友好,并且使我更易读:

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

8

当我还是Michael Stonebraker的研究员时,我为此做了一些工作-您知道,是提出IngresPostgreSQL等的教授。

我们意识到最快的方法是首先制作一个边界框,因为它超级快。如果它在边界框之外,那么它在外面。否则,您会做得更辛苦...

如果您想使用出色的算法,请查看开源项目PostgreSQL源代码中的地理信息...

我想指出的是,我们从来没有对右撇子和左撇子(也可以表示为“内部”与“外部”问题表达任何见解...


更新

BKB的链接提供了大量合理的算法。我当时正在研究地球科学问题,因此需要一种可以在纬度/经度上使用的解决方案,并且该解决方案具有特殊的惯用性问题-是较小的区域还是较大的区域?答案是顶点的“方向”很重要-它是左撇子还是右撇子,通过这种方式,您可以将任一区域表示为任何给定多边形的“内部”。因此,我的工作使用了该页面上列举的解决方案三。

另外,我的工作使用单独的功能进行“在线”测试。

...由于有人问:我们发现,当折点数超过一定数量时,边界框测试是最好的-如果需要,在进行更长的测试之前先做一个非常快速的测试...通过简单地将边界框最大x,最小x,最大y和最小y并将它们放在一起以构成一个盒子的四个点...

接下来的提示:我们在网格空间中的所有正点上都在网格空间中进行了所有更复杂的“调光”计算,然后将其重新投影回“真实”经度/纬度,从而避免了可能的误差。绕过一条经线180和处理极区时。很棒!


如果我碰巧没有边界框怎么办?:)
斯科特·埃弗登

8
您可以轻松地创建一个边界框-只是使用最大和最小x以及最大和最小y的四个点。很快。
理查德·T

“ ...避免在一条经线180和处理极性区域时回绕的可能的错误。” 您能否详细描述一下?我想我可以弄清楚如何向上移动所有东西,以避免我的多边形越过0经度,但是我不清楚如何处理包含任意一个极点的多边形...
tiritea

6

David Segond的答案几乎是标准的一般答案,Richard T的答案是最常见的优化方法,尽管还有其他一些优化方法。其他强大的优化则基于不太通用的解决方案。例如,如果您要检查具有很多点的同一个多边形,则对多边形进行三角剖分可以极大地加快处理速度,因为有许多非常快速的TIN搜索算法。另一个是如果多边形和点在低分辨率的有限平面上(例如屏幕显示),则可以将多边形以给定的颜色绘制到内存映射的显示缓冲区上,并检查给定像素的颜色以查看其是否位于在多边形中。

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

在该领域工作,我发现Joeseph O'Rourkes的C语言ISBN 0-521-44034-3中的“计算几何”是一个很大的帮助。


4

在平凡的解决办法是将多边形到三角形和命中测试三角形的解释这里

如果您的多边形是CONVEX,则可能会有更好的方法。将多边形视为无限线的集合。每行将空间分成两部分。对于每个点,很容易说出它是在线的一侧还是另一侧。如果点在所有线的同一侧,则该点在面内。


非常快,并且可以应用于更一般的形状。大约在1990年,我们有了“曲线”,其中某些侧面是圆弧。通过将这些侧面分析成圆形楔形和将楔形连接到原点(多边形质心)的一对三角形,可以轻松快捷地进行命中测试。
2009年

1
在这些曲线上有什么参考吗?
shoosh

我也很喜欢为curvigons设计的参考。
乔尔(Joel)在09年

您错过了凸多边形情况的重要解决方案:通过将点与对角线进行比较,可以将顶点数量减半。并重复这一过程,可以减少在日志(N)一个三角形的操作,而不是N.
伊夫·达斯特

4

我意识到这很旧,但是这里是可可中实现的光线投射算法,以防万一有人感兴趣。不确定这是做事的最有效方法,但可能会帮助某人。

- (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;
}

5
请注意,如果您确实在Cocoa中这样做,则可以使用[NSBezierPath containsPoint:]方法。
ThomasW 2012年

4

nirg答案的Obj-C版本,带有用于测试点的样本方法。Nirg的回答对我来说效果很好。

- (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");
    }
}

样本多边形


2
当然,在Objective-C中,CGPathContainsPoint()是您的朋友。
Zev Eisenberg 2014年

@ZevEisenberg,但这太容易了!感谢您的来信。我将在某个时候挖掘该项目,以了解为什么使用定制解决方案。我可能不知道CGPathContainsPoint()
2014年

4

没有什么比对问题的归纳定义更美了。为了完整起见,您在序言中有一个版本,该版本可能还会阐明ray cast背后的观点:

基于http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html中简化算法的仿真

一些辅助谓词:

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(Line(A,B))的直线方程为:

                    (YB-YA)
           Y - YA = ------- * (X - XA) 
                    (XB-YB) 

重要的是,将直线的旋转方向设置为边界的顺时针方向,将孔设置为逆时针方向。我们将检查点(X,Y),即测试点是否在直线的左半平面上(这是一个问题,可能在右边,也可能在边界方向上)在这种情况下,必须更改直线),这是将光线从该点投射到右侧(或左侧)并确认与直线的交点。我们选择在水平方向上投射光线(同样,这是一个问题,也可以在垂直方向上进行类似的限制),因此我们具有:

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

现在我们需要知道该点是否仅在线段的左侧(或右侧),而不是整个平面,因此我们只需要将搜索限制在该线段即可,但这很容易,因为它位于线段内直线上只有一个点可以高于垂直轴上的Y。由于这是一个更严格的限制,因此需要首先进行检查,因此我们仅首先选择满足此要求的那些行,然后再检查其位置。根据约旦曲线定理,任何投射到多边形的光线都必须以偶数条线相交。这样就完成了,我们将射线向右投射,然后每次与直线相交时,切换其状态。但是,在我们的实施中,我们要检查符合给定限制的一揽子解决方案的长度,并确定其内在条件。对于多边形中的每条线,都必须这样做。

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).

3

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

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

这在大多数情况下都是有效的,但是是错误的,并且不能始终正常工作!使用来自M Katz的解决方案是正确的
Lukas Hanacek

3

Java版本:

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

}

2

.Net端口:

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

2

VBA版本:

注意:请记住,如果多边形是地图中的某个区域,则纬度/经度是Y / X值,而不是X / Y(纬度= Y,经度= X),这是由于据我所知经度不是度量。

课程模块:CPoint

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

模组:

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

2

我已经完成了Nirg C ++ 代码的Python实现

输入项

  • bounding_points:组成多边形的节点。
  • bounding_box_positions:要过滤的候选点。(在我的实现中是从边界框创建的。

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

退货

  • 多边形内的所有点。
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

再次,这个想法是从这里


2

令人惊讶的是没有人提过这个,但是对于要求数据库的实用主义者来说:MongoDB对包括此在内的Geo查询提供了出色的支持。

您正在寻找的是:

db.neighborhoods.findOne({几何:{$ geoIntersects:{$ geometry:{类型:“点”,坐标:[“经度”,“纬度”]}}}})

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

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

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


1

这是C语言中多边形测试中未使用光线投射的一点。它可以用于重叠区域(自相交),请参见use_holes参数。

/* 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,但是对于开发人员来说,读取此线程可能会引起人们的兴趣(在我的测试中,此线程的速度比使用线相交方法慢23倍)。


0

为了检测多边形的命中,我们需要测试两件事:

  1. 如果Point在多边形区域内。(可以通过射线投射算法完成)
  2. 如果Point在多边形边界上(可以通过用于折线(线)上的点检测的相同算法来完成)。

0

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

  1. 射线与多边形的一侧重叠。
  2. 该点在多边形的内部,并且射线穿过多边形的顶点。
  3. 该点在多边形的外部,并且射线刚好接触多边形的一个角度。

确定点是否在复杂多边形内的检查。本文为解决这些问题提供了一种简便的方法,因此对于上述情况,无需特殊处理。


0

您可以通过检查将所需点连接到多边形的顶点所形成的面积是否与多边形本身的面积匹配来实现。

或者,您可以检查从您的点到两个连续多边形的每对顶点到检查点的内角之和是否为360,但是我觉得第一种选择的速度更快,因为它不涉及除法和计算函数的逆函数。

我不知道如果您的多边形内部有孔会发生什么,但是在我看来主要思想可以适应这种情况

您也可以在数学社区中发布问题。我敢打赌他们有100万种方法


0

如果您正在寻找Java脚本库,则可以使用Polygon类的javascript google maps v3扩展来检测点是否位于其中。

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

Google Extension Github



0

答案取决于您是否具有简单或复杂的多边形。简单的多边形不得有任何线段相交。因此它们可以有孔,但线不能交叉。复杂区域可以具有线相交-因此它们可以具有重叠区域,也可以只有一个点相互接触的区域。

对于简单的多边形,最好的算法是射线投射(交叉数)算法。对于复杂的多边形,此算法不会检测重叠区域内的点。因此,对于复杂的多边形,您必须使用绕组数算法。

这是一篇很好的文章,介绍了两种算法的C实现。我尝试了它们,它们运行良好。

http://geomalgorithms.com/a03-_inclusion.html


0

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

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)
}

0

这是@nirg答案的golang版本(受@@ m-katz的C#代码启发)

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
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.