Алгоритм Рамера–Дугласа–Пойкера

Отличная штука при векторизации растра. Мы нашли контур ромба, но он содержит тысячу точек. Хотя там должно быть всего четыре точки. Четыре, Карл! Вот для того, чтобы точек стало четыре, и может пригодиться алгоритм Рамера–Дугласа–Пойкера.

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

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

Лично мне алгоритм понадобился в своё время для векторизации замкнутых контуров, которые получаются после обработки образов. Поэтому, разговор будет с акцентом в замкнутые ломаные, полигоны.

Описание алгоритма

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

Алгоритм в картинках

Итак, у нас есть некая ломаная:

[]

Массив результирующей ломаной пуст. Стартуем алгоритм.

[A]

A и B это начальная и конечная точки ломаной. Рассматриваем весь диапазон точек между A и B. Точку A помещаем в результирующий массив.

[A]

На диапазоне AB находим максимально удалённую точку С, которая отстоит от отрезка AB на расстояние, больше указанного. Значит, точка С — это новая точка. У нас образовалось два новых диапазона — AC и CB. Точку С пока не добавляем в массив, потому что в процессе прогона по AC вдруг окажутся ещё точки.

[A]

Рассматриваем диапазон AC. На нём ничего выдающегося не оказалось.

[A,C]

Добавляем C в массив точек новой ломаной и рассматриваем диапазон CB. На диапазоне CB оказалась выдающаяся точка D, которая делит диапазон на два — CD и DB.

[A,C]

На диапазоне CD обнаружилась выдающаяся точка E, которая снова делит диапазон на CE и ED. По прежнему ничего не добавляем в массив итоговой ломаной. Потому что не знаем, как сложится ситуация дальше, в новых диапазонах.

[A,C]

На диапазоне CE не нашлось ничего выдающегося. Точки m и n находятся на слишком малом расстоянии от отрезка CE, чтобы считаться выдающимися.

[A,C,E]

Поэтому дописываем в массив точку E и переходим к рассмотрению диапазона ED.

[A,C,E,D]

На диапазоне ED не нашлось ничего выдающегося, поэтому дописываем D. И переходим к рассмотрению диапазона DB.

[A,C,E,D,B]

На диапазоне DB не нашлось ничего выдающегося, поэтому дописываем B.

[A,C,E,D,B]

Реализация алгоритма

В теории существует две разновидности алгоритма — рекурсивная и итеративная. Мы реализуем рекурсивный алгоритм.

Рекурсивный алгоритм

Вначале, небольшие вспомогательные типы и функции.

Рекурсивная часть будет называться, как в Вики — DouglasPeucker.

Для нахождения расстояния от точки до отрезка, взята формула высоты треугольника:

h_{a}={\frac  {2S}{a}}, где Sплощадь треугольника, a — длина стороны треугольника, на которую опущена высота.

Площадь треугольника считаем по формуле:

S = 1/2 * |(X1-X3)*(Y2-Y3) — (X2-X3)(Y1-Y3)|

Формулу немного оптимизируем, чтобы не считать всякий раз в цикле те вещи, которые можно вычислить заранее.

APoints TPointFDynArray
Массив вещественных точек исходной ломаной
AStartIdxInteger
Индекс точки в APoints, означающий начало текущего рассматриваемого диапазона
AEndIdxInteger
Индекс точки в APoints, означающий конец текущего рассматриваемого диапазона
AEpsilonSingle
Величина, превышение которой означает появление новой выдающейся точки. Все точки, отстоящие от отрезка, заданного точками AStartIdx и AEndIdx, и меньшие этой величины, считаются принадлежащими этому отрезку
AResLengthInteger
Текущая длина массива точек результирующей ломаной
AResTPointFDynArray
Массив точек результирующей ломаной

Запуск алгоритма

У картины должна быть рама. Она придаёт устойчивость произведению, преподносит его в лучшем виде и задаёт настроение.

Так исторически сложилось, что у меня очень часто для хранения точек используется список:

Поэтому первоисточником данных считаем его.

Подготовка

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

Из списка можно получить массив точек сразу. Но раз уж всё равно пробег по списку неизбежен, да ещё и дубликаты хотим убрать, то переписываем подходящие точки из списка в массив.

В конце будет листинг полностью. Сейчас фрагментарно. Если нужна информация полностью, легче перейти непосредственно к листингу.

Итак, инициализируем массив List и ищем максимально удалённую точку в нём с индексом Idx. Исходные данные ломаной хранятся в списке AList.

Нюанс первый. Для полигона не надо, чтобы первая и последняя точка совпадали. Однако, если мы строим сглаженную ломаную, которая зиждется на своей начальной и конечной точках, то мы можем получить такую картинку:

A и B — это начало и конец ломаной, которая состоит из 823 точек. Всё находит изумительно, но вместо 5 вершин, я хочу видеть 4. Это же явный ромб.

Поэтому у нас появляется параметр AClosed, который определяет, с каким типом кривой работаем — замкнутой или незамкнутой.

После инициализации массива допишем следующий фрагмент, совмещающий начало и конец ломаной.

Запуск

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

Перед вызовом каждой части инициализируем начало результирующего массива ALines стартовой точкой, потому что алгоритм этого не сделает.

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

Концовка

После всех манипуляций с данными имеем на руках массив, который должен быть явно короче изначального. Нам необходимо «урезать» его, выставив правильную длину.

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

Тут возникает нюанс второй.

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

Пишем дополнительную функцию, которая возвращает TRUE, когда нулевая вершина успешно убрана.

Дополним список параметров аргументом ACheckFirst. И если он равен TRUE, в самом конце нашей запускающей процедуры вызовем CheckFirstPoint.

Алгоритм Рамера–Дугласа–Пойкера: Листинг

Работает и на замкнутых, и не незамкнутых ломаных. На списке из одной точки тоже будет работать. Если убрать все комментарии, он станет раза в два меньше. Это простой и очень полезный алгоритм.


Скачать

Друзья, спасибо за внимание!

Надеюсь, материл будет полезен.

Исходник (zip) 65 Кб. Delphi XE 7, XE 11

Исполняемый файл (zip) 941 Кб.


5 7 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x