How do I code, in Processing, a fixed number of lines that are semi-randomly spaced apart and still fit into a fixed-size image?

Viewed 104

I'm both new to stack overflow and programming in general. I'm doing a basic python course at university and I've got to hand in a final project soon. I decided to try making some generative art in Python mode in Processing. My plan is to create a basic (abstracted) image of the skyline of a city by using lines of various length, height and color, with some semi-random spacing in between the lines. I've written some code that is able to do it to a degree. However, I am struggling to write it in a way that obeys some of the global variables I define at the start. I do likely get 50 lines but they don't cover the entire image and frequently overlap one another, making it difficult to tell them apart. Plus, I would like to incorporate some code which would specify where the buildings end, to get even blank space on the left and right side of the image. I tried using the building_start and building_end variables for this but clearly my way of adding separation between the buildings is off. Could you help me out with this?

Thanks in advance. The code and its output is shown below.

import random as rd
w, h = 1000, 500
building_start = 25
building_end = 975
n_buildings = 50
building_sep = (building_end - building_start)/(n_buildings*1.2)
roof_style = [ROUND, SQUARE]

def setup():
    size(w,h)
    background(60, 60, 60)

building_x = building_start
building_y = 500

for i in range(n_buildings):
    building_x = building_sep * i 
    building_top = rd.randrange(50, 370, 10)
    
    line(building_x, building_y, building_x, building_y - building_length)
    stroke(rd.randint(200, 255), rd.randint(200, 255), rd.randint(200, 255))
    strokeWeight(rd.randrange(10, 28, 2))
    strokeCap(rd.choice(roof_style))

noFill()
rect(0, 0, 1000, 500)
stroke(255, 247, 247)

the output of the code

3 Answers

If you want that there were no overlaps, you can try to implement this algorithm:

enter image description here

Another similar algorithm: if you need N buildngs inside the width W, you make N*2-1 buildings that have a total width W and remove every second building. Drawback of this algorithm: the spaces and buildings will have about the same widths.

Even easier implementation with random.sample() function:

import random

num_of_buildings = 5
total_width      = 100
max_height       = 50
min_height       = 10

x_coords = sorted(random.sample(range(0, total_width), num_of_buildings * 2))
print('x_coords:', x_coords)

heights = random.sample(range(min_height, max_height), num_of_buildings)
print('heights:', heights)

buildings = list(zip(x_coords[0::2], x_coords[1::2], heights))
print('buildings:', buildings)

for b in buildings:
    x = b[0]
    w = b[1] - b[0]
    h = b[2]
    print (f'Make the building: x = {x :2}, width = {w :2}, height = {h :2}')

Output:

x_coords: [2, 7, 9, 13, 21, 46, 47, 62, 79, 83]

heights: [35, 14, 28, 20, 12]

buildings: [(2, 7, 35), (9, 13, 14), (21, 46, 28), (47, 62, 20), (79, 83, 12)]

Make the building: x =  2, width =  5, height = 35
Make the building: x =  9, width =  4, height = 14
Make the building: x = 21, width = 25, height = 28
Make the building: x = 47, width = 15, height = 20
Make the building: x = 79, width =  4, height = 12

One of drawbacks of this implementation is you rare will get a building at the very edge of your area. There will be almost always a gap an the begin and at the end. It can be fixed roughly: just make for the first building x = 0 and for the last building width = total_width - x. Not exactly pretty solution, though.

https://docs.python.org/3/library/random.html


Full variant:

import random
from PIL import Image, ImageDraw

num_of_buildings = 30
total_width      = 800
max_height       = 400
min_height       = 100

im   = Image.new('RGB', (total_width, max_height), (255, 255, 255))
draw = ImageDraw.Draw(im)

x_coords  = sorted(random.sample(range(0, total_width), num_of_buildings * 2))
heights   = random.sample(range(min_height, max_height), num_of_buildings)
buildings = zip(x_coords[0::2], x_coords[1::2], heights)

for b in buildings:
    x1    = b[0]
    x2    = b[1]
    h     = max_height - b[2]
    color = (random.randint(128, 230), random.randint(128, 230), random.randint(128, 230))
    draw.rectangle((x1, max_height, x2, h), fill=color)

im.save('image.jpg')

Output:

enter image description here

It's obviously desperate for max and min width limits for buildings.

Ok, here is another approach based on Multinomial Distribution.

It is handy to use here because sum of all blocks (block is building plus left and right margins) is automatically, by definition, equal to view size.

So first we sample all blocks, second inside block we sample margins and get building position

Code, Python 3.8 Win 10 x64

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw

rng = np.random.default_rng()

building_start = 25
building_end   = 975
nof_buildings  = 50

p = nof_buildings*[1.0/nof_buildings] # probabilities

q = None
while True:
    q = rng.multinomial(building_end-building_start, p)

    if np.any(q < 7): # minimum width condition, continue sampling
        continue

    break

print(q) # sampled blocks array
print(np.sum(q)) # be sure sum is equal to 950

# making positions/separators
min_height       = 100
max_height       = 400

buildings = list()
left = building_start
for k in range(0, nof_buildings):
    # position a block
    left +=  (0 if k == 0 else q[k])
    rght  = left + (0 if k == nof_buildings-1 else q[k]-1)

    # add margins
    left += rng.choice([1, 2, 3], p=[0.4, 0.35, 0.25])
    rght -= rng.choice([1, 2, 3], p=[0.4, 0.35, 0.25])

    height = rng.integers(min_height, max_height)

    buildings.append((left, rght, height)) # here is single building

im   = Image.new('RGB', (1024, 512), (255, 255, 255))
draw = ImageDraw.Draw(im)

for bld in buildings: # plot all buildings
    l    = bld[0]
    r    = bld[1]
    h    = max_height - bld[2]
    color = (rng.integers(128, 230),
             rng.integers(128, 230),
             rng.integers(128, 230))
    draw.rectangle((l, max_height, r, h), fill=color)

im.save('buildings.jpg')

And at the end you'll get something like

enter image description here

Related