Получение координат ограниченного многоугольника из ячеек Вороного

У меня есть точки (например, lat, lon пары местоположений вышек сотовой связи), и мне нужно получить многоугольник ячеек Вороного, которые они образуют.

from scipy.spatial import Voronoi

tower = [[ 24.686 ,  46.7081],
       [ 24.686 ,  46.7081],
       [ 24.686 ,  46.7081]]

c = Voronoi(towers)

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


person amaatouq    schedule 23.02.2015    source источник


Ответы (2)


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

Однако вот вторая идея (хак?), Которая пришла мне в голову: алгоритмы вычисления диаграммы Вороного для набора n точек на плоскости имеют временную сложность O(n ln(n)). Как насчет добавления точек, чтобы ограничить ячейки Вороного исходных точек, чтобы они лежали в ограничивающей рамке?

Решение ограниченной диаграммы Вороного

Картина достойна отличного выступления:

введите здесь описание изображения

Что я здесь делал? Это очень просто! Начальные точки (отмечены синим цветом) лежат в [0.0, 1.0] x [0.0, 1.0]. Затем я получаю точки (синие) слева (т.е. [-1.0, 0.0] x [0.0, 1.0]) симметрией отражения в соответствии с x = 0.0 (левый край ограничивающей рамки). С симметрией отражения в соответствии с x = 1.0, y = 0.0 и y = 1.0 (другие края ограничивающей рамки) я получаю все точки (отмечены синим цветом), которые мне нужны для работы.

Потом бегаю scipy.spatial.Voronoi. На предыдущем изображении показана получившаяся диаграмма Вороного (я использую scipy.spatial.voronoi_plot_2d).

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

введите здесь описание изображения

Немного повеселимся перед тем, как показывать код

Большой! Вроде работает. Что, если после одной итерации я попытаюсь повторно запустить алгоритм по центроидам (красным), а не начальным точкам (синим)? Что, если я попробую снова и снова?

Шаг 2

введите здесь описание изображения

Шаг 10

введите здесь описание изображения

Шаг 25

введите описание изображения здесь

Прохладный! Клетки Вороного стремятся минимизировать свою энергию ...

Вот код

import matplotlib.pyplot as pl
import numpy as np
import scipy as sp
import scipy.spatial
import sys

eps = sys.float_info.epsilon

n_towers = 100
towers = np.random.rand(n_towers, 2)
bounding_box = np.array([0., 1., 0., 1.]) # [x_min, x_max, y_min, y_max]

def in_box(towers, bounding_box):
    return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0],
                                         towers[:, 0] <= bounding_box[1]),
                          np.logical_and(bounding_box[2] <= towers[:, 1],
                                         towers[:, 1] <= bounding_box[3]))


def voronoi(towers, bounding_box):
    # Select towers inside the bounding box
    i = in_box(towers, bounding_box)
    # Mirror points
    points_center = towers[i, :]
    points_left = np.copy(points_center)
    points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0])
    points_right = np.copy(points_center)
    points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0])
    points_down = np.copy(points_center)
    points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2])
    points_up = np.copy(points_center)
    points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1])
    points = np.append(points_center,
                       np.append(np.append(points_left,
                                           points_right,
                                           axis=0),
                                 np.append(points_down,
                                           points_up,
                                           axis=0),
                                 axis=0),
                       axis=0)
    # Compute Voronoi
    vor = sp.spatial.Voronoi(points)
    # Filter regions
    regions = []
    for region in vor.regions:
        flag = True
        for index in region:
            if index == -1:
                flag = False
                break
            else:
                x = vor.vertices[index, 0]
                y = vor.vertices[index, 1]
                if not(bounding_box[0] - eps <= x and x <= bounding_box[1] + eps and
                       bounding_box[2] - eps <= y and y <= bounding_box[3] + eps):
                    flag = False
                    break
        if region != [] and flag:
            regions.append(region)
    vor.filtered_points = points_center
    vor.filtered_regions = regions
    return vor

def centroid_region(vertices):
    # Polygon's signed area
    A = 0
    # Centroid's x
    C_x = 0
    # Centroid's y
    C_y = 0
    for i in range(0, len(vertices) - 1):
        s = (vertices[i, 0] * vertices[i + 1, 1] - vertices[i + 1, 0] * vertices[i, 1])
        A = A + s
        C_x = C_x + (vertices[i, 0] + vertices[i + 1, 0]) * s
        C_y = C_y + (vertices[i, 1] + vertices[i + 1, 1]) * s
    A = 0.5 * A
    C_x = (1.0 / (6.0 * A)) * C_x
    C_y = (1.0 / (6.0 * A)) * C_y
    return np.array([[C_x, C_y]])

vor = voronoi(towers, bounding_box)

fig = pl.figure()
ax = fig.gca()
# Plot initial points
ax.plot(vor.filtered_points[:, 0], vor.filtered_points[:, 1], 'b.')
# Plot ridges points
for region in vor.filtered_regions:
    vertices = vor.vertices[region, :]
    ax.plot(vertices[:, 0], vertices[:, 1], 'go')
# Plot ridges
for region in vor.filtered_regions:
    vertices = vor.vertices[region + [region[0]], :]
    ax.plot(vertices[:, 0], vertices[:, 1], 'k-')
# Compute and plot centroids
centroids = []
for region in vor.filtered_regions:
    vertices = vor.vertices[region + [region[0]], :]
    centroid = centroid_region(vertices)
    centroids.append(list(centroid[0, :]))
    ax.plot(centroid[:, 0], centroid[:, 1], 'r.')

ax.set_xlim([-0.1, 1.1])
ax.set_ylim([-0.1, 1.1])
pl.savefig("bounded_voronoi.png")

sp.spatial.voronoi_plot_2d(vor)
pl.savefig("voronoi.png")
person Flabetvibes    schedule 09.11.2015
comment
почему вы вычитаете эпсилон, когда проверяете, находятся ли точки в пределах ограничивающей рамки? Чтобы убедиться, что ошибки округления с плавающей запятой не заставляют точку появляться за пределами ограничивающей рамки, когда она на самом деле находится внутри? - person duhaime; 25.09.2018
comment
Кроме того, почему вы умножаете A на 6,0 при получении координат центроида? Мы будем очень благодарны за любую помощь, которую вы можете предложить по этим вопросам! - person duhaime; 25.09.2018
comment
Ах, это отвечает на мой второй вопрос: en.wikipedia.org/wiki/Centroid#Of_a_polygon - person duhaime; 25.09.2018
comment
Чтобы ответить на ваш первый вопрос: да, это вычитание присутствует только для обработки ошибок округления с плавающей запятой. Если я хорошо помню, я начал без, но не смог получить ожидаемый результат. Если вы попробуете код без вычитания, скажите мне, правильно ли он работает. - person Flabetvibes; 25.09.2018
comment
Большое спасибо за этот код и понимание! У меня есть улучшение для вашей процедуры: по конструкции регионы, которые вы хотите сохранить, относятся к первой 1/5 баллов, которые вы дали scipy.spatial.Voronoi(). Это означает, что вы можете использовать атрибут Voronoi.point_region, который дает для каждой точки индекс региона, к которому она принадлежит: vor.filtered_regions = np.array(vor.regions)[vor.point_region[:vor.npoints//5]]. Это а) экономит кучу ручных проверок и б) гарантирует, что регионы возвращаются в том же порядке, что и их точки сопоставления (что было критически важно для моего варианта использования :)) - person Energya; 28.05.2019
comment
Спасибо за отличный ответ @Flabetvibes! Я использовал его здесь stackoverflow.com/a/61893015/139144 для устранения артефакта с графикой KDE, который меня беспокоил . - person Gabriel; 19.05.2020

У меня было много проблем с использованием функции scipy voronoi и созданием CVD, поэтому эти замечательные сообщения и комментарии мне очень помогли. Как новичок в программировании, я попытался понять код из ответа Flabetvvibes, и я собираюсь поделиться своей интерпретацией того, как он работает, вместе с Energya и моими собственными модификациями. Я также полностью разместил свою версию кода внизу этого ответа.

import matplotlib.pyplot as pl
import numpy as np
import scipy as sp
import scipy.spatial
import sys
import copy

eps = sys.float_info.epsilon

# Returns a new np.array of towers that within the bounding_box
def in_box(towers, bounding_box):
    return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0],
                                         towers[:, 0] <= bounding_box[1]),
                          np.logical_and(bounding_box[2] <= towers[:, 1],
                                         towers[:, 1] <= bounding_box[3]))

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

# Generates a bounded vornoi diagram with finite regions in the bounding box
def bounded_voronoi(towers, bounding_box):
    # Select towers inside the bounding box
    i = in_box(towers, bounding_box)

    # Mirror points left, right, above, and under to provide finite regions for the
    # edge regions of the bounding box
    points_center = towers[i, :]

    points_left = np.copy(points_center)
    points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0])

    points_right = np.copy(points_center)
    points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0])

    points_down = np.copy(points_center)
    points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2])

    points_up = np.copy(points_center)
    points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1])

    points = np.append(points_center,
                       np.append(np.append(points_left,
                                           points_right,
                                           axis=0),
                                 np.append(points_down,
                                           points_up,
                                           axis=0),
                                 axis=0),
                       axis=0)

Flabetvvibes отражает точки, чтобы области вдоль внутренних краев ограничивающей рамки были конечными. Метод voronoi Scipy возвращает -1 для вершин, которые не определены, поэтому зеркальное отображение точек позволяет всем областям внутри ограничивающего прямоугольника быть конечными, а все бесконечные области находятся в зеркальных областях за пределами ограничивающего прямоугольника, которые позже будут отброшены.

# Compute Voronoi
vor = sp.spatial.Voronoi(points)

# creates a new attibute for points that form the diagram within the region
vor.filtered_points = points_center 
# grabs the first fifth of the regions, which are the original regions
vor.filtered_regions = np.array(vor.regions)[vor.point_region[:vor.npoints//5]]

return vor

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

def generate_CVD(points, iterations, bounding_box):
    p = copy.copy(points)

    for i in range(iterations):
        vor = bounded_voronoi(p, bounding_box)
        centroids = []

        for region in vor.filtered_regions:
            # grabs vertices for the region and adds a duplicate
            # of the first one to the end
            vertices = vor.vertices[region + [region[0]], :] 
            centroid = centroid_region(vertices)
            centroids.append(list(centroid[0, :]))

        p = np.array(centroids)

    return bounded_voronoi(p, bounding_box)

Я взял код Flabetvvibe, который выполняет итерацию алгоритма Лойда, и превратил его в метод для простоты использования. Для каждой итерации вызывается предыдущая функция bounded_voronoi, затем для каждой ячейки находятся центроиды, которые становятся новым набором точек для следующей итерации. vertices = vor.vertices[region + [region[0]], :] просто захватывает все вершины для текущего региона и дублирует первые вершины до конца, так что первая и последняя вершины совпадают для вычисления центроидов.

Спасибо Flabetvvibes и Energya. Ваши сообщения / ответы научили меня использовать метод scipy voronoi лучше, чем его документация. Я также разместил код как одно тело под любым другим, ищущим копию / вставку.

import matplotlib.pyplot as pl
import numpy as np
import scipy as sp
import scipy.spatial
import sys
import copy

eps = sys.float_info.epsilon

# Returns a new np.array of towers that within the bounding_box
def in_box(towers, bounding_box):
    return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0],
                                         towers[:, 0] <= bounding_box[1]),
                          np.logical_and(bounding_box[2] <= towers[:, 1],
                                         towers[:, 1] <= bounding_box[3]))


# Generates a bounded vornoi diagram with finite regions
def bounded_voronoi(towers, bounding_box):
    # Select towers inside the bounding box
    i = in_box(towers, bounding_box)

    # Mirror points left, right, above, and under to provide finite regions for the edge regions of the bounding box
    points_center = towers[i, :]

    points_left = np.copy(points_center)
    points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0])

    points_right = np.copy(points_center)
    points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0])

    points_down = np.copy(points_center)
    points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2])

    points_up = np.copy(points_center)
    points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1])

    points = np.append(points_center,
                       np.append(np.append(points_left,
                                           points_right,
                                           axis=0),
                                 np.append(points_down,
                                           points_up,
                                           axis=0),
                                 axis=0),
                       axis=0)

    # Compute Voronoi
    vor = sp.spatial.Voronoi(points)

    vor.filtered_points = points_center # creates a new attibute for points that form the diagram within the region
    vor.filtered_regions = np.array(vor.regions)[vor.point_region[:vor.npoints//5]] # grabs the first fifth of the regions, which are the original regions

    return vor


# Finds the centroid of a region. First and last point should be the same.
def centroid_region(vertices):
    # Polygon's signed area
    A = 0
    # Centroid's x
    C_x = 0
    # Centroid's y
    C_y = 0
    for i in range(0, len(vertices) - 1):
        s = (vertices[i, 0] * vertices[i + 1, 1] - vertices[i + 1, 0] * vertices[i, 1])
        A = A + s
        C_x = C_x + (vertices[i, 0] + vertices[i + 1, 0]) * s
        C_y = C_y + (vertices[i, 1] + vertices[i + 1, 1]) * s
    A = 0.5 * A
    C_x = (1.0 / (6.0 * A)) * C_x
    C_y = (1.0 / (6.0 * A)) * C_y
    return np.array([[C_x, C_y]])


# Performs x iterations of loyd's algorithm to calculate a centroidal vornoi diagram
def generate_CVD(points, iterations, bounding_box):
    p = copy.copy(points)

    for i in range(iterations):
        vor = bounded_voronoi(p, bounding_box)
        centroids = []

        for region in vor.filtered_regions:
            vertices = vor.vertices[region + [region[0]], :] # grabs vertices for the region and adds a duplicate of the first one to the end
            centroid = centroid_region(vertices)
            centroids.append(list(centroid[0, :]))

        p = np.array(centroids)

    return bounded_voronoi(p, bounding_box)


# returns a pyplot of given voronoi data
def plot_vornoi_diagram(vor, bounding_box, show_figure):
    # Initializes pyplot stuff
    fig = pl.figure()
    ax = fig.gca()

    # Plot initial points
    ax.plot(vor.filtered_points[:, 0], vor.filtered_points[:, 1], 'b.')

    # Plot ridges points
    for region in vor.filtered_regions:
        vertices = vor.vertices[region, :]
        ax.plot(vertices[:, 0], vertices[:, 1], 'go')

    # Plot ridges
    for region in vor.filtered_regions:
        vertices = vor.vertices[region + [region[0]], :]
        ax.plot(vertices[:, 0], vertices[:, 1], 'k-')

    # stores references to numbers for setting axes limits
    margin_percent = .1
    width = bounding_box[1]-bounding_box[0]
    height = bounding_box[3]-bounding_box[2]

    ax.set_xlim([bounding_box[0]-width*margin_percent, bounding_box[1]+width*margin_percent])
    ax.set_ylim([bounding_box[2]-height*margin_percent, bounding_box[3]+height*margin_percent])

    if show_figure:
        pl.show()
    return fig
person Nick Maclean    schedule 26.05.2020