I am trying to create a single point within a polygon using a class for use in an agent based model.
Currently I am able to create random points constrained to the bounds of the polygon, but not the polygon itself. My code at present appears to ignore the if statement within the while loop. I am very new to python so this could be a limitation I am missing.
Here is my current code:
import geopandas as gpd
import matplotlib.pyplot as plt
import random
import pandas as pd
bounds = gpd.read_file("./data/liverpool_bounds.gpkg")
class Agent():
def __init__(self, bounds):
x_min, y_min, x_max, y_max = bounds.total_bounds
counter = 0
while counter != 1:
x = random.uniform(x_min, x_max)
y = random.uniform(y_min, y_max)
df = pd.DataFrame({'x': [x], 'y': [y]})
self.agent = gpd.GeoDataFrame(
df, geometry=gpd.points_from_xy(df.x, df.y))
if self.agent.within(bounds) is True:
counter = 1
# counter does not increase
print(counter)
# gives both True and False
print(self.agent.within(bounds))
Agent(bounds).agent
This code gives an infinite loop. Expected behavior would be to stop given a Boolean True value, and to continue with False, until a True value.
Don't use the counter variable, but a break statement when the point is sampled within the polygon. The counter variable will always be one on exit so this does not carry new information. I'm not really familiar with the Geopandas library, but you can achieve a solution with Shapely, which is a very nice library imo. With this program structure your object becomes more generally useable.
from shapely.geometry import Point, Polygon
import random
bounds = [(0, 0), (1, 0), (1, 1), (0, 1)]
class Agent():
def __init__(self, bounds):
self.polygon = Polygon(bounds)
# implement your object wide dataframe here to which you can append
def add_random_point(self):
xmin, ymin, xmax, ymax = self.polygon.bounds
while True:
x = random.uniform(xmin, xmax)
y = random.uniform(ymin, ymax)
if Point(x, y).within(self.polygon):
# if this condition is true, add to a dataframe here
print(x, y)
break
obj = Agent(bounds)
obj.add_random_point()
Related
I'm trying to test which points in a list (numpy array or pandas) are inside a given boolean mask (or labelled image).
I have found way to compare with polygons but not with a mask
From this dataset example how can I test which coords are inside the mask? (best would be to add a column in pandas saying which label they are inside of - that, or add a new column in the "coords" variable saying which label it belongs to).
Masks/labels won't be rectangles in my implementation (basically cell shapes), I'm just doing so here because it's easier.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# import numpy as np
coords = np.random.rand(40, 2) *1024
mask = np.zeros((1024,1024))
mask[300:600,50:125] = 1
mask[700:800,400:650] = 2
plt.imshow(mask)
plt.scatter(coords[:,0],coords[:,1],color='red')
You can use NumPy indexing of the mask with the coordinates after a bit of massaging.
coords_int = np.round(coords).astype(int) # or np.floor, depends
values_at_coords = mask[tuple(coords_int.T)]
points_per_value = np.bincount(values_at_coords)
Now points_per_value contains an array such that points_per_value[i] contains the number of coordinates that landed in mask label i. (docs for np.bincount)
For more about the second line, you can read about NumPy integer array indexing in the NumPy docs.
from collections import defaultdict
import matplotlib.pyplot as plt
coords = np.random.rand(40, 2) *1024
mask = np.zeros((1024,1024))
mask[300:600,50:125] = 1
mask[700:800,400:650] = 2
plt.imshow(mask, origin='lower')
plt.scatter(coords[:,1],coords[:,0],color='red')
res = defaultdict(list)
for i in np.unique(mask)[1:]:
temp = coords[(coords[:, 0] >= (mask == i).nonzero()[0][0]) & (coords[:, 0] <= (mask == i).nonzero()[0][-1])]
res[i] = temp[(temp[:, 1] >= (mask == i).nonzero()[1][0]) & (temp[:, 1] <= (mask == i).nonzero()[1][-1])]
print(res)
You can use shapely to assert whether a point is inside a polygon or not.
from shapely.geometry import Point
from shapely.geometry.polygon import Polygon
Your first mask is defined by the following polygon.
polygon = Polygon([(300, 50), (300, 125), (600, 50), (600, 125)])
Test for two sample points.
point1 = Point(315, 1200)
point2 = Point(315, 75)
print(polygon.contains(point1)) #False
print(polygon.contains(point2)) #True
Here is the code for a random walk I made which I attempted to constrain where -5 < y < 5:
import random
import numpy as np
import matplotlib.pyplot as plt
import math
import decimal
def dirs(x):
return np.array( [math.cos(x), math.sin(x)] )
def constrainedRandomWalk(x):
numSteps = x
locations = np.zeros( (numSteps, 2) )
for i in range(1, numSteps):
r = random.randrange(628318)/100000
move = dirs(r)
locations[i] = locations[i-1] + move
if -5<locations[i][1]<5:
continue
#return locations
plt.figure(figsize=(8,8))
plt.plot( locations[:,0], locations[:,1], alpha=0.7 );
plt.xlim([-20,20])
plt.ylim([-20,20])
I attempted to constrain the "walking character" by setting a condition on the loop that
if -5<locations[i][1]<5:
continue
However, as you can see here, the character leaves the -5<y<5 region:
Can anyone let me know how to actually constrain the random walk and why this method doesn't work? Thank you!
You're updating locations before you test if the move is valid:
import math
import random
import matplotlib.pyplot as plt
import numpy as np
def dirs(x):
return np.array([math.cos(x), math.sin(x)])
def constrained_random_walk(num_steps):
# Specify Start Point
locations = [np.array([0, 0])]
# Go Until locations is long enough
while len(locations) < num_steps:
r = random.randrange(628318) / 100000
move = dirs(r)
# Test if the new move is in bounds first
new_location = locations[-1] + move
if -5 < new_location[1] < 5:
locations.append(new_location)
locations = np.array(locations)
plt.figure(figsize=(8, 8))
plt.plot(locations[:, 0], locations[:, 1], alpha=0.7)
plt.xlim([-20, 20])
plt.ylim([-20, 20])
Sample Output on:
constrained_random_walk(2000)
Edit: Updated so all skipped values are not (0,0) but every value in locations is populated by a generated move. Except for the first, which is specified as the start point. (Currently (0,0))
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
Is there any way to restrict the range that a user can interactively pan a plot in matplotlib?
For example, I'd like to limit the user's panning range to the positive quadrant of the plot, so x > 0 and y > 0, effectively creating floors at x = 0 and y = 0.
There is not built-in way to restrict zooming or panning other than restricting it to either the x or y direction. So you need to implement that yourself.
Connecting to the 'xlim_changed' and 'ylim_changed' events, you may check if the limits are valid or not, and potentially reset them to the valid range.
import matplotlib.pyplot as plt
import numpy as np
fig,ax=plt.subplots()
x = np.sin(np.linspace(0,10, 266))+1
ax.plot(x)
class Restrictor():
def __init__(self, ax, x=lambda x: True,y=lambda x: True):
self.res = [x,y]
self.ax =ax
self.limits = self.get_lim()
self.ax.callbacks.connect('xlim_changed', lambda evt: self.lims_change(axis=0))
self.ax.callbacks.connect('ylim_changed', lambda evt: self.lims_change(axis=1))
def get_lim(self):
return [self.ax.get_xlim(), self.ax.get_ylim()]
def set_lim(self, axis, lim):
if axis==0:
self.ax.set_xlim(lim)
else:
self.ax.set_ylim(lim)
self.limits[axis] = self.get_lim()[axis]
def lims_change(self, event=None, axis=0):
curlim = np.array(self.get_lim()[axis])
if self.limits[axis] != self.get_lim()[axis]:
# avoid recursion
if not np.all(self.res[axis](curlim)):
# if limits are invalid, reset them to previous state
self.set_lim(axis, self.limits[axis])
else:
# if limits are valid, update previous stored limits
self.limits[axis] = self.get_lim()[axis]
res = Restrictor(ax, x=lambda x: x>=0,y=lambda x: x>=0)
plt.show()
The user experience of such limitations are not great, since a certain action (like moving the mouse) is not answered with the expected behaviour (plot does not move); but one has to judge for oneself if this would still make sense.
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).