0

My question is similar to this one (R + ggplot) but for Python.

Question

How to generate a (2D) Voronoi diagram where cells have a maximum extend / growth limit ? it would result in some cells with curved boundary (circle arc), and some space regions not filled by any cell.

An image processing interpretation of the desired output would be to perform some "AND" mask between the voronoi cells and circles centered on each cell with the required radius.

(For my particular problem, I am working with geolocated data, and for now I am willing to accept discrepancies from most voronoi libs that will use lat/lon as cartesian coordinates. That is another matter...)

This question is NOT about clipping or handling of "inifinite" voronoi cells (see scypi.spatial.Voronoi), so the following links are NOT related :

Vaguely related topics:


Example from the R + ggplot answer: enter image description here


sample code for tests:

from typing import Tuple, List, Union

import geovoronoi
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
from geographiclib.geodesic import Geodesic
from shapely.geometry import Polygon, Point
from simplekml import LineStyle, Color, PolyStyle, Container

WGS84_Tool = Geodesic.WGS84

T_Color_List = List[Union[clr.Colormap, clr.LinearSegmentedColormap, clr.ListedColormap]]


def generate_n_colors(n, cmap_name='tab20') -> T_Color_List:
    """
    https://github.com/WZBSocialScienceCenter/geovoronoi/blob/master/geovoronoi/plotting.py
    Get a list of `n` numbers from matplotlib color map `cmap_name`. If `n` is larger than the number of colors in the
    color map, the colors will be recycled, i.e. they are not unique in this case.
    :param n: number of colors to generate
    :param cmap_name: matplotlib color map name
    :return: list of `n` colors
    """
    pt_region_colormap = plt.get_cmap(cmap_name)
    max_i = len(pt_region_colormap.colors)
    return [pt_region_colormap(i % max_i) for i in range(n)]



def _plot_cell_and_seed(kml: Container,
                        name: str,
                        region_polygon: Polygon,
                        seed_coords: Tuple[float, float],
                        _kml_color: str):
    _p = kml.newpolygon(name=f"{name} zone",
                        outerboundaryis=[(lon, lat)
                                         for lat, lon
                                         in region_polygon.exterior.coords], )
    _p.style.linestyle = LineStyle(color=Color.darkgrey, width=1.)
    _p.style.polystyle = PolyStyle(color=_kml_color)
    p = kml.newpoint(coords=[seed_coords],
                     name=name)
    p.style.iconstyle.icon.href = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png"
    p.style.iconstyle.scale = 0.5
    p.style.labelstyle.scale = 0.5


def plot_regions(region_polys,
                 region_pts,
                 seeds_coords_list: np.ndarray,
                 seeds_names: List[str],
                 kml: Container,
                 colors: T_Color_List,
                 ):
    assert (len(seeds_names) == len(seeds_coords_list))
    index = 0
    for region_id, region_polygon in region_polys.items():
        _cell_point_indexes = region_pts[region_id]
        _cell_seed_coords = seeds_coords_list[_cell_point_indexes][0]
        name = seeds_names[_cell_point_indexes[0]]
        _kml_airport_coords = (_cell_seed_coords[-1], _cell_seed_coords[0])
        _mpl_color = colors[index]
        _mpl_hexa_color = clr.to_hex(_mpl_color, keep_alpha=True)
        _hexa_color_no_sharp = _mpl_hexa_color.split("#")[-1]
        _kml_color = Color.hexa(_hexa_color_no_sharp)
        _kml_color = Color.changealphaint(alpha=7 * 255 // 10, gehex=_kml_color)
        _plot_cell_and_seed(kml=kml,
                            name=name,
                            region_polygon=region_polygon,
                            seed_coords=_kml_airport_coords,
                            _kml_color=_kml_color)
        index += 1


# bounding box for geovoronoi
geo_boundaries = {"min": {"lat": +30, "lon": -12},
                  "max": {"lat": +75, "lon": +35}, }
# a list of [[lat,lon],[lat,lon],[lat,lon],[lat,lon],]
n = 150
seeds_coords_list = np.dstack(
    [np.random.uniform(low=geo_boundaries["min"]["lat"], high=geo_boundaries["max"]["lat"], size=n),
     np.random.uniform(low=geo_boundaries["min"]["lon"], high=geo_boundaries["max"]["lon"], size=n), ]).reshape((n, 2))
seeds_names = [f"{lat:+_.2f};{lon:+_.2f}" for lat, lon in seeds_coords_list]

boundary_points = geovoronoi.coords_to_points([[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]],
                                               [geo_boundaries["min"]["lat"], geo_boundaries["max"]["lon"]],
                                               [geo_boundaries["max"]["lat"], geo_boundaries["max"]["lon"]],
                                               [geo_boundaries["max"]["lat"], geo_boundaries["min"]["lon"]],
                                               # last necessary ?
                                               [geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]]])
boundary_polygon = Polygon(boundary_points)

# beware that geodesics and geovoronoi may not be accurate since it uses planar cartesian formulas...
region_polys, region_pts = geovoronoi.voronoi_regions_from_coords(seeds_coords_list, boundary_polygon)

# DO SOMETHING HERE

#...
#...
#...
kdoc = Kml()
p_kml = Path.cwd() / "voronoi_so.kml"
colors: T_Color_List = generate_n_colors(len(region_polys))
plot_regions(region_polys=region_polys,
             region_pts=region_pts,
             seeds_coords_list=seeds_coords_list,
             seeds_names=seeds_names,
             kml=kdoc.newfolder(name="raw regions"),
             colors=colors)
print("save KML")
kdoc.save(p_kml.as_posix())
print(p_kml.as_uri())
LoneWanderer
  • 3,058
  • 1
  • 23
  • 41

1 Answers1

0

After some thinking, here is an approach using Shapely and using the intersection between computed circles (360 vertices) and each voronoi cell.

Performance is not convincing since we create 360 points and a polygon + 1 intersection computation for each cell... (at least it is a bounded ...).

However, the underlying voronoi computation data such as cells adjacency is lost, and we can't know if some cells are isolated after this transformation. Maybe some ideas here: Determining and storing Voronoi Cell Adjacency

I do believe better solutions may exist.

So here is a solution, with random geographical data, and corresponding kml rendering.

EDIT: using weighted voronoi diagrams, a possible weight function could be f(d) = d <= radius ? I could not explore this for now...

Raw regions:

enter image description here

Modified raw regions:

enter image description here

from pathlib import Path
from typing import Tuple, List, Union

import geovoronoi
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
from geographiclib.geodesic import Geodesic
from shapely.geometry import Polygon, Point
from simplekml import LineStyle, Kml, Color, PolyStyle, Container

WGS84_Tool = Geodesic.WGS84

T_Color_List = List[Union[clr.Colormap, clr.LinearSegmentedColormap, clr.ListedColormap]]


def generate_n_colors(n, cmap_name='tab20') -> T_Color_List:
    """
    https://github.com/WZBSocialScienceCenter/geovoronoi/blob/master/geovoronoi/plotting.py
    Get a list of `n` numbers from matplotlib color map `cmap_name`. If `n` is larger than the number of colors in the
    color map, the colors will be recycled, i.e. they are not unique in this case.
    :param n: number of colors to generate
    :param cmap_name: matplotlib color map name
    :return: list of `n` colors
    """
    pt_region_colormap = plt.get_cmap(cmap_name)
    max_i = len(pt_region_colormap.colors)
    return [pt_region_colormap(i % max_i) for i in range(n)]


def _create_bounded_regions(region_polys,
                            region_pts,
                            distance_criteria_m: float,
                            cell_seed_coords_list: np.ndarray,
                            nb_vertices:int=36):
    new_polygons = {}
    for region_id, region_polygon in region_polys.items():
        _cell_point_indexes = region_pts[region_id]
        _cell_seed_coords = cell_seed_coords_list[_cell_point_indexes][0]
        _arpt_lat = _cell_seed_coords[0]
        _arpt_lon = _cell_seed_coords[-1]
        cycle_vertices = []
        for a in np.linspace(0,359,nb_vertices):
            p = WGS84_Tool.Direct(lat1=_arpt_lat, lon1=_arpt_lon, azi1=a, s12=distance_criteria_m)
            _point = Point(p["lat2"], p["lon2"])
            cycle_vertices.append(_point)
        circle = Polygon(cycle_vertices)
        new_polygons[region_id] = region_polygon.intersection(circle)
    return new_polygons


def _plot_cell_and_seed(kml: Container,
                        name: str,
                        region_polygon: Polygon,
                        seed_coords: Tuple[float, float],
                        _kml_color: str):
    _p = kml.newpolygon(name=f"{name} zone",
                        outerboundaryis=[(lon, lat)
                                         for lat, lon
                                         in region_polygon.exterior.coords], )
    _p.style.linestyle = LineStyle(color=Color.darkgrey, width=1.)
    _p.style.polystyle = PolyStyle(color=_kml_color)
    p = kml.newpoint(coords=[seed_coords],
                     name=name)
    p.style.iconstyle.icon.href = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png"
    p.style.iconstyle.scale = 0.5
    p.style.labelstyle.scale = 0.5


def plot_regions(region_polys,
                 region_pts,
                 seeds_coords_list: np.ndarray,
                 seeds_names: List[str],
                 kml: Container,
                 colors: T_Color_List,
                 ):
    assert (len(seeds_names) == len(seeds_coords_list))
    index = 0
    for region_id, region_polygon in region_polys.items():
        _cell_point_indexes = region_pts[region_id]
        _cell_seed_coords = seeds_coords_list[_cell_point_indexes][0]
        name = seeds_names[_cell_point_indexes[0]]
        _kml_airport_coords = (_cell_seed_coords[-1], _cell_seed_coords[0])
        _mpl_color = colors[index]
        _mpl_hexa_color = clr.to_hex(_mpl_color, keep_alpha=True)
        _hexa_color_no_sharp = _mpl_hexa_color.split("#")[-1]
        _kml_color = Color.hexa(_hexa_color_no_sharp)
        _kml_color = Color.changealphaint(alpha=7 * 255 // 10, gehex=_kml_color)
        _plot_cell_and_seed(kml=kml,
                            name=name,
                            region_polygon=region_polygon,
                            seed_coords=_kml_airport_coords,
                            _kml_color=_kml_color)
        index += 1


# bounding box for geovoronoi
geo_boundaries = {"min": {"lat": +30, "lon": -12},
                  "max": {"lat": +75, "lon": +35}, }
# a list of [[lat,lon],[lat,lon],[lat,lon],[lat,lon],]
n = 150
seeds_coords_list = np.dstack(
    [np.random.uniform(low=geo_boundaries["min"]["lat"], high=geo_boundaries["max"]["lat"], size=n),
     np.random.uniform(low=geo_boundaries["min"]["lon"], high=geo_boundaries["max"]["lon"], size=n), ]).reshape((n, 2))
seeds_names = [f"{lat:+_.2f};{lon:+_.2f}" for lat, lon in seeds_coords_list]

boundary_points = geovoronoi.coords_to_points([[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]],
                                               [geo_boundaries["min"]["lat"], geo_boundaries["max"]["lon"]],
                                               [geo_boundaries["max"]["lat"], geo_boundaries["max"]["lon"]],
                                               [geo_boundaries["max"]["lat"], geo_boundaries["min"]["lon"]],
                                               # last necessary ?
                                               [geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]]])
boundary_polygon = Polygon(boundary_points)

# beware that geodesics and geovoronoi may not be accurate since it uses planar cartesian formulas...
region_polys, region_pts = geovoronoi.voronoi_regions_from_coords(seeds_coords_list, boundary_polygon)
new_region_polys = _create_bounded_regions(region_polys=region_polys,
                                           region_pts=region_pts,
                                           distance_criteria_m=300_000.,
                                           cell_seed_coords_list=seeds_coords_list, )
kdoc = Kml()
p_kml = Path.cwd() / "voronoi_so.kml"
colors: T_Color_List = generate_n_colors(len(region_polys))

plot_regions(region_polys=region_polys,
             region_pts=region_pts,
             seeds_coords_list=seeds_coords_list,
             seeds_names=seeds_names,
             kml=kdoc.newfolder(name="raw regions"),
             colors=colors)

plot_regions(region_polys=new_region_polys,
             region_pts=region_pts,
             seeds_coords_list=seeds_coords_list,
             seeds_names=seeds_names,
             kml=kdoc.newfolder(name="new_regions (range)"),
             colors=colors)

print("save KML")
kdoc.save(p_kml.as_posix())
print(p_kml.as_uri())

LoneWanderer
  • 3,058
  • 1
  • 23
  • 41