Как я могу определить, находится ли 2D точка внутри многоугольника? - PullRequest
459 голосов
/ 20 октября 2008

Я пытаюсь создать быструю 2D точку внутри алгоритма многоугольника для использования при тестировании попаданий (например, Polygon.contains(p:Point)). Буду признателен за эффективные методы.

Ответы [ 32 ]

675 голосов
/ 20 октября 2008

Для графики я бы предпочел не использовать целые числа. Многие системы используют целые числа для рисования пользовательского интерфейса (в конце концов, пиксели - это целые числа), но, например, macOS использует float для всего. macOS знает только точки, и точка может переводиться в один пиксель, но в зависимости от разрешения монитора она может переводиться во что-то другое. На экранах сетчатки половина точки (0,5 / 0,5) составляет пиксель. Тем не менее, я никогда не замечал, что пользовательские интерфейсы macOS значительно медленнее, чем другие пользовательские интерфейсы. Ведь 3D API (OpenGL или Direct3D) также работают с плавающими элементами, а современные графические библиотеки очень часто используют преимущества ускорения графического процессора.

Теперь вы сказали, что скорость - ваша главная задача, хорошо, давайте перейдем к скорости. Прежде чем запускать какой-либо сложный алгоритм, сначала выполните простой тест. Создайте ограничивающую рамку с выравниванием по оси вокруг многоугольника. Это очень легко, быстро и уже может обезопасить вас от многих вычислений. Как это работает? Выполните итерацию по всем точкам многоугольника и найдите минимальные / максимальные значения 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

Зеленый имеет отверстие в середине!

Самый простой алгоритм, который может обрабатывать все три описанных выше случая и который все еще довольно быстр, называется приведение лучей . Идея алгоритма довольно проста: нарисуйте виртуальный луч из любой точки за пределами многоугольника к своей точке и посчитайте, как часто он попадает на сторону многоугольника. Если число совпадений четное, оно находится за пределами многоугольника, если оно нечетное, оно внутри.

Demonstrating how the ray cuts through a polygon

Алгоритм с числом обмоток был бы альтернативой, он более точен для точек, находящихся очень близко к линии многоугольника, но он также намного медленнее. Приведение лучей может быть неудачным для точек, расположенных слишком близко к стороне многоугольника из-за ограниченной точности с плавающей точкой и проблем с округлением, но в действительности это вряд ли проблема, так как если точка находится близко к стороне, это часто визуально даже невозможно зритель распознает, находится ли он уже внутри или все еще снаружи.

У вас все еще есть ограничительная рамка выше, помните? Просто выберите точку за пределами ограничительной рамки и используйте ее в качестве отправной точки для вашего луча. Например. точка (Xmin - e/p.y) точно находится за пределами многоугольника.

Но что такое e? Ну, e (на самом деле epsilon) дает ограничивающей рамке padding . Как я уже сказал, трассировка лучей завершится неудачей, если мы начнем слишком близко к линии многоугольника. Поскольку ограничивающий прямоугольник может равняться многоугольнику (если многоугольник является выровненным по оси прямоугольником, ограничивающий прямоугольник равен самому многоугольнику!), Для обеспечения безопасности нам понадобятся некоторые отступы, вот и все. Насколько большой вы должны выбрать 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)
:

Вам нужно проверить луч со всех сторон. Считайте, что луч - вектор, а каждая сторона - вектор. Луч должен поразить каждую сторону ровно один раз или никогда. Он не может попасть в одну и ту же сторону дважды. Две линии в 2D-пространстве всегда будут пересекаться ровно один раз, если они не параллельны, и в этом случае они никогда не пересекаются. Однако, поскольку векторы имеют ограниченную длину, два вектора могут не быть параллельными и никогда не пересекаться, потому что они слишком короткие, чтобы когда-либо встречаться друг с другом.

// 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) выборка из памяти.

Конечно, этот метод можно использовать, только если ваша поверхность для рисования не обязательно должна быть огромной. Если он не может поместиться в память графического процессора, этот метод медленнее, чем в ЦП. Если он должен быть огромным, и ваш графический процессор поддерживает современные шейдеры, вы все равно можете использовать графический процессор, реализовав приведенное выше приведение лучей в качестве графического шейдера, что абсолютно возможно. Для большого количества полигонов или большого количества точек для тестирования это окупится, учитывая, что некоторые графические процессоры смогут тестировать от 64 до 256 точек параллельно. Тем не менее, обратите внимание, что передача данных из CPU в GPU и обратно всегда обходится дорого, поэтому для простого тестирования пары точек на пару простых многоугольников, где либо точки, либо многоугольники являются динамическими и будут часто меняться, подход с использованием графического процессора редко платит выкл.

560 голосов
/ 27 мая 2010

Я думаю, что следующий фрагмент кода - лучшее решение (взято из здесь ):

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 означает нечетное.

61 голосов
/ 06 мая 2013

Вот версия C # 1001 * ответа от 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;
    }

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

    return inside;
}
43 голосов
/ 05 июля 2013

Вот вариант ответа М. Каца на 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;
}
28 голосов
/ 20 октября 2008

Вычислить ориентированную сумму углов между точкой p и каждым из вершин многоугольника. Если общий угол ориентирования составляет 360 градусов, точка находится внутри. Если сумма равна 0, точка находится вне.

Мне больше нравится этот метод, потому что он более надежный и менее зависит от числовой точности.

Методы, которые вычисляют равномерность количества пересечений, ограничены, потому что вы можете «ударить» вершину во время вычисления количества пересечений.

РЕДАКТИРОВАТЬ: Кстати, этот метод работает с вогнутыми и выпуклыми многоугольниками.

РЕДАКТИРОВАТЬ: я недавно нашел целую статью Википедии по теме.

18 голосов
/ 29 декабря 2009

Статья Эрика Хейнса , цитируемая Бобобо, действительно превосходна. Особенно интересны таблицы, сравнивающие производительность алгоритмов; метод суммирования углов действительно плох по сравнению с другими. Также интересно то, что оптимизация, например использование поисковой сетки для дальнейшего разбиения полигона на секторы «in» и «out», может сделать тест невероятно быстрым даже на полигонах с> 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
17 голосов
/ 06 мая 2017

Этот вопрос очень интересный. У меня есть другая работоспособная идея, отличная от других ответов этого поста.

Идея состоит в том, чтобы использовать сумму углов, чтобы решить, находится ли цель внутри или снаружи. Если цель находится внутри области, сумма углов, образованных целью и каждыми двумя граничными точками, будет 360. Если цель находится снаружи, сумма не будет 360. Угол имеет направление. Если угол идет назад, он отрицательный. Это так же, как вычисление числа обмоток .

Обратитесь к этому изображению, чтобы получить общее представление о идее: enter image description here

Мой алгоритм предполагает, что по часовой стрелке это положительное направление. Вот потенциальный вклад:

[[-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
8 голосов
/ 25 мая 2015

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
    }
}
7 голосов
/ 05 июля 2013

Очень нравится решение, опубликованное 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;
}
7 голосов
/ 20 октября 2008

Я работал над этим, когда был исследователем в Майкл Стоунбрейкер - вы знаете, профессор, который придумал Ingres , PostgreSQL , и т.д.

Мы поняли, что самым быстрым способом было сначала создать ограничивающий прямоугольник, потому что он СУПЕР быстрый. Если это вне ограничительной рамки, это снаружи. В противном случае, вы делаете тяжелую работу ...

Если вам нужен отличный алгоритм, обратитесь к исходному коду PostgreSQL с открытым исходным кодом для гео-работы ...

Я хочу отметить, что мы никогда не понимали право против левши (также выражается как «внутри» против «внешней» проблемы ...


UPDATE

Ссылка БКБ предоставила большое количество разумных алгоритмов. Я работал над проблемами наук о Земле и поэтому нуждался в решении, которое работает по широте / долготе, и у него есть особая проблема с ручностью - область внутри меньшей области или большей области? Ответ заключается в том, что «направление» вершин имеет значение - оно либо левостороннее, либо правостороннее, и таким образом вы можете указать любую область как «внутри» любого данного многоугольника. Таким образом, моя работа использовала решение три, перечисленное на этой странице.

Кроме того, в моей работе использовались отдельные функции для тестов «на линии».

... Так как кто-то спросил: мы выяснили, что тесты ограничивающего прямоугольника были наилучшими, когда число вершин превысило некоторое число - выполните очень быстрый тест, прежде чем выполнять более длинный тест, если это необходимо ... Ограничивающий прямоугольник создается просто беря самый большой x, наименьший x, наибольший y и наименьший y и складывая их вместе, чтобы получить четыре точки коробки ...

Еще один совет для тех, кто следует: мы выполнили все наши более сложные и «светорегуляторные» вычисления в пространстве сетки, все в положительных точках на плоскости, а затем снова спроецировали обратно в «реальную» долготу / широту, таким образом избегая возможные ошибки переноса при пересечении линии 180 долготы и при работе с полярными областями. Работал отлично!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...