Example: Voronoi Hexagonal Tiles

_images/vor_idea.jpg

How to go about creating the image above? Did you notice the hexagons hidden in plain sight? The colored fragments do combine to form hexagons. I got this idea to fill a 2-D space with Hexagonal Voronoi polygons. But the journey turned out to be trickier than I originally expected. In this example, I try to recreate my various attempts.

A Voronoi diagram is “the partitioning of a plane with n points into convex polygons such that each polygon contains exactly one generating point and every point in a given polygon is closer to its generating point than to any other.” (Wolfram)

I have always liked Voronoi diagrams. So it was natural that I should try and use them to create Hexagonal tiles. I knew that in Python, this was easily done by using the SciPy module.

The example seemed straightforward to try.

 points = np.random.rand(7,2)
 # compute Voronoi tesselation
 vor = Voronoi(points)

 voronoi_plot_2d(vor, show_vertices = True)

 # colorize
 for region in vor.regions:
     if not -1 in region:
         polygon = [vor.vertices[i] for i in region]
         plt.fill(*zip(*polygon), fc='red')
_images/vor_att1.png

Now, if we take a few points inside a hexagon, we can then try and divide up the entire hexagon into Voronoi spaces.

 #Generate 10 random points inside a hexagon
 points = []
 for _ in range(10):
     size=0.5
     dist = np.random.random() * size
     theta_offset = np.random.randint(9) * 10
     i = np.random.randint(6)
     points.append( h.get_points_center_rtheta(dist, theta_offset, index=i)[0])

 voronoi_plot_2d(vor, show_vertices = True)
 h.render(fill=None)
 plt.axis('scaled')
_images/vor_att2.png

I was able to create Voronoi polygons, but they simply did not fill the space at all.

plt.plot(vor.vertices[:,0],vor.vertices[:,1], 'o', color='orange')

hverts = np.array(h.get_verts())
plt.plot(hverts[:,0], hverts[:, 1], 'b-')
for r in result:
    plt.fill(*zip(*np.array(list(
        zip(r.boundary.coords.xy[0][:-1], r.boundary.coords.xy[1][:-1])))),
        alpha=0.6)
plt.axis('scaled')
_images/vor_att3.png _images/vor_att4.png

The orange dots in the image above are Voronoi vertices. But as you can see, there a vast part of the Hexagon that is simply not filled. Drawing the region by itself didn’t lead to any insights.

I was stuck in this point for quite some time. More than once, I was ready to give up. But I kept searching to see how others had approached this.

After many more such failed attempts and mishaps, I got an inspiration from this Stackoverflow answer

I noticed that this person was able to take this

_images/vor_so1.png

and create this, which is very similar to what I was after.

_images/vor_so2.png

To achieve this, this person had added 4 points well outside the shape of interest. And then by cutting off the x and y limits they got the image they desired.

But my problem is that my base shape is not rectangular. It is a hexagon. After a lot of web searches I figured that an “shape” intersection was the answer. For that, I would have to use the very popular shapely package. Another SO answer really helped me understand how to approach intersecting polygons.

#Adding 6 points well OUTSIDE the Hexagon,
# so that Voronoi will surely cover the entire hexagon
#Doing this so that when intersected with the hexagon mask,
# no part of the original hexagon will be missed
pull_points = h.get_points_center_rtheta(size*4, theta_offset, index=None)
points = np.append(points, pull_points, axis = 0)

Side note: Installing shapely in windows was a bit tricky. But anyway, I finally was getting somewhere.

_images/vor_att5.png

Now, all that remains is to “intersect” my bigger voronoi area and cut out just the hexagon inside it. In order to make sure that I don’t miss any part of the hexagon, my “pull” points are at least 4*size of the hexagon-side away. That seemed to work.

 ridge_lines = [
     LineString(vor.vertices[line])
     for line in vor.ridge_vertices if -1 not in line]

 pts = MultiPoint([Point(i) for i in h.get_verts()])
 hex_mask = pts.convex_hull.union(pts.buffer(0.1, resolution=5, cap_style=2))
 result = MultiPolygon([poly.intersection(hex_mask) for poly in polygonize(ridge_lines)])
_images/vor_att6.png

Once the idea works for one Hexagon, and it is filled entirely with Voronoi polygons, it was just a matter of choosing the right color palette and putting the pieces together.

_images/vor_final.jpg