Для реализации одномерного случая алгоритма Quickhull годится функция std::minmax_element. В сети можно найти множество реализаций алгоритма Quickhull для плоского случая. Однако, для случая произвольной размерности сходу находится лишь одна тяжёловесная реализация с сайта qhull.org.
В скобках рядом с терминами я буду давать перевод на английский (в стиле Туґбо, уж извините). Это может быть полезно для тех, кто ещё не знаком с терминами и будет читать англоязычные тексты по ссылкам, либо решит разобраться с текстами исходного кода.
Упомянутая выше «каноническая» реализация написана на C (есть C++ биндинги), используется давно и широко (в составе пакетов GNU Octave и MATLAB) и, следовательно, хорошо проверена. Но если вы решите изолировать код, относящийся только лишь к одному алгоритму Quickhull, то столкнётесь со сложностями. Для простейших применений хотелось бы иметь возможность обходится чем-то более компактным. Немного постаравшись, я написал свою реализацию (http://ift.tt/1ztCDRj): это единственный класс, без зависимостей. Всего порядка 750 строк.
Геометрические понятия.
Начиная работать с многомерными сущностями, понимаешь, что справедливы какие-то аналогии с привычными двухмерными и трёхмерными объектами. Необходимо несколько упорядочить знания: ввести некоторые новые термины и прояснить отношения между ними.
Для программиста геометрические объекты — это прежде всего структуры данных. Для начала я дам геометрические определения (не очень строгие). Дальше по тексту будут приведены описания соответственных структур данных, удобных для того, чтобы хранить данные и оперировать с ними достаточно эффективно.
Обозначим размерность пространства как
- Симплекс (англ. simplex) задаётся
аффинно независимой (англ. affinely independent) точкой. Об аффинной независимости поясню далее. Эти точки называются вершинами (англ. ед. vertex, мн. vertices).
- Многогранник (син. политоп, многогранник, полиэдр, англ. polytope, polyhedron) определяется как минимум
аффинно независимой точкой (вершиной); симплекс (англ. simplicial polytope) — простейший случай
-мерного тела, многогранники с меньшим числом вершин обязательно будут иметь нулевой
-мерный объём.
- Параллелотоп (англ. parallelotope) — обобщение плоского параллелограмма и объёмного параллелепипеда. Для симплекса можно построить
соответственных параллелотопов (все они будут иметь одинаковый объём, равный
объёмам симплекса), взяв в качестве образующих (векторов) параллелотопа вектора, выходящие из одной фиксированной вершины в остальные
вершины.
- Понятие выпуклого многогранника (англ. convex polytope, convex polyhedron) я здесь приводить не буду, сгодится ваше интуитивное представление о нём. Симплекс, как частный случай многогранника, всегда является выпуклым.
- Понятие плоскости я также приводить не буду. Только замечу, что она имеет на единицу меньшую размерность, чем пространство, в которое вложена.
- Симплициальная грань (англ. simplicial facet) определяется
аффинно независимыми точками (вершинами). Далее речь будет идти только о симплициальных объектах (кроме выпуклого многогранника), так что определение «симплициальный» я буду опускать. Многие утверждения в дальнейшем будут справедливы только для невырожденных (все точки попарно различны) случаев симплициальных геометрических объектов.
- Ребро (англ. ridge) определяется как пересечение двух граней и имеет
вершину. Две грани могут иметь только одно общее ребро. Одна грань, таким образом, может иметь
соседей через рёбра.
- Пик (англ. peak) определяется
точками. Здесь интуитивная аналогия с трёхмерным пространством может поплыть, так как понятие пика и вершины не совпадают, но такими объектами мы не будем оперировать. Грань может иметь любое количество соседних граней через пик, отсюда можно сделать вывод, что хранить граф смежности граней через пики накладно и по памяти и по затратам на поддержку актуальности структур данных при каких-либо преобразованиях.
Грань многогранника, её границы, границы её границ и т.д. на английском именуются face. Дам ссылку на хороший ресурс, где логически обосновывается, что в
Имея дело с некоторым набором из
Кроме всего прочего необходимо ввести понятие гиперобъёма (англ. hypervolume).
Здесь мы выписали координаты векторов в строки. Аналогичные формулы и рассуждения можно привести и для векторов-столбцов (т.е. если мы транспонируем все матрицы, то на результат и выводы это не повлияет).
Делитель детерминанта в формуле выше — это количество симплексов (все они имеют одинаковый объём), на который можно разбить параллелотоп , построенный на векторах
. Соответственно сам детерминант — это гиперобъём параллелотопа. Интересующимся основаниями, лежащими в основе этого утверждения, рекомендую прочитать про определитель матрицы Грама и о его геометрической интерпретации.
Следует отметить, что данная мера для «объёмных» объектов будет иметь ненулевое значение, а также может быть как положительной, так и отрицательной величиной. Понять смысл знака нетрудно из следующих соображений: при обмене местами двух точек симплекса мы получаем смену знака детерминанта; порядок точек — это «лево- и право- ориентированность» симплекса. На плоскости это несложно себе представить: если стороны треугольника перечислены против часовой стрелки
, то определитель положителен
, иначе
— отрицателен
. Для тетраэдра знак будет зависеть от того, в каком порядке (по часовой стрелке или против) первые 3 точки будут перечисляться при взгляде на них из последней. Таким образом, в дальнейшем, при задании геометрических объектов, мы должны принять, что порядок перечисления точек должен быть фиксированным.
Множитель перед детерминантом мы можем опустить, так как в алгоритме будет использоваться лишь информация о знаке величины ориентированного гиперобъёма.
Детерминант равен нулю, если хотя бы две строки линейно зависимы (помним, что точки попарно различны, то есть ни одна строка не нулевая). Несложно проверить соответствие приведённых выше рассуждений об аффинно зависимых точках и линейно зависимых векторах, соответствующих им.
На абсолютное значение детерминанта не будет влиять то, какую именно точку вычитаем при переходе к векторам, — только на знак. Следует вычитать всегда последнюю точку из первых, иначе для одинаково ориентированных аналогичных объектов, используемых в дальнейшем, знак меры будет разным в чётных и нечётных размерностях.
Что же делать с мерой для объектов, вложенных в пространство слишком большой размерности, например, с гранями? Если мы сконструируем матрицу так же, как это показано выше, то она будет прямоугольная. Детерминант от такой матрицы взять не получится. Оказывается формулу можно обобщить (это обобщение связано с формулой Бине-Коши), используя всё ту же матрицу составленную из векторов-строк:
Для аффинно независимых попарно различных точек матрица под детерминантом всегда получается квадратной положительно определённой, а сам детерминант такой матрицы — число всегда положительное. Для аффинно зависимых точек матрица получится сингулярной (т.е. её определитель равен нулю). Ясно, что мера всегда неотрицательная и информации об ориентации мы не имеем.
С одной стороны детерминант произведения квадратных матриц равен произведению детерминантов, с другой — детерминант транспонированной квадратной матрицы совпадает с детерминантом исходной, таким образом последняя формула сводится к формуле для точки, за исключением корня из квадрата, т.е. модуля, который мы можем опустить, с целью получить дополнительную информацию об относительной ориентации точек в пространстве.
Алгоритм.
Сам алгоритм Quickhull для случая произвольной размерности был предложен в труде Barber, C. Bradford; Dobkin, David P.; Huhdanpaa, Hannu (1 December 1996). «The quickhull algorithm for convex hulls». ACM Transactions on Mathematical Software 22 (4): 469–483. «Каноническая» реализация на C от авторов располагается на уже упомянутом сайте http://qhull.org/, репозиторий с C++ интерфейсом http://ift.tt/1PEX3O7. Процитирую алгоритм из первоисточника:
create a simplex of d + 1 points for each facet F for each unassigned point p if p is above F assign p to F`s outside set for each facet F with a non-empty outside set select the furthest point p of F`s outside set initialize the visible set V to F for all unvisited neighbours N of facets in V if p is above N add N to V the set of horizon ridges H is the boundary of V for each ridge R in H create a new facet from R and P link the new facet to its neighbours for each new facet F' for each unassigned point q in an outside set of a facet in V if q is above F' assign q to F'`s outside set delete the facets in V
Выпуклая оболочка представляет собой список граней.
Стартовый симплекс.
Как видно, мы должны начать с некоторого стартового симплекса. Для этого можно выбрать любые
Ведётся два списка точек: исходный список
Формула расстояния от
Так как вектор непосредственно раскладывается в сумму проекций на любое подпространство и на ортогональное дополнение этого подпространства, то:
Имея ортонормированный базис
Координаты векторов
После завершения алгоритма в
Далее мы должны распределить оставшиеся точки в так называемые списки внешних точек (англ. outside sets) граней — это такие списки точек, которые ведутся для каждой грани и в которые попадают ещё не рассмотренные (далее — неназначенные, англ. unassigned) точки, находящиеся относительно плоскости грани по другую сторону, нежели внутренность симплекса (или временного многогранника в дальнейшем). Здесь мы пользуемся тем свойством выпуклого многогранника (и симплекса в частности), что весь он лежит целиком только в одном из двух полупространств, получаемых при разбиении всего пространства плоскостью каждой грани. Из этого свойства следует тот факт, что если точка не попала ни в один из списков внешних точек граней выпуклой оболочки, то она достоверно находится внутри неё. В дальнейшем в алгоритме понадобится информация о том, какая из точек, содержащихся в списке внешних точек и наиболее удалена от плоскости грани, поэтому, добавляя точки в список внешних точек, необходимо сохранять информацию о том, которая из них самая дальняя. Любая точка попадает только в один список внешних точек. На асимптотическую сложность не влияет то как именно распределяются точки по спискам.
На данном этапе мне необходимо сделать отступление и рассказать о том, как задать грань, как вычислять расстояние до неё, как определить ориентацию точки относительно грани.
Только лишь единожды — в самом начале — нам понадобится вычислить значение гиперобъёма стартового симплекса чтобы по знаку определить его ориентацию. Все дальнейшие операции касательно граней будут заключаться только лишь в вычислении ориентированного расстояния (англ. directed distance) до точки. Здесь мы можем вспомнить школьную формулу, допускающую обобщение на случай произвольной размерности, а именно: «площадь треугольника равна половине произведения основания, на высоту» или «объём пирамиды равен одной третьей от произведения площади основания на высоту». Меру тела и грани мы высилять умеем, следовательно мы можем выразить высоту (одномерную длину):
В алгоритме нам понадобится вычислять ориентированное расстояние от фиксированной плоскости (грани) до точек, которых может быть много. В этом случае мы можем заметить, что в знаменателе стоит положительная константа. От положения точки («над» или «под») зависит лишь гиперобъём в числителе. — тоже константа. Таким образом мы можем рассматривать только относительные величины гиперобъёмов симплексов (а точнее, соответственных параллелотопов) построенных из грани и точки. Если определитель считать не самым медленным способом (по определению за
) и не самым быстрым (
), а способом через LUP-разложение (англ. LUP-decomposition), то можно добиться сложности
и приличной численной устойчивости (англ. numerical stability). Не берусь оценивать как сложность вычисления детерминанта влияет на конечную сложность всего алгоритма, но экспериментируя я заключил, что в худшем случае (например, случай точек расположенных на сфере (англ. cospherical points)) даже при малых размерностях пространства (
) вычисление детерминантов для оценки расстояния слишком вычислительно затратно.
Другой подход — это использование нормированного уравнения плоскости (англ. hyperplane equation). Это уравнение первой степени, которое связывает координаты точки плоскости, координаты нормированного вектора нормали
(
— ненормированная нормаль) к плоскости (англ. normalized normal vector) и её смещения
от начала координат (англ. offset):
Координаты нормали :
Величина — смещение плоскости от начала координат:
Невязка , получаемая при подстановке произвольной точки в нормированное уравнения плоскости, — это как раз ориентированное расстояние от плоскости до точки.
Если точка лежит по ту же сторону от плоскости, куда направлена нормаль, то величина будет положительной, иначе — отрицательной.
Отличие матрицы под детерминантом в выражениях для и
только лишь в замене
-ой координаты (столбца) на единицы.
Таким образом для плоскости грани необходимо вычислить и хранить число:
координат нормированной нормали и смещение от начала координат. Вычисление расстояния сводится к вызову функции std::inner_product, в которую передаются итераторы начала и конца массива координат нормированной нормали, начало массива координат точки, а последним параметром — значение
. Так как везде далее в алгоритме используются лишь относительные значения расстояний, то, вообще говоря, можно и не нормировать, но это довольно дешёвая операция, а правильное значение расстояния может понадобиться и при работе с выпуклой оболочкой после действий алгоритма.
Теперь описывать плоскость грани мы умеем, но осталась одна неясность: как же выбрать порядок перечисления точек в списках вершин граней стартового симплекса? Авторы не хранят список вершин упорядоченным. Они поступают иначе. Задав уравнение плоскости для произвольного, но фиксированного порядка вершин, они выясняют ориентацию какой-либо внутренней точки (англ. inner point) относительно этой плоскости. То есть вычисляют ориентированное расстояние. В случае, если расстояние отрицательное они меняют знак у координат нормали к гиперплоскости и у её смещения от начала координат. Либо задают значение флага, сигнализирующее о том, что грань перевёрнута (англ. flipped). Внутренней точкой для выпуклого многогранника будет среднее арифметическое хотя бы
Первая грань и следующая грань с вершинами
Следует отметить, что в массиве точек, с целью сформировать наборы из
Для каждой грани кроме задания списка вершин, списка внешних точек необходимо задать список соседних (англ. adjacency list) граней. Для каждой грани он будет содержать ровно
Главный цикл.
Теперь есть временный многоугольник с которым можно работать в главном цикле. В главном цикле этот временный многоугольник достраивается, «захватывая» всё больше и больше пространства, а вместе с ним и точки исходного множества, пока не настанет такой момент, когда вовне многогранника не останется ни одной точки. Далее опишу этот процесс более подробно. А именно, то, как он устроен в моей реализации.
Главный цикл — это цикл по граням. На каждой итерации среди граней с непустыми списками внешних точек выбирается самая лучшая грань
В теле главного цикла выбранная грань удаляется как заведомо не являющаяся гранью выпуклой оболочки. Но предварительно она «разбирается на составляющие». Из списка внешних точек извлекается самая дальняя точка
Как оказалось (при семплировании профайлером из Google Performance Tools), больше всего времени в случае больших размерностей и/или плохих входных данных (т.е. когда большая часть точек является вершинами выпуклой оболочки) алгоритм проводит в поиске соседних граней для новых граней. В моей реализации изначально алгоритм был устроен предельно просто: перебирались все возможные пары (два вложенных цикла) граней из списка новых граней и производились сравнения упорядоченных (по номеру из входного списка точек) списков вершин с одним несовпадением (модифицированный алгоритм std::set_difference или std::set_symmetric_difference). То есть
Сравнительное тестирование.
Образцом служит «каноническая» реализация алгоритма Quickhull qhull, установленная через менеджер пакетов (в Ubuntu 14.04 LTS). Для генерации входных данных из этого же пакета qhull-bin использовалась утилита rbox. Команда rbox t n D4 30000 s > /tmp/qhuckhull.txt создаёт файл с координатами 30000 точек на четырёхмерной сфере. Команда rbox t n D10 30 s > /tmp/quickhull.txt создаёт файл с координатами 30 точек на десятимерной сфере. Количество памяти, которое затрачивает программа можно посмотреть в выводе утилиты /usr/bin/time с ключём -v. Таким образом в выводе /usr/bin/time -v bin/quickhull /tmp/quickhull.txt | head -7 можно узнать потребление и памяти и процессорного времени (очищенного от времени на чтение файлов и вывод на терминал) для моей реализации, а в выводе /usr/bin/time -v qconvex s Qt Tv TI /tmp/quickhull.txt — для «канонической» реализации qhull.
Корректность своей реализации я определяю по совпадению количества граней выпуклых оболочек. Но для отладки (в режимах двух- и трёхмерном) на некотором этапе я реализовывал пошагово (через команду pause) анимированный вывод в формате gnuplot. Где-то в коммитах он есть. Вывод программы — это выпуклая оболочка и пронумерованное множество входных точек представленные в формате gnuplot.
Кроме того, как-то было начал писать (но не закончил) утилиту randombox — аналог rbox. randombox задумывалась как утилита, которая генерирует равномерно распределённые в пространстве (англ. uniform spatial distribution) точки, в отличие от rbox, которая генерирует точки распределённые не равномерно. randombox может генерировать наборы точек, ограниченные симплексом (любой размерности, которая может быть вложена в пространство с точками), на единичной сфере (англ. unit sphere), в шаре (англ. ball), на поверхности единичного «ромбовидного» многогранника (англ. unit diamond), внутри единичного куба (англ. unit cube), внутри параллелотопа, в пределах стандартного единичного симплекса (англ. unit simplex), а так же проецирует множество точек в объём конуса (англ. cone), либо цилиндра (англ. cylinder). Для генерации координат, равномерно распределённых в пределах произвольного сиплекса (вложимого в пространство), я нашёл численно стабильный алгоритм в интернете (через распределение Дирихле), а такеже придумал свой алгоритм, который не отличается таким свойством. В итоге выбрал первый.
Для того, чтобы «пощупать» выпуклые оболочки взглядом, используя gnuplot, из корня проекта введите команду rbox n D3 40 t | bin/quickhull > /tmp/quickhull.plt && gnuplot -p -e «load '/tmp/quickhull.plt'». rbox n D3 40 t сгенерирует 40 точек внутри ограничивающего куба
Итог или запоздалый TL;DR.
Итогом моих трудов явилась релизация алгоритма Quickhull на C++, довольно компактная и не сильно медленнее реализации с qhull.org. Алгоритм получает на входе значение размерности пространства, значение малой константы eps и набор точек, представляемый как диапазон, задаваемый парой итераторов, как это принято в STL. На первом этапе create_simplex строится стартовый симплекс и возвращается точечный базис аффинного (под)пространства, вмещающего входные точки. Если количество точек в базисе больше, чем размерность Евклидова пространства, вмещающего точки, то далее необходимо запустить алгоритм достройки выпуклой оболочки. На выходе алгоритм даст массив структур данных описывающих грани выпуклой оболочки входного множества, являющийся ответом. В случае неудачи мы имеем точечный базис некоторого подпространства меньшей размерности, которое вмещает все точки. Используя алгоритм Хаусхолдера можно некоторым образом повернуть входное множество, занулив при этом старшие координаты точек. Такие координаты можно отбросить и применить алгоритм Quickhull уже в пространстве меньшей размерности.
Данная реализация уже кое-кому пригодилась, и была оплачена благодарностью (в Acknowledgements), что очень приятно.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий