-
Notifications
You must be signed in to change notification settings - Fork 0
/
anime.py
539 lines (385 loc) · 15.1 KB
/
anime.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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
'''
Commands to aggregate the frames to MP4:
for high resolution:
ffmpeg -r 48 -i frames/primaryschool/%05d.png -vcodec libx264 -pix_fmt yuv420p -strict -2 -acodec aac clips/primaryschool.mp4
for twitter:
ffmpeg -i frames/primaryschool2/%05d.png -pix_fmt yuv420p -vcodec libx264 -vf 'scale=640:trunc(ow/a/2)*2' -acodec aac -vb 1024k -minrate 1024k -maxrate 1024k -bufsize 1024k -ar 44100 -ac 2 -strict experimental -r 30 clips/primaryschool2.mp4
'''
import matplotlib
matplotlib.use('Agg')
import csv
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
from sys import argv
import graph_tool.all as gt
from copy import deepcopy
from alive_progress import alive_bar
from scipy.spatial import ConvexHull
from scipy.ndimage import uniform_filter1d
layout_step = 0.2 # for the layout algorithm
layout_k = 5.0 # preferred edge length
layout_c = 0.3 # Relative strength of repulsive forces
layout_p = 2.0 # Repulsive force exponent
size_fac = 0.02 # rescaling node sizes
recfac = 0.95 # how much smaller a square relative to circles
expansion_fac = 1.4 # how much larger blobs should be after the smoothing
link_weight_low = 0.5 # lower link weight cut off
node_size_low = 0.9 # lower node size limit
weight2sizefac = 0.2 # for converting node weight to size
weight2widthfac = 0.15 # for converting link weight to width
nwindow = 600 # for smoothing of bounding box in the time dimension
xymargin = 0.07 # fraction of margins to the (non-strict) bounding box
niter = 2 # how long to iterate the spring algorithm
clock_x = 0.96 # setting dimensions of the clock
clock_y = 0.055
clock_radius = 0.05
nframes = 10 # number of frames per round 4 sec = 4 * 25 fps = 100
dt = 20 # time (in seconds) between frames
fpm = 60 // dt # frames per minute
fph = fpm * 60 * nframes # frames per hour
fpd = 12 * fph # frames per 12h
frame0 = int(fph * 8.5) # 8:30 AM = time of initial frame
decay_fac = 0.9 # for edge weights
frame_decay_fac = decay_fac ** (1.0 / nframes)
weight_threshold = 0.1
node_ids = {} # translating between the file and internal ID numbers. These should rather be replaced byt graph meta data
teacher = {} # indicator function of the proterty of being a teacher
teacher_class_count = {} # counting the times a teached is in connection with a student (to assign a class to them)
node_class = {} # deictionary giving the class of a node
class_nodes = [[] for i in range(10)]
link_weight = 1.0 # all links have this weight in the layout algorithm
group_weight = 0.003 # light links between all nodes of a group to keep them together
colors = [[32/255,142/255,183/255],[10/255,79/255,78/255],[68/255,124/255,254/255],[115/255,42/255,102/255],[202/255,80/255,211/255],[99/255,20/255,175/255],[136/255,125/255,175/255],[63/255,67/255,109/255],[226/255,50/255,9/255],[110/255,57/255,13/255]]
init_pos = [[0,0], [0,40], [10,0], [10,40], [20,0], [20,40], [40,0], [40,40], [50,0], [50,40], [20,20]]
class_list = ['1A', '1B', '2A', '2B', '3A', '3B', '4A', '4B', '5A', '5B', 'Teachers'] # list of the classes in the meta data
# # # # # # # # # # # # # # # # #
# drawing the clock from the current frame index
def draw_clock (iframe, ax):
global aspect
# adding circle
ax.add_artist(matplotlib.patches.Ellipse((clock_x, clock_y), 2.0 * clock_radius * aspect, 2.0 * clock_radius, linewidth = 1.5, edgecolor = 'black', facecolor = 'none', zorder = 1))
# adding minute hand
x = ((iframe + frame0) % fph) / fph
hand_dx = clock_radius * aspect * np.sin(2.0 * np.pi * x)
hand_dy = clock_radius * np.cos(2.0 * np.pi * x)
xx = [clock_x, clock_x + hand_dx]
yy = [clock_y, clock_y + hand_dy]
ax.add_artist(matplotlib.lines.Line2D(xx, yy, linewidth = 1.5, color = 'black', solid_capstyle = 'round', zorder = 1))
# adding hour hand
x = ((iframe + frame0) % fpd) / fpd
hand_dx = 0.5 * clock_radius * aspect * np.sin(2.0 * np.pi * x)
hand_dy = 0.5 * clock_radius * np.cos(2.0 * np.pi * x)
xx = [clock_x, clock_x + hand_dx]
yy = [clock_y, clock_y + hand_dy]
ax.add_artist(matplotlib.lines.Line2D(xx, yy, linewidth = 1.5, color = 'black', solid_capstyle = 'round', zorder = 1))
# # # # # # # # # # # # # # # # #
# calculating the area of a blob (for expanding it)
def polygon_area (xs, ys):
return 0.5 * (np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1)))
# # # # # # # # # # # # # # # # #
# calculating the coordinates of a centroid
def polygon_centroid (yx):
xs = yx[:,0]
ys = yx[:,1]
xy = np.array([xs, ys])
return np.dot(xy + np.roll(xy, 1, axis = 1), xs * np.roll(ys, 1) - np.roll(xs, 1) * ys) / (6 * polygon_area(xs, ys))
# # # # # # # # # # # # # # # # #
# Chaikin's_corner cutting for smoothing the convex hulls
def chaikins_corner_cutting (a, refinements = 1):
for i in range(refinements):
nn = a.shape[0]
qr = np.zeros((2 * nn - 1,2))
for j in range(1,nn):
qr[2*j-2,:] = 0.75 * a[j-1,:] + 0.25 * a[j,:]
qr[2*j-1,:] = 0.25 * a[j-1,:] + 0.75 * a[j,:]
qr[-2,:] = 0.75 * a[-1,:] + 0.25 * a[0,:]
qr[-1,:] = 0.25 * a[-1,:] + 0.75 * a[0,:]
a = qr
return qr
# # # # # # # # # # # # # # # # #
# expanding the size of a blob around its centroid
def expand_path (xy):
c = polygon_centroid(xy)
a = np.empty_like(xy)
for i in range(xy.shape[0]):
for j in range(2):
a[i,j] = (xy[i,j] - c[j]) * expansion_fac + c[j]
return a
# # # # # # # # # # # # # # # # #
# creating a blob roughly (but not certainly) enclosing the students and teacher of a class
def smooth_patch (points):
points = np.array(points)
# get the convex hull of the nodes of a class
hull = ConvexHull(points)
a = np.array([points[hull.vertices,0],points[hull.vertices,1]]).T
b = chaikins_corner_cutting(a, 5) # smoothing
b = expand_path(b)
b = np.append(b,b[0:1,:], axis = 0)
codes = [mpath.Path.LINETO] * b.shape[0] # all elements are LINETO except the first (Bezier curves seems a bit unstable)
codes[0] = mpath.Path.MOVETO
return mpath.Path([(b[i,0],b[i,1]) for i in range(b.shape[0])], codes)
# # # # # # # # # # # # # # # # #
# getting the ID number + some book keeping, if the node is new
def add_node (a,c):
if a not in node_ids:
me = len(node_ids)
node_ids[a] = me
node_class[me] = c
if c == class_list.index('Teachers'):
teacher[me] = True
teacher_class_count[me] = [0] * 10
else:
teacher[me] = False
return me
return node_ids[a]
# # # # # # # # # # # # # # # # #
# main input routine
def read_data (fname):
global n
# reading class attributes and node indices
with open('data/' + name + '.txt') as f:
for l in f:
a = l.strip().split()
cme = class_list.index(a[3])
cyou = class_list.index(a[4])
me = add_node(a[1],cme)
you = add_node(a[2],cyou)
if teacher[me]:
if not teacher[you]:
teacher_class_count[me][cyou] += 1 # counting the times a teacher interacted with the students of a class
if teacher[you]:
if not teacher[me]:
teacher_class_count[you][cme] += 1
n = len(node_ids) # now we know the number of nodes
# resetting the class lables of a teacher
for v in teacher:
if teacher[v]:
node_class[v] = np.argmax(teacher_class_count[v])
for i in range(n):
class_nodes[node_class[i]].append(i)
t0 = -1
last_t = 0
ts = []
w = np.zeros((n,n))
# creating the a weighted network at the resolution of the data
with open('data/' + name + '.txt') as f:
for l in f:
a = l.strip().split()
if len(a) < 5:
break
if t0 < 0:
t0 = int(a[0])
t = 0
else:
t = (int(a[0]) - t0) // dt # t is now in units of timesteps from the first contact
if t > last_t: # if the time step is later, save the old info
now = []
wnow = []
for i in range(1,n):
for j in range(i):
if w[j,i] > weight_threshold: # thresholding the weighte graph to get edges
now.append((j,i))
wnow.append(w[j,i]) # discard links under the threshold
tt = {}
tt['NOW'] = now
tt['WNOW'] = wnow
tt['T'] = last_t
tt['SIZE'] = [1.0] * n
ts.append(tt)
expo = t - last_t # how many steps since the last?
for i in range(1,n):
for j in range(i):
w[j,i] *= decay_fac**expo # decay the edge weights (the neat thing with exponential decay)
last_t = t
me = node_ids[a[1]]
you = node_ids[a[2]]
if me < you:
w[me,you] += 1.0 # adding weight to links
elif you < me:
w[you,me] += 1.0
if len(now) > 0:
tt = {}
tt['NOW'] = now
tt['WNOW'] = wnow
tt['T'] = t
tt['SIZE'] = [1.0] * n
ts.append(tt)
return ts
# # # # # # # # # # # # # # # # #
#get the thickness of links in the intermediate frames
def get_size (g, thick, ii):
global n
s = [node_size_low] * n
w = [link_weight_low] * n
for e in g.edges():
u = g.vertex_index[e.source()]
v = g.vertex_index[e.target()]
x = thick[e] * frame_decay_fac**ii * weight2sizefac
if s[u] < x:
s[u] = x
if s[v] < x:
s[v] = x
x = thick[e] * frame_decay_fac**ii * weight2widthfac
if w[u] < x:
w[u] = x
if w[v] < x:
w[v] = x
return s, w
# # # # # # # # # # # # # # # # #
# mixing the colors for cross-class links
def get_color (c1, c2):
return [0.5*(c1[0]+c2[0]),0.5*(c1[1]+c2[1]),0.5*(c1[2]+c2[2])]
# # # # # # # # # # # # # # # # #
# getting the coordinates from the pos objects
def get_pos (g, pos):
global n
x = np.zeros(n)
y = np.zeros(n)
for i in range(n):
x[i] = pos[g.vertex(i)].a[0]
y[i] = pos[g.vertex(i)].a[1]
return x,y
# # # # # # # # # # # # # # # # #
# finding an initial configuration by first separating the classes, then relaxing the network
def init_layout (g):
global n
pos = g.new_vertex_property('vector<double>')
a = np.zeros((2,n))
for i in range(2):
for j in range(n):
a[i,j] = init_pos[node_class[j]][i] + 9 * np.random.random()
pos.set_2d_array(a) # relaxing to convergence
return pos
# # # # # # # # # # # # # # # # #
# main routin including the drawing
if __name__ == "__main__":
global n, aspect
if len(argv) != 2:
print('isage: python3 anime.py [file name]')
exit()
name = argv[1]
# read and construct network
ts = read_data(name)
g = gt.Graph(directed = False)
g.add_vertex(n = n)
# init layout
pos = init_layout(g)
# edge weights
ew = g.new_edge_property('double')
thick = g.new_edge_property('double')
#classes = g.new_vertex_property('int')
for u in range(1,n):
for v in range(u):
if node_class[u] == node_class[v]:
e = g.add_edge(u,v)
ew[e] = group_weight
# get all the parameters of all the frames
frames = []
node_colors = {i:colors[node_class[i]] for i in range(n)}
gt.sfdp_layout(g, pos = pos, eweight = ew, C = layout_c, K = layout_k, p = layout_p)
nts = len(ts)
with alive_bar(nts, bar = 'blocks', spinner = 'vertical') as bar: # create the layout
for it in range(nts):
tt = ts[it]
sizes = tt['SIZE']
to_remove = []
for e in g.edges():
u = g.vertex_index[e.source()]
v = g.vertex_index[e.target()]
if node_class[u] != node_class[v]:
to_remove.append(e)
else:
ew[e] = group_weight
for e in to_remove:
g.remove_edge(e)
for j, (u,v) in enumerate(tt['NOW']):
if node_class[u] != node_class[v]:
e = g.add_edge(u,v)
else:
e = g.edge(u,v)
ew[e] = link_weight
thick[e] = tt['WNOW'][j]
for j in range(nframes): # nframes frames per time step of the data
frame = {}
gt.sfdp_layout(g, pos = pos, eweight = ew, C = layout_c, K = layout_k, p = layout_p, init_step = layout_step, max_iter = niter) # the layout algorithm, note that the weight here only takes two values (as opposed to the continous weights of the network construction and lines)
frame['X'], frame['Y'] = get_pos(g, pos)
frame['COLOR'] = node_colors
frame['E1'] = []
frame['E2'] = []
frame['THICK'] = []
frame['ECOLOR'] = []
for e in g.edges():
if 2 * ew[e] > link_weight: # i.e. if the link is declared a link in the data (as opposed to the dummy links keeping the classes together)
u = g.vertex_index[e.source()]
v = g.vertex_index[e.target()]
frame['E1'].append(u)
frame['E2'].append(v)
x = weight2widthfac * thick[e] * frame_decay_fac**j
frame['THICK'].append(max(x,link_weight_low))
frame['ECOLOR'].append(get_color(node_colors[u],node_colors[v]))
frame['SIZE'], frame['WIDTH'] = get_size(g,thick,j)
frames.append(frame)
bar()
# find bounding box for the film, using a moving average over nwindow frames
lims = {'XMIN':[],'YMIN':[],'XMAX':[],'YMAX':[]}
for i, frame in enumerate(frames):
lims['XMIN'].append(np.amin(frame['X']))
lims['XMAX'].append(np.amax(frame['X']))
lims['YMIN'].append(np.amin(frame['Y']))
lims['YMAX'].append(np.amax(frame['Y']))
for k in lims:
lims[k] = uniform_filter1d(lims[k], nwindow, mode = 'mirror') # smoothing the movement of the focus
# do the actual plotting
nf = len(frames)
with alive_bar(nf, bar = 'bubbles', spinner = 'arrows') as bar:
for i in range(nf):
frame = frames[i]
fig = matplotlib.pyplot.figure()
xlen,ylen = matplotlib.pyplot.rcParams.get('figure.figsize')
aspect = ylen / xlen
ax = fig.add_subplot(111)
ax.set_xlim(-xymargin, 1.0 + xymargin)
ax.set_ylim(-xymargin, 1.0 + xymargin)
matplotlib.pyplot.axis('off')
# draw vertices
cpoints = [[] for j in range(10)]
lx = lims['XMAX'][i] - lims['XMIN'][i]
ly = lims['YMAX'][i] - lims['YMIN'][i]
for j in range(n):
x1 = (frame['X'][j] - lims['XMIN'][i]) / lx # rescale coordinates
y1 = (frame['Y'][j] - lims['YMIN'][i]) / ly
cpoints[node_class[j]].append((x1,y1))
rad = size_fac * frame['SIZE'][j]
if teacher[j]:
dx = recfac * rad
dy = dx / aspect
ax.add_artist(mpatches.Rectangle((x1 - 0.5 * dx, y1 - 0.5 * dy), dx, dy, linewidth = frame['WIDTH'][j], edgecolor=frame['COLOR'][j], facecolor = frame['COLOR'][j], zorder = 20))
else:
ax.add_artist(mpatches.Ellipse((x1,y1), rad, rad / aspect, linewidth = frame['WIDTH'][j], edgecolor = frame['COLOR'][j], facecolor = 'white', zorder = 20))
# draw patches
for j in range(10):
ax.add_patch(mpatches.PathPatch(smooth_patch(cpoints[j]), facecolor = colors[j], linestyle = '', alpha = 0.4, zorder = 3))
# draw edges
if 'E1' in frame:
for j in range(len(frame['E1'])):
v = frame['E1'][j]
x1 = (frame['X'][v] - lims['XMIN'][i]) / lx
y1 = (frame['Y'][v] - lims['YMIN'][i]) / ly
v = frame['E2'][j]
x2 = (frame['X'][v] - lims['XMIN'][i]) / lx
y2 = (frame['Y'][v] - lims['YMIN'][i]) / ly
lw = frame['THICK'][j]
lc = frame['ECOLOR'][j]
ax.add_artist(matplotlib.lines.Line2D([x1,x2],[y1,y2], linewidth = lw, color = lc, solid_capstyle = 'butt', zorder = 5))
# draw clock
draw_clock(i, ax)
# background color = white
background = mpatches.Rectangle((-0.05, -0.05), 1.1, 1.1, edgecolor = 'none', facecolor = 'white', zorder = 0)
ax.add_artist(background)
# saving the output to a directory
plt.savefig('frames/' + name + '/%05d' % i + '.png', format = 'png', bbox_inches = 'tight', bbox_extra_artists = [background], dpi = 300)
plt.close()
bar()
# # # # # # # # # # # # # # # # #