Предисловие


Не так давно пришлось поработать с упрощением полигональной цепи (процесс, позволяющий уменьшить число точек полилинии). В целом, данный тип задач очень распространен при обработке векторной графики и при построении карт. В качестве примера можно взять цепь, несколько точек которой попадают в один и тот же пиксель – очевидно, что все эти точки можно упростить в одну. Некоторое время назад я практически ничего не знал об этом от слова «совсем», в связи с чем, пришлось в быстром темпе восполнять необходимый багаж знаний по этой теме. Но каково было мое удивление, когда в интернете я не нашел достаточно полных руководств по этому вопросу… Мне приходилось отрывками искать информацию с совершенно разных источников и, после проведенного анализа, выстраивать все в общую картину. Занятие не из самых приятных, если честно. Поэтому мне хотелось бы написать цикл статей, посвященных алгоритмам упрощения полигональной цепи. Как раз-таки начать я решил с наиболее популярного алгоритма Дугласа-Пекера.



Описание


Алгоритму задается исходная полилиния и максимальное расстояние (?), которое может быть между исходной и упрощенной полилиниями (то есть, максимальное расстояние от точек исходной к ближайшему участку полученной полилинии). Алгоритм рекурсивно делит полилинию. Входом алгоритма служат координаты всех точек между первой и последней, включая их, а также значение ?. Первая и последняя точка сохраняются неизменными. После чего алгоритм находит точку, наиболее удаленную от отрезка, состоящего из первой и последней (алгоритм поиска расстояние от точки до отрезка). Если точка находится на расстоянии меньшем, чем ?, то все точки, которые еще не были отмечены к сохранению, могут быть выброшены из набора, и получившаяся прямая сглаживает кривую с точностью не ниже ?. Если же это расстояние больше ?, то алгоритм рекурсивно вызывает себя на наборе от начальной точки до данной и от данной до конечных точек. Стоит отметить, что алгоритм Дугласа-Пекера в ходе своей работы не сохраняет топологию. Это означает, что в результате мы можем получить линию с самопересечениями. В качестве наглядного примера возьмем полилинию со следующим набором точек: [ { 1; 5 }, { 2; 3 }, { 5; 1 }, { 6; 4 }, { 9; 6 }, { 11; 4 }, { 13; 3 }, { 14; 2 }, { 18; 5 } ] и посмотрим на процесс упрощения при разных значениях ?:

Исходная полилиния из представленного набора точек:



Полилиния с ? равной 0.5:



Полилиния с ? равной 1:



Полилиния с ? равной 1.5:



Можно и дальше продолжить увеличение максимального расстояния и визуализацию алгоритма, но, думаю, основная суть после этих примеров стала понятной.

Реализация


В качестве языка программирования для реализации алгоритма был выбран С++, полный исходный код алгоритма вы можете увидеть здесь. А теперь непосредственно к самой реализации:

#define X_COORDINATE 0
#define Y_COORDINATE 1

template<typename T>
using point_t = std::pair<T, T>;

template<typename T>
using line_segment_t = std::pair<point_t<T>, point_t<T>>;

template<typename T>
using points_t = std::vector<point_t<T>>;

template<typename CoordinateType>
double get_distance_between_point_and_line_segment(const line_segment_t<CoordinateType>& line_segment, const point_t<CoordinateType>& point) noexcept
{
    const CoordinateType x = std::get<X_COORDINATE>(point);
    const CoordinateType y = std::get<Y_COORDINATE>(point);

    const CoordinateType x1 = std::get<X_COORDINATE>(line_segment.first);
    const CoordinateType y1 = std::get<Y_COORDINATE>(line_segment.first);

    const CoordinateType x2 = std::get<X_COORDINATE>(line_segment.second);
    const CoordinateType y2 = std::get<Y_COORDINATE>(line_segment.second);

    const double double_area = abs((y2-y1)*x - (x2-x1)*y + x2*y1 - y2*x1);
    const double line_segment_length = sqrt(pow((x2-x1), 2) + pow((y2-y1), 2));
    if (line_segment_length != 0.0)
        return double_area / line_segment_length;
    else
        return 0.0;
}

template<typename CoordinateType>
void simplify_points(const points_t<CoordinateType>& src_points, points_t<CoordinateType>& dest_points, double tolerance, std::size_t begin, std::size_t end)
{
    if (begin + 1 == end)
        return;

    double max_distance = -1.0;
    std::size_t max_index = 0;
    for (std::size_t i = begin + 1; i < end; i++)
    {
        const point_t<CoordinateType>& cur_point = src_points.at(i);
        const point_t<CoordinateType>& start_point = src_points.at(begin);
        const point_t<CoordinateType>& end_point = src_points.at(end);
        const double distance = get_distance_between_point_and_line_segment({ start_point, end_point }, cur_point);
        if (distance > max_distance)
        {
            max_distance = distance;
            max_index = i;
        }
    }

    if (max_distance > tolerance)
    {
        simplify_points(src_points, dest_points, tolerance, begin, max_index);
        dest_points.push_back(src_points.at(max_index));
        simplify_points(src_points, dest_points, tolerance, max_index, end);
    }
}

template<
    typename CoordinateType,
    typename = std::enable_if<std::is_integral<CoordinateType>::value || std::is_floating_point<CoordinateType>::value>::type>
points_t<CoordinateType> duglas_peucker(const points_t<CoordinateType>& src_points, double tolerance) noexcept
{
    if (tolerance <= 0)
        return src_points;

    points_t<CoordinateType> dest_points{};
    dest_points.push_back(src_points.front());
    simplify_points(src_points, dest_points, tolerance, 0, src_points.size() - 1);
    dest_points.push_back(src_points.back());
    return dest_points;
}

Ну и собственно само использование алгоритма:

int main()
{
    points_t<int> source_points{ { 1, 5 }, { 2, 3 }, { 5, 1 }, { 6, 4 }, { 9, 6 }, { 11, 4 }, { 13, 3 }, { 14, 2 }, { 18, 5 } };
    points_t<int> simplify_points = duglas_peucker(source_points, 1);
    return EXIT_SUCCESS;
}

Пример выполнения алгоритма


В качестве входных данных возьмем раннее известный нам набор точек [ { 1; 5 }, { 2; 3 }, { 5; 1 }, { 6; 4 }, { 9; 6 }, { 11; 4 }, { 13; 3 }, { 14; 2 }, { 18; 5 } ] и ? равной 1:

  • Найдем наиболее удаленную точку от отрезка { 1; 5 } — { 18; 5 }, данной точкой окажется точка { 5; 1 }.

  • Проверяем ее расстояние до отрезка { 1; 5 } — { 18; 5 }. Оно оказывается больше 1, значит добавляем ее в результирующую полилинию.
  • Запускаем рекурсивно алгоритм для отрезка { 1; 5 } — { 5; 1 } и находим наиболее удаленную точку для этого отрезка. Данная точка — это { 2; 3 }.

  • Проверяем ее расстояние до отрезка { 1; 5 } — { 5; 1 }. В данном случае оно меньше 1, значит мы можем спокойно отбросить эту точку.
  • Запускаем рекурсивно алгоритм для отрезка { 5; 1 } — { 18; 5 } и находим наиболее удаленную точку для этого отрезка…

  • И так далее по такому же плану...

В результате дерево рекурсивных вызовов для этих тестовых данных будет выглядеть следующим образом:

Время работы


Ожидаемая сложность алгоритма в лучшем случае составляет О(nlogn), это когда номер наиболее удаленной точки всегда оказывается лексикографически центральным. Однако, в худшем случае сложность алгоритма составляет O(n^2). Это достигается, например, в случае, если номер наиболее удаленной точки всегда соседний к номеру граничной точки.

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

Комментарии (10)


  1. pallada92
    18.04.2019 18:24

    Спасибо за статью! Периодически приходится решать подобные задачи отбражении карт в d3.js или WebGL, когда topoJSON файл границ регионов нужно сконверировать в набор полигонов с разумным числом точек и заанимировать. Мне было достаточно простого алгоритма, который пропускает точки, которые находятся слишком близко друг другу и дробит длинные отрезки. Не думал, что существует семейство алгоритмов для решения этой задачи с более точными гарантиями.
    Если не секрет, для какой именно задачи вам потребовалась упрощать цепи?


    1. VladimirBalun Автор
      19.04.2019 08:08

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


  1. little-brother
    18.04.2019 18:45
    +1

    Конкретно для графиков неплох Largest Triangle Three Buckets от Sveinn Steinarsson. К тому же для популярных языков уже имеется реализация.


    1. VladimirBalun Автор
      19.04.2019 07:54

      Спасибо, действительно очень неплох, буду иметь в виду.


  1. SaNNy32
    18.04.2019 21:04

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


    1. VladimirBalun Автор
      19.04.2019 07:57

      Да, что-то я об этом совсем не подумал…
      Постараюсь добавить это в статью в скором времени.


  1. Alexufo
    19.04.2019 03:58

    Тут выше уже упомянули Largest Triangle Three Buckets
    Здесь я писал о варианте для одномерного массива.
    habr.com/en/post/412629


  1. Dmirry
    19.04.2019 08:26
    +1

    Огромное спасибо! Понятно, лаконично, наглядно. Жду продолжения.


  1. freeExec
    19.04.2019 09:05
    +1

    А можно пример где нарушается топология и происходит пересечение.


    1. VladimirBalun Автор
      19.04.2019 09:24
      +1

      Когда буду добавлять в статью правки по комментарию SaNNy32:

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