<jupyter_start><jupyter_text>AboutThis notebook shows how to make a generative art based on Voronoi diagram. This is revised and annotated based on the great tutorial from [https://www.generativehut.com/post/robots-and-generative-art-and-python-oh-my](https://www.generativehut.com/post/robots-and-generative-art-and-python-oh-my)<jupyter_code># import packages
import numpy as np
from scipy.spatial import Voronoi, voronoi_plot_2d

import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection

from ipywidgets import interactive<jupyter_output><empty_output><jupyter_text>Basic ConceptsIn order to understand how the Voronoi art is made, you need to understand some basic concepts as I illustrate next.<jupyter_code># given a set of points, a voronoi diagram can be generated as follows
points = np.array([[0.5, 0.5], [0, 1], [0, 2.5], [1.5, 0], [1.7, 1], [1, 2], [2, 0], [2, 1], [2, 2]])
vor = Voronoi(points)
fig = voronoi_plot_2d(vor)
plt.show()
# 9 vertices as shown as the orange points above with index from 0 to 8
# note that one vertex is outside the diagram [1.5, 4.25], which is different from the vertices connected via dashed lines
# e.g. the closed region in the middle have 6 vertices respectively

vertices = vor.vertices
vertices
# the index of each vertex of each region, -1 indicates vertex outside the Voronoi diagram
regions = vor.regions
regions
# show the vertices in a specific region, i.e., the closed region in the center
vertices[vor.regions[3]]  # show the vertices for region with index of 3
# generate 5 random points with x and y coordinates and then generate the voronoi diagram from the points
num_of_points = 5

# generate 10 random number with range [1, 10]
coordinates = np.random.uniform(1, 10, num_of_points*2)

# reshape coordinates to get points
points = coordinates.reshape((5, 2))

# generate the voronoi diagram from the random points
voronoi = Voronoi(points)
vertices = voronoi.vertices
regions = voronoi.regions 

fig = voronoi_plot_2d(voronoi)
plt.show()
# how to use connect points with lines to draw a triangle
# note that the starting and ending points must be repeated for the shape to be "closed"
triangle = np.array([[1, 1], [2, 1], [2, 2], [1, 1]])

fig, ax = plt.subplots(figsize=(5, 5))
ax.set_xlim(0, 3)
ax.set_ylim(0, 3)
lc = LineCollection([triangle])  # note [] outside triangle here
ax.add_collection(lc)
# evenly spaced numbers in a specified interval 
# 5 numbers from 0.1 to 1, 1 is excluded by using endpoint=False
np.linspace(0.1, 1, 5, endpoint=False)
# "fill" the triangle by creating some smaller triangles via scaling

center = np.mean(triangle, axis=0)  # get the center of the vertices

triangles = []
num_fill = 5  # number of smaller triangles
scalers = np.linspace(0.1, 1, num_fill, endpoint=False)

for scaler in scalers:
    triangle_scaled = scaler * (triangle - center) + center  # scale the distance to center
    triangles.append(triangle_scaled)

triangles.append(triangle)  # add the original triangle
    
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_xlim(0, 3)
ax.set_ylim(0, 3)
lc = LineCollection(triangles)
ax.add_collection(lc)<jupyter_output><empty_output><jupyter_text>Create Voronoi ArtNow, we are ready to create a Voronoi art based on 100 randomly generated points and "fill" 40% randomly chosen regions with smaller regions.<jupyter_code>num_of_points = 100
# get random coordinates for x and y
# each points has two coordinates, that's why we need num_of_points*2 below
coordinates = np.random.uniform(1, 10, num_of_points*2)
# reshape coordinates to get points
points = coordinates.reshape((num_of_points, 2))
print(points.shape)

voronoi = Voronoi(points)
vertices = voronoi.vertices
regions = voronoi.regions 
# this is a list of all the points that are the verticies in are diagram
fig = voronoi_plot_2d(voronoi)
plt.show()
# in order to use lines to connect all vertices for each region, we need to do the following:
# 1. remove empty regions and regions with index -1, which means vertex outside the Voronoi diagram
# 2. add the starting index to the end of the array so that lines can close, e.g., [1, 3, 5] will become [1, 3, 5, 1]

regions_updated = []
for r in regions:
    if len(r)>0 and -1 not in r:
        regions_updated.append(r + r[0:1])

print(regions_updated[:2])

# now we know the index of vertices in each regions
# we can get the coordinates for each vertices in each regions

region_vertices = []
for r in regions_updated:
    region_vertices.append(vertices[r])

print(region_vertices[:2])
fig, ax = plt.subplots(figsize=(5, 5))
# if we send x, y limits as the range of random coordinates for the points
# some areas along the borders are quite empty, so we set the x, y limits smaller
ax.set_xlim(1, 10)
ax.set_ylim(1, 10)
lc = LineCollection(region_vertices)
ax.add_collection(lc)
fig, ax = plt.subplots(figsize=(5, 5))
# if we send x, y limits as the range of random coordinates for the points
# some areas along the borders are quite empty, so we set the x, y limits smaller
ax.set_xlim(1+1, 10-1)
ax.set_ylim(1+1, 10-1)
lc = LineCollection(region_vertices)
ax.add_collection(lc)
# total regions 
print(len(regions_updated))

# percentage of regions to be "filled"
percent_to_fill = 0.3

num_regions_to_fill = int(percent_to_fill * len(regions_updated))
print(num_regions_to_fill)

# random choose the regions to fill
regions_to_fill = np.random.choice(region_vertices, num_regions_to_fill, replace=False)

fill = []
scalers = np.linspace(0.1, 1, num_fill, endpoint=False)
    
for r in regions_to_fill:
    center = np.mean(r, axis=0)
    for scaler in scalers:
        scaled = scaler*(r - center) + center
        fill.append(scaled)

fig, ax = plt.subplots(figsize=(5, 5))
#ax.set_aspect('equal')
ax.set_xlim(1+1, 10-1)
ax.set_ylim(1+1, 10-1)

lc = LineCollection(region_vertices + fill)
ax.add_collection(lc)<jupyter_output>85
25<jupyter_text>Next, we put everything into a function and make an interactive interface to call the function with different parameters to get different variations of the generated art.<jupyter_code>def create_art(num_of_points=200, percent_to_fill=0.4, num_of_fill=5):
    """
    generate voronoi art
    num_of_points: number of points on the diagram
    percent_to_fill: percentage of regions to be filled
    num_of_fill: total smaller regions to fill
    """

    # bounds for the random generated point coordinates
    coordinate_bounds = np.array([1, 10])

    # get random coordinates for x and y
    # each points has two coordinates, that's why we need num_of_points*2 below
    #coordinates = np.random.uniform(1, 10, num_of_points*2)
    coordinates = np.random.uniform(*coordinate_bounds, num_of_points*2)
    # reshape coordinates to get points
    points = coordinates.reshape((num_of_points, 2))

    voronoi = Voronoi(points)
    vertices = voronoi.vertices
    regions = voronoi.regions 
    
    # in order to use lines to connect all vertices for each region, we need to do the following:
    # 1. remove empty regions and regions with index -1, which means vertex outside the Voronoi diagram
    # 2. add the starting index to the end of the array so that lines can close, e.g., [1, 3, 5] will become [1, 3, 5, 1]

    regions_updated = []
    for r in regions:
        if len(r)>0 and -1 not in r:
            regions_updated.append(r + r[0:1])

    # now we know the index of vertices in each regions
    # we can get the coordinates for each vertices in each regions

    region_vertices = []
    for r in regions_updated:
        region_vertices.append(vertices[r])

    num_regions_to_fill = int(percent_to_fill * len(regions_updated))

    # random choose the regions to fill
    regions_to_fill = np.random.choice(region_vertices, num_regions_to_fill, replace=False)

    regions_filled = []

    # get the scalers between 0.1 and 1
    scalers = np.linspace(0.1, 1, num_of_fill, endpoint=False)
    
    # loop over the regions to fill to create smaller regions
    for r in regions_to_fill:
        center = np.mean(r, axis=0)
        for scaler in scalers:
            scaled = scaler * (r - center) + center  # scaled regions
            regions_filled.append(scaled)

    fig, ax = plt.subplots(figsize=(5, 5))
    #ax.set_aspect('equal')

    # the edge space to be cut from the generated plot
    # see explaination above
    edge_space = 1

    plot_lim = coordinate_bounds + np.array([edge_space, -edge_space])
    ax.set_xlim(*plot_lim)  #ax.set_xlim(1+1, 10-1)
    ax.set_ylim(*plot_lim)  #ax.set_ylim(1+1, 10-1)

    lc = LineCollection(region_vertices + regions_filled)
    ax.add_collection(lc)

    plt.grid(False)
    plt.axis('off')
    plt.show()
    
    return fig, ax
# make interactive function call
art = interactive(
    create_art,
    num_of_points=(10, 1000, 25),
    percent_to_fill=(0., 1., 0.05),
    num_of_fill=(1, 10, 1),
)

display(art)
# save to SVG
fig, ax = art.result
fig.savefig('art.svg', bbox_inches = 'tight', pad_inches = 0)
fig.savefig('art.png', bbox_inches = 'tight', pad_inches = 0)
# See https://vpype.readthedocs.io/en/stable/cookbook.html#laying-out-a-svg-for-plotting
# Use vpype to rescale the SVG to 3x3 (inch) for letter size page plotting in landscape

!vpype read art.svg scaleto 3in 3in layout --landscape letter write art-scaled.svg<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09496248660235798, 'fraction_numerical': 0.024758842443729903, 'mean_word_length': 4.1524019878520155, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 18, 'https://': 3, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '661039', 'license': 'CC0-1.0', 'n_tokens_mistral': 3229, 'n_tokens_neox': 3041, 'n_words': 1078, 'path': 'voronoi-art.ipynb', 'repo_name': 'harrywang/generative', 'size': '9330'}