1
\$\begingroup\$

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):

enter image description here

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

enter image description here

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:

enter image description here

two point:

enter image description here

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

enter image description here

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

enter image description here

Randomly place rectangles of similar sizes into region:

enter image description here

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

enter image description here

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

enter image description here

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:

enter image description here

Random colors:

enter image description here


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 = fig.add_subplot(111)
    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))
    ax.add_collection(collection)

    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 = fig.add_subplot(111)
    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)
    ax.add_collection(collection)

    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)
    radius = data['total_length']/2
    fig = plt.figure(figsize=(length/100, length/100),
                     dpi=100, facecolor='black')
    ax = fig.add_subplot(111)
    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)
    ax.add_collection(collection)
    plt.xlim(-radius, radius)
    plt.ylim(-radius, radius)
    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:

enter image description here

enter image description here

enter image description here

enter image description here

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.