Rectangles

Posted on Jul 28, 2022

Rectangle Recursion

A sub-component of my electrical circuit generation project.
Spatial linked node structures can be generated with little code.

The algorithm can be used to generate some pleasing “modern art” images.
Here is a collection of the outputs.

The code used to generate is at the bottom of the page.

The algorithm comes from the following paper.
credit: Automatic Real-Time Generation of Floor Plans Based on Squarified Treemaps Algorithm

Modern Art Gif Modern Art Bold Lines Modern Neon black_white shifting_squares

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
from dataclasses import dataclass
from random import randint, uniform

import cv2
import numpy as np 

@dataclass
class node:
    """node class cardinal links"""
    # pixel position
    x: float
    y: float

    # used to have setters that asserted object had different ID
    # north easth, south, west
    n = None
    e = None
    s = None
    w = None

    def pos(self):
        return (int(self.x), int(self.y))

    def __str__(self):
        return f'x: {self.x} y: {self.y} n: {type(self.n)} e: {type(self.e)} s: {type(self.s)} w: {type(self.w)}'

@dataclass
class rectangle:
    """nodes to form a rectangle, top left, top right, bottom right, bottom left"""
    tl: node
    tr: node
    br: node
    bl: node 

def node_average(node1: node, node2: node, weight=.5):
    """create node with weighted average of given node positions"""
    return node(node1.x * weight + node2.x * (1-weight), 
                node1.y * weight + node2.y * (1-weight))

def rectangle_from_dimensions(w: float, h: float, ox: float = 0, oy: float = 0):
    """create initial rectangle from width, height, offset"""
    tl, tr, br, bl = node(ox, oy), node(ox+w, oy), node(ox+w, oy+h), node(ox, oy+h)

    tl.e, tr.w = tr, tl
    tr.s, br.n = br, tr
    br.w, bl.e = bl, br
    bl.n, tl.s = tl, bl

    return rectangle(tl, tr, br, bl)

def vertical_split(rect: rectangle, weight=(.5, .5)):
    tl, tr, br, bl = rect.tl, rect.tr, rect.br, rect.bl
    
    weight = uniform(*weight)
    mt = node_average(tl, tr, weight=weight) # use the same weight for both to create rectangular images
    mb = node_average(bl, br, weight=weight)

    # ======== left ========
    
    # iterate down the linked list until pixel height is lower
    n1 = tl
    while n1.x < mt.x: n1 = n1.e

    # if pixel high it the same us present node (prevent multiple nodes in same location)
    if n1.x == mt.x:  
        mt = n1
    # insert node in between nodes
    else:
        mt.e, mt.w = n1, n1.w
        n1.w.e, n1.w = mt, mt
    # =======================

    # ======== right ========
    n2 = bl
    while n2.x < mb.x: n2 = n2.e

    if n2.x == bl.x: 
        bl = n2
    else:
        mb.e, mb.w = n2, n2.w
        n2.w.e, n2.w = mb, mb
    # =======================

    mt.s, mb.n = mb, mt

    return rectangle(tl, mt, mb, bl), rectangle(mt, tr, br, mb)

def horizontal_split(rect: rectangle, weight=(.5, .5)):
    tl, tr, br, bl = rect.tl, rect.tr, rect.br, rect.bl

    weight = uniform(*weight)
    ml = node_average(tl, bl, weight=weight)
    mr = node_average(tr, br, weight=weight)

    # top
    n1 = tl
    while n1.y < ml.y: n1 = n1.s
    
    if n1.y == ml.y: 
        ml = n1
    else:
        ml.s, ml.n = n1, n1.n
        n1.n.s, n1.n = ml, ml

    # bottom
    n2 = tr
    while n2.y < mr.y: n2 = n2.s
    
    if n2.y == mr.y:
        mr = n2
    else:
        mr.s, mr.n = n2, n2.n
        n2.n.s, n2.n = mr, mr

    ml.e, mr.w = mr, ml

    return rectangle(tl, tr, mr, ml), rectangle(ml, mr, br, bl)

def random_n_deep(rect: rectangle, n: int, weight=(.5, .5)):
    """recursive rectangle splitting"""
    rect1, rect2 = vertical_split(rect, weight=weight) if randint(0, 1) else horizontal_split(rect, weight=weight)
    
    if n > 0:
        s1 = random_n_deep(rect1, n-1, weight=weight)
        s2 = random_n_deep(rect2, n-1, weight=weight)
        return [*s1, *s2]
    
    return rect1, rect2

def draw_rect(canvas, rect: rectangle, color=(0, 0, 0), thickness=1, shrink=0):
    """helper function for drawing rectangles"""
    
    p1, p2, p3, p4 = rect.tl.pos(), rect.tr.pos(), rect.br.pos(), rect.bl.pos()
    cv2.line(canvas, p1, p2, color=color, thickness=max(thickness, 1))
    cv2.line(canvas, p2, p3, color=color, thickness=max(thickness, 1))
    cv2.line(canvas, p3, p4, color=color, thickness=max(thickness, 1))
    cv2.line(canvas, p4, p1, color=color, thickness=max(thickness, 1))
    # cv2.polylines(canvas, [np.array([p1, p2, p3, p4], dtype=np.int32)], color=color, thickness=min(thickness, 1), isClosed=True)

    # p1, p2 = rect.tl.pos(), rect.br.pos()
    # p1 = (p1[0] + shrink, p1[1] + shrink)
    # p2 = (p2[0] - shrink, p2[1] - shrink)
    # cv2.rectangle(canvas, p1, p2, color=color, thickness=thickness)

if __name__ == "__main__":
    import time

    # white space around image
    offset = 50

    # image dimensions
    aspect = (700, 500)

    # generate rectangular slices
    rects = random_n_deep(rectangle_from_dimensions(aspect[1], aspect[0], offset, offset), 6, weight=(.40, .60))

    # create canvas
    canvas = np.ones((aspect[0] + offset*2, aspect[1] + offset*2, 3), dtype=np.uint8) * 255

    # # draw filled rectanges
    # for rect in rects:
    #     color = list(map(int, (uniform(0, 255), uniform(0, 255), uniform(0, 255))))
    #     colors = [(0, 0, 200), (0, 200, 200), (200, 0, 0), (255, 255, 255)]

    #     # draw_rect(canvas, rect, color=colors[min(randint(0, 4), 3)], thickness=-1)
    #     draw_rect(canvas, rect, color=color, thickness=-1)

    # draw shrunk outlines
    for rect in rects:
        color = list(map(int, (uniform(0, 255), uniform(0, 255), uniform(0, 255))))
        draw_rect(canvas, rect, color=color, thickness=5, shrink=3)

    # display image
    cv2.imshow('image', canvas)
    q = cv2.waitKey(0)

    # save if 's' pressed
    if q == ord('s'):
        cv2.imwrite(f"./image_{str(time.time()).replace('.', '')}.png", canvas)