-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrectangles.py
359 lines (312 loc) · 11 KB
/
rectangles.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
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
#!/usr/bin/env python3
import itertools
import sys
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import scipy.optimize as opt
# Helper class with rectangle information, used for the moment only when
# drawing rectangulations.
class RectangleLayout:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
def max_x(self):
return self.x + self.width
def max_y(self):
return self.y + self.height
def __eq__(self, other):
return (self.x, self.y, self.width, self.height) \
== (other.x, other.y, other.width, other.height)
def __str__(self):
return 'Origin: (' + str(self.x) + ',' + str(self.y) + '). W: ' \
+ str(self.width) + '. H: ' + str(self.height)
# We consider a cell taken if its value points to a rectangle or if
# outside background.
def _is_b_empty(B, N, i, j):
if i < 0 or i >= N or j < 0 or j >= N:
return False
return B[i, j] < 0
# Do a diagonal rectangulation
# seq: sequence with order in which to create the rectangles.
# Length N, with values between 0 and N-1.
# Returns a numpy matrix where each cell contains an integer for the
# rectangle in that position and a 2xN matrix with the dimensions of
# each rectangle as a percentage of the bounding segments.
def do_diagonal_rectangulation(seq):
N = len(seq)
# Background rectangle, as a grid where we will indicate the overlaying
# rectangle in each cell (each of them can span acrosss multiple cells).
B = np.full((N, N), -1, dtype=int)
dim = np.zeros((2, N))
for r in seq:
# Top-left corner (top, left)
top = r
left = r
if _is_b_empty(B, N, r-1, r-1):
# Go left
while _is_b_empty(B, N, top, left - 1):
left -= 1
else:
# Go up
while top - 1 >= 0 and not _is_b_empty(B, N, top - 1, left - 1):
top -= 1
# Bottom-right corner
bottom = r
right = r
if _is_b_empty(B, N, r+1, r+1):
# Go down
while _is_b_empty(B, N, bottom + 1, right):
bottom += 1
else:
# Go right
while right + 1 <= N - 1 and \
not _is_b_empty(B, N, bottom + 1, right + 1):
right += 1
dim[0, r] = (right - left + 1)/N
dim[1, r] = (bottom - top + 1)/N
# Fill inside B
for i in range(top, bottom + 1):
for j in range(left, right + 1):
B[i, j] = r
return B, dim
# Creates equations for the rectangulation restrictions, without the
# independent terms.
# B: rectangulation as created by do_diagonal_rectangulation
def build_rectangulation_equations(B):
N = B.shape[0]
E = np.zeros((2, 2*N))
# Top and left of rectangulation
for j in range(N):
E[0, B[0, j]] = 1
for i in range(N):
E[1, N + B[i, 0]] = 1
# Horizontal segments
eq = None
for i in range(N - 1):
for j in range(N):
if B[i, j] != B[i + 1, j]:
# Different rectangle up and down
if eq is None:
eq = np.zeros(2*N)
eq[B[i, j]] = 1
eq[B[i + 1, j]] = -1
else:
# Close equation if we were creating one
if eq is not None:
E = np.vstack([E, eq])
eq = None
if eq is not None:
E = np.vstack([E, eq])
eq = None
# Vertical segments
for j in range(N - 1):
for i in range(N):
if B[i, j] != B[i, j + 1]:
# Different rectangle left and right
if eq is None:
eq = np.zeros(2*N)
eq[N + B[i, j]] = 1
eq[N + B[i, j + 1]] = -1
else:
# Close equation if we were creating one
if eq is not None:
E = np.vstack([E, eq])
eq = None
if eq is not None:
E = np.vstack([E, eq])
eq = None
return E
# Get value for target function
def opt_f_val(X, w, h, k, c):
N = len(X)//2
v = 0
avg_sz = w*h/N
T = c*h*h
# Function to optimize
for r in range(N):
v += (X[r]*X[N + r] - avg_sz)**2
for r in range(N):
v += T*(X[r] - k*X[N + r])**2
return v
# Get Jacobian for target function
def opt_jac_val(X, w, h, k, c):
N = len(X)//2
diff = np.zeros(2*N)
avg_sz = w*h/N
T = c*h*h
# dF/dw_i
for i in range(N):
diff[i] = 2*T*(X[i] - k*X[N + i])
diff[i] += 2*X[N + i]*(X[i]*X[N + i] - avg_sz)
# dF/dh_i
for i in range(N):
diff[N + i] = -2*T*k*(X[i] - k*X[N + i])
diff[N + i] += 2*X[i]*(X[i]*X[N + i] - avg_sz)
return diff
# Variables to minimize for are [w_1,..,w_N,h_1,..,h_N]
# E: rectangulation equations coefficients (N+1)x2N matrix
# est: initial estimation for the sizes in a 2xN matrix
# w: width of bounding rectangle
# h: height of bounding rectangle
# k: desired w_i/h_i ratio
# c: controls the balance between the two optimization criteria.
def minimize_rectangulation(E, est, w, h, k, c):
n_rect_vars = E.shape[1]
N = n_rect_vars//2
initial_est = est.reshape(est.size)
n_cons_eqs = E.shape[0]
indep = np.zeros(n_cons_eqs)
indep[0] = w
indep[1] = h
constr = opt.LinearConstraint(E, indep, indep)
sol = opt.minimize(opt_f_val, initial_est, args=(w, h, k, c),
jac=opt_jac_val, constraints=constr)
if not sol.success:
print('Could not optimize:', sol.message)
ret = np.zeros((2, N))
ret[0, :] = sol.x[:N]
ret[1, :] = sol.x[N:]
return ret
# Draw rectangulation. We get the relative positions of the rectangles
# from B, and the real dimension from D. Altough relative positions of
# rectangles might have changed with the new sizes, the relative
# positions of rectangles across segments defined by B do not change. We
# use that to draw the rectangles.
# B: rectangulation square
# D: Vector with dimensions (w, h) in each cell
# w_pix: width in pixels
# h_pix: height in pixesl
# kwargs: title: title for the plot
# save_file: file to save figure to
def draw_resized_rectangles(B, D, w_pix, h_pix, **kwargs):
Nx = B.shape[0]
Ny = B.shape[1]
fig, ax = plt.subplots()
Rs = {}
# Horizontal segment by horizontal segment (we could use the
# vertical ones as well).
for i in range(Ny):
for j in range(Nx):
# 1. We are right below an horizontal line
# 2. We have just moved to a new rectangle
# -> we draw the rectangle
if (i == 0 or B[i, j] != B[i - 1, j]) and \
(j == 0 or B[i, j] != B[i, j - 1]):
if j == 0:
r_x = 0
else:
# Use left rectangle
r_x = Rs[B[i, j - 1]].max_x()
if i == 0:
r_y = 0
else:
# Use top rectangle
r_y = Rs[B[i - 1, j]].max_y()
r_w = D[0, B[i, j]]
r_h = D[1, B[i, j]]
Rs[B[i, j]] = RectangleLayout(r_x, r_y, r_w, r_h)
rect = patches.Rectangle((r_x, r_y), r_w, r_h,
facecolor='w', edgecolor='k')
ax.add_patch(rect)
ax.set_xlim(0, w_pix)
ax.set_ylim(h_pix, 0)
ax.set_aspect('equal')
title = kwargs.get('title', None)
if title:
plt.title(title)
save_file = kwargs.get('save_file', None)
if save_file:
plt.savefig(save_file, format='svg')
plt.show()
# Calculate best rectangulation in the sense of equal area distribution and
# best aspect ration for N rectangles and background size w x h.
# c: balance between area and proportions
# 0.1: proportion is predominant
# 0.05: seems well-balanced
def get_best_rect_for_window(N, c, k, w, h):
# Check all permutations
f_best = sys.float_info.max
sol_best = np.zeros(0)
B_best = None
seq_best = None
# If the new optimum is only very slightly better, keep the old
# value so solutions are more congruent when the parameters change
# slightly (itertools.permutations() always produces sequences in
# the same order). This delta is the allowed difference.
delta = (w*h)**2/1e12
seq_first = [r for r in range(0, N)]
for seq in itertools.permutations(seq_first):
if not is_baxter_permutation(seq):
continue
B, dim = do_diagonal_rectangulation(seq)
E = build_rectangulation_equations(B)
dim[0, :] *= w
dim[1, :] *= h
sol = minimize_rectangulation(E, dim, w, h, k, c)
vals = sol[0, :].tolist()
vals.extend(sol[1, :].tolist())
f_val = opt_f_val(vals, w, h, k, c)
if f_val + delta < f_best:
f_best = f_val
sol_best = sol
B_best = B
seq_best = seq
return B_best, sol_best, seq_best
# Returns True if subseq matches the generalized pattern
# pattern: contains numbers
# gaps: booleans list, gaps[i]=true if dash before pattern[i]
def subseq_matches_pattern(seq, subseq, pattern, gaps):
# i is seq index, j is subseq/pattern index
j = 0
gap = False
len_subseq = len(subseq)
for i, sv in enumerate(seq):
if sv != subseq[j]:
gap = True
continue
# No match if gap found and no dash
if j > 0 and gaps[j] is False and gap is True:
return False
# Check previous values in subseq
for k in range(0, j - 1):
if pattern[k] < pattern[j] and subseq[k] >= subseq[j]:
return False
if subseq[k] < subseq[j] and pattern[k] >= pattern[j]:
return False
gap = False
j += 1
if j == len_subseq:
break
return True
# Generator, get subsequences of n elements from seq
def get_subsequence(n, seq):
if n == 0:
yield []
for i, s in enumerate(seq):
for l in get_subsequence(n - 1, seq[i + 1:]):
subseq = [s]
subseq.extend(l)
yield subseq
# Returns true if seq is a Baxter permutation
def is_baxter_permutation(seq):
is_baxter = True
for subseq in get_subsequence(4, seq):
if subseq_matches_pattern(seq, subseq, [3, 1, 4, 2],
[False, True, False, True]) or \
subseq_matches_pattern(seq, subseq, [2, 4, 1, 3],
[False, True, False, True]):
is_baxter = False
break
return is_baxter
# Count number of diagonal rectangulations for N rectangles, by finding
# out all baxter permutations for N elements.
def count_number_diagonal_rects(N):
num_rects = 0
seq_first = [r for r in range(0, N)]
for seq in itertools.permutations(seq_first):
if is_baxter_permutation(seq):
num_rects += 1
return num_rects