# A collection of Python functions that fill a given region with rectangles

These are what I was working on in the last few days, I wrote a bunch of functions that fill a given region with rectangles of different colors.

The functions can:

Randomly split a region into sub regions recursively based on n random points:

one point (each region is split into four sub regions):

two point (each region is split into nine sub regions):

Randomly split a region into n sub regions based on a number of points so that each sub region has roughly equal area:

one point:

two point:

randomly split a region into square with normalized length and three other sub regions and only recursively split regions that are not squares:

Same as above, but doesn't draw non-squares:

Randomly place rectangles of similar sizes into region:

Randomly place rectangles into region without them overlapping (this isn't my idea, all credits goes to the original poster of this answer):

Split a square into four small squares, and recursively split top-left, top-right and bottom-right squares:

Draw a big square at the center, take one nth of the length of its side as the side length of a new layer of squares, construct multiple squares from the sides of the square so that every new square touches a side of the center square, all lengths are covered, and the new squares don't overlap, the recursively use the outer layer as center square, and one nth of current layer's side as new side, construct new squares same as above:

Four colors:

Random colors:

## Code

import matplotlib.pyplot as plt
import numpy as np
import random
from itertools import product
from matplotlib.collections import PolyCollection
from PIL import Image

def gauss(x, weight):
return np.exp(-(x - 0.5) * (x - 0.5) / (2 * weight**2))

def randBias(base, top, bias, weight=1):
assert 0 < weight <= 1
influence = random.random()
x = random.random() * (top - base) + base
if x > bias:
return x + gauss(influence, weight) * (bias - x)
return x - gauss(influence, weight) * (x - bias)

def spectrum_position(n, string=False):
if not isinstance(n, int):
raise TypeError('n should be an integer')
if n < 0:
raise ValueError('n must be non-negative')
n %= 1530
if 0 <= n < 255:
return (255, n, 0) if not string else f'ff{n:02x}00'
elif 255 <= n < 510:
return (510-n, 255, 0) if not string else f'{510-n:02x}ff00'
elif 510 <= n < 765:
return (0, 255, n-510) if not string else f'00ff{n-510:02x}'
elif 765 <= n < 1020:
return (0, 1020-n, 255) if not string else f'00{1020-n:02x}ff'
elif 1020 <= n < 1275:
return (n-1020, 0, 255) if not string else f'{n-1020:02x}00ff'
elif 1275 <= n < 1530:
return (255, 0, 1530-n) if not string else f'ff00{1530-n:02x}'

def recursive_split(division, iterations, unit=1):
length = unit
divisions = 1
total_count = 1
total_length = unit
area = unit**2
color_start = random.random()*1530
colors = ['#'+spectrum_position(round(color_start+382.5*i), True) for i in range(4)]
colors = [(colors[0], colors[2]), (colors[1], colors[3])]
levels = [{
'length': length,
'divisions': 1,
'unit_area': area,
'count': 1,
'shapes': [{'position': (-unit/2, -unit/2), 'color': colors[0][0]}]
}]
half = unit/2
geometries = [[(-half, -half), (half, -half), (half, half), (-half, half)]]
all_colors = [colors[0][0]]
for i in range(iterations-1):
color_level = colors[(i + 1) % 2]
length = length/division
total_length = total_length + length * 2
unit_area = length**2
side = divisions*division
count = (divisions*division+1)*4
total_count += count
area += unit_area*count
divisions = (divisions*division)+2
shapes = []
min_x = -half - length
for i in range(side+2):
x = min_x + length * i
y1 = half
y2 = -half - length
i1 = i % 2
i2 = (i + 1) % 2 if not division % 2 else i1
geometries.append(
[(x, y1), (x+length, y1), (x+length, y1+length), (x, y1+length)])
all_colors.append(color_level[i1])
geometries.append(
[(x, y2), (x+length, y2), (x+length, y2+length), (x, y2+length)])
all_colors.append(color_level[i2])

for i in range(1, side+1):
y = half - length * i
i1 = i % 2
i2 = (i + 1) % 2 if not division % 2 else i1
geometries.append([(min_x, y), (min_x+length, y),
(min_x+length, y+length), (min_x, y+length)])
all_colors.append(color_level[i1])
geometries.append([(half, y), (half+length, y),
(half+length, y+length), (half, y+length)])
all_colors.append(color_level[i2])

half = half+length
levels.append({
'length': length,
'divisions': divisions,
'unit_area': unit_area,
'count': count,
})
return {'levels': levels, 'total_area': area, 'total_count': total_count, 'total_length': total_length, 'geometries': geometries, 'colors': all_colors}

class Grid:
root = None

def __init__(self, x1, x2, y1, y2, parent=None, gid=0):
assert x2 > x1 and y2 > y1
self.x1 = x1
self.x2 = x2
self.y1 = y1
self.y2 = y2
self.subgrids = []
self.id = gid
self.parent = parent
self.issquare = False
if np.isclose(y2 - y1, x2 - x1):
self.issquare = True
if not self.parent:
Grid.root = self
self.gid = 0
self.rectangles = {0: [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]}
self.limits = {'min_x': x1, 'max_x': 0, 'min_y': y1, 'max_y': 0}
else:
self.rectangles = parent.rectangles

if self.issquare:
if x2 > Grid.root.limits['max_x']:
Grid.root.limits['max_x'] = x2
if y2 > Grid.root.limits['max_y']:
Grid.root.limits['max_y'] = y2

def random(self, number=1, control=False, weight=0.75):
assert number >= 1
if not self.subgrids:
self.rectangles.pop(self.id, None)
x_axis, y_axis = [self.x1], [self.y1]
if not control:
for i in range(number):
x_axis.append(self.x1 + random.random()
* (self.x2 - self.x1))
y_axis.append(self.y1 + random.random()
* (self.y2 - self.y1))
else:
cur_x, cur_y = self.x1, self.y1
for i in range(number, 0, -1):
cur_x = randBias(cur_x, self.x2, cur_x +
(self.x2 - cur_x) / (i + 1), weight)
cur_y = randBias(cur_y, self.y2, cur_y +
(self.y2 - cur_y) / (i + 1), weight)
x_axis.append(cur_x)
y_axis.append(cur_y)

x_axis.append(self.x2)
y_axis.append(self.y2)
x_axis, y_axis = sorted(x_axis), sorted(y_axis)
cells = product(zip(x_axis, x_axis[1:]), zip(y_axis, y_axis[1:]))
for (a, b), (c, d) in cells:
if a != b and c != d:
gid = Grid.root.gid + 1
Grid.root.gid += 1
subgrid = Grid(a, b, c, d, self, gid)
self.subgrids.append(subgrid)
self.rectangles[gid] = [(a, c), (b, c), (b, d), (a, d)]

else:
random.choice(self.subgrids).random(number, control, weight)

def square(self, strict=False):
if not self.subgrids:
self.rectangles.pop(self.id, None)
w = self.x2 - self.x1
h = self.y2 - self.y1
lim = min(w, h)
diff = randBias(0, lim, 0.5*lim, 2/3)
x_axis = [self.x1, self.x1+diff, self.x2]
y_axis = [self.y1, self.y1+diff, self.y2]
cells = product(zip(x_axis, x_axis[1:]), zip(y_axis, y_axis[1:]))
for (a, b), (c, d) in cells:
if a != b and c != d:
gid = Grid.root.gid + 1
Grid.root.gid += 1
subgrid = Grid(a, b, c, d, self, gid)
self.subgrids.append(subgrid)
if not strict or np.isclose(b - a, d - c):
self.rectangles[gid] = [(a, c), (b, c), (b, d), (a, d)]
else:
valid = [subgrid for subgrid in self.subgrids if not subgrid.issquare]
random.choice(valid).square(strict)

def flatten(self):
if not self.subgrids:
return

return self.rectangles.values()

class Recur_Bisector:
def __init__(self, x1, x2, y1, y2, level, fill=True, drop=False):
assert x2 > x1 and y2 > y1
assert np.isclose(x2 - x1, y2 - y1)
self.x1 = x1
self.x2 = x2
self.y1 = y1
self.y2 = y2
self.level = level
self.rectangles = []
step = 1530 / (level+2)
start = random.random() * 1530
color_levels = ['#'+spectrum_position(
round(start+step*i), True) for i in range(level+2)]
self.color_levels = color_levels
self.colors = []
self.fill = fill
self.drop = drop
self.processed = False

def recur_bisect_worker(self, x1, x2, y1, y2, cur_level=0, top_right=False):
if cur_level != self.level:
mid_x = x1 + (x2 - x1) / 2
mid_y = y1 + (y2 - y1) / 2
self.rectangles.append(
[(x1, y1), (mid_x, y1), (mid_x, mid_y), (x1, mid_y)])
self.colors.append(self.color_levels[cur_level])
if not self.drop or random.randrange(3):
self.recur_bisect_worker(x1, mid_x, mid_y, y2, cur_level+1)
if not self.drop or random.randrange(3):
self.recur_bisect_worker(
mid_x, x2, mid_y, y2, cur_level+1, top_right=True)
if not self.drop or random.randrange(3):
self.recur_bisect_worker(mid_x, x2, y1, mid_y, cur_level+1)

if cur_level == self.level:
if not top_right or self.fill:
self.rectangles.append(
[(x1, y1), (x2, y1), (x2, y2), (x1, y2)])
if not top_right:
self.colors.append(self.color_levels[cur_level])
if top_right and self.fill:
self.colors.append(self.color_levels[cur_level+1])

if not cur_level:
self.processed = True

def recur_bisect(self):
if not self.processed:
self.recur_bisect_worker(self.x1, self.x2, self.y1, self.y2, 0)

return {'shapes': self.rectangles, 'colors': self.colors}

def patches(width, height, number):
unit = np.gcd(width, height)
rectangles = []
for i in range(number):
x = random.random() * width
y = random.random() * height
w = min(unit, width-x)*(0.5+random.random()*0.5)
h = min(unit, height-y)*(0.5+random.random()*0.5)
rectangles.append([(x, y), (x+w, y), (x+w, y+h), (x, y+h)])

return rectangles

def glyphs(n: int) -> np.ndarray:
ceilsqrtn = int(np.ceil(n**.5))
n_grids = ceilsqrtn * ceilsqrtn
rects = np.random.uniform(size=(n, 4))
rects[:, :2].sort(1)
rects[:, 2:].sort(1)
flat_grid_indices = np.random.choice(n_grids, n, False)
offsets = np.unravel_index(flat_grid_indices, (ceilsqrtn, ceilsqrtn))
rects[:, :2] += offsets[1][..., None]
rects[:, 2:] += offsets[0][..., None]
rects /= ceilsqrtn
x1, x2, y1, y2 = rects.T
ll = np.array((x1, y1)).T
lr = np.array((x2, y1)).T
ur = np.array((x2, y2)).T
ul = np.array((x1, y2)).T
return np.stack((ll, lr, ur, ul), axis=1)

def random_rectangles(width=1920, height=1080, number=100, points=1, mode='random', control=False, strict=False, weight=0.75, show=False):
fig = plt.figure(figsize=(width/100, height/100),
dpi=100, facecolor='black')
ax.set_axis_off()
grid = Grid(0, width, 0, height)
if mode == 'random':
for i in range(number):
grid.random(points, control, weight)
elif mode == 'square':
for i in range(number):
grid.square(strict)
if strict:
max_x = grid.limits['max_x']
max_y = grid.limits['max_y']
fig.set_size_inches(max_x/100, max_y/100)
elif mode == 'patches':
rectangles = patches(width, height, number)
elif mode == 'glyphs':
rectangles = glyphs(number)
length = min(width, height)
width, height = 1, 1
fig.set_size_inches(length/100, length/100)
if mode not in ('patches', 'glyphs'):
rectangles = grid.flatten()
collection = PolyCollection(rectangles, edgecolor="w")
collection.set_facecolor(np.random.rand(len(rectangles), 3))

plt.xlim(0, width)
plt.ylim(0, height)
plt.axis('scaled')
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.canvas.draw()
image = Image.frombytes(
'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
if not show:
plt.close(fig)
else:
plt.show()
return image

def plot_recur_bisect(length=1080, level=4, show=False, fill=True, drop=False, random_colors=False):
fig = plt.figure(figsize=(length/100, length/100),
dpi=100, facecolor='black')
ax.set_axis_off()
bisector = Recur_Bisector(0, length, 0, length, level, fill, drop)
squares, colors = bisector.recur_bisect().values()
collection = PolyCollection(squares, edgecolor="w")
if random_colors:
colors = np.random.rand(len(squares), 3)
collection.set_facecolor(colors)

plt.xlim(0, length)
plt.ylim(0, length)
plt.axis('scaled')
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.canvas.draw()
image = Image.frombytes(
'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
if not show:
plt.close(fig)
else:
plt.show()
return image

def plot_split(division, iterations, length=1080, unit=1, random_color=False, show=False):
data = recursive_split(division, iterations, unit)
fig = plt.figure(figsize=(length/100, length/100),
dpi=100, facecolor='black')
ax.set_axis_off()
collection = PolyCollection(data['geometries'], edgecolor="w")
if random_color:
colors = np.random.rand(data['total_count'], 3)
else:
colors = data['colors']
collection.set_facecolor(colors)
plt.axis('scaled')
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.canvas.draw()
image = Image.frombytes(
'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
if not show:
plt.close(fig)
else:
plt.show()
return image

if __name__ == '__main__':
for i in range(4):
random_rectangles().save('D:/images/random_rect_default_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(control=1).save(
'D:/images/random_rect_normalized_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(points=2).save(
'D:/images/random_rect_3_section_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(points=2, control=1).save(
'D:/images/random_rect_3_section_normalized_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(mode='square').save(
'D:/images/random_rect_square_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(mode='square', strict=1).save(
'D:/images/random_rect_square_strict_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(mode='patches').save(
'D:/images/random_rect_patches_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
random_rectangles(mode='glyphs').save(
'D:/images/random_rect_glyphs_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=2).save('D:/images/bisect_2_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=3).save('D:/images/bisect_3_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=4).save('D:/images/bisect_4_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=5).save('D:/images/bisect_5_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=5, random_colors=1).save('D:/images/bisect_5_random_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=5, fill=False).save('D:/images/bisect_5_no_fill_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=5, drop=1).save('D:/images/bisect_5_drop_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_recur_bisect(level=5, drop=1, fill=0).save('D:/images/bisect_5_drop_no_fill_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(2, 4).save('D:/images/split_2_4_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(2, 4, random_color=1).save(
'D:/images/split_2_4_random'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(3, 4).save('D:/images/split_3_4_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(3, 4, random_color=1).save(
'D:/images/split_3_4_random_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(4, 4).save('D:/images/split_4_4_' +
random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)
plot_split(4, 4, random_color=1).save(
'D:/images/split_4_4_random_'+random.randbytes(6).hex()+'.jpg', 'JPEG', quality=95)


How can this script be improved?

## Update

I updated my script so it can generate things like these: