Русский
Русский
English
Статистика
Реклама

Qgis

Из песочницы Алгоритм ранжирования сегментов речной сети с использованием графов для геоинформационного анализа

10.08.2020 14:22:42 | Автор: admin

Привет, Хабр.


В данной статье хотелось бы затронуть тему применения информационных технологий в Науках о Земле, а именно, в Гидрологии и Картографии. Под катом представлено описание алгоритма ранжирования водотоков и реализованного нами плагина для открытой геоинформационной системы QGIS.


Введение


В настоящее время важным аспектом гидрологических изысканий являются не только натурные наблюдения, но и камеральная работа с гидрологическими данными, в том числе и с использованием ГИС (геоинформационных систем). Однако, изучение пространственной структуры гидрологических систем может быть затруднено в виду большого объема данных. В таких случаях не обойтись без применения дополнительных инструментов, позволяющих специалисту автоматизировать процесс.


Важную роль при работе с цифровыми данными в целом и гидрологическими данными в частности играет визуализация корректное и наглядное представление результатов анализа. Для отображения водотоков в классической картографии принят следующий способ: реки изображаются сплошной линией с постепенным (в зависимости, от количества впадающих в реку притоков) утолщением от истока к устью. Кроме того, для сегментов речной сети часто необходимо некоторое ранжирование по степени удаленности от истока. Информация такого рода важна не только с точки зрения визуализации, но и пригодна для более полного восприятия структуры данных, их пространственного распределения, и корректной последующей обработки.


Проиллюстрировать задачу ранжирования водотоков можно следующим образом (Рис. 1):

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


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


Современные ГИС, такие как ArcGIS или его open-source конкурент QGIS, имеют в своем арсенале инструменты работы с речными сетями. Однако, как правило, инструментам ранжирования рек требуется большое количество дополнительных вспомогательных материалов и, как нам кажется, лишних преобразований. Так, редко какой-либо существующий инструмент работы с речными сетями в ГИС может начинать свою работу без цифровой модели рельефа. Существенным недостатком, помимо сложной и многоступенчатой подготовки данных, является отсутствие возможности использовать уже подготовленные векторные слои с речной сетью для анализа, что ограничивает возможность применения в том числе цифровых основ из открытых источников (OpenStreetMap, Natural Earth, ВСЕГЕИ).


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


Мы решили автоматизировать данную процедуру с помощью представления речной сети в математической форме в виде графа и последующего применения алгоритмов обхода графов. Для упрощения работы пользователя с реализованным алгоритмом был написан плагин для геоинформационной системы QGIS "Lines Ranking". Код распространяется свободно и доступен в репозитории QGIS, а также на GitHub.


Установка и запуск

Для работы плагина необходима версия QGIS >= 3.14, а также следующие зависимости: библиотеки языка программирования Python networkx и pandas.


В качестве входных данных алгоритму подается векторный слой, состоящий из объектов с линейным типом геометрии (Line, MultiLine). Пользовательские атрибуты входного слоя сохраняются для результирующего слоя, поэтому могут быть значимыми.


  • Не рекомендуется использовать для входного слоя поле с названием fid. На этапе соединения разрывов в речной сети использование встроенного модуля пакета GRASS v.clean приводит к непредвиденным исключениям в связи с тем, что такое название поля является системным.

Также обязательным входным параметром является точка (Start Point Coordinates), определяющая положение устья речной сети. Она может быть задана с карты, из файла, либо из слоя, загруженного в QGIS. Положение устья может быть примерным, в расчет берется ближайший к точке сегмент речной сети (замыкающий сегмент будущего графа).


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


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


В любой ГИС данные могут представляться в двух основных форматах: растровом и векторном. Растр это матрица, где в каждом пикселе хранится некоторое значение параметра. В растровом формате в Науках о Земле часто представлены спутниковые снимки, поля реанализа, различные выходные слои из климатических моделей и др. Векторные данные представляются в виде простых геометрических объектов, таких как точки, линии и полигоны. Каждому объекту векторного формата может быть сопоставлена некоторая информация в виде атрибутов. Все описанные ниже действия мы будет производить именно над векторным слоем речной сети.


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


Предобработка входных данных


Отметим, что топология изначального векторного слоя может быть повреждена. Причиной может быть экспорт/импорт между различными ГИС, некорректное создание файла и т.д. Поврежденная топология слоя может выражаться в отсутствии соединений между объектами, т.е. образовании различных разрывов (Рис. 2), создании дополнительных замыканий, пересечений и т.п.



Рисунок 2. Нарушенная топология векторных объектов


Поэтому первым этапом предобработки является исправление топологии объектов: подтянуть узлы, сделать исходный векторный слой консистентным. Для этого используются инструменты из панели анализа данных в QGIS Исправить геометрии (fixgeometries встроенный) и v.clean (из пакета GRASS).


После того как топология исправлена, слой разбивается на сегменты в точках соприкосновения и пересечения линий. Результат после разбиения проиллюстрирован ниже (Рис. 3).



Рисунок 3. Разбиение линейных объектов на сегменты


Таким образом, с помощью инструмента в QGIS "Разбить линиями" (splitwithlines встроенный) мы делим исходный слой на сегменты.


Для каждого сегмента средствами QGIS мы рассчитываем протяженность и заносим данные в атрибутивную таблицу слоя (Рис. 4). Длина сегмента рассчитывается в соответствии с пользовательскими настройками проекта (Проект -> Свойства -> Общие -> Эллипсоид).



Рисунок 4. Расчет протяженности сегментов


После этого с помощью инструмента "Пересечение линий" (lineintersections встроенный) мы получаем точечный векторный слой, где в таблице атрибутов занесена информация о пересечениях сегментов. Такую таблицу атрибутов можно интерпретировать как список смежности.


Схематично этапы предобработки представлены на следующих изображениях (Рис. 5).



Рисунок 5. Этапы предобработки векторного линейного слоя для представления его в виде графа


В результате предобработки формируется граф как математический объект Python библиотеки networkx. Таким образом, сегменты реки это вершины графа, если сегменты связаны между собой (имеют пересечения), то между вершинами есть ребра.


Алгоритм ранжирования линейных объектов


После того, как граф сформирован, нам известно, из какой вершины следует начинать обход (точка впадения главной реки в водоем). В дальнейшем, будем называть эту вершину замыкающей, так как именно в неё "впадают" все остальные сегменты речной сети (вершины). Алгоритм мы разделили на несколько частей:


  1. Ранжирование вершин графа по удаленности от замыкающего сегмента с присваиванием атрибута "rank" (мера удаленности сегмента) и атрибута "offspring" (количество напрямую впадающих в данный сегмент участков речной сети);
  2. Обход графа с целью присваивания атрибута совокупно впадающих в данный участок сегментов водотоков "value" и атрибута "distance" (удаленности крайней точки сегмента от устья в метрах).

Оба этапа делятся на несколько блоков, однако, основная идея заключается в двуэтапной схеме присваивания атрибутов.


Присваивание атрибутов "rank" и "offspring"


Переходим к первому этапу обхода графа ранжирование вершин по степени удаленности от замыкающей. Изначально, мы планировали осуществлять присваивание атрибута "rank" при помощи итеративного обхода в ширину. Таким образом, начиная от замыкающей вершины, мы бы на каждом шаге отдалялись все дальше и дальше, и вместе с этим, присваивали бы атрибут. Но в таком случае может возникнуть следующий конфликт (Анимация ниже)



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


Предложенное решение: можно однозначно определить ранги для какой-то части речной сети (главной реки), и ранжировать сегменты опираясь уже на эту информацию. Данный подход можно проиллюстрировать следующей картинкой (Рис. 6).



Рисунок 6. Разрешение конфликта путем введения опорного маршрута в графе


Таким образом, подграфы могут примыкать к опорному маршруту только через 1 ребро, остальные ребра исключаются.


Здесь возникает следующая проблема как найти такой маршрут? Будем считать, что кратчайший маршрут между двумя наиболее удаленными друг от друга вершинами в графе, одна из которых является замыкающей, опорный маршрут (скоро мы добавим обновление, чтобы пользователь мог вручную задать такой маршрут, если у него есть априорные знания какая река является главной, но при отсутствии такой информации опорный маршрут определяется именно таким образом). Такой маршрут можно получить при помощи алгоритма A* (A-star), но данный алгоритм работает на взвешенном графе, а на ребрах нашего пока никаких весов нет. Но мы можем задать веса ребрам графа на основе длин сегментов (мы рассчитали их ранее).


1) Присваивание весов ребрам графа на основе протяженностей сегментов. Одновременно с этим этапом происходит выделение одной компоненты связности в графе. Перемещение по графу осуществляется при помощи поиска в ширину. Присваивание весов можно продемонстрировать следующим рисунком (Рис. 7)



Рисунок 7. Присваивание весов ребрам графа


Таким образом, каждая вершина графа имеет атрибут "length", который обозначает протяженность данного сегмента реки в метрах. Мы же переносим значения атрибутов с вершин графа на ребра итеративно, начиная обход графа в ширину из замыкающей вершины.


Данную задачу выполняет следующая функция


distance_attr

Input:


  • G исходный граф
  • start вершина, из которой требуется начинать обход
  • dataframe pandas датафрейм, в котором присутствуют 2 столбца: id_field (идентификатор сегмента/вершины) и 'length' (длина данного сегмента)
  • id_field поле в dataframe, из которого требуется сопоставлять идентификаторы с вершинами графа
  • main_id индекс, которым обозначается главная река в речной сети (по умолчанию = None)

Output:


  • На исходном графе ребрам присваиваются веса таким образом, что если начальная вершина 1, соседние с ней узлы графа: 2 и 3, где значение атрибута 'length' для сегментов 1 10, 2 15, 3 20. В результате ребрам будет присвоены значения следующим образом: (1, 2) и (2, 1) 15; (1, 3) и (3, 1) 20 и т.д.
  • last_vertex одна из самых удаленных вершин от начальной в графе (данная вершина необходима для нахождения кратчайшего пути между двумя самыми удаленными сегментами в речной сети)

# Function for assigning weights to graph edgesdef distance_attr(G, start, dataframe, id_field, main_id = None):    # List of all vertexes that can be reached from the start vertex using BFS    vert_list = list(nx.bfs_successors(G, source=start))    # One of the most remote vertices in the graph (this will be necessary for A*)    last_vertex = vert_list[-1][-1][0]    for component in vert_list:        vertex = component[0]  # The vertex where we are at this iteration        neighbors = component[1]  # Vertices that are neighboring (which we haven't visited yet)        dist_vertex = int(dataframe['length'][dataframe[id_field] == vertex])        # Assign the segment length value as a vertex attribute        attrs = {vertex: {'component' : 1, 'size' : dist_vertex}}        nx.set_node_attributes(G, attrs)        for n in neighbors:              # If the main index value is not set            if main_id == None:                # Assign weights to the edges of the graph                # The length of the section in meters (int)                dist_n = int(dataframe['length'][dataframe[id_field] == n])               # Otherwise we are dealing with a complex composite index            else:                # If the vertex we are at is part of the main river                if vertex.split(':')[0] == main_id:                    # And at the same time, the vertex that we "look" at from the vertex "vertex" also, then                    if n.split(':')[0] == main_id:                        # The weight value must be zero                        dist_n = 0                    else:                        dist_n = int(dataframe['length'][dataframe[id_field] == n])                else:                    dist_n = int(dataframe['length'][dataframe[id_field] == n])            attrs = {(vertex, n): {'weight': dist_n},                     (n, vertex): {'weight': dist_n}}            nx.set_edge_attributes(G, attrs)                # Assign attributes to the nodes of the graph            attrs = {n: {'component' : 1, 'size' : dist_n}}            nx.set_node_attributes(G, attrs)        # Look at the surroundings of the vertex where we are located        offspring = list(nx.bfs_successors(G, source = vertex, depth_limit = 1))         offspring = offspring[0][1]        # If the weight value was not assigned, we assign it        for n in offspring:            if len(G.get_edge_data(vertex, n)) == 0:                ##############################                # Assigning weights to edges #                ##############################                if main_id == None:                    dist_n = int(dataframe['length'][dataframe[id_field] == n])                   else:                    if vertex.split(':')[0] == main_id:                        if n.split(':')[0] == main_id:                            dist_n = 0                        else:                            dist_n = int(dataframe['length'][dataframe[id_field] == n])                    else:                        dist_n = int(dataframe['length'][dataframe[id_field] == n])                attrs = {(vertex, n): {'weight': dist_n},                         (n, vertex): {'weight': dist_n}}                nx.set_edge_attributes(G, attrs)                ##############################                # Assigning weights to edges #                ##############################            elif len(G.get_edge_data(n, vertex)) == 0:                ##############################                # Assigning weights to edges #                ##############################                if main_id == None:                    dist_n = int(dataframe['length'][dataframe[id_field] == n])                   else:                    if vertex.split(':')[0] == main_id:                        if n.split(':')[0] == main_id:                            dist_n = 0                        else:                            dist_n = int(dataframe['length'][dataframe[id_field] == n])                    else:                        dist_n = int(dataframe['length'][dataframe[id_field] == n])                attrs = {(vertex, n): {'weight': dist_n},                         (n, vertex): {'weight': dist_n}}                nx.set_edge_attributes(G, attrs)                    ##############################                # Assigning weights to edges #                ##############################    for vertex in list(G.nodes()):        # If the graph is incompletely connected, then we delete the elements that we can't get to        if G.nodes[vertex].get('component') == None:            G.remove_node(vertex)        else:            pass        return(last_vertex)    # The application of the algorithmlast_vertex = distance_attr(G, '7126:23', dataframe, id_field = 'id', main_id = '7126')

2) A* (А-star) поиск кратчайшего пути на взвешенном графе между замыкающей и одной из самых удаленных вершин (сегмента в речной сети). Данный самый короткий маршрут между двумя самыми удаленными вершинами в графе мы назовем "опорным";


3) Ранжирование по удаленности всех вершин в опорном маршруте. Вершине, из которой мы начинаем обход, присваивается значение ранга 1, следующей вершине 2, далее 3 и т.д.


4) Итеративный обход графа с началом в вершинах опорного маршрута с изоляцией рассматриваемых подграфов. Если одна из ветвей графа уже имеет соединение с вершинами опорного маршрута, то ребра, связывающие подграф с другими опорными вершинами, удаляются.


Исходный код можно увидеть ниже


rank_set

Input:


  • G граф с присвоенными весами на ребрах
  • start вершина, из которой требуется начинать обход
  • last_vertex одна из самых удаленных вершин от начальной в графе

Output:


  • Каждой вершине графа присваивается значение его ранга в речной сети. Ранг 1 имеет сегмент реки, который является замыкающим в речной сети. Ранг 2 имеют притоки, которые впадают в сегмент ранга 1. Ранг 3 имеют притоки, которые впадают в сегмент ранга 2 и т.д. После того, как все вершины графа имеют значение атрибута 'rank', производится подсчет потомков для каждой вершины 'offspring'. Значение атрибута 'offspring' можно интерпретировать как 'количество сегментов, которое впадает в данный сегмент речной сети'.

# Function for assigning 'rank' and 'offspring' attributes to graph verticesdef rank_set(G, start, last_vertex):    # Traversing a graph with attribute assignment    # G           --- graph as a networkx object    # vertex      --- vertex from which the graph search begins    # kernel_path --- list of vertexes that are part of the main path that the search is being built from    def bfs_attributes(G, vertex, kernel_path):        # Creating a copy of the graph        G_copy = G.copy()        # Deleting all edges that are associated with the reference vertexes        for kernel_vertex in kernel_path:            # Leaving the reference vertex from which we start the crawl            if kernel_vertex == vertex:                pass            else:                # For all other vertexes, we delete edges                kernel_n = list(nx.bfs_successors(G_copy, source = kernel_vertex, depth_limit = 1))                   kernel_n = kernel_n[0][1]                for i in kernel_n:                    try:                        G_copy.remove_edge(i, kernel_vertex)                    except Exception:                        pass        # The obtained subgraph is isolated from all reference vertices, except the one         # from which the search begins at this iteration                           # Breadth-first search        all_neighbors = list(nx.bfs_successors(G_copy, source = vertex))            # Attention!                                         # Labels are not assigned on an isolated subgraph, but on the source graph         for component in all_neighbors:            v = component[0] # The vertex where we are at this iteration            neighbors = component[1] # Vertices that are neighboring (which we haven't visited yet)            # Value of the 'rank' attribute in the considering vertex            att = G.nodes[v].get('rank')            if att != None:                # The value of the attribute to be assigned to neighboring vertices                att_number = att + 1            # We look at all the closest first offspring            first_n = list(nx.bfs_successors(G, source = v, depth_limit = 1))               first_n = first_n[0][1]            # Assigning ranks to vertices            for i in first_n:                 # If the neighboring vertex is the main node in this iteration, then skip it                # vertex - the reference point from which we started the search                if i == vertex:                    pass                 else:                    current_i_rank = G.nodes[i].get('rank')                    # If the rank value has not yet been assigned, then assign it                    if current_i_rank == None:                        attrs = {i: {'rank': att_number}}                        nx.set_node_attributes(G, attrs)                    # If the rank in this node is already assigned                    else:                        # The algorithm either "looks back" at vertices that it has already visited                        # In this case we don't do anything                        # Either the algorithm "came up" to the main path (kernel path) in the graph                        if any(i == bearing_v for bearing_v in kernel_path):                            G.remove_edge(v, i)                        else:                            pass            # Additional "search"            for neighbor in neighbors:                # We look at all the closest first offspring                first_n = list(nx.bfs_successors(G, source = neighbor, depth_limit = 1))                   first_n = first_n[0][1]                for i in first_n:                     # If the neighboring vertex is the main node in this iteration, then skip it                    # vertex - the reference point from which we started the search                    if i == vertex:                        pass                     else:                        # The algorithm either "looks back" at vertices that it has already visited                        # In this case we don't do anything                        # Either the algorithm "came up" to the main path (kernel path) in the graph                        if any(i == bearing_v for bearing_v in kernel_path):                            G.remove_edge(neighbor, i)                        else:                            pass                   # Finding the shortest path A* - building a route around which we will build the next searchs    a_path = list(nx.astar_path(G, source = start, target = last_vertex, weight = 'weight'))    ##############################    #   Route validation block   #    ##############################    true_a_path = []    for index, V in enumerate(a_path):        if index == 0:            true_a_path.append(V)        elif index == (len(a_path) - 1):            true_a_path.append(V)        else:            # Previous and next vertices for the reference path (a_path)            V_prev = a_path[index - 1]            V_next = a_path[index + 1]            # Which vertexes are adjacent to this one            V_prev_neighborhood = list(nx.bfs_successors(G, source = V_prev, depth_limit = 1))             V_prev_neighborhood = V_prev_neighborhood[0][1]            V_next_neighborhood = list(nx.bfs_successors(G, source = V_next, depth_limit = 1))            V_next_neighborhood = V_next_neighborhood[0][1]            # If the next and previous vertices are connected to each other without an intermediary            # in the form of vertex V, then vertex V is excluded from the reference path            if any(V_next == VPREV for VPREV in V_prev_neighborhood):                if any(V_prev == VNEXT for VNEXT in V_next_neighborhood):                    pass                else:                    true_a_path.append(V)            else:                true_a_path.append(V)    ##############################    #   Route validation block   #    ##############################    # Verification completed    a_path = true_a_path         RANK = 1    for v in a_path:        # Assign the attribute rank value - 1 to the starting vertex. The further away, the greater the value        attrs = {v: {'rank' : RANK}}        nx.set_node_attributes(G, attrs)         RANK += 1    # The main route is ready, then we will iteratively move from each node    for index, vertex in enumerate(a_path):        # Starting vertex        if index == 0:            next_vertex = a_path[index + 1]            # Disconnect vertices            G.remove_edge(vertex, next_vertex)            # Subgraph BFS block            bfs_attributes(G, vertex = vertex, kernel_path = a_path)                  # Connect vertices back            G.add_edge(vertex, next_vertex)        # Finishing vertex        elif index == (len(a_path) - 1):            prev_vertex = a_path[index - 1]            # Disconnect vertices            G.remove_edge(prev_vertex, vertex)            # Subgraph BFS block            bfs_attributes(G, vertex = vertex, kernel_path = a_path)            # Connect vertices back            G.add_edge(prev_vertex, vertex)        # Vertices that are not the first or last in the reference path        else:            prev_vertex = a_path[index - 1]            next_vertex = a_path[index + 1]            # Disconnect vertices             # Previous with current vertex            try:                G.remove_edge(prev_vertex, vertex)            except Exception:                pass            # Current with next vertex            try:                G.remove_edge(vertex, next_vertex)            except Exception:                pass            # Subgraph BFS block            bfs_attributes(G, vertex = vertex, kernel_path = a_path)            # Connect vertices back            try:                G.add_edge(prev_vertex, vertex)                G.add_edge(vertex, next_vertex)            except Exception:                pass    # Attribute assignment block - number of descendants     vert_list = list(nx.bfs_successors(G, source = start))     for component in vert_list:        vertex = component[0] # The vertex where we are at this iteration        neighbors = component[1] # Vertices that are neighboring (which we haven't visited yet)        # Adding an attribute - the number of descendants of this vertex        n_offspring = len(neighbors)        attrs = {vertex: {'offspring' : n_offspring}}        nx.set_node_attributes(G, attrs)

Демонстрацию описанных действий можно увидеть на анимации:



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


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


Присваивание атрибутов "value" и "distance"


Итак, на графе всем вершинам присвоены значения атрибутов "rank" и "offspring". Дальнейшие рассуждения можно свести к следующему.


Если вершина графа не имеет потомков, то это значит, что ни один приток в данный сегмент речной сети не впадает, следовательно, данной вершине требуется присвоить значение "value" 1. Затем, для каждого узла, у которого имеются потомки со значением "value", равному 1, необходимо сложить значения "value" у потомков (ранг потомков всегда на 1 меньше, чем ранг рассматриваемой вершины) и присвоить полученное число как атрибут вершины "value". Затем, данная процедура повторяется ровно столько раз, сколько рангов присутствует в графе.


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


iter_set_values

Input:


  • G граф с присвоенными значениями атрибутов 'rank' и 'offspring'
  • start вершина, из которой требуется начинать обход

Output:


  • Каждой вершине графа G присваивается значение атрибута 'value'. Значение данного атрибута можно интерпретировать как 'количество совокупно впадающих притоков в данную часть речной сети'. Также присваивается атрибут 'distance', который означает расстояние, которое потребуется преодолеть, чтобы достичь устья реки из данного сегмента.

# Function for determining the order of river segments similar to the Shreve method# In addition, the "distance" attribute is assigneddef set_values(G, start, considering_rank, vert_list):     # For each vertex in the list    for vertex in vert_list:        # If value has already been assigned, then skip it        if G.nodes[vertex].get('value') == 1:            pass        else:                                # Defining descendants            offspring = list(nx.bfs_successors(G, source = vertex, depth_limit = 1))             # We use only the nearest neighbors to this vertex (first descendants)            offspring = offspring[0][1]            # The cycle of determining the values at the vertices of a descendant            last_values = []            for child in offspring:                # We only need descendants whose rank value is greater than that of the vertex                if G.nodes[child].get('rank') > considering_rank:                    if G.nodes[child].get('value') != None:                        last_values.append(G.nodes[child].get('value'))                    else:                        pass                else:                    pass            last_values = np.array(last_values)            sum_values = np.sum(last_values)            # If the amount is not equal to 0, the attribute is assigned            if sum_values != 0:                attrs = {vertex: {'value' : sum_values}}                nx.set_node_attributes(G, attrs)            else:                pass# Function for iteratively assigning the value attributedef iter_set_values(G, start):    # Vertices and corresponding attribute values    ranks_list = []    vertices_list = []    offspring_list = []    for vertex in list(G.nodes()):        ranks_list.append(G.nodes[vertex].get('rank'))        vertices_list.append(vertex)        att_offspring = G.nodes[vertex].get('offspring')        if att_offspring == None:            offspring_list.append(0)         else:            offspring_list.append(att_offspring)    # Largest rank value in a graph    max_rank = max(ranks_list)    # Creating pandas dataframe    df = pd.DataFrame({'ranks': ranks_list,                       'vertices': vertices_list,                       'offspring': offspring_list})    # We assign value = 1 to all vertices of the graph that have no offspring    value_1_list = list(df['vertices'][df['offspring'] == 0])    for vertex in value_1_list:        attrs = {vertex: {'value' : 1}}        nx.set_node_attributes(G, attrs)     # For each rank, we begin to assign attributes    for considering_rank in range(max_rank, 0, -1):        # List of vertices of suitable rank        vert_list = list(df['vertices'][df['ranks'] == considering_rank])        set_values(G, start, considering_rank, vert_list)     # Assigning the "distance" attribute to the graph vertices    # List of all vertexes that can be reached from the start vertex using BFS    vert_list = list(nx.bfs_successors(G, source = start))    for component in vert_list:        vertex = component[0] # The vertex where we are at this iteration        neighbors = component[1] # Vertices that are neighboring (which we haven't visited yet)        # If we are at the closing vertex        if vertex == start:            # Length of this segment            att_vertex_size = G.nodes[vertex].get('size')            # Adding the value of the distance attribute            attrs = {vertex: {'distance' : att_vertex_size}}            nx.set_node_attributes(G, attrs)        else:            pass        vertex_distance = G.nodes[vertex].get('distance')                # For each neighbor, we assign an attribute        for i in neighbors:                        # Adding the value of the distance attribute            i_size = G.nodes[i].get('size')            attrs = {i: {'distance' : (vertex_distance + i_size)}}            nx.set_node_attributes(G, attrs) 


Одновременно с присваиванием вершинам графа атрибута "value", производится присваивание атрибута "distance", который характеризует удаленность сегментов от устья не количеством сегментов, которое необходимо преодолеть, чтобы достичь замыкающего, а расстояние в метрах, которое необходимо будет преодолеть, чтобы достичь устья реки.


Результат использования алгоритма можно увидеть на рисунке 8. Удаленность сегментов речной сети показана на основе атрибута "rank" и совокупное количество притоков показано на основе атрибута "value".



Рисунок 8. Результаты использования алгоритма (данные OpenStreetMap, Удаленность сегментов речной сети показана на основе атрибута "rank", Совокупное количество впадающих притоков- на основе атрибута "value")


Заключение


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


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


Открытой реализацией алгоритма на языке Python, а также встраиваемым плагином для QGIS может воспользоваться любой желающий. Вся обработка осуществляется одним потоком, у пользователя нет необходимости запускать все описанные функции по отдельности.


Мы будем рады ответить на ваши вопросы и увидеть ваши комментарии.


Репозиторий с исходным кодом:



Над алгоритмом и статьей работали Михаил Сарафанов, Юлия Борисова.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru