Comments 109

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

А вот триангуляция произвольного полигона в общем случае — не такая уж тривиальная задача, как может показаться на первый взгляд.
З.Ы. Надеюсь не надо рассказывать как проверить пересекаются ли 2 отрезка.
Достаточно проверить с какой стороны относительно каждого ребра лежит точка. Как только обнаруживается ребро, относительно которого точка лежит с противоположной стороны, можно перебор прекращать, так как точка за пределами многогранника. Соответственно, если точка лежит по одну сторону от всех рёбер, то она лежит внутри.
Сторона определяется знаком векторного произведения (P2 — P1)^(P — P1), где Pi — точки многогранника, а P — тестируемая точка.

Красный луч — два пересечения, но точка внутри полигона. Для устранения ошибки достаточно не считать пересечение с вершиной. Но тогда получаем следующее. Синий луч — одно пересечение с вершиной. Вершину не считаем, итого ноль пересечений.
Вот вам и неопределенность
Более того, есть еще случай, когда все ребро лежит на луче, его тоже неплохо было бы рассмотреть отдельно.
Ребро лежит на луче — это интересный случай, его разрешение зависит от того, считать точку, лежащую на ограничивающей кривой, принадлежащей фигуре. Если да — то случай сводится к лучу, проходящему через вершину (для его разрешения возможны варианты), если нет — то вершину не считаем.
По другому варианту, не требующему повторного пуска луча, в точке пересечения нужно считать производную функции полигона (для полигона весьма тривиальная задача), если она равна нулю — то вершина есть экстремум и ее считать не надо.
Это не проблема. Если касание мы считаем пересечением, то тут два пересечения в точке касания, и на четность не повлияет. Если не считаем, то ноль пересечений в точке касания и тоже не повлияет на четность.
Алгоритм в итоге сводится к проверке не лежит ли точка на границе полигона, а если нет, то просто считаем число пересечений. Касания вершин считаем либо за два пересечения, либо за ноль. Не важно какой из двух вариантов выберем.
А вы почему-то предложили касание считать один раз, но ведь касание луча присходит с двумя отрезками (рёбрами, сторонами).
А алгоритмически как это сделать? Как различить случаи, нарисованные выше?
Вот тут детально изложено: https://en.wikipedia.org/wiki/Point_in_polygon
Более универсальным и простым решением будет https://en.wikipedia.org/wiki/Winding_number
Прямо привет из прошлого )
Если считать что за ноль, что за 2 - получится четное количество пересечений для синего луча
Да, лучше, конечно, для определения внутри или снаружи полгона использовать алгоритм https://en.wikipedia.org/wiki/Winding_number
Если луч попадает в вершину, смотрим другие концы сторон при этой вершине: по одну они сторону от луча или по разные.
Не надо бояться попасть в вершину. Там всё просто. Если луч проходит через вершину (а в вершине сходится два отрезка), то он либо две стороны пересекает либо ноль. Это не важно для алгоритма, лишь бы мы везде одинаково это воспринимали. Например везде считать, что край отрезка входит в отрезок. Это значит касание луча вершины всегда будет добаалять 2 пересечения.
Удобнее ортогональный луч пускать, но да, не важно в какую сторону, хоть по диагонали.
Ортогональный удобнее потому, что пересечения искать легче.
Кстати, есть ешё интересный кейс когда одна из граней лежит на нём частично или полностью.
Но такой кейс тоже легко решается. Алгоритм примерно такой: перебираем все грани и считаем число пересечений луча с каждой гранью.
Считаем сколько концов грани лежит на луче. К этому числу прибавляем 1, если грань пересекается с лучом не будучи параллельной ему.
Суммируем для каждой грани и проверяем на четность. Всё.
есть более подробное описание метода?
А в этом случае у нас тоже самое на плоскости — теорема Стокса (хотя, вообще говоря, обе теоремы — частные случаи обощенной формулы Стокса для дифференциальных форм).
Возможно, связь основных векторных теорем и лучей в случайные стороны неочевидна на первый взгляд, но все так :) А насчет крайнего случая с попаданием луча в вершину — так в математике такого не бывает))
Это почему, простите?
Так будет лучше?
Хотя, в общем, это вопрос терминологии — считать ли точку на ограничивающей кривой лежащей внутри фигуры. В теории обычно — нет, на практике чаще всего — да.
Знаете, это все довольно забавно. Изначально задача относится к прикладным задачам численных методов (назовем это так — хотя бы потому что речь идет о решении ее на компьютере), автор статьи решил очень успешно приткнул теорию функций комплексной переменной, а мне все электродинамика мерещится :)
Точно также меня беспокоит идея интегрирования по произвольным н-мерным многообразиям с помощью дифференциальных форм и обобщенной формулы Стокса — уж больно элегантно и соблазнительно выглядит оператор границы. Но если мы возьмем хотя бы н-мерную сферу — хороший метод через квадрат интеграла Пуассона или плохой через н-мерный якобиан сферических координат окажутся во многие-многие разы быстрее. Потому что внешние формы сильно связаны с определителями, а определитель — уж очень дорогая операция.
Оперирует цифрами порядка:
— тысяча точек в полигоне границы
— 5-6 миллионов точек всего
— 3-4 миллиона точек попадает в границы
Сами посчитаете количество операций на решение?
1. Обрезка, оно же отсечение — это совсем другая задача, решается другими алгоритмами.
2. Судя по тому, что 3-4 млн. точек из 5-6 млн попадает на границы — имеются ввиду не точки, а пикселы. А в статье решается непрерывная задача. Для дискретных задач, опять же есть другие алгоритмы.
И попадают не «на границы», а в «границы» — т.е. должны оказаться в результирующем наборе.
Соответственно по алгоритму «сканирующей прямой» для того чтобы определить что точка нам нужна/не нужна необходимо сделать количество_проверок = количество_точек * количество_рёбер_границы
И это количество одинаково как для случая попадания, так и для случая непопадания.
В реальных условиях это непозволительные затраты.
Количество_проверок == количество_ребер, и вот почему. Например, луч ориентирован вправо. Для каждого отрезка границы (ребра) нужно найти точку пересечения прямой-продолжения луча и прямой-продолжения отрезка, и проверить принадлежность этой точки лучу и отрезку, что делается довольно просто за О(1). Таким образом, алгоритм не зависит от количества_точек как параметра.

Где ошибка?
>>Считается количество пересечений горизонтального луча из точки с рёбрами полигона. Чётное число пересечений — точка снаружи, нечётное — внутри.
Насчет абсолютной точности — я с Вами не соглашусь. С чем я могу согласиться, так это с тем, что координаты пересечения луча и отрезка вычисляются достаточно точно (гораздо точнее, чем использование тангенсов), но я пока не представляю себе, как вы решили избавиться от eps в вычислительной геометрии — буду рад услышать ответ.
Окей, вот вам тест для формы Н: нашу точку выносим «далеко-далеко». Тогда сумма площадей треугольников будет, очевидно, «очень-очень большой», а площадь самой буквы Н — сравнительно маленькой.
Возможно, вы имели ввиду ориентированную площадь? В таком случае да, он не всегда сработает вообще. Но для абсолютных (неотрицательных) площадей такой алгоритм, кажется, должен работать.
И еще: плюс такого подхода не только в быстроте работы, а и в быстроте реализации (пригодится на олимпиадках) :)
Более того, как считать площадь многоугольника, кроме как способом модуля от ориентинованной площади? :-D
Ориентированная площадь для меня наиболее простой и очевидный способ при наличии координат вершин многоугольника.
Если луч совпал с отрезком фигуры, тогда алгоритму задница.
Принадлежность к одной и той же стороне от отрезка легко посчитать через векторные произведения. Если все векторные произведения отрезков и вектора на исследуемую точку имеют один и тот же знак (положительны или отрицательны) то точка принадлежит фигуре, если они разные, то не принадлежит.
Тут всё элементарно просто, но если кому то нужно могу описать подробнее.
Тут лежит картинка, опровергающая ваш метод. Красным помечены отрезки, для которых точка лежит по другую сторону, в отличие от зеленых.
Правильно брать atan2(y_{i+1},x_{i+1})-atan2(y_i,x_i) и нормировать до (-\pi;\pi] (если результат близок к \pm\pi, то точка лежит на стороне многоугольника). Еще нужно учесть совпадение какой-либо вершины многоугольника с нулем, если это допускается.
В режиме graph рисуем зеленый треугольник, ставим красную точку. Далее заливаем треугольник белым. Проверяем цвета соседних с точкой пикселей. Если черные, значит точка не входит. Да, есть нюансы и вообще это мега читерство, но на олимпиаде главное чтобы программа правильно отрабатывала, а как никого не волнует.
для каждой грани находим ее MBR и записываем в некий двумерный массив NxM(100х100?) флаги присутствия граней в какой либо ячейке.
Далее имеем точку в X,Y или в nX,mY в пространстве наложенной на полигон сетке.
достаточно пустить луч в минимальную сторону( влево\право\верх\низ ) и проверить попадание через обычный алгоритм пересечения отрезка чтобы получить профит.
Время создания изначальной структуры данных по присутствию граней aka двумерный массив ~ в 10 раз дольше чем одиночная проверка.
Итоговый тест вхождения, при наличии структуры, работает примерно в ~NxM раз быстрее чем одиночная проверка
В вашем алгоритме на каждую итерацию выполняется: 4 сложения, 2 вычитания, 8 умножений, 2 деления и 2 арктангенса. Где профит?
Результат был простой:
• если сумма равна ±2π, то точка внутри.
• если сумма равна 0, то точка снаружи.
• побочно: если сумма равна +-π, то точка на границе (с оговорками на точность арифметики).
Дырка была в том случае, если концы незамкнутой ломаной лежат на луче, исходящем из точки.
Но по условиям на входе был таки замкнутый многоугольник. ;-)
Позже проконсультировался с математиком, он предложил в общем случае интеграл по контуру (забыл какой функции), а в частном случае с ломаной таки сумму видимым углов обозрения, так как «изобрёл» я.
исключения, я назвал, незамкнутые ломаные и точка на ребре (приходится доверять значению п+-epsilon).
угол вспомню чуть позже — с компа вместо мобилки.
но, вероятно вы правы: скорее через арккосинус скалярного.
тогда вычисления самозащищены от деления на ноль.
Фрагмент 1, через арксинус векторного произведения:
//=============================================================
//sin(a, b) = len(vector_prod(a, b)) / (len(a) * len(b))
float P0x, P0y; // origin of the angle
float P1x, P1y; // start of an angle segment
float P2x, P2y; // finish of an angle segment
float Ax = P1x — P0x, Ay = P1y — P0y; // convert segment (P0,P1) to (0,A)
float Bx = P2x — P0x, By = P2y — P0y; // convert segment (P0,P2) to (0,B)
if ( Ax == 0.0 && Ay == 0.0 ) // len_a == 0.0
throw Exception__Point_is_on_Vertex;
if ( Bx == 0.0 && By == 0.0 ) // len_b == 0.0
throw Exception__Point_is_on_Vertex;
float len_a = sqrt(Ax * Ax + Ay * Ay); // length of vector a
float len_b = sqrt(Bx * Bx + By * By); // length of vector b
float vector_prod_x = Ay * 0.0 — 0.0 * By; // Bz and Az are zero
float vector_prod_y = 0.0 * Bx — Ax * 0.0; // Az and Bz are zero
float vector_prod_z = Ax * By — Ay * Bx;
float vector_prod_len = vector_prod_z; // singular case, no sqrt() needed
{//bug here!!!
if ( vector_prod_len == 0.0 )
throw Exception__Point_is_on_Segment;
float sin_gamma = vector_prod_len / (len_a * len_b);
};//bugged block finished
float gamma = asin(sin_gamma);
return gamma;
// total:
// arcsine: 1
// square root: 2
// multiplication: 11
// division: 1
// addition: 2
// substraction: 7
// test condition: 2
// conjunction: 2
// equality: 4
Фрагмент 2, через арккосинус скалярного произведения:
//cos(a, b) = scalar_prod(a, b) / (len(a) * len(b))
float P0x, P0y; // origin of the angle
float P1x, P1y; // start of an angle segment
float P2x, P2y; // finish of an angle segment
float Ax = P1x — P0x, Ay = P1y — P0y; // convert segment (P0,P1) to (0,A)
float Bx = P2x — P0x, By = P2y — P0y; // convert segment (P0,P2) to (0,B)
if ( Ax == 0.0 && Ay == 0.0 ) // len_a == 0.0
throw Exception__Point_is_on_Vertex;
if ( Bx == 0.0 && By == 0.0 ) // len_b == 0.0
throw Exception__Point_is_on_Vertex;
float len_a = sqrt(Ax * Ax + Ay * Ay); // length of vector a
float len_b = sqrt(Bx * Bx + By * By); // length of vector b
float scalar_prod = Ax * Bx + Ay * By;
float cos_gamma = cos_gamma = scalar_prod / (len_a * len_b);
float gamma = acos(cos_gamma);
return gamma;
// total:
// arccosine: 1
// square root: 2
// multiplication: 7
// division: 1
// addition: 3
// substraction: 4
// test condition: 2
// conjunction: 2
// equality: 4
Фрагмент 3, через площадь треугольника детерминантом и площадь синусом:
//=============================================================
// surface of a triangulum is equal to a half
// of the coordinated matrix determinant:
// |Ox, Oy, 1|
// S = (1/2) * |Ax, Ay, 1|
// |Bx, By, 1|
// S = (1/2) * (Ox * (Ay — By) + Ax * (By — Oy) + Bx * (Oy — Ay) =
// (considering Ox, Oy are zero)
// = Ax * By – Bx * Ay
//
// also
// surface of triangulum is
// S = (1/2) * |OA| * |OB| * sin_gamma
//
// therefore
// sin_gamma = 2 * S / (|OA| * |OB|) = determinant / (|OA| * |OB|)
float x0, y0; // point to test, point_0
float x1, y1; // segment start, point_1
float x2, y2; // segment finish, point_2
float Ax = P1x — P0x, Ay = P1y — P0y; // convert segment (P0,P1) to (0,A)
float Bx = P2x — P0x, By = P2y — P0y; // convert segment (P0,P2) to (0,B)
if ( Ax == 0.0 && Ay == 0.0 ) // len_a == 0.0
throw Exception__Point_is_on_Vertex;
if ( Bx == 0.0 && By == 0.0 ) // len_b == 0.0
throw Exception__Point_is_on_Vertex;
float len_a = sqrt(Ax * Ax + Ay * Ay); // length of vector a
float len_b = sqrt(Bx * Bx + By * By); // length of vector b
float determinant = Ax * By — Bx * Ay;
float sin_gamma;
// if the triangle is degenerate, determinant is zero, angle is zero,
// and point is on the segment
{//bug here!!!
if ( determinant == 0.0 )
throw Exception__Point_is_on_Segment;
float sin_gamma = determinant / (len_a * len_b);
};//bugged block finished
float gamma = asin(sin_gamma);
return gamma;
// total:
// arcsine: 1
// square root: 2
// multiplication: 6
// division: 1
// addition: 2
// substraction: 5
// test condition: 3
// conjunction: 2
// equality: 5
Первый фрагмент глюкавый (помечено).
Второй фрагмент на одно умножение сложнее третьего и
Третий фрагмент получается самым оптимальным, но опять глюк на синусе…
Ну, так и получается, что через арккосинус скалярного проще и вернее.
Кстати, если точка внутри фигуры, то я насчитал два случая +2π и -2π.
И если точка на ребре, то тоже вижу два случая π и 3π.
А так вообще везде надо перепроверить операцию «== 0.0» на корректность. Мало ли…
Если число чётное — мы вне полигона, иначе — внутри.

Алгоритм определения попадания точки в контур на основе комплексного анализа