Some time ago I was looking for an algorithms that can generate a ‘map like’ like pictures – e.g. tessellation of a plane into set of more or less random polygons. I found Voronoi diagrams – which give very nice pictures and have many useful properties.
Most common case of Voronoi diagram is known in an Euclidean plane, where we have a set of points (call seeds) then Voronoi diagram splits the plane into areas – called Voronoi cells – around each seed, where inside each area any point is closer to that seed then to any other. Areas are then convex polygons (for Euclidean metric). This definition is best illustrated on the picture below – the Voronoi diagram for 100 random points in range 0-100 – Voronoi cells are marked by red lines, blue points are seeds:
To play with Voronoi diagrams we can use python – recent version of scipy already contains function for calculating Voronoi diagram (see later). But we can use duality of Voronoi diagram to Delaunay triangulation. Both scipy and matplotlib contain functions for Delaunay triangulation. From a given triangulation we can get Voronoi diagram, if we connect centers of circumcircles of each triangle through edges of adjacent triangles. Also there is a pure python implementation of Fortune algorithm available on web here.
Below is the code for calculation Voronoi diagram using Delaunay triangulation function in matplotlib and using numpy for numerical calculations:
#Inspired by code https://github.com/rougier/neural-networks/blob/master/voronoi.py from Nicolas P. Rougier
import sys
import numpy as np
import matplotlib.tri
import matplotlib.pyplot as plt
import time
def circumcircle2(T):
P1,P2,P3=T[:,0], T[:,1], T[:,2]
b = P2 - P1
c = P3 - P1
d=2*(b[:,0]*c[:,1]-b[:,1]*c[:,0])
center_x=(c[:,1]*(np.square(b[:,0])+np.square(b[:,1]))- b[:,1]*(np.square(c[:,0])+np.square(c[:,1])))/d + P1[:,0]
center_y=(b[:,0]*(np.square(c[:,0])+np.square(c[:,1]))- c[:,0]*(np.square(b[:,0])+np.square(b[:,1])))/d + P1[:,1]
return np.array((center_x, center_y)).T
def check_outside(point, bbox):
point=np.round(point, 4)
return point[0]<bbox[0] or point[0]>bbox[2] or point[1]< bbox[1] or point[1]>bbox[3]
def move_point(start, end, bbox):
vector=end-start
c=calc_shift(start, vector, bbox)
if c>0 and c<1:
start=start+c*vector
return start
def calc_shift(point, vector, bbox):
c=sys.float_info.max
for l,m in enumerate(bbox):
a=(float(m)-point[l%2])/vector[l%2]
if a>0 and not check_outside(point+a*vector, bbox):
if abs(a)<abs(c):
c=a
return c if c<sys.float_info.max else None
def voronoi2(P, bbox=None):
if not isinstance(P, np.ndarray):
P=np.array(P)
if not bbox:
xmin=P[:,0].min()
xmax=P[:,0].max()
ymin=P[:,1].min()
ymax=P[:,1].max()
xrange=(xmax-xmin) * 0.3333333
yrange=(ymax-ymin) * 0.3333333
bbox=(xmin-xrange, ymin-yrange, xmax+xrange, ymax+yrange)
bbox=np.round(bbox,4)
D = matplotlib.tri.Triangulation(P[:,0],P[:,1])
T = D.triangles
n = T.shape[0]
C = circumcircle2(P[T])
segments = []
for i in range(n):
for j in range(3):
k = D.neighbors[i][j]
if k != -1:
#cut segment to part in bbox
start,end=C[i], C[k]
if check_outside(start, bbox):
start=move_point(start,end, bbox)
if start is None:
continue
if check_outside(end, bbox):
end=move_point(end,start, bbox)
if end is None:
continue
segments.append( [start, end] )
else:
#ignore center outside of bbox
if check_outside(C[i], bbox) :
continue
first, second, third=P[T[i,j]], P[T[i,(j+1)%3]], P[T[i,(j+2)%3]]
edge=np.array([first, second])
vector=np.array([[0,1], [-1,0]]).dot(edge[1]-edge[0])
line=lambda p: (p[0]-first[0])*(second[1]-first[1])/(second[0]-first[0]) -p[1] + first[1]
orientation=np.sign(line(third))*np.sign( line(first+vector))
if orientation>0:
vector=-orientation*vector
c=calc_shift(C[i], vector, bbox)
if c is not None:
segments.append([C[i],C[i]+c*vector])
return segments
if __name__=='__main__':
points=np.random.rand(100,2)*100
lines=voronoi2(points, (-20,-20, 120, 120))
plt.scatter(points[:,0], points[:,1], color="blue")
lines = matplotlib.collections.LineCollection(lines, color='red')
plt.gca().add_collection(lines)
plt.axis((-20,120, -20,120))
plt.show()
I was inspired by code mentioned on first line comment, I refactor it to leverage numpy matrix computations and also added feature to limit diagram to a bounding box.
Pictures below illustrateshow Voronoi diagram is created from triangulation – we have a set of seed points (blue), then Delaunay triangulation is calculated (yellow lines), for each triangle we calculate circumcircle – center of circle is green cross and circle itself is grey. Finally we connect circle centers (red lines) and for border triangles we create line from the circumcircle center in direction perpendicular to the outer triangle edge and line extends to infinity ( to bounding box boundary in our case).
As already said there is also function scipy.spatial.Voronoi, which returns directly vertices and edges of Voronoi diagram. Generally this function is faster then our example above (app. 2 – 3 times) – below is code that also calculates missing edges (leading to infinity) and limits picture to a bounding box ( similar to previous example):
import sys
import numpy as np
def check_outside(point, bbox):
point=np.round(point, 4)
return point[0]<bbox[0] or point[0]>bbox[2] or point[1]< bbox[1] or point[1]>bbox[3]
def move_point(start, end, bbox):
vector=end-start
c=calc_shift(start, vector, bbox)
if c>0 and c<1:
start=start+c*vector
return start
def calc_shift(point, vector, bbox):
c=sys.float_info.max
for l,m in enumerate(bbox):
a=(float(m)-point[l%2])/vector[l%2]
if a>0 and not check_outside(point+a*vector, bbox):
if abs(a)<abs(c):
c=a
return c if c<sys.float_info.max else None
from scipy.spatial import Voronoi
def voronoi3(P, bbox=None):
P=np.asarray(P)
if not bbox:
xmin=P[:,0].min()
xmax=P[:,0].max()
ymin=P[:,1].min()
ymax=P[:,1].max()
xrange=(xmax-xmin) * 0.3333333
yrange=(ymax-ymin) * 0.3333333
bbox=(xmin-xrange, ymin-yrange, xmax+xrange, ymax+yrange)
bbox=np.round(bbox,4)
vor=Voronoi(P)
center = vor.points.mean(axis=0)
vs=vor.vertices
segments=[]
for i,(istart,iend) in enumerate(vor.ridge_vertices):
if istart<0 or iend<=0:
start=vs[istart] if istart>=0 else vs[iend]
if check_outside(start, bbox) :
continue
first,second = vor.ridge_points[i]
first,second = vor.points[first], vor.points[second]
edge= second - first
vector=np.array([-edge[1], edge[0]])
midpoint= (second+first)/2
orientation=np.sign(np.dot(midpoint-center, vector))
vector=orientation*vector
c=calc_shift(start, vector, bbox)
if c is not None:
segments.append([start,start+c*vector])
else:
start,end=vs[istart], vs[iend]
if check_outside(start, bbox):
start=move_point(start,end, bbox)
if start is None:
continue
if check_outside(end, bbox):
end=move_point(end,start, bbox)
if end is None:
continue
segments.append( [start, end] )
return segments
Voronoi diagrams have many different application , some of them in geography – for instance below is a map of Czech Republic, overlayed with Voronoi diagram, where seeds are breweries in that country.






One thought on “Voronoi Diagrams”