I need to edit the geometry of intersecting polygons and I don't know how I can save modified geometry to a shapefile. Is it even possible?
from shapely.geometry import Polygon, shape
import matplotlib.pyplot as plt
import fiona
c = fiona.open('polygon23.shp', 'r')
d = fiona.open('polygon23.shp', 'r')
for poly in c.values():
for poly2 in d.values():
p_poly = shape(poly['geometry'])
p_poly2 = shape(poly2['geometry'])
intersect_polygons = p_poly.intersection(p_poly2)
if type(intersect_polygons) == Polygon:
intersect_polygons = p_poly.intersection(p_poly2).exterior.coords
if p_poly.exterior.xy != p_poly2.exterior.xy:
y_difference = abs(intersect_polygons[0][1]) - abs(intersect_polygons[2][1])
coords_polygonB = p_poly2.exterior.coords[:]
coords_polygonB[0] = (coords_polygonB[0][0], coords_polygonB[0][1] + (y_difference))
coords_polygonB[1] = (coords_polygonB[1][0], coords_polygonB[1][1] + (y_difference))
coords_polygonB[2] = (coords_polygonB[2][0], coords_polygonB[2][1] + (y_difference))
coords_polygonB[3] = (coords_polygonB[3][0], coords_polygonB[3][1] + (y_difference))
coords_polygonB[4] = (coords_polygonB[4][0], coords_polygonB[4][1] + (y_difference))
p_poly2 = Polygon(coords_polygonB)
x,y = p_poly.exterior.xy
plt.plot(x,y)
x,y = p_poly2.exterior.xy
plt.plot(x,y)
plt.show()
The removal of intersections between many polygons is most likely a complex problem. Moreover, I used your method as the solver in my solution.
Answer
The answer to your question, is yes. You can rectify the intersections between the polygons in your shp file; however, you need to create new Polygon objects, you can't just change the exterior coordinates of an existing Polygon.
Store metadata and disc from original shp file
The solution below writes/creates the resulting polygon set to a new shp file. This requires us to store the metadata from the original shp file, and pass it to the new one. We also need to store the properties of each polygon, I store these in a separate list, set_of_properties.
No need for two for loops
You don't need to for loops, just use combinations from the itertools standard library to loop through all possible polygon combinations. I use index combinations to replace polygons that are intersecting with new ones.
Outer do...while-loop
In very cringe caes, a rectification using your method may actually introduce new intersections. We can catch these and rectify them by looping through your solver until there are no intersections left. This requires a do... while loop, but there is no do...while loop in Python. Moreover, it can be implemented with while-loops (see Solution for implementation).
Solution
from itertools import combinations
from shapely.geometry import Polygon, Point, shape, mapping
import matplotlib.pyplot as plt
import fiona
SHOW_NEW_POLYGONS = False
polygons, set_of_properties = [], []
with fiona.open("polygon23.shp", "r") as source:
for line in source:
polygons.append(shape(line["geometry"]))
set_of_properties.append(line["properties"])
meta = source.meta
poly_index_combinations = combinations(tuple(range(len(polygons))), 2)
while True:
intersection_record = []
for i_poly_a, i_poly_b in poly_index_combinations:
poly_a, poly_b = polygons[i_poly_a], polygons[i_poly_b]
if poly_a.exterior.xy == poly_b.exterior.xy:
# print(f"The polygons have identical exterior coordinates:\n{poly_a} and {poly_b}\n")
continue
intersecting = poly_a.intersection(poly_b)
if type(intersecting) != Polygon:
continue
intersecting_polygons = intersecting.exterior.coords
if not intersecting_polygons:
# print(f"No intersections between\n{poly_a} and {poly_b}\n")
continue
print("Rectifying intersection")
y_diff = abs(intersecting_polygons[0][1]) - abs(intersecting_polygons[2][1])
new_poly_b = Polygon((
Point(float(poly_b.exterior.coords[0][0]), float(poly_b.exterior.coords[0][1] + y_diff)),
Point(float(poly_b.exterior.coords[1][0]), float(poly_b.exterior.coords[1][1] + y_diff)),
Point(float(poly_b.exterior.coords[2][0]), float(poly_b.exterior.coords[2][1] + y_diff)),
Point(float(poly_b.exterior.coords[3][0]), float(poly_b.exterior.coords[3][1] + y_diff)),
Point(float(poly_b.exterior.coords[4][0]), float(poly_b.exterior.coords[4][1] + y_diff))
))
if SHOW_NEW_POLYGONS:
x, y = poly_a.exterior.xy
plt.plot(x, y)
x, y = new_poly_b.exterior.xy
plt.plot(x, y)
plt.show()
polygons[i_poly_b] = new_poly_b
intersection_record.append(True)
if not intersection_record:
break
with fiona.open("new.shp", "w", **meta) as sink:
for poly, properties in zip(polygons, set_of_properties):
sink.write({
"geometry": mapping(poly),
"properties": properties
})
Related
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 :
clipping a voronoi diagram python
How to limit Voronoi cells even when infinite with python?
Vaguely related topics:
Is there a way to vary the rate of Voronoi cell growth?
https://gis.stackexchange.com/questions/366471/weighted-voronoi-polygons-in-r
https://gis.stackexchange.com/questions/17282/create-weighted-thiessen-polygons/17284#17284
https://github.com/HichamZouarhi/Weighted-Voronoi-PyQGIS
Example from the R + ggplot answer:
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())
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:
Modified raw regions:
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())
I'd like to split a polygon into a list of polygons corresponding to all intersections with other polygons (and intersections between themselves).
from shapely.geometry import Point
circleA = Point((0, 0)).buffer(1)
circleB = Point((1, 0)).buffer(1)
circleC = Point((1, 1)).buffer(1)
def cascaded_intersections(poly1, lst_poly):
# ???
return result
result = cascaded_intersections(circleA, (circleB, circleC))
The result should be a list of 4 Polygons, corresponding to the 4 complementary parts of A (above: [AC!B, ABC, AB!C, rest of A]).
The problem is the same than spitting a polygon into its smallest parts from a list of covering LineStrings.
How to write cascaded_intersections ?
A colleague of mine, Pascal L., found a solution :
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from shapely.geometry import MultiPolygon, Polygon, Point, GeometryCollection
from shapely.ops import cascaded_union
EMPTY = GeometryCollection()
def partition(poly_a, poly_b):
"""
Splits polygons A and B into their differences and intersection.
"""
if not poly_a.intersects(poly_b):
return poly_a, poly_b, EMPTY
only_a = poly_a.difference(poly_b)
only_b = poly_b.difference(poly_a)
inter = poly_a.intersection(poly_b)
return only_a, only_b, inter
def eliminate_small_areas(poly, small_area):
"""
Eliminates tiny parts of a MultiPolygon (or Polygon)
"""
if poly.area < small_area:
return EMPTY
if isinstance(poly, Polygon):
return poly
assert isinstance(poly, MultiPolygon)
l = [p for p in poly if p.area > small_area]
if len(l) == 0:
return EMPTY
if len(l) == 1:
return l[0]
return MultiPolygon(l)
def cascaded_intersections(poly1, lst_poly):
"""
Splits Polygon poly1 into intersections of/with list of other polygons.
"""
result = [(lst_poly[0], (0,))]
for i, poly in enumerate(lst_poly[1:], start=1):
current = []
while result:
result_geometry, result_indexes = result.pop(0)
only_result, only_poly, inter = partition(result_geometry, poly)
for geometry, indexes in ((only_result, result_indexes), (inter, result_indexes + (i,))):
if not geometry.is_empty:
current.append((geometry, indexes))
current_union = cascaded_union([elt[0] for elt in current])
only_poly = poly.difference(current_union)
if not only_poly.is_empty:
current.append((only_poly, (i,)))
result = current
for r in range(len(result)-1, -1, -1):
geometry, indexes = result[r]
if poly1.intersects(geometry):
inter = poly1.intersection(geometry)
result[r] = (inter, indexes)
else:
del result[r]
only_poly1 = poly1.difference(cascaded_union([elt[0] for elt in result]))
only_poly1 = eliminate_small_areas(only_poly1, 1e-16*poly1.area)
if not only_poly1.is_empty:
result.append((only_poly1, None))
return [r[0] for r in result]
a=Point(0,0).buffer(1)
b1=Point(0,1).buffer(1)
b2=Point(1,0).buffer(1)
b3=Point(1,1).buffer(1)
result = cascaded_intersections(a, (b1,b2,b3))
Hi hi again, here's a better solution than my own, using part of gene's answer # stackexchange.com it uses shapely.ops functions cascaded_union, unary_union and polygonize.
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sg
from shapely.ops import cascaded_union, unary_union, polygonize
import shapely.affinity
import descartes
from itertools import combinations
circleA = sg.Point((0, 0)).buffer(1)
circleB = sg.Point((1, 0)).buffer(1)
circleC = sg.Point((1, 1)).buffer(1)
circles = [circleA,circleB,circleC]
listpoly = [a.intersection(b) for a, b in combinations(circles, 2)] #list of intersections
rings = [sg.LineString(list(pol.exterior.coords)) for pol in listpoly] #list of rings
union = unary_union(rings)
result = [geom for geom in polygonize(union)] #list all intersection geometries
multi = cascaded_union(result) #Create a single geometry out of all intersections
fin = [c.difference(multi) for c in circles] #Cut multi from circles and leave only outside geometries.
result = result + fin #add the outside geometries to the intersections geometries
#Plot settings:
plt.figure(figsize=(5,5))
ax = plt.gca()
name = 1
for e in result:
ax.add_patch(descartes.PolygonPatch(e,
fc=np.random.rand(3),
ec=None,
alpha=0.5))
ax.text(e.centroid.x,e.centroid.y,
'%s'%name,fontsize=9,
bbox=dict(facecolor='orange', alpha=0.5),
color='blue',
horizontalalignment='center')
name += 1
plt.xlim(-1.5,2.5)
plt.ylim(-1.5,2.5)
plt.show()
Como va. Hi Eric, I tried using the split function from shapely.ops. Here is the result. This is not the most time efficient or elegant solution but it works:
import matplotlib.pyplot as plt
import numpy as np #use np.random to give random RGB color to each polygon
import shapely.geometry as sg
from shapely.ops import split
import descartes
from itertools import combinations
def cascade_split(to_split,splitters): #Helper function for split recursion
'''
Return a list of all intersections between multiple polygons.
to_split: list, polygons or sub-polygons to split
splitters: list, polygons used as splitters
Returns a list of all the polygons formed by the multiple intersections.
'''
if len(splitters) == 0: # Each splitting geometry will be removed
return to_split # at the end of the function, reaching len == 0 at some point,
# only the it will return all the final splits.
new_to_split = [] # make a list that will run again though the function
for ts in to_split:
s = split(ts,splitters[0].boundary) # split geometry using the boundaries of another
for i in list(s):
new_to_split.append(i) #save the splits
splitters.remove(splitters[0]) #remove the splitting geometry to
#allow the split with the next polygon in line.
return cascade_split(new_to_split,splitters) #Use recursion to exhaust all splitting possibilities
#Create polygons, in this case circles.
circleA = sg.Point((0, 0)).buffer(1)
circleB = sg.Point((1, 0)).buffer(1)
circleC = sg.Point((1, 1)).buffer(1)
#Put all circles in list
circles = [circleA,circleB,circleC]
#The combinations tool takes the last polygon
#from list to split with the remaning polygons in list,
#creating a backwards copy of the circles list will help keep track of shapes.
back_circles = circles[::-1] #backwards copy of circles list
index_count = 0 #Keep track of which circle will get splitted
polys = [] #Final list of splitted polygons
for i in combinations(circles,len(circles)-1):
c_split = cascade_split([back_circles[index_count]],list(i)) #Use helper function here
for p in c_split:
#There will be duplicate polygon splits, the following condition will filter those:
if not any(poly.equals(p) for poly in polys):
polys.append(p)
index_count += 1
#plotting settings
plt.figure(figsize=(5,5))
ax = plt.gca()
for e in range(len(polys)):
ax.add_patch(descartes.PolygonPatch(polys[e],
fc=np.random.rand(3), #give random color to each split
ec=None,
alpha=0.5))
ax.text(polys[e].centroid.x,polys[e].centroid.y,
'%s' %(e+1),fontsize=9,
bbox=dict(facecolor='orange', alpha=0.5),
color='blue',
horizontalalignment='center')
plt.xlim(-1.5,2.5)
plt.ylim(-1.5,2.5)
plt.show()
polys #Output the polys list to see all the splits
I am using Python 3.5 64 bit in Windows 7 64 bit, shapely version 1.5.13.
I have the following code that returned me a self-intersecting polygon:
import numpy as np
from shapely.geometry import Polygon, MultiPolygon
import matplotlib.pyplot as plt
x = np.array([ 0.38517325, 0.40859912, 0.43296919, 0.4583215 , 0.4583215 ,
0.43296919, 0.40859912, 0.38517325, 0.36265506, 0.34100929])
y = np.array([ 62.5 , 56.17977528, 39.39698492, 0. ,
0. , 17.34605377, 39.13341671, 60.4180932 ,
76.02574417, 85.47008547])
polygon = Polygon(np.c_[x, y])
plt.plot(*polygon.exterior.xy)
This is correct. Then I tried to obtain the two individual polygons by using buffer(0):
split_polygon = polygon.buffer(0)
plt.plot(*polygon.exterior.xy)
print(type(split_polygon))
plt.fill(*split_polygon.exterior.xy)
Unfortunately, it only returned of the the two polygons:
Could anyone please help? Thanks!
The first step is to close the LineString to make a LinearRing, which is what Polygons are made of.
from shapely.geometry import LineString, MultiPolygon
from shapely.ops import polygonize, unary_union
# original data
ls = LineString(np.c_[x, y])
# closed, non-simple
lr = LineString(ls.coords[:] + ls.coords[0:1])
lr.is_simple # False
However, note that it is non-simple, since the lines cross to make a bow-tie. (The widely used buffer(0) trick usually does not work for fixing bow-ties in my experience). This is unsuitable for a LinearRing, so it needs further work. Make it simple and MultiLineString with unary_union:
mls = unary_union(lr)
mls.geom_type # MultiLineString'
Then use polygonize to find the Polygons from the linework:
for polygon in polygonize(mls):
print(polygon)
Or if you want one MultiPolygon geometry:
mp = MultiPolygon(list(polygonize(mls)))
I struggled with this for a while still in 2020, and finally just wrote a method that cleans up self intersections.
This requires Shapely v 1.2.1 explain_validity() method to work.
def clean_bowtie_geom(base_linearring):
base_polygon = Polygon(base_linearring)
invalidity = explain_validity(base_polygon)
invalid_regex = re.compile('^(Self-intersection)[[](.+)\s(.+)[]]$')
match = invalid_regex.match(invalidity)
if match:
groups = match.groups()
intersect_point = (float(groups[1]), float(groups[2]))
new_linring_coords1 = []
new_linring_coords2 = []
pop_new_linring = False
for i in range(0, len(base_linearring.coords)):
if i == len(base_linearring.coords) - 1:
end_point = base_linearring.coords[0]
else:
end_point = base_linearring.coords[i + 1]
start_point = base_linearring.coords[i]
if not pop_new_linring:
if is_point_on_line_and_between(start=start_point, end=end_point, pt=intersect_point):
new_linring_coords2.append(intersect_point)
new_linring_coords1.append(intersect_point)
pop_new_linring = True
else:
new_linring_coords1.append(start_point)
else:
new_linring_coords2.append(start_point)
if is_point_on_line_and_between(start=start_point, end=end_point, pt=intersect_point):
new_linring_coords2.append(intersect_point)
pop_new_linring = False
corrected_linear_ring1 = LinearRing(coordinates=new_linring_coords1)
corrected_linear_ring2 = LinearRing(coordinates=new_linring_coords2)
polygon1 = Polygon(corrected_linear_ring1)
polygon2 = Polygon(corrected_linear_ring2)
def is_point_on_line_and_between(start, end, pt, tol=0.0005):
"""
Checks to see if pt is directly in line and between start and end coords
:param start: list or tuple of x, y coordinates of start point of line
:param end: list or tuple of x, y coordinates of end point of line
:param pt: list or tuple of x, y coordinates of point to check if it is on the line
:param tol: Tolerance for checking if point on line
:return: True if on the line, False if not on the line
"""
v1 = (end[0] - start[0], end[1] - start[1])
v2 = (pt[0] - start[0], pt[1] - start[1])
cross = cross_product(v1, v2)
if cross <= tol:
# The point lays on the line, but need to check if in between
if ((start[0] <= pt[0] <= end[0]) or (start[0] >= pt[0] >= end[0])) and ((start[1] <= pt[1] <= end[1]) or (start[1] >= pt[1] >= end[1])):
return True
return False
This is not the cleanest code, but it gets the job done for me.
Input is a LinearRing with self intersecting geometry (is_simple=False) and output can be either 2 LinearRings, or Two Polygons, whichever you prefer (or have condition to pick one or the other, the world is your oyster, really).
EDIT
In Shapely 1.8.0, new function added.
shapely.validation.make_valid() will take a self intersecting Polygon and return a MultiPolygon with each polygon created by splitting at the self intersection point(s).
I have a project that requires me to export a model in a .dae Collada format. I am struggling to figure out how to do this. I have been trying to use pyCollada to do so, but I have limited experience with 3D modeling and the file structure is confusing me. Why does have to specify an array of "normal_floats" to build a polygon. Aren't the vertices themselves enough?
I have the all the vertices of each face of the object and need to export the data into a collada format? Is there an easy way to do this since each face is two dimensional? Is there an algorithm I can simply feed the vertices into to generate the appropriate faces of the object? Any help would be appreciated.
Additionally, I currently have an algorithm that draws the object in openGL. Is there a way I can reuse code for this in generating the export file?
Update this is the tutorial I was attempting to follow to create the object: http://pycollada.github.io/creating.html
#Allows the laminate to get exported as a DAE.
def toDAE(self):
"""
Exports the current lamiante to a DAE file format
"""
import collada
mesh = collada.Collada()
layerdef = self.layerdef
nodes = [] # Each node of the mesh scene. Typically one per layer.
for layer in layerdef.layers:
layer_thickness = layer.thickness
shapes = self.geoms[layer]
zvalue = layerdef.z_values[layer]
height = float(zvalue) #* 100 #*
if (len(shapes) == 0) : #In case there are no shapes.
continue
for s in shapes:
geom = self.createDAEFromShape(s, height, mesh, layer_thickness)
mesh.geometries.append(geom)
effect = collada.material.Effect("effect", [], "phone", diffuse=(1,0,0), specular=(0,1,0))
mat = collada.material.Material("material", "mymaterial" + str(s.id), effect)
matnode = collada.scene.MaterialNode("materialref" + str(s.id), mat, inputs=[])
mesh.effects.append(effect)
mesh.materials.append(mat)
geomnode = collada.scene.GeometryNode(geom, [matnode])
node = collada.scene.Node("node" + str(s.id), children=[geomnode])
nodes.append(node)
myscene = collada.scene.Scene("myscene", nodes)
mesh.scenes.append(myscene)
mesh.scene = myscene
filename = popupcad.exportdir + os.path.sep + str(self.id) + '.dae' #
mesh.write(filename)
def createDAEFromShape(self, s, layer_num, mesh, thickness): #TODO Move this method into the shape class.
import collada
vertices = s.extrudeVertices(thickness, z0=layer_num)
#This scales the verticies properly. So that they are in millimeters.
vert_floats = [float(x)/(popupcad.SI_length_scaling) for x in vertices]
vert_src_name = str(self.id) + '|' + str(s.id) + "-array"
vert_src = collada.source.FloatSource(vert_src_name, numpy.array(vert_floats), ('X', 'Y', 'Z'))
geom = collada.geometry.Geometry(mesh, "geometry-" + str(s.id), str(self.id), [vert_src])
input_list = collada.source.InputList()
input_list.addInput(0, 'VERTEX', "#" + vert_src_name)
indices = numpy.array(range(0,(len(vertices) // 3)));
triset = geom.createTriangleSet(indices, input_list, "materialref" + str(s.id))
triset.generateNormals()
geom.primitives.append(triset)
return geom
Apparently you can actually compute it without normals, but the documentation was not clear.
My goal is to trace drawings that have a lot of separate shapes in them and to split these shapes into individual images. It is black on white. I'm quite new to numpy,opencv&co - but here is my current thought:
scan for black pixels
black pixel found -> watershed
find watershed boundary (as polygon path)
continue searching, but ignore points within the already found boundaries
I'm not very good at these kind of things, is there a better way?
First I tried to find the rectangular bounding box of the watershed results (this is more or less a collage of examples):
from numpy import *
import numpy as np
from scipy import ndimage
np.set_printoptions(threshold=np.nan)
a = np.zeros((512, 512)).astype(np.uint8) #unsigned integer type needed by watershed
y, x = np.ogrid[0:512, 0:512]
m1 = ((y-200)**2 + (x-100)**2 < 30**2)
m2 = ((y-350)**2 + (x-400)**2 < 20**2)
m3 = ((y-260)**2 + (x-200)**2 < 20**2)
a[m1+m2+m3]=1
markers = np.zeros_like(a).astype(int16)
markers[0, 0] = 1
markers[200, 100] = 2
markers[350, 400] = 3
markers[260, 200] = 4
res = ndimage.watershed_ift(a.astype(uint8), markers)
unique(res)
B = argwhere(res.astype(uint8))
(ystart, xstart), (ystop, xstop) = B.min(0), B.max(0) + 1
tr = a[ystart:ystop, xstart:xstop]
print tr
Somehow, when I use the original array (a) then argwhere seems to work, but after the watershed (res) it just outputs the complete array again.
The next step could be to find the polygon path around the shape, but the bounding box would be great for now!
Please help!
#Hooked has already answered most of your question, but I was in the middle of writing this up when he answered, so I'll post it in the hopes that it's still useful...
You're trying to jump through a few too many hoops. You don't need watershed_ift.
You use scipy.ndimage.label to differentiate separate objects in a boolean array and scipy.ndimage.find_objects to find the bounding box of each object.
Let's break things down a bit.
import numpy as np
from scipy import ndimage
import matplotlib.pyplot as plt
def draw_circle(grid, x0, y0, radius):
ny, nx = grid.shape
y, x = np.ogrid[:ny, :nx]
dist = np.hypot(x - x0, y - y0)
grid[dist < radius] = True
return grid
# Generate 3 circles...
a = np.zeros((512, 512), dtype=np.bool)
draw_circle(a, 100, 200, 30)
draw_circle(a, 400, 350, 20)
draw_circle(a, 200, 260, 20)
# Label the objects in the array.
labels, numobjects = ndimage.label(a)
# Now find their bounding boxes (This will be a tuple of slice objects)
# You can use each one to directly index your data.
# E.g. a[slices[0]] gives you the original data within the bounding box of the
# first object.
slices = ndimage.find_objects(labels)
#-- Plotting... -------------------------------------
fig, ax = plt.subplots()
ax.imshow(a)
ax.set_title('Original Data')
fig, ax = plt.subplots()
ax.imshow(labels)
ax.set_title('Labeled objects')
fig, axes = plt.subplots(ncols=numobjects)
for ax, sli in zip(axes.flat, slices):
ax.imshow(labels[sli], vmin=0, vmax=numobjects)
tpl = 'BBox:\nymin:{0.start}, ymax:{0.stop}\nxmin:{1.start}, xmax:{1.stop}'
ax.set_title(tpl.format(*sli))
fig.suptitle('Individual Objects')
plt.show()
Hopefully that makes it a bit clearer how to find the bounding boxes of the objects.
Use the ndimage library from scipy. The function label places a unique tag on each block of pixels that are within a threshold. This identifies the unique clusters (shapes). Starting with your definition of a:
from scipy import ndimage
image_threshold = .5
label_array, n_features = ndimage.label(a>image_threshold)
# Plot the resulting shapes
import pylab as plt
plt.subplot(121)
plt.imshow(a)
plt.subplot(122)
plt.imshow(label_array)
plt.show()