Я намагаюся створити швидку 2D точку в алгоритмі полігону, для використання в хіт-тесті (наприклад Polygon.contains(p:Point)
). Пропозиції щодо ефективних методик будуть вдячні.
Я намагаюся створити швидку 2D точку в алгоритмі полігону, для використання в хіт-тесті (наприклад Polygon.contains(p:Point)
). Пропозиції щодо ефективних методик будуть вдячні.
Відповіді:
Для графіки я не віддаю перевагу цілим числам. Багато систем використовують цілі числа для малювання в інтерфейсі користувача (пікселі все-таки є ints), але macOS, наприклад, використовує float для всього. macOS знає лише точки, і точка може переводитись на один піксель, але залежно від роздільної здатності монітора, це може перекласти щось інше. На екранах сітківки половина точки (0,5 / 0,5) - піксель. Проте я ніколи не помічав, що користувальницькі інтерфейси macOS значно повільніші за інші інтерфейси. Зрештою, 3D-API (OpenGL або Direct3D) також працює з поплавками, а сучасні графічні бібліотеки дуже часто використовують переваги прискорення GPU.
Тепер ви сказали, що швидкість - це ваша головна турбота, добре, давайте продовжимо швидкість. Перш ніж запустити будь-який складний алгоритм, спочатку зробіть простий тест. Створіть навколо свого полігону обмежувальне поле, вирівняне по осі . Це дуже просто, швидко і вже може вберегти вас від багатьох розрахунків. Як це працює? Ітерації над усіма точками багатокутника і знайдіть значення min / max 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!
}
Це перший тест, який виконується для будь-якої точки. Як бачите, цей тест є надшвидким, але також дуже грубим. Для обробки точок, що знаходяться в межах обмежуючого прямокутника, нам потрібен більш складний алгоритм. Існує кілька способів, як це можна обчислити. Який метод працює, також залежить від того, чи може багатокутник мати отвори чи завжди буде суцільним. Ось приклади суцільних (один опуклий, один увігнутий):
І ось один з отвором:
У зеленому є отвір посередині!
Найпростіший алгоритм, який може впоратися з усіма трьома вищевикладеними випадками і все ще досить швидким, називається рейндж-кастинг . Ідея алгоритму досить проста: намалюйте віртуальний промінь з будь-якої точки поза полігоном до вашої точки і порахуйте, як часто він потрапляє в бік полігона. Якщо кількість ударів парне, воно знаходиться поза полігоном, якщо воно непарне - це всередині.
Алгоритм намотування чисел був би альтернативою, він більш точний, коли точки наближаються до лінії багатокутника, але це також набагато повільніше. Рейтове лиття може бути невдалим для очок, занадто близьких до сторони багатокутника через обмежену точність плаваючої точки та проблеми округлення, але насправді це навряд чи є проблемою, так як якщо точка лежить близько до сторони, це часто візуально навіть неможливо для глядач розпізнає, чи є він всередині або все ще зовні.
У вас ще є обмежувальний ящик зверху, пам’ятаєте? Просто виберіть точку поза межами рамки та використовуйте її як вихідну точку для вашого променя. Наприклад, точка (Xmin - e/p.y)
знаходиться поза полігоном точно.
Але що таке e
? Ну, e
(насправді епсілон) надає обмежувальній коробці деяку підкладку . Як я вже говорив, трасування променів провалюється, якщо ми починаємо занадто близько до лінії багатокутника. Оскільки обмежувальний ящик може дорівнювати багатокутнику (якщо полігон є прямокутником, орієнтованим на вісь, то обмежувальний ящик дорівнює самому багатокутнику!), Нам потрібно трохи прокладки, щоб зробити це безпечним, ось і все. Яку велику слід вибрати 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 ( v1x1/v1y1
і v1x2/v1y2
) та вектора 2 ( v2x1/v2y1
і v2x2/v2y2
). Отже, у вас є 2 вектори, 4 точки, 8 координат. YES
і NO
зрозумілі. YES
збільшує перехрестя, NO
нічого не робить.
А що з КОЛЛІНАРОМ? Це означає, що обидва вектори лежать на одній нескінченній лінії, залежно від положення та довжини, вони взагалі не перетинаються або перетинаються у нескінченній кількості точок. Я не зовсім впевнений, як поводитися з цим випадком, я б не вважав це перехрестям в будь-якому випадку. Що ж, такий випадок на практиці досить рідкісний через помилки округлення плаваючої точки; кращий код, ймовірно, не перевірятиме, == 0.0f
а натомість для чогось подібного < epsilon
, де epsilon - досить невелика кількість.
Якщо вам потрібно перевірити більшу кількість балів, ви, звичайно, можете трохи прискорити все, зберігаючи в пам’яті стандартні форми лінійних рівнянь сторін полігону, тому вам не доведеться їх перераховувати кожен раз. Це дозволить вам заощадити два множення плаваючої точки та три віднімання плаваючої точки на кожному тесті в обмін на збереження в пам'яті трьох значень плаваючої точки на стороні багатокутника. Це типова компенсація часу на пам'ять проти обчислень.
І останнє, але не менш важливе: якщо для вирішення проблеми ви можете використовувати апаратне забезпечення 3D, є цікава альтернатива. Просто дозвольте GPU зробити всю роботу за вас. Створіть поверхню фарбування, яка відключена. Заповніть його повністю кольором чорного. Тепер дозвольте OpenGL або Direct3D пофарбувати ваш багатокутник (або навіть усі ваші багатокутники, якщо ви просто хочете перевірити, чи крапка знаходиться в будь-якому з них, але вам не байдуже, який саме), і заповніть багатокутник іншим колір, наприклад, білий. Щоб перевірити, чи є точка у полігоні, знайдіть колір цієї точки з поверхні малювання. Це лише вибір O (1) пам'яті.
Звичайно, цей метод корисний лише у тому випадку, якщо поверхня малюнка не повинна бути величезною. Якщо він не може вписатися в пам'ять GPU, цей спосіб проходить повільніше, ніж робити це на процесорі. Якщо це повинно бути величезним, і ваш GPU підтримує сучасні шейдери, ви все одно можете використовувати GPU, застосувавши кастинг променів, показаний вище як шейдер GPU, що абсолютно можливо. Для більшої кількості полігонів або великої кількості балів для тестування це окупиться, вважайте, деякі графічні процесори зможуть тестувати 64 - 256 балів паралельно. Зауважте, однак, що передача даних з процесора в GPU і назад завжди дорога, тому для тестування пари очок на пару простих полігонів, де точки або полігони є динамічними і часто змінюються, підхід GPU рідко платить вимкнено.
Я думаю, що наступний фрагмент коду - найкраще рішення (взято звідси ):
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;
}
Це і коротко, і ефективно, і працює як для опуклих, так і увігнутих багатокутників. Як було запропоновано раніше, слід спочатку перевірити обмежуючий прямокутник і обробити багатокутні отвори окремо.
Ідея цього досить проста. Автор описує це так:
Я запускаю напівскінченний промінь по горизонталі (збільшуючи х, фіксований у) від точки тестування і рахую, скільки ребер він перетинається. При кожному перетині промінь перемикається між всередині і зовні. Це називається теоремою кривої Йордана.
Змінна c перемикається з 0 на 1 і 1 на 0 щоразу, коли горизонтальний промінь перетинає будь-який край. Так що в основному це відстеження того, чи число перекреслених країв парне чи непарне. 0 означає парне і 1 означає непарне.
verty[i]
і verty[j]
є обома сторонами testy
, тому вони ніколи не є рівними.
Ось версія C # відповіді, даної nirg , яка походить від цього професора RPI . Зауважте, що використання коду з цього джерела RPI вимагає атрибуції.
У верхній частині додано обмежувальний прапорець. Однак, як зазначає Джеймс Браун, основний код майже такий же швидкий, як і сама перевірка обмежувального поля, тому перевірка обмежувального поля може насправді уповільнити загальну роботу, якщо ви бачите, що більшість точок, які ви перевіряєте, знаходяться всередині обмежувального поля . Таким чином, ви можете залишити обмежувальне поле, щоб перевірити, або альтернативою може бути попередньо обчислити обмежувальні поля ваших багатокутників, якщо вони не змінюють форму занадто часто.
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;
}
Ось варіант 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;
}
Обчисліть орієнтовану суму кутів між точкою p та кожним з вершин багатокутника. Якщо загальний орієнтований кут становить 360 градусів, точка знаходиться всередині. Якщо загальна сума 0, точка знаходиться поза.
Мені подобається цей метод краще, тому що він більш міцний і менш залежить від числової точності.
Методи, які обчислюють рівномірність кількості перехресть, обмежені, оскільки ви можете «вдарити» по вершині під час обчислення кількості перехресть.
EDIT: До речі, цей метод працює з увігнутими та опуклими багатокутниками.
EDIT: Нещодавно я знайшов цілу статтю у Вікіпедії на цю тему.
Це питання таке цікаве. У мене є ще одна працездатна ідея, відмінна від інших відповідей на цю посаду. Ідея полягає у використанні суми кутів, щоб визначити, ціль знаходиться всередині чи зовні. Більш відомий як обмотка номер .
Нехай х - цільова точка. Нехай масив [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
Стаття Еріка Хейнса, яку цитує bobobobo, справді відмінна. Особливо цікавими є таблиці, що порівнюють продуктивність алгоритмів; метод підсумовування кутів дійсно поганий порівняно з іншими. Також цікаво те, що такі оптимізації, як використання сітки пошуку для подальшого підрозділу полігону на "в" та "поза" сектори, можуть зробити тест неймовірно швидким навіть на полігонах з> 1000 сторонами.
У будь-якому випадку, це ранні дні, але моє голосування йде за методом "перетину", який, напевно, описує Мецький. Однак я знайшов це найбільш вдало описаним та кодифікованим Девідом Борком . Мені подобається, що не потрібна реальна тригонометрія, і вона працює на опуклу і увігнуту, і вона працює досить добре, оскільки кількість сторін збільшується.
До речі, ось одна із таблиць продуктивності із статті Еріка Хейнса, яка цікавить, тестуючи на випадкових багатокутників.
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
Швидка версія відповіді 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
}
}
Дуже подобається рішення, розміщене 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;
}
Я трохи працював над цим, коли я був дослідником Майкла Стоунбракера - ви знаєте, професор, який придумав Інгреса , PostgreSQL тощо.
Ми зрозуміли, що найшвидшим способом було спочатку зробити обмежувальну коробку, оскільки це СУПЕР швидко. Якщо вона знаходиться поза межами рамки, вона знаходиться поза. Інакше ти робиш більш важку роботу ...
Якщо ви хочете чудовий алгоритм, зверніть увагу на вихідний код проекту PostgreSQL з відкритим кодом для гео-роботи ...
Я хочу зазначити, що ми ніколи не розуміли правильності та ліворукості (також виражається як проблема "всередині" проти "зовні" ...
ОНОВЛЕННЯ
Посилання BKB дало велику кількість розумних алгоритмів. Я працював над проблемами науки про Землю, і тому мені було потрібне рішення, яке працює в широті / довготі, і це особлива проблема рукотворності - це площа всередині меншої площі чи більшої площі? Відповідь полягає в тому, що "напрямок" вершин має значення - це ліворуч або праворук, і таким чином ви можете вказати будь-яку область як "всередині" будь-якого даного багатокутника. Як такий, моя робота використовувала рішення три, перелічене на цій сторінці.
Крім того, моя робота використовувала окремі функції для тестів "на лінії".
... Оскільки хтось запитав: ми з'ясували, що тести на обмежувальну коробку найкращі, коли кількість вершин перевищує деяку кількість - зробіть дуже швидкий тест, перш ніж робити довший тест, якщо це необхідно ... Обмежувальне поле створюється простим взяттям найбільший х, найменший х, найбільший у і найменший у і склавши їх разом, щоб скласти чотири точки коробки ...
Ще одна порада для тих, що випливають: ми зробили всі наші більш досконалі та «затуманення світла» обчислення в просторі сітки, все в позитивних точках на площині, а потім повторно проектували назад у «реальну» довготу / широту, таким чином уникаючи можливих помилок обертання навколо однієї перекресленої лінії 180 довготи і при поводженні з полярними областями. Працювали чудово!
Відповідь Девіда Сегонда - це майже стандартна загальна відповідь, а Річард Т - найпоширеніша оптимізація, хоча там є деякі інші. Інші сильні оптимізації базуються на менш загальних рішеннях. Наприклад, якщо ви збираєтеся перевірити той самий багатокутник з великою кількістю точок, тріангулювання багатокутника може значно прискорити роботу, оскільки існує ряд дуже швидких алгоритмів пошуку TIN. Інша справа, якщо багатокутник і точки знаходяться на обмеженій площині з низькою роздільною здатністю, скажімо, на екрані, ви можете пофарбувати полігон на відображений в пам'яті буфер відображення в заданий колір і перевірити колір даного пікселя, щоб побачити, чи лежить він у полігонах.
Як і багато оптимізацій, вони базуються на конкретних, а не на загальних випадках, а користь для вигідності базується на амортизованому часі, а не на разовому використанні.
Працюючи в цій галузі, я знайшов відмінною підмогою Джозефа О'Руркеса «Обчислювальна геометрія в C» ISBN 0-521-44034-3.
Тривіальним рішенням було б поділити багатокутник на трикутники і вдарити тест на трикутники, як пояснено тут
Якщо ваш багатокутник CONVEX , можливо, буде кращий підхід. Подивіться на полігон як на сукупність нескінченних ліній. Кожен рядок ділить простір на два. для кожного пункту легко сказати, чи є його з тієї чи іншої сторони лінії. Якщо точка знаходиться на одній стороні всіх ліній, то вона знаходиться всередині многокутника.
Я усвідомлюю, що це старе, але ось алгоритм рентгенівського лиття, реалізований у Какао, на випадок, коли хтось зацікавлений. Не впевнений, що це найефективніший спосіб робити речі, але це може допомогти комусь.
- (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;
}
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");
}
}
CGPathContainsPoint()
- твій друг.
CGPathContainsPoint()
Немає нічого кращого, ніж спонукальне визначення проблеми. Для повноти тут ви маєте версію в пролозі, яка також може уточнити припущення про кастинг :
На основі моделювання алгоритму простоти в 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 (Рядок (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).
C # версія відповіді nirg тут: я просто поділюсь кодом. Це може заощадити когось деякий час.
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;
}
Версія 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;
}
}
.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;
}
}
ВЕРСІЯ 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
Я зробив реалізацію Python з nirg в C ++ код :
Вхідні дані
limiting_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
І знову ідея взята звідси
Здивувавшись, ніхто раніше цього не піднімав, але для прагматиків, які потребують бази даних: MongoDB має чудову підтримку для гео запитів, включаючи цей.
Що ви шукаєте:
db.neighborhoods.findOne ({геометрія: {$ geoIntersects: {$ геометрія: {тип: "Точка", координати: ["довгота", "широта"]}}}})
Neighborhoods
це колекція, яка зберігає один або кілька полігонів у стандартному форматі GeoJson. Якщо запит повертає нуль, він не перетинається інакше.
Дуже добре задокументовано тут: https://docs.mongodb.com/manual/tutorial/geospatial-tutorial/
Продуктивність для більш ніж 6000 балів, класифікованих в 330 неправильній багатокутній сітці, була менше однієї хвилини, без оптимізації взагалі і включаючи час на оновлення документів на відповідному полігоні.
Ось крапка в полігоновому тесті на 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
, але розробникам, які читають цю нитку, це може зацікавити (у моїх тестах його ~ 23x повільніше, ніж використовувати метод перетину ліній).
Для виявлення хіта на Polygon нам потрібно перевірити дві речі:
Для вирішення наступних спеціальних випадків в алгоритмі реєстрування Рея :
Перевірте, чи визначається, чи знаходиться точка всередині складного багатокутника . У статті запропоновано простий спосіб їх вирішити, щоб не було необхідного спеціального лікування для вищезазначених випадків.
Це можна зробити, перевіривши, чи відповідає площа, утворена підключенням потрібної точки до вершин вашого багатокутника, площі самого полігону.
Або ви можете перевірити, чи сума внутрішніх кутів від вашої точки до кожної пари двох послідовних вершин багатокутника до вашої контрольної точки становить 360, але я маю відчуття, що перший варіант швидше, тому що він не передбачає поділів та обчислень. зворотних тригонометричних функцій.
Я не знаю, що станеться, якщо ваш полігон має в ньому отвір, але мені здається, що основну ідею можна адаптувати до цієї ситуації
Ви також можете опублікувати питання в математичному співтоваристві. Надіюсь, у них є один мільйон способів зробити це
Якщо ви шукаєте бібліотеку java-скриптів, то для класу Polygon розширення google maps v3 для JavaScript визначає, чи знаходиться в ньому точка чи ні.
var polygon = new google.maps.Polygon([], "#000000", 1, 1, "#336699", 0.3);
var isWithinPolygon = polygon.containsLatLng(40, -90);
При використанні qt(Qt 4.3+), можна використовувати функцію QPolygon в containsPoint
Відповідь залежить від того, чи є у вас прості чи складні багатокутники. Прості багатокутники не повинні мати жодних перетинів відрізка ліній. Таким чином, вони можуть мати отвори, але лінії не можуть перетинатися одна з одною. Складні регіони можуть мати перехрестя ліній - тому вони можуть мати області, що перекриваються, або регіони, які торкаються один одного лише однією точкою.
Для простих багатокутників найкращим алгоритмом є алгоритм відливання Рея (номер перехрещення). Для складних багатокутників цей алгоритм не виявляє точки, які знаходяться всередині областей, що перекриваються. Отже, для складних багатокутників потрібно використовувати алгоритм числення Winding.
Ось чудова стаття із С реалізацією обох алгоритмів. Я спробував їх, і вони добре працюють.
Версія рішення Scala за допомогою nirg (передбачається, що попередня перевірка обмеження прямокутника проводиться окремо):
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 (натхненна кодом C # @@ 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
}