


Для реализации одномерного случая алгоритма Quickhull годится функция std::minmax_element. В сети можно найти множество реализаций алгоритма Quickhull для плоского случая. Однако, для случая произвольной размерности сходу находится лишь одна тяжёловесная реализация с сайта qhull.org.
В скобках рядом с терминами я буду давать перевод на английский (в стиле Туґбо, уж извините). Это может быть полезно для тех, кто ещё не знаком с терминами и будет читать англоязычные тексты по ссылкам, либо решит разобраться с текстами исходного кода.
Упомянутая выше «каноническая» реализация написана на C (есть C++ биндинги), используется давно и широко (в составе пакетов GNU Octave и MATLAB) и, следовательно, хорошо проверена. Но если вы решите изолировать код, относящийся только лишь к одному алгоритму Quickhull, то столкнётесь со сложностями. Для простейших применений хотелось бы иметь возможность обходится чем-то более компактным. Немного постаравшись, я написал свою реализацию (https://bitbucket.org/tomilov/quickhull/): это единственный класс, без зависимостей. Всего порядка 750 строк.
Геометрические понятия.
Начиная работать с многомерными сущностями, понимаешь, что справедливы какие-то аналогии с привычными двухмерными и трёхмерными объектами. Необходимо несколько упорядочить знания: ввести некоторые новые термины и прояснить отношения между ними.
Для программиста геометрические объекты — это прежде всего структуры данных. Для начала я дам геометрические определения (не очень строгие). Дальше по тексту будут приведены описания соответственных структур данных, удобных для того, чтобы хранить данные и оперировать с ними достаточно эффективно.
Обозначим размерность пространства как

- Симплекс (англ. simplex) задаётся
аффинно независимой (англ. affinely independent) точкой. Об аффинной независимости поясню далее. Эти точки называются вершинами (англ. ед. vertex, мн. vertices).
- Многогранник (син. политоп, многогранник, полиэдр, англ. polytope, polyhedron) определяется как минимум
аффинно независимой точкой (вершиной); симплекс (англ. simplicial polytope) — простейший случай
-мерного тела, многогранники с меньшим числом вершин обязательно будут иметь нулевой
-мерный объём.
- Параллелотоп (англ. parallelotope) — обобщение плоского параллелограмма и объёмного параллелепипеда. Для симплекса можно построить
соответственных параллелотопов (все они будут иметь одинаковый объём, равный
объёмам симплекса), взяв в качестве образующих (векторов) параллелотопа вектора, выходящие из одной фиксированной вершины в остальные
вершины.
- Понятие выпуклого многогранника (англ. convex polytope, convex polyhedron) я здесь приводить не буду, сгодится ваше интуитивное представление о нём. Симплекс, как частный случай многогранника, всегда является выпуклым.
- Понятие плоскости я также приводить не буду. Только замечу, что она имеет на единицу меньшую размерность, чем пространство, в которое вложена.
- Симплициальная грань (англ. simplicial facet) определяется
аффинно независимыми точками (вершинами). Далее речь будет идти только о симплициальных объектах (кроме выпуклого многогранника), так что определение «симплициальный» я буду опускать. Многие утверждения в дальнейшем будут справедливы только для невырожденных (все точки попарно различны) случаев симплициальных геометрических объектов.
- Ребро (англ. ridge) определяется как пересечение двух граней и имеет
вершину. Две грани могут иметь только одно общее ребро. Одна грань, таким образом, может иметь
соседей через рёбра.
- Пик (англ. peak) определяется
точками. Здесь интуитивная аналогия с трёхмерным пространством может поплыть, так как понятие пика и вершины не совпадают, но такими объектами мы не будем оперировать. Грань может иметь любое количество соседних граней через пик, отсюда можно сделать вывод, что хранить граф смежности граней через пики накладно и по памяти и по затратам на поддержку актуальности структур данных при каких-либо преобразованиях.
Грань многогранника, её границы, границы её границ и т.д. на английском именуются face. Дам ссылку на хороший ресурс, где логически обосновывается, что в



Имея дело с некоторым набором из






Кроме всего прочего необходимо ввести понятие гиперобъёма (англ. hypervolume).




Здесь мы выписали координаты векторов в строки. Аналогичные формулы и рассуждения можно привести и для векторов-столбцов (т.е. если мы транспонируем все матрицы, то на результат и выводы это не повлияет).
Делитель детерминанта в формуле выше — это количество симплексов (все они имеют одинаковый объём), на который можно разбить параллелотоп


Следует отметить, что данная мера для «объёмных» объектов будет иметь ненулевое значение, а также может быть как положительной, так и отрицательной величиной. Понять смысл знака нетрудно из следующих соображений: при обмене местами двух точек симплекса мы получаем смену знака детерминанта; порядок точек — это «лево- и право- ориентированность» симплекса. На плоскости это несложно себе представить: если стороны треугольника





Множитель перед детерминантом мы можем опустить, так как в алгоритме будет использоваться лишь информация о знаке величины ориентированного гиперобъёма.
Детерминант равен нулю, если хотя бы две строки линейно зависимы (помним, что точки попарно различны, то есть ни одна строка не нулевая). Несложно проверить соответствие приведённых выше рассуждений об аффинно зависимых точках и линейно зависимых векторах, соответствующих им.
На абсолютное значение детерминанта не будет влиять то, какую именно точку вычитаем при переходе к векторам, — только на знак. Следует вычитать всегда последнюю точку из первых, иначе для одинаково ориентированных аналогичных объектов, используемых в дальнейшем, знак меры будет разным в чётных и нечётных размерностях.
Что же делать с мерой для объектов, вложенных в пространство слишком большой размерности, например, с гранями? Если мы сконструируем матрицу так же, как это показано выше, то она будет прямоугольная. Детерминант от такой матрицы взять не получится. Оказывается формулу можно обобщить (это обобщение связано с формулой Бине-Коши), используя всё ту же матрицу составленную из векторов-строк:





Для аффинно независимых попарно различных точек матрица под детерминантом всегда получается квадратной положительно определённой, а сам детерминант такой матрицы — число всегда положительное. Для аффинно зависимых точек матрица получится сингулярной (т.е. её определитель равен нулю). Ясно, что мера всегда неотрицательная и информации об ориентации мы не имеем.
С одной стороны, детерминант произведения квадратных матриц равен произведению детерминантов, с другой — детерминант транспонированной квадратной матрицы совпадает с детерминантом исходной, таким образом последняя формула сводится к формуле для

Алгоритм.
Сам алгоритм 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++ интерфейсом https://gitorious.org/qhull/qhull. Процитирую алгоритм из первоисточника:
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) до точки. Здесь мы можем вспомнить школьную формулу, допускающую обобщение на случай произвольной размерности, а именно: «площадь треугольника равна половине произведения основания, на высоту» или «объём пирамиды равен одной третьей от произведения площади основания на высоту». Меру тела и грани мы высилять умеем, следовательно мы можем выразить высоту (одномерную длину):

В алгоритме нам понадобится вычислять ориентированное расстояние от фиксированной плоскости (грани) до точек, которых может быть много. В этом случае мы можем заметить, что в знаменателе стоит положительная константа. От положения точки («над» или «под») зависит лишь гиперобъём в числителе.





Другой подход — это использование нормированного уравнения плоскости (англ. hyperplane equation). Это уравнение первой степени, которое связывает координаты точки





Координаты нормали


Величина


Невязка


Если точка лежит по ту же сторону от плоскости, куда направлена нормаль, то величина

Отличие матрицы под детерминантом в выражениях для



Таким образом для плоскости грани необходимо вычислить и хранить



Теперь описывать плоскость грани мы умеем, но осталась одна неясность: как же выбрать порядок перечисления точек в списках вершин граней стартового симплекса? Авторы не хранят список вершин упорядоченным. Они поступают иначе. Задав уравнение плоскости для произвольного, но фиксированного порядка вершин, они выясняют ориентацию какой-либо внутренней точки (англ. 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), что очень приятно.
Комментарии (12)
CaptainTrunky
29.04.2015 16:52А в каком контексте, если не секрет, понадобилась новая реализация? Какую проблему вы решаете?
anegrey
12.05.2015 09:20Уважаемый, коллега.
Я прошу прощения, что не смог дочитать статью до конца, завис после слов
Дам ссылку на хороший ресурс, где логически обосновывается, что в D-мерном пространстве видимыми или наблюдаемыми объектами могут являться только лишь (D-1)-мерные объекты, то есть границы (англ. boundaries) D-мерных объектов.
Может я ошибаюсь? Давайте обсудим. Что значит видеть? Это значит уметь считывать информацию с помощью измерительных приборов из материального пространства. Два глаза человеку позволяют считывать-видеть 3D. Один глаз позволяет считывать-видеть 2D. Закройте глаз полностью, оставив только одну одномерную полоску видимости — глаз сможет видеть только 1D.
Восприятие размерности пространства — свойство используемых измерительных приборов, но никак не объективная данность. В размерности D мы можем видеть размерность D или меньше, в зависимости от имеющихся в нашем распоряжении измерительных приборов.
В задаче выпуклой оболочки, в программировании у нас абстрактное пространство, позволяющее видеть весь объект целиком. Если дана матрица определяющая D-мерный объект (симплекс), то мы видим весь этот объект целиком и сразу.Mrrl
12.05.2015 09:38+1В большинстве случаев мы не видим 3D. Мы видим только поверхности (например, грани) объектов, а они является двумерными.
Ситуация, когда мы можем видеть 3D в целом — это наблюдение какой-нибудь полупрозрачной сцены, в которой видны все внутренние точки всех объектов. Но она встречается довольно редко, и для неё нужны другие подходы.anegrey
12.05.2015 18:43Одним глазом мы видим ширину и высоту и не можем оценить = увидеть = измерить дальность. Если глаза, т.е. два глаза, считать одним органом зрения то каждый день, регулярно, этим органом мы видим 3D.
Перископ позволяет измерять расстояния, ширину и высоту — вот вам 3 измерения.Mrrl
12.05.2015 18:48+1Всё равно, то, что мы видим — это двумерная поверхность, вложенная в трёхмерное пространство. Увидеть трёхмерный объект целиком, не используя рентгеновский аппарат, томограф или бензопилу, не так то просто.
anegrey
12.05.2015 22:12Да, кажется понял идею. Если есть множество точек в D-мерии и выпуклая непрозрачная оболочка, то мы никогда не увидим точки попавшие внутрь оболочки и не сможем узнать их координаты.
Но всё-таки на ЭВМ, в рамках поставленной задачи, мы знаем все координаты, в том числе и внутренних точек.
Грани выпуклой оболочки, конечно имеют на одну размерность ниже, но сама оболочка (многогранник) остаётся D-мерной в D-мерном пространстве. И всю фигуру (многогранник, с неизвестной внутренностью) можно увидеть целиком, все координаты известны, значит мы всё видим.
Утрирую: если мы не видим что внутри многогранника, то не значит, что он стал на одну размерность ниже.
Надо подумать. :)
Orient Автор
12.05.2015 12:10Вопрос, безусловно, философский. По ссылке, насколько я помню, автор придерживается несколько другого подхода, а именно: он рассуждает в терминах луча зрения, который должен иметь возможность падать на сетчатку без помех (have an unobstructed path). Автор делает вывод:
A 2D retina works, because there is a 3rd dimension in which the light can travel unobstructed from the object onto the retina.
Т.о. сетчатка должна иметь размерность (D — 1) и меньше.anegrey
12.05.2015 19:02Сетчатка — это 2D измерительный прибор, т.е. на одну размерность ниже 3D. Утрирую, но смысл статьи на которую дана ссылка: в D-мерном пространстве возьмём измерительный прибор размерности (D-1), тогда мы сможем видеть не более (D-1) измерений.
Вероятно это не принципиально для данной статьи на хабре, это больше принципиально для меня лично.
iroln
Orient Автор
Спасибо за ссылку. Полезным будет реализовать проверку выпуклости результата как у них (Mehlhorn algorithm).
Orient Автор
Добавил проверку выпуклости. В CGAL не Quickhull, так что противоречия нет.