Триангуляция монотонного многоугольника с коллинеарными вертикальными краевыми сегментами
Я пытаюсь реализовать триангуляцию полигонов, используя широко известный алгоритм двухпроходной линии развертки, который делит многоугольник на монотонные подкомпоненты в первом проходе линии развертки, а затем триангулирует эти монотонные компоненты во втором проходе. Моя текущая реализация работает для общего случая, но я не могу понять, как ее адаптировать для работы с входными данными, содержащими несколько совпадающих краевых сегментов (сегменты с равными x-координатами при перемещении слева направо, или равные y-координаты при движении справа налево).
Редактировать: я только что понял, что то, как я сформулировал этот вопрос, делает его довольно длинным и многословным, так что вот быстрый TL;DR; для тех, кто знает о полигональной триангуляции, но не хочет читать все целиком: является ли следующая форма допустимым входным сигналом для 2-го прохода алгоритма триангуляции? Если да: как адаптировать второй проход для обработки, если нет: как адаптировать 1-й проход таким образом, чтобы он генерировал монотонные подкомпоненты, которые можно подавать во 2-й проход:
http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png
Длинная версия вопроса ниже этого пункта;-)
Очень краткое изложение алгоритма:
Первый проход делит входной многоугольник на "монотонные подкомпоненты". Монотонный подкомпонент - это многоугольник, который можно разбить на 2 соединенных цепочки, координаты которых отсортированы либо слева направо (если алгоритм реализован с использованием вертикальной линии развертки), либо сверху вниз (при использовании горизонтальной развертки линия). Давайте предположим, что мы используем вертикальную линию развертки: каждый монотонный подкомпонент может быть затем разбит на верхнюю цепочку и нижнюю цепочку, соединенные в минимальной и максимальной x-координатах, а при сканировании вершин любой из цепей x-координаты растет. Если подкомпонент строго мононтонный, верхняя и нижняя цепочки не могут иметь краевые сегменты с одинаковыми x-координатами (то есть: вертикальные краевые сегменты).
Второй проход подметает монотонные подкомпоненты и делит их на треугольники, добавляя внутренние ребра. Идея состоит в том, что каждый подкомпонент перемещается слева направо, и в каждой вершине, пораженной линией развертки, может произойти одно из двух: а) нетриангулированная область слева от линии развертки может быть триангулирована путем добавления диагоналей, или b) текущая вершина не может "видеть" ни одну из ранее пройденных, но необработанных вершин в нетриангулированной области слева от линии развертки. В случае b) вершина помещается в стек ("цепочка рефлексов"), и по построению в некоторой точке будет иметь место случай а), и вершины цепочки рефлексов будут выталкиваться один за другим и соединяться с диагонали до последней вершины линии развертки.
В приведенном выше описании отсутствуют некоторые детали, но я бы предположил, что любой, кто знает, как ответить на мой вопрос, уже понимает алгоритм, поэтому я не буду вдаваться в подробности.
У меня проблема в следующем: предположим, у меня есть многоугольник, представляющий стрелку, которая указывает влево, например, вот так:
http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png
Когда я ввожу эту форму в свой алгоритм, проход монотонного подразделения оставляет форму нетронутой: в ней есть вертикальные ребра, поэтому она не является строго монотонной, но является монотонной, и, насколько я понимаю, алгоритм не должен быть разделенным, прежде чем вы сможете триангулировать его (возможно, именно здесь я ошибаюсь, потому что мое предположение неверно).
Теперь предположим, что я подаю (неизмененный) многоугольник со стрелкой во второй проход, чтобы триангулировать его. Как мне разобраться с 2 вертикальными краевыми сегментами в основании наконечника стрелки? Алгоритм развертки требует, чтобы вершины многоугольника сортировались слева направо, поэтому можно предположить, что ответ сводится к тому, как отсортировать вершины с одинаковыми координатами x (например, в порядке цепочки, или по координате y, или на граничный индекс многоугольника), но какую бы сортировку я ни использовал, триангуляция всегда будет неудачной.
Давайте назовем самую левую вершину вершиной 0 и упорядочим вершины против часовой стрелки. Это означает, что 4 вершины основания стрелки являются вершинами 1, 2, 5 и 6. У нас есть три варианта сортировки:
В некотором исходном материале, который я использовал для реализации алгоритма, написано: "сортировать вершины с равными координатами x при увеличении координат y, т. Е. 1, 2, 5, 6. Если я сделаю это и разверну их, первый треугольник получится нормальным (0, 1, 2), но после этого алгоритм добавит ребро (5, 2), которое создает 4-вершинный компонент (0, 2, 5, 6). Ребро (0, 5) не добавляется, потому что алгоритм триангуляции предписывает добавлять ребра во все предыдущие нетриангулированные вершины в цепочке рефлексов, кроме первой (изменение этого приведет к нарушению общего случая). Хотя область многоугольника, ограниченная четырьмя вершинами, имеет треугольную форму, она, очевидно, не является треугольником, поскольку имеет 4 точки, и в большинстве определений она также недопустима, поскольку имеет коллинеарные ребра.
В другой статье, которую я прочитал, говорится: "Разорви связи, чтобы порядок цепочек сохранялся" Это означало бы, что 4 вершины в моем примере будут отсортированы в 1, 2, 6, 5, так как нижняя и верхняя цепочки идут слева направо. Если я разверну их в этом порядке, я снова получу треугольник (0, 1, 2), но следующая сканированная вершина (6) создаст многоугольник (0, 1, 6), что даже хуже, чем в первом случае, потому что он создает ребро (1, 6), которое проходит по вершине 5, но не содержит его. Широкая вершина 5 полностью испортит состояние алгоритма, потому что она создаст вырожденный треугольник (1, 5, 6), линию, а чистая вершина хвоста не сможет триангулировать оставшуюся часть многоугольника.
Сортировка по исходному порядку вершин многоугольника (вдоль границы, против часовой стрелки): в этом случае это даст тот же результат, что и в случае 1), то есть: 1, 2, 5, 6, который, как уже было показано, потерпел неудачу.
Я начинаю думать, что, возможно, форму стрелки, подобную этой, следует считать немонотонной, или (хотя я никогда не видел этого в каком-либо описании алгоритма) алгоритм монотонной триангуляции требует, чтобы входные данные были строго монотонными. Может быть, это то, чего мне не хватает? И если да, как мне нужно адаптировать монотонный проход подразделения для работы с (множественными, совпадающими) вертикальными краевыми сегментами? Исходный материал, который я использовал, классифицирует все вершины как "начало", "конец", "объединение", "разделение" или "обычный" (нижний / верхний) во время подразделения, но в случае вертикального сегмента эти классификации неоднозначны: в соответствии с согласно определению этих классов вершин, вершинная часть вертикального сегмента могла бы рассматриваться как начальная / конечная вершина, но также как вершина расщепления или слияния. Или, может быть, мне нужно отсортировать 4 вершины по их y-координатам, затем создать недопустимый компонент треугольника с 4 вершинами с 2 коллинеарными ребрами, а затем "исправить это", удалив вершину на коллинеарных ребрах?
Основным источником, который я использовал для реализации этого алгоритма, является оригинальная статья GJPT-78, в которой был представлен алгоритм триангуляции, он не является общедоступным (paywall), поэтому я не могу связать его здесь, но в Интернете доступно множество материалов курса по CS, в которых описаны алгоритм, я также использовал их, например:
http://www.cs.ucsb.edu/~suri/cs235/Triangulation.pdf https://www.cs.umd.edu/class/spring2012/cmsc754/Lects/cmsc754-lects.pdf (см. "Лекцию 6". 'глава)
Я прочитал довольно много из них. Почти каждый набор слайдов, бумаги, блога или любого другого описания алгоритма специально упоминает вершины с равными x-координатами (или y-координатами, если использовать горизонтальную линию развертки), но все они просто говорят: "Мы предполагаем, что нет равных x- координаты "и что это ограничение" легко фиксируется и служит только для упрощения представления "или" не является фундаментальным для алгоритма "или чего-либо еще. Странно, что ни одному из них не нужно подробно останавливаться на требуемых изменениях или обходных путях для поддержки этого случая, или они содержат какое-то нечеткое утверждение о сортировке равных x-вершин таким образом, который на практике не решает проблему.
Может быть, я просто немного глуп или пропускаю что-то действительно очевидное, но я уже несколько дней бездельничаю, пытаясь исправить этот угловой случай безрезультатно, и это начинает становиться действительно разочаровывающим. Я предполагал реализовать алгоритм для основного случая (который включает в себя написание структуры данных DCEL, алгоритма линии развертки, отсортированной карты границ, тригонометрии, необходимой для определения внутренних углов и видимости, структур данных для эффективного хранения и поиска классификаций вершин и т. Д.) - это почти вся работа, и впоследствии исправление проблемы с вертикальными краями будет тривиальным. Прямо сейчас я потратил больше времени, пытаясь исправить вертикальные ребра, чем на все остальные вещи, которые мне понадобились, чтобы объединить алгоритм для общего случая (он отлично работает для любого многоугольника, который я на него набрасываю, если только он этого не делает). имеют вертикальные края).
Спасибо! Wouter
2 ответа
Я наконец-то понял это сам, поэтому я отвечу на свой вопрос для потомков;-)
Оказывается, что изменения в алгоритме триангуляции для полигонов с вертикальными ребрами минимальны, и для их обработки не требуется особых случаев. Мне пришлось изменить следующие вещи:
- Сортировать вершины с равными x-координатами по возрастающей y-координате (примечание: я использую вертикальную линию развертки, для сортировки по горизонтальной линии развертки сначала y, затем x)
- Классифицируйте вершины с падающими вертикальными ребрами как "слияние" или "разделение" вместо "обычного вверх / вниз" (он же "верхняя цепь / нижняя цепь")
- Никогда не добавляйте диагонали, если последние 2 точки цепи рефлекса и текущая вершина линии развертки коллинеарны
Первое требование упоминается в большинстве работ по алгоритму. Сортировка снизу вверх имеет тот же эффект символического вращения по часовой стрелке, как упоминал Дэвид Эйзенстат.
Второе изменение, которое я должен был сделать, было потому, что я неправильно истолковал различные классификации вершин. Мое предположение состояло в том, что у вершины слияния всегда должны быть оба инцидентных ребра полностью слева от нее, а у расщепленной вершины - полностью справа, что неверно. Если один из 2 инцидентных ребер является вертикальным, а другой слева от вершины, он должен быть классифицирован как "объединение", если другой ребро направо, он должен быть классифицирован как "разделение". В частности, мне пришлось изменить следующие строки из этого:
// Classify vertex based on the interior angle and which side of the sweep line the two edges are
BOOL reflex_vertex = (interiorAngle < 0);
BOOL both_left = (e_in.origin.coordinates.x < vertex.coordinates.x) && (e_out.destination.coordinates.x < vertex.coordinates.x);
BOOL both_right = (e_in.origin.coordinates.x > vertex.coordinates.x) && (e_out.destination.coordinates.x > vertex.coordinates.x);
if (!reflex_vertex && both_right)
type = K14SweepLineVertexTypeStart;
else if (!reflex_vertex && both_left)
type = K14SweepLineVertexTypeEnd;
else if (reflex_vertex && both_right)
type = K14SweepLineVertexTypeSplit;
else if (reflex_vertex && both_left)
type = K14SweepLineVertexTypeMerge;
else if ([_lowerChainVertices containsObject:@(vertex.id)])
type = K14SweepLineVertexTypeLowerChain;
else
type = K14SweepLineVertexTypeUpperChain;
К этому:
// Classify vertex based on the interior angle and which side of the sweep line the two edges are
BOOL reflex_vertex = (interiorAngle < 0);
BOOL both_left = (e_in.origin.coordinates.x <= vertex.coordinates.x) && (e_out.destination.coordinates.x <= vertex.coordinates.x);
BOOL both_right = (e_in.origin.coordinates.x >= vertex.coordinates.x) && (e_out.destination.coordinates.x >= vertex.coordinates.x);
...
Последнее изменение было необходимо для предотвращения вырожденных треугольников с 3 коллинеарными точками на выходе. При триангуляции монотонных подкомпонентов всякий раз, когда вершина находится в той же многоугольной цепочке, что и вершины стека ("цепочка рефлекса"), диагонали добавляются из текущей вершины линии развертки ко всем видимым вершинам цепочки рефлекса. В моей реализации видимость определялась путем просмотра (подписанного) внутреннего угла вершины в верхней части стека. Эта проверка смотрела только на знак угла, где положительный угол означает видимый (внутренняя часть меньше или равна пи радиан, или 180 градусов). Проблема была с или равной частью, если 2 точки на вершине стека плюс текущая вершина линии развертки коллинеарны, внутренний угол равен точно пи радианам, и диагональ не должна быть добавлена. Я должен был изменить чек с этого:
BOOL visible = (vi_x_interior_angle > 0.0f);
К этому:
BOOL visible = (vi_x_interior_angle > 0.0f) && ((vi_x_interior_angle + COMPARE_EPSILON) < M_PI);
Я использую небольшой эпсилон, который на самом деле не нужен, если ваши вершины статичны / жестко закодированы, а x-координаты вертикальных ребер точно совпадают, но в моем случае вершины могут быть вычислены и могут иметь небольшую ошибку округления. Не добавление диагонали в случае, когда 3 точки почти точно коллинеарны, в целом должно давать лучшие результаты, чем добавление треугольника с практически нулевой площадью.
Помимо этих трех вещей, никакой специальной обработки не требуется, чтобы алгоритм работал для любого простого многоугольника (без самопересечения, без дырок), который вы бросаете в него. Я немного обескуражен тем, что потратил как минимум 20 часов, чтобы понять это, но, по крайней мере, я наконец-то заработал эту глупость. Просто хотелось бы, чтобы хотя бы одна из многих статей, которые я читал об этом конкретном алгоритме, была бы немного более ясной о трех вещах, которые я пропустил в своей реализации:-/
В нашем двигателе стреловидности мы используем полное лексикографическое упорядочение среди всех точек, где мы рассматриваем y
координировать, чтобы быть главной координатой и x
координируйте мне второстепенную координату (в нашем случае мы перемещаемся в направлении снизу вверх). В нашей реализации, чтобы обобщить обработку точек, имеющих одинаковые y
координаты, мы предполагаем, что точки, которые расположены позже во входной очереди (имеют большую x
координаты) в то же время расположены немного "выше" на плоскости на бесконечно малую величину. Это, очевидно, концептуальный вариант преобразования сдвига с помощью некоторого бесконечно малого количества сдвига, о котором уже упоминалось в комментариях.
И, как вы уже упоминали выше, побочным эффектом этого подхода является то, что в общем случае он приводит к образованию "ненужных" срезов на этапе монотонизации, как показано ниже
Несмотря на то, что форма уже монотонна вдоль вертикального направления, алгоритм считает необходимым добавить режущую кромку, показанную красным (опять же, в этом случае мы выполняем движение снизу вверх). Но поскольку нашей конечной целью в любом случае является триангуляция, это не большая проблема. Конечно, если у вас есть некоторые дополнительные ограничения на качество конечной триангуляции, то эти "ранние" треугольники могут стать проблемой.
В моем случае триангуляция генерируется, чтобы служить входом для алгоритма, который выполняет кинетическое преобразование начальной триангуляции. Чтобы быть надежными, алгоритмы кинетической триангуляции должны в любом случае иметь дело с "плохими" иглоподобными треугольниками, по этой причине я не налагаю никаких ограничений на качество триангуляции.