-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshapegen.py
344 lines (285 loc) · 9.6 KB
/
shapegen.py
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
from utils import *
FREE = '.'
FILL = 'X'
def compute_symmetry( G, axis ):
center = Int2(G.W//2, G.H//2)
H = Int2(G.W//2, G.H//2)
perp = axis.turn(1)
def reflect(u):
perpcoord = (u-center).dot(perp)
return u - perp*(2*perpcoord)
singles = 0
fills = 0
for (u, p) in G.piter():
if p != FILL:
continue
fills += 1
v = reflect(u)
if not G.check(v) or G.pget(v) != FILL:
singles += 1
return 1.0 - singles * 1.0 / fills
def compute_convexity( G ):
H = compute_convex_mask(G, FILL)
hullarea = 0
cavityarea = 0
for (u,in_hull) in H.piter():
if in_hull:
hullarea += 1
if G.pget(u) != FILL:
cavityarea += 1
convexity = 1.0 - cavityarea * 1.0 / hullarea
# H.printself()
# save_grid_png(H, 'hull.png')
print('compute_convexity: 1.0 - %d / %d' % (cavityarea, hullarea))
return convexity
def compute_outside_mask(G):
""" A cell is 'outside' if it's 1) free and 2) reachable from the border via other free cells """
M = Grid2.new_same_size(G, False)
def check_edge(u,v):
return G.pget(v) == FREE and not M.pget(v)
for u in G.iterborder():
if G.pget(u) == FREE:
for v in G.bfs(u, check_edge, 4):
M.pset(v, True)
return M
def enforce_symmetry(G, min_symmetry, axis, force_contiguous):
""" returns true if G was modified """
assert 0 <= min_symmetry
assert min_symmetry <= 1.0
# having odd length simplifies some things
assert G.W % 2 == 1
assert G.H % 2 == 1
center = Int2(G.W//2, G.H//2)
perp = axis.turn(1)
def reflect(u):
perpcoord = (u-center).dot(perp)
return u - perp*(2*perpcoord)
# compute unmatched (single) fills
totalfills = 0
singles = []
for (u,p) in G.piter():
if p == FILL:
totalfills += 1
v = reflect(u)
if G.pget(v) != FILL:
singles += [u]
def curr_symmetry():
return 1.0 - len(singles)*1.0 / totalfills
modded = False
while curr_symmetry() < min_symmetry:
# find a single to pair up, but make sure to do it contiguously
eligible = []
for u in singles:
v = reflect(u)
if not force_contiguous or G.touches4(v, FILL):
eligible += [u]
if len(eligible) == 0:
G.printself()
print('UH OH could not find any eligible points to increase symmetry')
lucky = random.choice(eligible)
singles.remove(lucky)
other = reflect(lucky)
G.pset(other, FILL)
totalfills += 1
modded = True
return modded
def enforce_convexity(G, min_convexity):
""" returns true if G was modified """
H = compute_convex_mask(G, FILL)
outside = compute_outside_mask(G)
hullarea = 0
cavities = []
for (u,in_hull) in H.piter():
if in_hull:
hullarea += 1
if G.pget(u) != FILL and outside.pget(u):
cavities += [u]
# TEMP TEMP fill all non-outside holes
# or maybe not so temp..this works quite well
for (u, p) in G.piter():
if not outside.pget(u):
G.pset(u, FILL)
def curr_convexity():
return 1.0 - len(cavities) * 1.0 / hullarea
modded = False
while curr_convexity() < min_convexity:
# find a cavity to fill, but only if it's adjacent to an existing fill
eligible = []
for u in cavities:
if G.touches4(u, FILL):
eligible += [u]
lucky = random.choice(eligible)
cavities.remove(lucky)
G.pset( lucky, FILL )
modded = True
return modded
def enforce_min_nbors(G, min_nbors, on_value, off_value):
modded = False
freethese = []
for (u, p) in G.piter():
if p != on_value: continue
count = 0
for (v, q) in G.nbors4(u):
if q == on_value: count+= 1
if count < min_nbors:
freethese.append(u)
modded = True
for u in freethese:
G.pset(u, off_value)
return modded
def enforce_boxiness(G, min_boxiness):
bbox = G.bbox(lambda p : p == FILL)
cavities = []
numfills = 0
for (u, p) in G.piter():
if p == FREE and bbox.contains(u):
cavities += [u]
elif p == FILL:
numfills += 1
def curr_boxiness():
return 1.0 - len(cavities)*1.0/numfills
modded = False
while curr_boxiness() < min_boxiness:
eligible = []
for u in cavities:
if G.touches4(u, FILL):
eligible += [u]
lucky = random.choice(eligible)
cavities.remove(lucky)
G.pset( lucky, FILL )
modded = True
return modded
def test_symmetry():
L = 41
G = Grid2(L,L,FREE)
cent = Int2(L//2, L//2)
G.pset(cent, FILL)
seed_spread([FILL], 0, G, FREE, L*L//3)
for (u,p) in G.piter():
if u.y <= L//2:
G.pset(u, FREE)
assert compute_symmetry(G, Int2(1,0)) == 0.0
def test_y_symmetry():
L = 11
G = Grid2(L, L, FREE)
for (u, p) in G.piter():
if random.random() < 0.2:
G.pset(u, FILL)
center = Int2(L//2, L//2)
axis = Int2(0,1)
G.printself()
print(compute_symmetry(G, axis))
enforce_symmetry( G, 1.0, axis, False )
G.printself()
print(compute_symmetry(G, axis))
def fixed_point_iterate(steps, max_steps):
assert type(steps) == list
assert type(max_steps) == int
""" each step should return True if it changed the state """
modded = True
num_steps = 0
while modded and num_steps < max_steps:
modded = False
for (label, func) in steps:
with PROFILE(label):
thismodded = func()
modded = modded or thismodded
num_steps += 1
def clamp01(x):
return max(0.0, min(1.0, x))
def lerp(a, b, t):
return a + t*(b-a)
def ceilodd(x):
if x % 2 == 0:
return x+1
else:
return x
def quadratic_formula(a, b, c):
dis = b*b - 4*a*c
if dis < 0:
return (None, None)
else:
return ( (-1*b + math.sqrt(dis)) / (2*a),
(-1*b - math.sqrt(dis)) / (2*a))
def random_width_height(min_delta, max_delta):
assert min_delta < max_delta
delta = int(lerp( min_delta, max_delta, random.random() ))
# 1600 = x * y
# 1600 = x * (x+delta)
# 0 = x^2 + delta*x - 1600
width = int(math.ceil(max(quadratic_formula(1, delta, -1600))))
height = width + delta
return (width, height)
def create_shape(width, height):
# means and stdevs are hand-tuned
# good params are, xs=0.0, ys=1.0, cv=0.95
# so these gamma distributions are meant to typically yield values around those
# min_xsym = clamp01(random.gammavariate(1.0, 2.0)/20.0 * 1.0)
# min_ysym = clamp01(1.0 - random.gammavariate(1.0, 2.0)/20.0 * 0.2)
# min_convex = clamp01(1.0 - random.gammavariate(1.0, 2.0)/20.0 * 0.10)
# min_xsym, min_ysym, min_convex, min_boxiness = (0, 1, 1.0, 0.0)
# min_xsym, min_ysym, min_convex, min_boxiness = (0.5, 1, 0.90, 0.0)
# min_xsym, min_ysym, min_convex, min_boxiness = (0.7, 1, 0.98, 0.8)
# min_xsym, min_ysym, min_convex, min_boxiness = (0.5, 1, 0.95, 0.0) # best 9/10
min_xsym, min_ysym, min_convex, min_boxiness = (0.5, 1, 0.5, 0.0) # best 9/10
# min_xsym, min_ysym, min_convex, min_boxiness = (random.random(), 1.0, 0.95, 0.0)
width = ceilodd(width)
height = ceilodd(height)
G = Grid2(width,height,FREE)
cent = Int2(width//2, height//2)
G.pset(cent, FILL)
# initial random spread step
with PROFILE('spread'):
seed_spread([FILL], 0, G, FREE, width*height/4)
def print_status():
print('xsym %f \t ysym %f \t convex %f' % (
compute_symmetry( G, Int2(1,0) ),
compute_symmetry( G, Int2(0,1) ),
compute_convexity( G )))
return False
fixed_point_iterate([
('xsym', lambda : enforce_symmetry(G, min_xsym, Int2(1,0), True)),
('ysym', lambda : enforce_symmetry(G, min_ysym, Int2(0,1), True)),
('conv', lambda : enforce_convexity(G, min_convex)),
('box', lambda : enforce_boxiness(G, min_boxiness)),
('nborsout', lambda : enforce_min_nbors(G, 2, FREE, FILL)),
('nborsin', lambda : enforce_min_nbors(G, 2, FILL, FREE)),
('eval', lambda : print_status()) ],
3 )
return G
def create_clump_of_shapes(width, height, shape_rows, shape_cols):
F = 2
sw = int(math.floor(width/shape_cols/F))
sh = int(math.floor(height/shape_rows/F))
G = Grid2(width, height, FREE)
center = Int2(width/2, height/2)
shapes = []
shape_dims = Int2(shape_cols, shape_rows)
shape_id = 0
for shape_coord in shape_dims.range():
shape_ofs = shape_coord.scale(Int2(sw, sh)*F)
print('shape %s %s' % (shape_coord, shape_ofs))
shape_grid = create_shape(sw, sh)
for (u, p) in shape_grid.piter():
if p == FILL:
G[ shape_ofs + u ] = shape_id
shape = GridShape(G, shape_id)
shapes += [shape]
shape_id += 1
steps = 0
while True:
G.save_png("clump step %d.png" % steps)
steps += 1
random.shuffle(shapes)
movedarr = [s.move_towards(center, FREE) for s in shapes];
if not any(movedarr):
break
return (G, shapes)
if __name__ == '__main__':
test_symmetry()
test_y_symmetry()
for i in range(20):
with PROFILE('shape %d' % i):
G = create_shape( 75, 75 )
save_grid_png(G, 'shape-%d.png' % i)
G.printself()