-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfill_converter.py
432 lines (349 loc) · 16.7 KB
/
fill_converter.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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
import sys
import traceback
from converter import *
import cv2
import networkx as nx
from TSP_Solver import Graph_TSP
class Line:
def __init__(self, x1, x2, y):
self.x1 = x1
self.x2 = x2
self.y = y
self.i = -1
def get_hashable(self):
return self.x1, self.x2, self.y, self.i
def __str__(self):
return '%s:%s,%s,%s' % (self.x1, self.x2, self.y, self.i)
def line_distance(line1, line2):
return abs(line2[2] - line1[2]) if line2[0] <= line1[1] and line2[1] >= line1[0] else \
min(abs(line2[0] - line1[1]) + abs(line2[2] - line1[2]),
abs(line2[1] - line1[0]) + abs(line2[2] - line1[2]))
def draw_line(line, anchors):
anchors.append(np.array((line[0], line[2])))
anchors.append(np.array((line[1], line[2])))
def draw_path_to_line(start_line, end_line, path, anchors):
pos = (start_line[1], start_line[2])
lbound = None
rbound = None
prev = None
last_dir_up = None
for line in path:
dir_up = prev[0][2] < line[2] if prev is not None else None
# Interpolate and check if its inside the line
if lbound is None or rbound is None:
lbound = line[0]
rbound = line[1]
elif pos[1] != line[2]:
lbi = (line[0] - pos[0]) / abs(line[2] - pos[1]) + pos[0]
rbi = (line[1] - pos[0]) / abs(line[2] - pos[1]) + pos[0]
lbound = max(lbi, lbound)
rbound = min(rbi, rbound)
# If this is the last line then it will add a line straight to the start of the end line
# we do this check to make sure this doesn't pass through any black area
if line == end_line and lbi < lbound:
lbound = rbound + 1
if lbound > rbound or dir_up != last_dir_up:
if prev[0] != start_line and prev[0] != end_line:
nx = prev[2] if (line[0]+line[1])/2 > pos[0] else prev[1]
pos2 = (round((nx - pos[0]) * abs(prev[0][2] - pos[1]) + pos[0]), prev[0][2])
if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\
and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]):
anchors.append(np.array(pos2))
pos = pos2
lbound = line[0]
rbound = line[1]
# Make sure component connections go through the same two points every time
# so only one connection line is visible (all overlap)
if prev is not None and line_distance(prev[0], line) > 1:
# Add an anchor at the closest point
nx = prev[0][1] if line[0] > prev[0][1] else prev[0][0] if line[1] < prev[0][0] else max(line[0], prev[0][0])
pos2 = (nx, prev[0][2])
if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\
and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]):
anchors.append(np.array(pos2))
pos = pos2
nx = line[0] if line[0] > prev[0][1] else line[1] if line[1] < prev[0][0] else max(line[0], prev[0][0])
pos2 = (nx, line[2])
if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\
and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]):
anchors.append(np.array(pos2))
pos = pos2
last_dir_up = dir_up
prev = (line, lbound, rbound)
def triangle_area(a, b, c):
return abs(0.5 * (a[0] * (b[1] - c[1]) +
b[0] * (c[1] - a[1]) +
c[0] * (a[1] - b[1])))
def collinear(a, b, c):
return abs((a[1] - b[1]) * (a[0] - c[0]) - (a[1] - c[1]) * (a[0] - b[0])) <= 1e-6
# Basic image converter for multi-colour images
class FillConverter(Converter):
def __init__(self, config_file):
super().__init__(config_file)
def convert(self, path, time=0, start_pos=None):
self.load_image(path)
self.prepare_image()
if self.VERBOSE:
print("Image resolution: ", self.imgshape)
print("Slider resolution:", self.shape)
print("Slider size: ", self.shape * self.PIXEL_SPACING)
try:
return self.process_image(time)
except Exception:
print("Exception in user code:")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
def process_image(self, time):
SCALE = float(self.CONFIG['SETTINGS']['SCALE'])
WINDOW_WIDTH = 70 * 4 / SCALE
windowsize = int(np.ceil(WINDOW_WIDTH * 0.05))
radius = int(np.ceil(windowsize / 2))
circle_window = np.zeros((windowsize, windowsize), np.uint8)
cv2.circle(circle_window, (radius, radius), windowsize, (255, 255, 255), -1)
circle_window2 = np.zeros((windowsize // 3, windowsize // 3), np.uint8)
cv2.circle(circle_window2, (radius // 3, radius // 3), windowsize // 3, (255, 255, 255), -1)
img = self.data[:, :, 0].astype(np.uint8)
#img = cv2.copyMakeBorder(img, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0)
(thresh, img) = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
# Erode the image
img = cv2.erode(img, circle_window)
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
return None
hierarchy = hierarchy[0]
contours = [contour.reshape(-1, 2) for contour in contours]
# Erode again for the scan lines
img = cv2.erode(img, circle_window2)
line_width = int(windowsize * 2 / 3)
# Create horizontal scan lines for the graph
if self.VERBOSE:
print("Creating scan lines...")
lines_dict = {}
g = nx.Graph()
counter = 0
prev_line_lines = []
for y in range(int(time % line_width), img.shape[0], line_width):
x1 = -1
line_lines = []
for x in range(img.shape[1]):
pixel = img[y, x]
# Omit black pixels
if pixel < 128:
if x - x1 > 1:
line_lines.append(Line(x1 + 1, x - 1, y))
x1 = x
elif x == img.shape[1] - 1:
line_lines.append(Line(x1 + 1, x, y))
for line in line_lines:
line.i = counter
lines_dict[counter] = line
g.add_node(line.get_hashable(), weight=1)
counter += 1
# Add direct neighbors
for prev_line in prev_line_lines:
if prev_line.x1 <= line.x2 and prev_line.x2 >= line.x1:
# Lines are touching
g.add_edge(line.get_hashable(), prev_line.get_hashable())
prev_line_lines = line_lines
if self.VERBOSE:
print("counter: %s" % counter)
components = [g.subgraph(c).copy() for c in nx.connected_components(g)]
fillings = []
for c in components:
counter = 0
lines_dict2 = {}
for n in c:
lines_dict2[counter] = lines_dict[n[3]]
counter += 1
if counter == 0:
continue
# Use dijkstra's algorithm to calculate distance matrix with pathing
# Each node stores for each other node the distance (int) and the previous node in the path (int)
# This is enough to know the nearest nodes and how to get there
# This can be stored in a 3D int array
if self.VERBOSE:
print("Creating distance matrix...")
len_path = dict(nx.all_pairs_dijkstra(c))
# Make fully connected adjacency matrix
adj = np.empty((counter, counter))
ni = 0
for n in len_path:
total_dist = len_path[n][0]
di = 0
for d in total_dist:
adj[ni, di] = total_dist[d]
di += 1
ni += 1
# Solve traveling salesman
if self.VERBOSE:
print("Solving TSP...")
instance_graph = Graph_TSP(lines_dict2, adj, "frame", -1)
christofides = instance_graph.christofides()
result = christofides
# Get the part of the cycle which is in the top right corner
min_y = 99999999999
min_x = 99999999999
min_index = 0
for i in range(len(result)):
line = lines_dict2[result[i][0]]
if line.y < min_y:
min_y = line.y
min_x = line.x1
min_index = i
elif line.y == min_y and line.x1 < min_x:
min_x = line.x1
min_index = i
result = result[min_index:] + result[:min_index]
# Draw anchors
if self.VERBOSE:
print("Drawing anchors...")
anchors = []
if len(result) > 0:
prev_line = None
for t in result:
line = lines_dict2[t[0]].get_hashable()
if prev_line is not None:
paths = len_path[prev_line][1]
draw_path_to_line(prev_line, line, paths[line], anchors)
draw_line(line, anchors)
prev_line = line
# Add path to the start again
line = lines_dict2[result[0][0]].get_hashable()
if prev_line is not None:
paths = len_path[prev_line][1]
draw_path_to_line(prev_line, line, paths[line], anchors)
anchors.append(np.array((line[0], line[2])))
else:
line = lines_dict2[0].get_hashable()
draw_line(line, anchors)
anchors.append(np.array((line[0], line[2])))
fillings.append(anchors)
# Make sliders
if self.VERBOSE:
print("Making sliders...")
used_fillings = []
slidercode = ""
# Loop through all outer contours using the hierarchy
next_outer = 0
while next_outer != -1:
current_outer = next_outer
contour = contours[current_outer]
# Get the index of the next outer contour from the hierarchy
next_outer = hierarchy[current_outer][0]
if len(contour) < 2:
continue
# Get all the contours that are child of this contour
child_contours = []
next_child = hierarchy[current_outer][2]
while next_child != -1:
child_contours.append(contours[next_child])
next_child = hierarchy[next_child][0]
# Get the filling that fits inside this contour and the place to connect them
c_fillings = []
max_dist = windowsize * 1.5
c_start_indices = []
for i in range(len(contour)):
coord = contour[i]
# Find the filling for this contour start pos
for filling in fillings:
pos = filling[0]
dist = np.linalg.norm(pos - coord, ord=np.inf)
if dist <= max_dist and list(pos) not in used_fillings:
c_fillings.append(filling)
c_start_indices.append(i)
used_fillings.append(list(pos))
# Find the bet way to connect the child contours to the outer contour
# or add child contours to other child contours
# First we add any children to other children
added_children = []
for k, child in enumerate(child_contours):
# This is the top left point of the child contour
pos = child[0]
# Find the closest point on the outer contour that is to the top left of this point
best_dist = np.inf
best_i = -1
for i, coord in enumerate(contour):
dist = np.linalg.norm(pos - coord)
if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]:
best_dist = dist
best_i = i
# Check if any other child is closer than the outer contour
best_child = -1
for j, other_child in enumerate(child_contours):
if k == j:
continue
for i, coord in enumerate(other_child):
dist = np.linalg.norm(pos - coord)
if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]:
best_dist = dist
best_i = i
best_child = j
if best_child != -1:
child_contours[best_child] = np.vstack((child_contours[best_child][0:best_i], child, child[0], child_contours[best_child][best_i:-1]))
added_children.append(k)
# Add the remaining children to the outer contour
for k, child in enumerate(child_contours):
if k in added_children:
continue
# This is the top left point of the child contour
pos = child[0]
# Find the closest point on the outer contour that is to the top left of this point
best_dist = np.inf
best_i = -1
for i, coord in enumerate(contour):
dist = np.linalg.norm(pos - coord)
if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]:
best_dist = dist
best_i = i
# Add the index of the best connection point to the list of start indices
# and add the child contour to the list of fillings so it gets added
c_fillings.append(list(child) + [child[0]])
c_start_indices.append(best_i)
# Combine everything into a list of anchors
anchors = []
for i, coord in enumerate(contour):
anchors.append(coord)
while i in c_start_indices:
f_index = c_start_indices.index(i)
anchors += c_fillings.pop(f_index)
c_start_indices.pop(f_index)
anchors.append(coord)
if len(contour) > 0:
anchors.append(contour[0])
anchor1 = anchors.pop(0)
anchor1_rounded = np.round(anchors.pop(0) * SCALE)
slidercode += "%s,%s,%s,6,0,L" % (int(anchor1_rounded[0]), int(anchor1_rounded[1]), int(time))
total_dist = 0
last_anchor_rounded = anchor1_rounded
last_anchor = anchor1
lastlast_anchor = anchor1
for i, anchor in enumerate(anchors):
round_anchor = np.round(anchor * SCALE)
# We skip some anchors if they are ugly
if 0 < i < len(anchors) - 1:
next_anchor = anchors[i + 1]
nextnext_anchor = anchors[i + 2] if i + 2 < len(anchors) else next_anchor
big_space = np.linalg.norm(next_anchor - anchor) > 10
next_big_space = np.linalg.norm(next_anchor - nextnext_anchor) > 10
if ((np.linalg.norm(last_anchor - lastlast_anchor) > np.linalg.norm(anchor - next_anchor) or not big_space) and
np.linalg.norm(last_anchor - anchor, ord=np.inf) <= 1) or\
(np.linalg.norm(last_anchor - anchor) < np.linalg.norm(nextnext_anchor - next_anchor) and next_big_space and
np.linalg.norm(next_anchor - anchor, ord=np.inf) <= 1) or\
(triangle_area(last_anchor, anchor, next_anchor) < 1e-6 and
np.linalg.norm(last_anchor - anchor) <= np.linalg.norm(next_anchor - last_anchor)):
continue
if last_anchor_rounded is not None:
dist = np.linalg.norm(round_anchor - last_anchor_rounded)
total_dist += dist
anchor_string = "|%s:%s" % (int(round_anchor[0]), int(round_anchor[1]))
slidercode += anchor_string
lastlast_anchor = last_anchor
last_anchor_rounded = round_anchor
last_anchor = anchor
slidercode += ",1,%s\n" % total_dist
if self.VERBOSE:
print("num fillings: %s" % len(fillings))
print("num used fillings: %s" % len(used_fillings))
print("num contours: %s" % len(contours))
return slidercode