Estoy tratando de crear un punto 2D rápido dentro del algoritmo de polígono, para usar en pruebas de impacto (por ejemplo Polygon.contains(p:Point)
). Se agradecerán sugerencias para técnicas efectivas.
Estoy tratando de crear un punto 2D rápido dentro del algoritmo de polígono, para usar en pruebas de impacto (por ejemplo Polygon.contains(p:Point)
). Se agradecerán sugerencias para técnicas efectivas.
Respuestas:
Para los gráficos, prefiero no preferir los enteros. Muchos sistemas usan enteros para la pintura de la interfaz de usuario (los píxeles son ints después de todo), pero macOS, por ejemplo, usa flotante para todo. macOS solo conoce puntos y un punto puede traducirse a un píxel, pero dependiendo de la resolución del monitor, podría traducirse a otra cosa. En las pantallas de retina, medio punto (0.5 / 0.5) es píxel. Aún así, nunca noté que las interfaces de usuario de macOS son significativamente más lentas que otras interfaces de usuario. Después de todo, las API 3D (OpenGL o Direct3D) también funcionan con flotadores y las bibliotecas de gráficos modernos a menudo aprovechan la aceleración de la GPU.
Ahora dijiste que la velocidad es tu principal preocupación, de acuerdo, vamos por la velocidad. Antes de ejecutar cualquier algoritmo sofisticado, primero haga una prueba simple. Cree un cuadro delimitador alineado con el eje alrededor de su polígono. Esto es muy fácil, rápido y ya puede ahorrarle muchos cálculos. ¿Cómo funciona? Itere sobre todos los puntos del polígono y encuentre los valores mínimo / máximo de X e Y.
Por ejemplo, tienes los puntos (9/1), (4/3), (2/7), (8/2), (3/6)
. Esto significa que Xmin es 2, Xmax es 9, Ymin es 1 e Ymax es 7. Un punto fuera del rectángulo con los dos bordes (2/1) y (9/7) no puede estar dentro del polígono.
// 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!
}
Esta es la primera prueba que se ejecuta en cualquier punto. Como puede ver, esta prueba es ultra rápida pero también es muy tosca. Para manejar puntos que están dentro del rectángulo delimitador, necesitamos un algoritmo más sofisticado. Hay un par de maneras de cómo se puede calcular esto. El método que funcione también depende del hecho de si el polígono puede tener agujeros o siempre será sólido. Aquí hay ejemplos de sólidos (uno convexo, uno cóncavo):
Y aquí hay uno con un agujero:
¡El verde tiene un agujero en el medio!
El algoritmo más fácil, que puede manejar los tres casos anteriores y que sigue siendo bastante rápido, se llama proyección de rayos . La idea del algoritmo es bastante simple: dibuje un rayo virtual desde cualquier lugar fuera del polígono hasta su punto y cuente con qué frecuencia golpea un lado del polígono. Si el número de golpes es par, está fuera del polígono, si es impar, está dentro.
El algoritmo de número de bobinado sería una alternativa, es más preciso para los puntos que están muy cerca de una línea de polígono, pero también es mucho más lento. La proyección de rayos puede fallar para puntos demasiado cercanos a un lado del polígono debido a la precisión limitada del punto flotante y los problemas de redondeo, pero en realidad eso no es un problema, ya que si un punto se encuentra tan cerca de un lado, a menudo no es posible visualmente espectador para reconocer si ya está adentro o afuera.
Todavía tienes el cuadro delimitador de arriba, ¿recuerdas? Simplemente elija un punto fuera del cuadro delimitador y úselo como punto de partida para su rayo. Por ejemplo, el punto (Xmin - e/p.y)
está fuera del polígono seguro.
Pero que es e
? Bueno, e
(en realidad épsilon) le da al cuadro delimitador algo de relleno . Como dije, el trazado de rayos falla si comenzamos demasiado cerca de una línea de polígono. Dado que el cuadro delimitador puede ser igual al polígono (si el polígono es un rectángulo alineado con el eje, el cuadro delimitador es igual al polígono mismo), necesitamos algo de relleno para que esto sea seguro, eso es todo. ¿Qué tan grande debes elegir e
? No tan grande. Depende de la escala del sistema de coordenadas que use para dibujar. Si su ancho de paso de píxeles es 1.0, simplemente elija 1.0 (sin embargo, 0.1 también habría funcionado)
Ahora que tenemos el rayo con sus coordenadas de inicio y fin, el problema cambia de " es el punto dentro del polígono " a "con qué frecuencia el rayo interseca un lado del polígono ". Por lo tanto, no podemos simplemente trabajar con los puntos poligonales como antes, ahora necesitamos los lados reales. Un lado siempre se define por dos puntos.
side 1: (X1/Y1)-(X2/Y2)
side 2: (X2/Y2)-(X3/Y3)
side 3: (X3/Y3)-(X4/Y4)
:
Necesita probar el rayo contra todos los lados. Considere que el rayo es un vector y cada lado es un vector. El rayo tiene que golpear cada lado exactamente una vez o nunca. No puede golpear el mismo lado dos veces. Dos líneas en el espacio 2D siempre se cruzan exactamente una vez, a menos que sean paralelas, en cuyo caso nunca se cruzan. Sin embargo, dado que los vectores tienen una longitud limitada, es posible que dos vectores no sean paralelos y que nunca se crucen porque son demasiado cortos para encontrarse.
// 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
}
Hasta ahora todo bien, pero ¿cómo se prueba si dos vectores se cruzan? Aquí hay un código C (no probado), que debería hacer el truco:
#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;
}
Los valores de entrada son los dos puntos finales del vector 1 ( v1x1/v1y1
y v1x2/v1y2
) y el vector 2 ( v2x1/v2y1
y v2x2/v2y2
). Entonces tienes 2 vectores, 4 puntos, 8 coordenadas. YES
y NO
son claros YES
aumenta las intersecciones, NO
no hace nada.
¿Qué hay de COLLINEAR? Significa que ambos vectores se encuentran en la misma línea infinita, dependiendo de la posición y la longitud, no se cruzan en absoluto o se cruzan en un número interminable de puntos. No estoy absolutamente seguro de cómo manejar este caso, no lo consideraría como una intersección de ninguna manera. Bueno, este caso es bastante raro en la práctica de todos modos debido a errores de redondeo de coma flotante; un código mejor probablemente no probaría == 0.0f
sino algo como < epsilon
, donde epsilon es un número bastante pequeño.
Si necesita probar una mayor cantidad de puntos, sin duda puede acelerar un poco todo manteniendo las formas estándar de ecuaciones lineales de los lados del polígono en la memoria, para que no tenga que volver a calcularlas cada vez. Esto le ahorrará dos multiplicaciones de coma flotante y tres restas de coma flotante en cada prueba a cambio de almacenar tres valores de coma flotante por lado de polígono en la memoria. Es un intercambio típico de memoria vs tiempo de cálculo.
Por último, pero no menos importante: si puede usar hardware 3D para resolver el problema, existe una alternativa interesante. Solo deja que la GPU haga todo el trabajo por ti. Cree una superficie de pintura que esté fuera de la pantalla. Rellenar completamente con el color negro. Ahora deja que OpenGL o Direct3D pinten tu polígono (o incluso todos tus polígonos si solo quieres probar si el punto está dentro de alguno de ellos, pero no te importa cuál) y rellena los polígonos con un polígono diferente. color, por ejemplo, blanco. Para verificar si un punto está dentro del polígono, obtenga el color de este punto de la superficie de dibujo. Esto es solo una recuperación de memoria O (1).
Por supuesto, este método solo se puede usar si la superficie de dibujo no tiene que ser enorme. Si no puede caber en la memoria de la GPU, este método es más lento que hacerlo en la CPU. Si tuviera que ser enorme y su GPU admite sombreadores modernos, aún puede usar la GPU implementando la proyección de rayos que se muestra arriba como un sombreador de GPU, lo cual es absolutamente posible. Para una mayor cantidad de polígonos o una gran cantidad de puntos para probar, esto valdrá la pena, tenga en cuenta que algunas GPU podrán probar 64 a 256 puntos en paralelo. Sin embargo, tenga en cuenta que transferir datos de la CPU a la GPU y viceversa siempre es costoso, por lo que solo para probar un par de puntos contra un par de polígonos simples, donde los puntos o los polígonos son dinámicos y cambiarán con frecuencia, un enfoque de GPU raramente pagará apagado.
Creo que el siguiente fragmento de código es la mejor solución (tomada de aquí ):
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;
}
Es a la vez corto y eficiente y funciona tanto para polígonos convexos como cóncavos. Como se sugirió anteriormente, primero debe verificar el rectángulo delimitador y tratar los agujeros de polígono por separado.
La idea detrás de esto es bastante simple. El autor lo describe de la siguiente manera:
Ejecuto un rayo semi-infinito horizontalmente (aumentando x, fijo y) desde el punto de prueba, y cuento cuántos bordes cruza. En cada cruce, el rayo cambia entre adentro y afuera. Esto se llama el teorema de la curva de Jordan.
La variable c cambia de 0 a 1 y de 1 a 0 cada vez que el rayo horizontal cruza cualquier borde. Básicamente, se trata de realizar un seguimiento de si el número de bordes cruzados es par o impar. 0 significa par y 1 significa impar.
verty[i]
y verty[j]
están a ambos lados testy
, por lo que nunca son iguales.
Aquí hay una versión C # de la respuesta dada por nirg , que proviene de este profesor de RPI . Tenga en cuenta que el uso del código de esa fuente RPI requiere atribución.
Se ha agregado una casilla de verificación en la parte superior. Sin embargo, como señala James Brown, el código principal es casi tan rápido como la casilla del cuadro delimitador en sí mismo, por lo que la verificación del cuadro delimitador en realidad puede ralentizar la operación general, en el caso de que la mayoría de los puntos que está verificando estén dentro del cuadro delimitador . Por lo tanto, puede dejar el cuadro delimitador desprotegido, o una alternativa sería calcular previamente los cuadros delimitadores de sus polígonos si no cambian de forma con demasiada frecuencia.
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;
}
Aquí hay una variante de JavaScript de la respuesta de M. Katz basada en el enfoque de Nirg:
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;
}
Calcule la suma orientada de ángulos entre el punto p y cada uno de los vértices del polígono. Si el ángulo orientado total es de 360 grados, el punto está dentro. Si el total es 0, el punto está afuera.
Me gusta más este método porque es más robusto y menos dependiente de la precisión numérica.
Los métodos que calculan la uniformidad del número de intersecciones son limitados porque puede 'golpear' un vértice durante el cálculo del número de intersecciones.
EDITAR: Por cierto, este método funciona con polígonos cóncavos y convexos.
EDITAR: Recientemente encontré un artículo completo de Wikipedia sobre el tema.
Esta pregunta es muy interesante. Tengo otra idea viable diferente de otras respuestas a esta publicación. La idea es usar la suma de los ángulos para decidir si el objetivo está dentro o fuera. Mejor conocido como número de bobinado .
Deje x ser el punto objetivo. Deje que array [0, 1, .... n] sea todos los puntos del área. Conecte el punto objetivo con cada punto de borde con una línea. Si el punto objetivo está dentro de esta área. La suma de todos los ángulos será de 360 grados. Si no, los ángulos serán inferiores a 360.
Consulte esta imagen para obtener una comprensión básica de la idea:
Mi algoritmo asume que el sentido horario es la dirección positiva. Aquí hay una entrada potencial:
[[-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]]
El siguiente es el código de Python que implementa la idea:
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
El artículo de Eric Haines citado por bobobobo es realmente excelente. Particularmente interesantes son las tablas que comparan el rendimiento de los algoritmos; El método de suma de ángulos es realmente malo en comparación con los demás. También es interesante que las optimizaciones como el uso de una cuadrícula de búsqueda para subdividir aún más el polígono en sectores de "entrada" y "salida" pueden hacer que la prueba sea increíblemente rápida incluso en polígonos con> 1000 lados.
De todos modos, son los primeros días, pero mi voto va al método de "cruces", que es más o menos lo que Mecki describe, creo. Sin embargo, lo encontré descrito y codificado más sucintamente por David Bourke . Me encanta que no se requiera trigonometría real, y funciona para convexos y cóncavos, y funciona razonablemente bien a medida que aumenta el número de lados.
Por cierto, aquí está una de las tablas de rendimiento del artículo de Eric Haines para interesar, probar en polígonos aleatorios.
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
Versión rápida de la respuesta de 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
}
}
Realmente me gusta la solución publicada por Nirg y editada por bobobobo. Acabo de hacerlo compatible con JavaScript y un poco más legible para mi uso:
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;
}
Trabajé un poco en esto cuando era investigador de Michael Stonebraker , ya sabes, el profesor que ideó Ingres , PostgreSQL , etc.
Nos dimos cuenta de que la forma más rápida era primero hacer un cuadro delimitador porque es SUPER rápido. Si está fuera del cuadro delimitador, está afuera. De lo contrario, haces el trabajo más duro ...
Si desea un gran algoritmo, busque el código fuente del proyecto de código abierto PostgreSQL para el trabajo geográfico ...
Quiero señalar que nunca tuvimos una idea de la diestra frente a la zurda (también expresable como un problema "interno" frente a "externo" ...
ACTUALIZAR
El enlace de BKB proporcionó una buena cantidad de algoritmos razonables. Estaba trabajando en problemas de Ciencias de la Tierra y, por lo tanto, necesitaba una solución que funcione en latitud / longitud, y tiene el peculiar problema de la mano: ¿el área está dentro del área más pequeña o más grande? La respuesta es que la "dirección" de las verticies importa: es zurda o diestra y de esta manera puede indicar cualquier área como "dentro" de cualquier polígono dado. Como tal, mi trabajo utilizó la solución tres enumerada en esa página.
Además, mi trabajo utilizaba funciones separadas para las pruebas "en línea".
... Ya que alguien preguntó: descubrimos que las pruebas de cuadro delimitador eran mejores cuando el número de vértices superaba algún número; haga una prueba muy rápida antes de hacer la prueba más larga si es necesario ... Se crea un cuadro delimitador simplemente tomando el x más grande, x más pequeña, y más grande y y más pequeña y juntarlas para formar cuatro puntos de una caja ...
Otro consejo para los que siguen: hicimos toda nuestra informática más sofisticada y de "atenuación de la luz" en un espacio de cuadrícula, todo en puntos positivos en un avión y luego volvimos a proyectarlo en longitud / latitud "real", evitando así posibles errores de cuando se cruza una línea 180 de longitud y cuando se manejan regiones polares. Funcionó genial!
La respuesta de David Segond es más o menos la respuesta general estándar, y la de Richard T es la optimización más común, aunque hay algunas otras. Otras optimizaciones fuertes se basan en soluciones menos generales. Por ejemplo, si va a verificar el mismo polígono con muchos puntos, triangular el polígono puede acelerar las cosas enormemente, ya que hay una serie de algoritmos de búsqueda TIN muy rápidos. Otra es que si el polígono y los puntos están en un plano limitado a baja resolución, digamos una visualización en pantalla, puede pintar el polígono en un búfer de visualización mapeado en memoria en un color dado, y verificar el color de un píxel dado para ver si se encuentra en los polígonos
Al igual que muchas optimizaciones, estas se basan en casos específicos en lugar de generales, y generan beneficios basados en el tiempo amortizado en lugar de un solo uso.
Trabajando en este campo, encontré que la Geometría de Computación de Joeseph O'Rourkes en C 'ISBN 0-521-44034-3 es de gran ayuda.
La solución trivial sería dividir el polígono en triángulos y probar probar los triángulos como se explica aquí.
Sin embargo, si su polígono es CONVEXO , podría haber un mejor enfoque. Mira el polígono como una colección de líneas infinitas. Cada línea divide el espacio en dos. para cada punto es fácil decir si está en un lado o en el otro lado de la línea. Si un punto está en el mismo lado de todas las líneas, entonces está dentro del polígono.
Me doy cuenta de que esto es antiguo, pero aquí hay un algoritmo de proyección de rayos implementado en Cocoa, en caso de que alguien esté interesado. No estoy seguro de que sea la forma más eficiente de hacer las cosas, pero puede ayudar a alguien.
- (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;
}
Versión Obj-C de la respuesta de nirg con un método de muestra para puntos de prueba. La respuesta de Nirg funcionó bien para mí.
- (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");
}
}
CGPathContainsPoint()
es tu amigo.
CGPathContainsPoint()
No hay nada más bello que una definición inductiva de un problema. En aras de la exhaustividad, aquí tiene una versión en prólogo que también podría aclarar los pensamientos detrás del lanzamiento de rayos :
Basado en la simulación del algoritmo de simplicidad en http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
Algunos predicados auxiliares:
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]).
La ecuación de una línea dada 2 puntos A y B (Línea (A, B)) es:
(YB-YA)
Y - YA = ------- * (X - XA)
(XB-YB)
Es importante que la dirección de rotación de la línea se establezca en sentido horario para los límites y antihorario para los agujeros. Vamos a verificar si el punto (X, Y), es decir, el punto probado está en el semiplano izquierdo de nuestra línea (es cuestión de gustos, también podría ser el lado derecho, pero también la dirección de los límites las líneas deben cambiarse en ese caso), esto es para proyectar el rayo desde el punto a la derecha (o izquierda) y reconocer la intersección con la línea. Hemos optado por proyectar el rayo en dirección horizontal (de nuevo es una cuestión de gustos, también podría hacerse en vertical con restricciones similares), por lo que tenemos:
(XB-XA)
X < ------- * (Y - YA) + XA
(YB-YA)
Ahora necesitamos saber si el punto está en el lado izquierdo (o derecho) del segmento de línea solamente, no en todo el plano, por lo que debemos restringir la búsqueda solo a este segmento, pero esto es fácil ya que estar dentro del segmento solo un punto en la línea puede ser más alto que Y en el eje vertical. Como esta es una restricción más fuerte, debe ser el primero en verificar, por lo que tomamos primero solo aquellas líneas que cumplen con este requisito y luego verificamos su posición. Según el teorema de la curva de Jordan, cualquier rayo proyectado en un polígono debe intersecarse en un número par de líneas. Así que hemos terminado, tiraremos el rayo hacia la derecha y luego, cada vez que cruce una línea, cambiará su estado. Sin embargo, en nuestra implementación, vamos a verificar la longitud de la bolsa de soluciones que cumplan con las restricciones dadas y decidir la incorporación al respecto. para cada línea en el polígono esto debe hacerse.
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).
La versión C # de la respuesta de nirg está aquí: compartiré el código. Puede salvar a alguien algo de tiempo.
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;
}
Versión 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;
}
}
Puerto neto:
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;
}
}
VBA VERSIÓN:
Nota: Recuerde que si su polígono es un área dentro de un mapa, Latitud / Longitud son valores Y / X en lugar de X / Y (Latitud = Y, Longitud = X) debido a lo que entiendo son implicaciones históricas desde hace mucho tiempo. La longitud no era una medida.
MÓDULO DE CLASE: 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
MÓDULO:
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
He hecho una implementación de Python de la nirg C ++ código :
Entradas
bounding_box_positions: puntos candidatos para filtrar. (En mi implementación creada desde el cuadro delimitador.
(Las entradas son listas de tuplas en el formato: [(xcord, ycord), ...]
)
Devoluciones
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
Nuevamente, la idea se toma de aquí.
Sorprendido, nadie mencionó esto antes, pero para los pragmáticos que requieren una base de datos: MongoDB tiene un excelente soporte para consultas Geo, incluida esta.
Lo que estás buscando es:
db.neighborhoods.findOne ({geometry: {$ geoIntersects: {$ geometry: {type: "Point", coordenadas: ["longitud", "latitud"]}}}})
Neighborhoods
es la colección que almacena uno o más polígonos en formato estándar GeoJson. Si la consulta devuelve un valor nulo, no se intersecta, de lo contrario lo es.
Muy bien documentado aquí: https://docs.mongodb.com/manual/tutorial/geospatial-tutorial/
El rendimiento de más de 6,000 puntos clasificados en una cuadrícula de 330 polígonos irregulares fue de menos de un minuto sin optimización alguna e incluido el tiempo para actualizar documentos con sus respectivos polígonos.
Aquí hay un punto en la prueba de polígono en C que no está utilizando la proyección de rayos. Y puede funcionar para áreas superpuestas (auto intersecciones), vea el use_holes
argumento.
/* 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];
}
Nota: este es uno de los métodos menos óptimos, ya que incluye muchas llamadas a atan2f
, pero puede ser de interés para los desarrolladores que leen este hilo (en mis pruebas es ~ 23 veces más lento que el método de intersección de línea).
Para detectar el impacto en el polígono, debemos probar dos cosas:
Para tratar los siguientes casos especiales en el algoritmo de fundición de Ray :
Verifique Determinar si un punto está dentro de un polígono complejo . El artículo proporciona una manera fácil de resolverlos para que no se requiera un tratamiento especial para los casos anteriores.
Puede hacerlo comprobando si el área formada al conectar el punto deseado a los vértices de su polígono coincide con el área del polígono mismo.
O podría verificar si la suma de los ángulos internos desde su punto a cada par de dos vértices poligonales consecutivos a su punto de verificación suma 360, pero tengo la sensación de que la primera opción es más rápida porque no implica divisiones ni cálculos de inverso de funciones trigonométricas.
No sé qué sucede si su polígono tiene un agujero en su interior, pero me parece que la idea principal puede adaptarse a esta situación.
También puede publicar la pregunta en una comunidad matemática. Apuesto a que tienen un millón de formas de hacerlo
Si está buscando una biblioteca java-script, hay una extensión javascript google maps v3 para la clase Polygon para detectar si un punto reside o no dentro de ella.
var polygon = new google.maps.Polygon([], "#000000", 1, 1, "#336699", 0.3);
var isWithinPolygon = polygon.containsLatLng(40, -90);
Cuando usas qt(Qt 4.3+), uno puede usar la función QPolygon contienePoint
La respuesta depende de si tiene los polígonos simples o complejos. Los polígonos simples no deben tener ninguna intersección de segmento de línea. Entonces pueden tener los agujeros pero las líneas no pueden cruzarse entre sí. Las regiones complejas pueden tener las intersecciones de línea, por lo que pueden tener las regiones superpuestas, o regiones que se tocan entre sí con un solo punto.
Para polígonos simples, el mejor algoritmo es el algoritmo de fundición de rayos (número de cruce). Para polígonos complejos, este algoritmo no detecta puntos que están dentro de las regiones superpuestas. Entonces, para los polígonos complejos, debe usar el algoritmo de número de Winding.
Aquí hay un excelente artículo con implementación en C de ambos algoritmos. Los probé y funcionan bien.
Versión Scala de la solución por nirg (se supone que la verificación previa del rectángulo delimitador se realiza por separado):
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)
}
Aquí está la versión golang de @nirg answer (inspirada en el código C # por @@ m-katz)
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
}