-
Notifications
You must be signed in to change notification settings - Fork 73
/
timelapsetool.py
2222 lines (1935 loc) · 126 KB
/
timelapsetool.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
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
Dependencies:
Enthought Tool Suite (for Mayavi2): http://www.lfd.uci.edu/~gohlke/pythonlibs/#ets
VTK (5.10+): http://www.lfd.uci.edu/~gohlke/pythonlibs/#vtk
NetworkX (1.7+): http://www.lfd.uci.edu/~gohlke/pythonlibs/#networkx
NumPy-MKL (1.71+): http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy
configobj (required by Enthought): https://pypi.python.org/pypi/configobj
'''
import wx
# Looks like wx.combo becomes wx.adv in wx 2.9+ or Phoenix? http://comments.gmane.org/gmane.comp.python.wxpython.devel/5635
from wx.combo import OwnerDrawnComboBox as ComboBox
from wx.lib.scrolledpanel import ScrolledPanel
import networkx as nx
import numpy as np
from operator import itemgetter
import glayout
import logging
import time
import sortbin
import imagetools
from guiutils import get_main_frame_or_none
from dbconnect import DBConnect, image_key_columns, object_key_columns
from properties import Properties
from cpatool import CPATool
import tableviewer
import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
# traits imports
from traits.api import HasTraits, Int, Instance, on_trait_change
from traitsui.api import View, Item, HSplit, Group
# mayavi imports
from mayavi import mlab
from mayavi.core.ui.api import MlabSceneModel, SceneEditor
from mayavi.core.ui.mayavi_scene import MayaviScene
from tvtk.pyface.scene import Scene
from tvtk.api import tvtk
# Colormap names from an error msg (http://www.mail-archive.com/mayavi-users@lists.sourceforge.net/msg00615.html)
# TODO(?): Find a better way to captures these names
all_colormaps = ['Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'Dark2',
'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn',
'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu',
'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy',
'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2',
'Set3', 'Spectral', 'YlGn', 'YlGnBu', 'YlOrBr',
'YlOrRd', 'autumn', 'binary', 'black-white', 'blue-red',
'bone', 'cool', 'copper', 'file', 'flag', 'gist_earth',
'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow',
'gist_stern', 'gist_yarg', 'gray', 'hot', 'hsv', 'jet',
'pink', 'prism', 'spectral', 'spring', 'summer','winter']
all_colormaps.sort()
required_fields = ['series_id', 'group_id', 'timepoint_id','object_tracking_label']
db = DBConnect.getInstance()
props = Properties.getInstance()
TRACKING_MODULE_NAME = "TrackObjects"
OTHER_METRICS = "Other derived metrics..."
L_YCOORD = "Y_2"
L_TCOORD = "T_2"
T_XCOORD = "X_3"
T_YCOORD = "Y_3"
T_TCOORD = "T_3"
SCALAR_VAL = "S"
VISIBLE = "VISIBLE"
track_attributes = ["label","x","y","t",SCALAR_VAL,"f"]
SUBGRAPH_ID = "Subgraph"
VISIBLE_SUFFIX = "_VISIBLE"
METRIC_BC = "BetweennessCentrality"
METRIC_BC_VISIBLE = METRIC_BC + VISIBLE_SUFFIX
METRIC_SINGLETONS = "Singletons"
METRIC_SINGLETONS_VISIBLE = METRIC_SINGLETONS + VISIBLE_SUFFIX
METRIC_NODESWITHINDIST = "NodesWithinDistanceCutoff"
METRIC_NODESWITHINDIST_VISIBLE = METRIC_NODESWITHINDIST + VISIBLE_SUFFIX
METRIC_LOOPS = "Loops"
METRIC_LOOPS_VISIBLE = METRIC_LOOPS + VISIBLE_SUFFIX
BRANCH_NODES = "Branch_node"
END_NODES = "End_node"
START_NODES = "Start_node"
TERMINAL_NODES = "Terminal_node"
IS_REMOVED = "Is_removed"
EDITED_TABLE_SUFFIX = "_Edits"
ORIGINAL_TRACK = "Original"
def add_props_field(props):
# Temp declarations; these will be retrieved from the properties file directly, eventually
props.series_id = ["Image_Group_Number"]
#props.series_id = ["Image_Metadata_Plate"]
props.group_id = "Image_Group_Number"
props.timepoint_id = "Image_Group_Index"
obj = get_object_name()
# TODO: Allow for selection of tracking labels, since there may be multiple objects tracked in different ways. Right now, just pick the first one.
# TODO: Allow for selection of parent image/object fields, since there may be multiple tracked objects. Right now, just pick the first one.
if props.db_type == 'sqlite':
query = "PRAGMA table_info(%s)"%(props.object_table)
all_fields = [item[1] for item in db.execute(query)]
else:
query = "SELECT column_name FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'"%(props.db_name, props.object_table)
all_fields = [item[0] for item in db.execute(query)]
props.object_tracking_label = [item for item in all_fields if item.find("_".join((obj,TRACKING_MODULE_NAME,'Label'))) != -1][0]
props.parent_fields = [ [item for item in all_fields if item.find("_".join((obj,TRACKING_MODULE_NAME,'ParentImageNumber'))) != -1][0],
[item for item in all_fields if item.find("_".join((obj,TRACKING_MODULE_NAME,'ParentObjectNumber'))) != -1][0] ]
table_prefix = props.image_table.split("Per_Image")[0]
props.relationship_table = table_prefix + "Per_Relationships"
props.relationshiptypes_table = table_prefix + "Per_RelationshipTypes"
props.relationships_view = table_prefix + "Per_RelationshipsView"
return props
def retrieve_datasets():
series_list = ",".join(props.series_id)
query = "SELECT %s FROM %s GROUP BY %s"%(series_list,props.image_table,series_list)
all_datasets = [x[0] for x in db.execute(query)]
return all_datasets
def get_object_name():
return props.cell_x_loc.split('_Location_Center')[0]
def get_edited_relationship_tablename():
return props.relationship_table+EDITED_TABLE_SUFFIX
def is_LAP_tracking_data():
# If the data is LAP-based, then additional button(s) show up
LAP_field = "Kalman"
if props.db_type == 'sqlite':
query = "PRAGMA table_info(%s)"%(props.object_table)
return(len([item[1] for item in db.execute(query) if item[1].find(LAP_field) != -1]) > 0)
else:
query = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND COLUMN_NAME REGEXP '_Kalman_'"%(props.db_name, props.object_table)
return(len(db.execute(query)) > 0)
def where_stmt_for_tracked_objects(obj, group_id, selected_dataset):
# We want relationships that are cover the following:
# - Are parent/child, e.g, exclude neighborhood
# - Share the same parent/child object, e.g, exclude primary/secondary/tertiary, some cases of neighborhood
# - Are across-frame, e.g, exclude neighborhood, primary/secondary/tertiary
# - For the selected dataset
stmt = ("LOWER(relationship) = 'parent'",
"AND object_name1 = '%s'"%(obj),
"AND object_name1 = object_name2",
"AND image_number1 != image_number2",
"AND i1.%s = %d"%(group_id,selected_dataset)
)
return stmt
def create_update_relationship_table():
edited_relnship_table = get_edited_relationship_tablename()
is_table = 0
if props.db_type == 'sqlite':
query = "PRAGMA table_info(%s)"%(edited_relnship_table)
else:
query = "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'"%(props.db_name, edited_relnship_table)
is_table = len(db.execute(query)) > 0
reln_cols = ['image_number1', 'object_number1', 'image_number2', 'object_number2']
if not is_table:
logging.info("Creating custom relationship table...")
# If the table doens't exist, create it
obj = get_object_name()
# From http://stackoverflow.com/questions/12730390/copy-table-structure-to-new-table-in-sqlite3. Seems to work in MySQL too
query = ("CREATE TABLE %s AS"%(edited_relnship_table),
"SELECT %s"%(",".join(reln_cols)),
"FROM %s"%(props.relationships_view),
"WHERE 0")
query = " ".join(query)
db.execute(query)
query = "SELECT DISTINCT(%s) FROM %s"%(props.group_id, props.image_table)
all_datasets = [_[0] for _ in db.execute(query)]
for selected_dataset in all_datasets:
query = ("INSERT INTO %s"%(edited_relnship_table),
"SELECT %s"%(",".join(reln_cols)),
"FROM %s r"%(props.relationships_view),
"JOIN %s i1"%(props.image_table),
"ON r.image_number1 = i1.%s"%(props.image_id),
"WHERE") + \
where_stmt_for_tracked_objects(obj,props.group_id,int(selected_dataset))
query = " ".join(query)
db.execute(query)
# Add the grouping ID and the default track column
if props.db_type == 'sqlite':
query = "ALTER TABLE %s ADD COLUMN %s INT"%(edited_relnship_table, props.group_id)
db.execute(query)
query = "ALTER TABLE %s ADD COLUMN %s INT DEFAULT 1"%(edited_relnship_table, ORIGINAL_TRACK)
db.execute(query)
else:
query = "ALTER TABLE %s ADD %s INT, %s INT DEFAULT 1"%(edited_relnship_table, props.group_id, ORIGINAL_TRACK)
db.execute(query)
if props.db_type == 'sqlite':
# From http://stackoverflow.com/questions/19270259/update-join-sqlite
query = ("UPDATE %s"%(edited_relnship_table),
"SET %s ="%(props.group_id),
"(SELECT %s.%s"%(props.image_table,props.group_id),
"FROM %s"%(props.image_table),
"WHERE %s.image_number1 = %s.%s)"%(edited_relnship_table,props.image_table,props.image_id))
else:
# From http://dba.stackexchange.com/questions/21152/how-to-update-one-table-based-on-another-tables-values-on-the-fly
query = ("UPDATE %s"%(edited_relnship_table),
"INNER JOIN %s"%(props.image_table),
"ON %s.image_number1 = %s.%s"%(edited_relnship_table, props.image_table, props.image_id),
"SET %s.%s = %s.%s"%(edited_relnship_table, props.group_id, props.image_table, props.group_id))
query = " ".join(query)
db.execute(query)
# Spent the better part of a day figuring out that a 'commit' was needed here
db.Commit()
relationship_table_cols = []
# Retrieve column names
if props.db_type == 'sqlite':
query = "PRAGMA table_info(%s)"%(edited_relnship_table)
relationship_table_cols = [_[1] for _ in db.execute(query)]
else:
query = "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'"%(props.db_name, edited_relnship_table)
relationship_table_cols = db.execute(query)
query = "SELECT * FROM %s"%edited_relnship_table
relationship_table_data = [_ for _ in db.execute(query)]
defined_track_cols = list(set(relationship_table_cols).difference(set(reln_cols+[props.group_id])))
return relationship_table_cols, defined_track_cols, relationship_table_data
def obtain_tracking_data(selected_dataset, selected_dataset_track, selected_measurement, selected_filter):
def parse_dataset_selection(s):
return [x.strip() for x in s.split(',') if x.strip() is not '']
selection_list = parse_dataset_selection(selected_dataset)
dataset_clause = " AND ".join(["%s = '%s'"%(x[0], x[1]) for x in zip([props.image_table+"."+_ for _ in props.series_id], selection_list)])
columns_to_retrieve = list(object_key_columns(props.object_table)) # Node IDs
columns_to_retrieve += [props.object_table+"."+_ for _ in props.parent_fields] # Parent node IDs
columns_to_retrieve += [props.object_table+"."+props.object_tracking_label] # Label assigned by TrackObjects
columns_to_retrieve += [props.object_table+"."+props.cell_x_loc, props.object_table+"."+props.cell_y_loc] # x,y coordinates
columns_to_retrieve += [props.image_table+"."+props.timepoint_id] # Timepoint/frame
columns_to_retrieve += [props.object_table+"."+selected_measurement if selected_measurement is not None else 'NULL'] # Measured feature, insert NULL as placeholder if derived
columns_to_retrieve += [" AND ".join(selected_filter)] if selected_filter is not None else ['1'] # Filter
query = ["SELECT %s"%(",".join(columns_to_retrieve))]
query.append("FROM %s, %s"%(props.image_table, props.object_table))
query.append("WHERE %s = %s AND %s"%(props.image_table+"."+props.image_id, props.object_table+"."+props.image_id, dataset_clause))
query.append("ORDER BY %s, %s"%(props.object_tracking_label, props.timepoint_id))
data = db.execute(" ".join(query))
columns = [props.object_tracking_label, props.image_id, props.object_id, props.cell_x_loc, props.cell_y_loc, props.timepoint_id, "Filter", props.parent_fields]
return columns, data
################################################################################
class MeasurementFilter(wx.Panel):
'''
Widget for creating lists of filters
'''
def __init__(self, parent, allow_delete=True, **kwargs):
wx.Panel.__init__(self, parent, **kwargs)
self.measurement_choices = db.GetColumnNames(props.object_table)
self.colChoice = ComboBox(self, choices=self.measurement_choices, size=(-1,-1), style=wx.CB_READONLY)
self.colChoice.Select(0)
self.colChoice.Bind(wx.EVT_COMBOBOX, self.on_select_column)
self.comparatorChoice = ComboBox(self, size=(-1,-1))
self.update_comparator_choice()
self.valueField = wx.ComboBox(self, -1, value='')
if allow_delete:
self.minus_button = wx.Button(self, -1, label='-', size=(30,-1))
self.minus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_remove_filter(event,self))
self.plus_button = wx.Button(self, -1, label='+', size=(30,-1))
self.plus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_add_filter(event,self))
colSizer = wx.BoxSizer(wx.HORIZONTAL)
colSizer.Add(self.colChoice, 1, wx.EXPAND)
colSizer.AddSpacer((5,-1))
colSizer.Add(self.comparatorChoice, 1, wx.EXPAND)
colSizer.AddSpacer((5,-1))
colSizer.Add(self.valueField, 1, wx.EXPAND)
colSizer.AddSpacer((5,-1))
colSizer.Add(self.plus_button, 0, wx.EXPAND)
colSizer.AddSpacer((5,-1))
colSizer.Add(self.minus_button if allow_delete else wx.StaticText(self,-1,size=(30,-1)), 0, wx.EXPAND)
self.SetSizerAndFit(colSizer)
def on_select_column(self, evt):
self.update_comparator_choice()
self.update_value_choice()
def _get_column_type(self):
return db.GetColumnTypes(props.object_table)[self.colChoice.GetSelection()]
def update_comparator_choice(self):
coltype = self._get_column_type()
comparators = []
if coltype in [str, unicode]:
comparators = ['=', '!=', 'REGEXP', 'IS', 'IS NOT', 'IS NULL']
if coltype in [int, float, long]:
comparators = ['=', '!=', '<', '>', '<=', '>=', 'IS', 'IS NOT', 'IS NULL']
self.comparatorChoice.SetItems(comparators)
self.comparatorChoice.Select(0)
def update_value_choice(self):
column = self.colChoice.Value
column_type = db.GetColumnTypes(props.object_table)[self.colChoice.GetSelection()]
vals = []
if column_type == str:# or coltype == int or coltype == long:
res = db.execute('SELECT DISTINCT %s FROM %s ORDER BY %s'%(column, table, column))
vals = [str(row[0]) for row in res]
self.valueField.SetItems(vals)
################################################################################
class FilterPanel(ScrolledPanel):
'''
Panel for measurement filtering.
'''
def __init__(self, parent, **kwargs):
ScrolledPanel.__init__(self, parent, **kwargs)
self.panel_sizer = wx.BoxSizer( wx.VERTICAL )
self.filters = []
filt = MeasurementFilter(self, False)
self.panel_sizer.Add(filt, 0, wx.EXPAND)
self.filters.append(filt)
self.SetSizer(self.panel_sizer)
self.SetAutoLayout(1)
self.SetupScrolling(False,True)
self.Disable()
def on_add_filter(self,event,selected_filter):
self.filters.append(MeasurementFilter(self, True))
self.panel_sizer.Add(self.filters[-1], 0, wx.EXPAND|wx.BOTTOM|wx.LEFT|wx.RIGHT, 5)
self.SetupScrolling(False,True)
self.panel_sizer.SetMinSize(self.panel_sizer.GetMinSize())
self.SetSizerAndFit(self.panel_sizer)
self.SetAutoLayout(1)
self.Refresh()
self.Layout()
def on_remove_filter(self,event,selected_filter):
i = self.filters.index(selected_filter)
self.filters.remove(selected_filter)
self.panel_sizer.Remove(selected_filter)
selected_filter.Destroy()
self.SetupScrolling(False,len(self.filters) < 3 )
self.Refresh()
self.Layout()
################################################################################
class TimeLapseControlPanel(wx.Panel):
'''
A panel with controls for selecting the data for a visual
Some helpful tips on using sizers for layout: http://zetcode.com/wxpython/layout/
'''
def __init__(self, parent, **kwargs):
wx.Panel.__init__(self, parent, **kwargs)
# Get names of data sets
all_datasets = retrieve_datasets()
# Capture if LAP data is being used
self.isLAP = is_LAP_tracking_data()
# Get names of fields
measurements = db.GetColumnNames(props.object_table)
coltypes = db.GetColumnTypes(props.object_table)
fields = [m for m,t in zip(measurements, coltypes) if t in [float, int, long]]
self.dataset_measurement_choices = fields
sizer = wx.BoxSizer(wx.VERTICAL)
# Define widgets
self.dataset_choice = ComboBox(self, -1, choices=[str(_) for _ in all_datasets], size=(200,-1), style=wx.CB_READONLY)
self.dataset_choice.Select(0)
self.dataset_choice.SetHelpText("Select the time-lapse data set to visualize.")
self.track_collection = ComboBox(self, -1, choices=[ORIGINAL_TRACK], size=(200,-1), style=wx.CB_READONLY)
self.track_collection.Select(0)
self.track_collection.SetHelpText("Select the identifier specifying the tracked object relationships in the current data set.")
self.track_collection.Disable()
self.dataset_measurement_choice = ComboBox(self, -1, choices=self.dataset_measurement_choices, style=wx.CB_READONLY)
self.dataset_measurement_choice.Select(0)
self.dataset_measurement_choice.SetHelpText("Select the per-%s measurement to visualize the data with. The lineages and (xyt) trajectories will be color-coded by this measurement."%props.object_name[0])
self.colormap_choice = ComboBox(self, -1, choices=all_colormaps, style=wx.CB_READONLY)
self.colormap_choice.SetStringSelection("jet")
self.colormap_choice.SetHelpText("Select the colormap to use for color-coding the data.")
self.trajectory_selection_button = wx.Button(self, -1, "Select Tracks to Visualize...")
self.trajectory_selection_button.SetHelpText("Select the trajectories to show or hide in both panels.")
if self.isLAP:
self.trajectory_diagnosis_toggle = wx.ToggleButton(self, -1, "Show LAP Diagnostic Graphs")
self.trajectory_diagnosis_toggle.SetHelpText("If you have tracking data generated by the LAP method, a new box will open with diagnostic graphs indicating goodness of your settings.")
self.update_plot_color_button = wx.Button(self, -1, "Update Color")
self.update_plot_color_button.SetHelpText("Press this button after making selections to update the panels.")
self.help_button = wx.ContextHelpButton(self)
self.derived_measurement_choice = ComboBox(self, -1, style=wx.CB_READONLY)
self.derived_measurement_choice.SetHelpText("Select the derived measurement to visualize the data with.")
self.derived_measurement_choice.Disable()
self.distance_cutoff_value = wx.SpinCtrl(self, -1, value = "4", style=wx.SP_ARROW_KEYS, min=0, initial=4)
self.distance_cutoff_value.SetHelpText("Enter the number of nodes from a branch that a terminus must be found in order to be selected as a candidate for pruning.")
self.distance_cutoff_value.Disable()
self.bc_branch_ratio_value = wx.TextCtrl(self, -1, value = "0.5", style=wx.TE_PROCESS_ENTER)
self.bc_branch_ratio_value.SetHelpText("Enter the betweeness centrality fraction that a branch node must be in order be selected as a candidate for pruning.")
self.bc_branch_ratio_value.Disable()
self.preview_prune_button = wx.ToggleButton(self, -1, "Preview Pruned Branches")
self.preview_prune_button.SetHelpText("Redraws the graph with the pruned nodes removed.")
self.preview_prune_button.Disable()
self.add_pruning_to_edits_button = wx.Button(self, -1, "Add Pruning to Edits")
self.add_pruning_to_edits_button.SetHelpText("Adds the pruned graph to the list of edits.")
self.add_pruning_to_edits_button.Disable()
self.save_edited_tracks = wx.Button(self, -1, "Save Edited Tracks...")
self.save_edited_tracks.SetHelpText("Saves the edited graph as a new index into the relationship table.")
self.save_edited_tracks.Disable()
self.all_derived_measurements_widgets = [self.derived_measurement_choice,
self.distance_cutoff_value,
self.bc_branch_ratio_value,
self.preview_prune_button,
self.add_pruning_to_edits_button,
self.save_edited_tracks]
# Arrange widgets
# Row #1: Dataset drop-down + track selection button
sz = wx.BoxSizer(wx.HORIZONTAL)
sz.Add(wx.StaticText(self, -1, "Data Source:"), 0, wx.TOP, 4)
sz.AddSpacer((4,-1))
sz.Add(self.dataset_choice, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(wx.StaticText(self, -1, "Data Tracks:"), 0, wx.TOP, 4)
sz.Add(self.track_collection, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(self.trajectory_selection_button)
if self.isLAP:
sz.AddSpacer((4,-1))
sz.Add(self.trajectory_diagnosis_toggle)
sizer.Add(sz, 1, wx.EXPAND)
sizer.AddSpacer((-1,2))
# Row #2: Data measurement color selection, colormap, update button
sz = wx.BoxSizer(wx.HORIZONTAL)
sz.Add(wx.StaticText(self, -1, "Data Measurements:"), 0, wx.TOP, 4)
sz.AddSpacer((4,-1))
sz.Add(self.dataset_measurement_choice, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(wx.StaticText(self, -1, "Colormap:"), 0, wx.TOP, 4)
sz.AddSpacer((4,-1))
sz.Add(self.colormap_choice, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(self.update_plot_color_button)
sz.AddSpacer((4,-1))
sz.Add(self.help_button)
sizer.Add(sz, 1, wx.EXPAND)
sizer.AddSpacer((-1,2))
# Row #3: Derived measurement color selection
sz = wx.BoxSizer(wx.HORIZONTAL)
sz.Add(wx.StaticText(self, -1, "Derived Metrics:"), 0, wx.TOP, 4)
sz.AddSpacer((4,-1))
sz.Add(self.derived_measurement_choice, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(wx.StaticText(self, -1, "Distance cutoff:"), 0, wx.TOP, 4)
sz.Add(self.distance_cutoff_value, 1, wx.EXPAND)
sz.Add(wx.StaticText(self, -1, "Betweeness centrality cutoff:"), 0, wx.TOP, 4)
sz.Add(self.bc_branch_ratio_value, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(self.preview_prune_button, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(self.add_pruning_to_edits_button, 1, wx.EXPAND)
sz.AddSpacer((4,-1))
sz.Add(self.save_edited_tracks, 1, wx.EXPAND)
sizer.Add(sz, 1, wx.EXPAND)
sizer.AddSpacer((-1,2))
# Row #4: Measurement filter selection
sz = wx.BoxSizer(wx.HORIZONTAL)
self.enable_filtering_checkbox = wx.CheckBox(self, -1, label="Enable filtering")
self.enable_filtering_checkbox.SetValue(0)
sz.Add(self.enable_filtering_checkbox, 0, wx.TOP, 4)
sz.AddSpacer((4,-1))
self.filter_panel = FilterPanel(self)
sz.Add(self.filter_panel,1, wx.TOP, 4)
sz.Layout()
sizer.Add(sz, 1, wx.EXPAND)
sizer.AddSpacer((-1,2))
sizer.Layout()
self.SetSizer(sizer)
self.Layout()
self.Show(True)
################################################################################
class MayaviView(HasTraits):
""" Create a mayavi scene"""
lineage_scene = Instance(MlabSceneModel, ())
trajectory_scene = Instance(MlabSceneModel, ())
dataset = Int(0)
# The layout of the dialog created
view = View(HSplit(Group(Item('trajectory_scene',
#editor = SceneEditor(scene_class = Scene),
editor = SceneEditor(scene_class=MayaviScene),
resizable=True, show_label=False)),
Group(Item('lineage_scene',
editor = SceneEditor(scene_class = Scene),
#editor = SceneEditor(scene_class=MayaviScene),
resizable=True, show_label=False))),
resizable=True)
def __init__(self, parent):
HasTraits.__init__(self)
self.parent = parent
self.axes_opacity = 0.25
self.lineage_figure = self.lineage_scene.mlab.gcf()
self.trajectory_figure = self.trajectory_scene.mlab.gcf()
# Apparently, I cannot use mlab.clf to clear the figure without disconnecting the picker
# So remove the children to get the same effect.
# See: http://stackoverflow.com/questions/23435986/mayavi-help-in-resetting-mouse-picker-and-connecting-wx-event-to-on-trait-chan
# Note that the respondent says I will still need to reattach the picker, but that doesn't seem to be the case here...
def clear_figures(self, scene):
for child in scene.mayavi_scene.children:
child.remove()
@on_trait_change('lineage_scene.activated')
def activate_lineage_scene(self):
# An trajectory picker object is created to trigger an event when a trajectory is picked.
# Can press 'p' to get UI on current pick
#
# Helpful pages re: pickers
# https://gist.github.com/syamajala/8804396
# http://sourceforge.net/p/mayavi/mailman/message/27239432/ (not identical problem)
picker = self.lineage_scene.mayavi_scene.on_mouse_pick(self.on_pick_lineage)
picker.tolerance = 0.01
# Why is this here? Well, apparently the axes need to be oriented to a camera, which needs the view to be opened first.
# See http://en.it-usenet.org/thread/15952/8170/
mlab.axes(self.lineage_node_collection,
xlabel='T', ylabel='',
extent = self.lineage_extent,
opacity = self.axes_opacity,
x_axis_visibility=True, y_axis_visibility=False, z_axis_visibility=False)
# Constrain view to 2D
self.lineage_scene.interactor.interactor_style = tvtk.InteractorStyleImage()
self.lineage_scene.reset_zoom()
# Add object label text to the left
# Why is this here? The text module needs to have a scene opened to work
# http://enthought-dev.117412.n3.nabble.com/How-to-clear-AttributeError-NoneType-object-has-no-attribute-active-camera-td2181947.html
text_scale_factor = self.lineage_node_scale_factor/1.0
t = nx.get_node_attributes(self.directed_graph,L_TCOORD)
y = nx.get_node_attributes(self.directed_graph,L_YCOORD)
start_nodes = {}
for key,subgraph in self.connected_nodes.items():
start_nodes[key] = [_[0] for _ in nx.get_node_attributes(subgraph,START_NODES).items() if _[1]]
self.lineage_label_collection = dict(zip(self.connected_nodes.keys(),
[mlab.text3d(t[start_nodes[key][0]]-0.75*self.lineage_temporal_scaling,
y[start_nodes[key][0]],
0,
str(key),
scale = text_scale_factor,
figure = self.lineage_scene.mayavi_scene)
for key,subgraph in self.connected_nodes.items()]))
#self.dataset = int(self.parent.selected_dataset)
@on_trait_change('trajectory_scene.activated')
def activate_trajectory_scene(self):
picker = self.trajectory_scene.mayavi_scene.on_mouse_pick(self.on_pick_trajectory)
picker.tolerance = 0.01
# TODO: Incorporate image dimensions into axes viz
# Get image dimensions
if props.db_type == 'sqlite':
query = "PRAGMA table_info(%s)"%(props.image_table)
w_col = [_[1] for _ in db.execute(query) if _[1].find('Image_Width') >= 0][0]
h_col = [_[1] for _ in db.execute(query) if _[1].find('Image_Height') >= 0][0]
else:
query = "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND COLUMN_NAME REGEXP 'Image_Width' LIMIT 1"%(props.db_name, props.image_table)
w_col = db.execute(query)[0][0]
query = "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND COLUMN_NAME REGEXP 'Image_Height' LIMIT 1"%(props.db_name, props.image_table)
h_col = db.execute(query)[0][0]
query = "SELECT %s FROM %s LIMIT 1"%(w_col, props.image_table)
self.parent.image_x_dims = db.execute(query)[0][0]
query = "SELECT %s FROM %s LIMIT 1"%(h_col, props.image_table)
self.parent.image_y_dims = db.execute(query)[0][0]
ax = mlab.axes(self.trajectory_line_source,
xlabel='X', ylabel='Y',zlabel='T',
#extent = (1,self.parent.image_x_dims,1,self.parent.image_y_dims,self.parent.start_frame,self.parent.end_frame),
opacity = self.axes_opacity,
x_axis_visibility=True, y_axis_visibility=True, z_axis_visibility=True)
# Set axes to MATLAB's default 3d view
mlab.view(azimuth = 322.5,elevation = 30.0,
figure = self.trajectory_scene.mayavi_scene)
# Add object label text at end of trajectory
text_scale_factor = self.trajectory_node_scale_factor*5
end_nodes = {}
for (key,subgraph) in self.connected_nodes.items():
end_nodes[key] = [_[0] for _ in nx.get_node_attributes(subgraph,END_NODES).items() if _[1]][0]
self.trajectory_label_collection = dict(zip(self.connected_nodes.keys(),
[mlab.text3d(subgraph.node[end_nodes[key]]["x"],
subgraph.node[end_nodes[key]]["y"],
subgraph.node[end_nodes[key]]["t"]*self.trajectory_temporal_scaling,
str(key),
scale = text_scale_factor,
name = str(key),
figure = self.trajectory_scene.mayavi_scene)
for (key,subgraph) in self.connected_nodes.items()]))
#self.dataset = int(self.parent.selected_dataset)
self.trajectory_scene.reset_zoom()
def on_pick_lineage(self, picker):
""" Lineage picker callback: this gets called upon pick events.
"""
picked_graph_node = None
picked_lineage_coords = None
picked_trajectory_coords = None
if picker.actor in self.lineage_node_collection.actor.actors + self.lineage_edge_collection.actor.actors:
# TODO: Figure what the difference is between node_collection and edge_collection being clicked on.
# Retrieve to which point corresponds the picked point.
# Here, we grab the points describing the individual glyph, to figure
# out how many points are in an individual glyph.
n_glyph = self.lineage_node_collection.glyph.glyph_source.glyph_source.output.points.to_array().shape[0]
# Find which data point corresponds to the point picked:
# we have to account for the fact that each data point is
# represented by a glyph with several points
point_id = picker.point_id/n_glyph
picked_lineage_coords = self.lineage_node_collection.mlab_source.points[point_id,:]
picked_trajectory_coords = self.trajectory_node_collection.mlab_source.points[point_id,:]
picked_graph_node = sorted(self.directed_graph)[point_id]
self.on_pick(picked_graph_node, picked_lineage_coords, picked_trajectory_coords)
def on_select_point(self):
# Used if a coordinate is picked via somethign other than a mouse pick
point_id = sorted(self.directed_graph).index(self.parent.selected_node)
picked_trajectory_coords = self.trajectory_node_collection.mlab_source.points[point_id,:]
picked_lineage_coords = self.lineage_node_collection.mlab_source.points[point_id,:]
#self.parent.selected_node = None # Disable temporailty since it gets set back in on_pick
self.on_pick(self.parent.selected_node, picked_lineage_coords, picked_trajectory_coords, True)
def on_pick_trajectory(self,picker):
""" Trajectory picker callback: this gets called upon pick events.
"""
picked_graph_node = None
picked_lineage_coords = None
picked_trajectory_coords = None
if picker.actor in self.trajectory_node_collection.actor.actors:
n_glyph = self.trajectory_node_collection.glyph.glyph_source.glyph_source.output.points.to_array().shape[0]
point_id = picker.point_id/n_glyph
picked_trajectory_coords = self.trajectory_node_collection.mlab_source.points[point_id,:]
picked_lineage_coords = self.lineage_node_collection.mlab_source.points[point_id,:]
picked_graph_node = sorted(self.directed_graph)[point_id]
else:
picked_graph_node = None
self.on_pick(picked_graph_node, picked_lineage_coords, picked_trajectory_coords)
def on_pick(self,picked_graph_node, picked_lineage_coords = None, picked_trajectory_coords = None, no_mouse = False):
if picked_graph_node != None:
# If the picked node is not one of the selected trajectories, then don't select it
if not no_mouse and picked_graph_node == self.parent.selected_node:
self.parent.selected_node = None
self.parent.selected_trajectory = None
self.lineage_selection_outline.actor.actor.visibility = 0
self.trajectory_selection_outline.actor.actor.visibility = 0
else:
self.parent.selected_node = picked_graph_node
self.parent.selected_trajectory = [self.parent.directed_graph[self.parent.selected_dataset][self.parent.selected_dataset_track].node[picked_graph_node][SUBGRAPH_ID]]
# Move the outline to the data point
dx = np.diff(self.lineage_selection_outline.bounds[:2])[0]/2
dy = np.diff(self.lineage_selection_outline.bounds[2:4])[0]/2
self.lineage_selection_outline.bounds = (picked_lineage_coords[0]-dx, picked_lineage_coords[0]+dx,
picked_lineage_coords[1]-dy, picked_lineage_coords[1]+dy,
0, 0)
self.lineage_selection_outline.actor.actor.visibility = 1
dx = np.diff(self.trajectory_selection_outline.bounds[:2])[0]/2
dy = np.diff(self.trajectory_selection_outline.bounds[2:4])[0]/2
dt = np.diff(self.trajectory_selection_outline.bounds[4:6])[0]/2
self.trajectory_selection_outline.bounds = (picked_trajectory_coords[0]-dx, picked_trajectory_coords[0]+dx,
picked_trajectory_coords[1]-dy, picked_trajectory_coords[1]+dy,
picked_trajectory_coords[2]-dt, picked_trajectory_coords[2]+dt)
self.trajectory_selection_outline.actor.actor.visibility = 1
def draw_lineage(self, do_plots_need_updating, directed_graph=None, connected_nodes=None, selected_colormap=None, scalar_data = None):
# Rendering temporarily disabled
self.lineage_scene.disable_render = True
# Helpful pages on using NetworkX and Mayavi:
# http://docs.enthought.com/mayavi/mayavi/auto/example_delaunay_graph.html
# https://groups.google.com/forum/?fromgroups=#!topic/networkx-discuss/wdhYIPeuilo
# http://www.mail-archive.com/mayavi-users@lists.sourceforge.net/msg00727.html
# Draw the lineage tree if the dataset has been updated
if do_plots_need_updating["dataset"]:
self.connected_nodes = connected_nodes
self.directed_graph = directed_graph
# Clear the scene
logging.info("Drawing lineage graph...")
if self.parent.plot_initialized:
self.clear_figures(self.lineage_scene)
#mlab.title("Lineage tree",size=2.0,figure=self.lineage_scene.mayavi_scene)
t1 = time.clock()
G = nx.convert_node_labels_to_integers(directed_graph,ordering="sorted")
xys = np.array([[directed_graph.node[node][L_TCOORD],directed_graph.node[node][L_YCOORD],directed_graph.node[node][SCALAR_VAL]] for node in sorted(directed_graph.nodes()) ])
#if len(xys) == 0:
#xys = np.array(3*[np.NaN],ndmin=2)
dt = np.median(np.diff(np.unique(nx.get_node_attributes(directed_graph,"t").values())))
# The scale factor defaults to the typical interpoint distance, which may not be appropriate.
# So I set it explicitly here to a fraction of delta_t
# To inspect the value, see pts.glyph.glpyh.scale_factor
node_scale_factor = 0.5*dt
self.lineage_node_collection = mlab.points3d(xys[:,0], xys[:,1], np.zeros_like(xys[:,0]), xys[:,2],
scale_factor = node_scale_factor,
scale_mode = 'none',
colormap = selected_colormap,
resolution = 8,
figure = self.lineage_scene.mayavi_scene)
self.lineage_node_collection.glyph.color_mode = 'color_by_scalar'
#tube_radius = node_scale_factor/10.0
#tube = mlab.pipeline.tube(self.lineage_node_collection,
# tube_radius = tube_radius, # Default tube_radius results in v. thin lines: tube.filter.radius = 0.05
# figure = self.lineage_scene.mayavi_scene)
#self.lineage_tube = tube
self.lineage_edge_collection = mlab.pipeline.surface(self.lineage_node_collection,
color=(0.8, 0.8, 0.8),
figure = self.lineage_scene.mayavi_scene)
self.lineage_edge_collection.mlab_source.dataset.lines = np.array(G.edges())
self.lineage_edge_collection.mlab_source.update()
# Add outline to be used later when selecting points
self.lineage_selection_outline = mlab.outline(line_width=3,
figure = self.lineage_scene.mayavi_scene)
self.lineage_selection_outline.outline_mode = 'cornered'
self.lineage_selection_outline.actor.actor.visibility = 0
self.lineage_selection_outline.bounds = (-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor)
# Add 2 more outlines to be used later when selecting points
self.lineage_point_selection_outline = []
for i in range(2):
ol = mlab.points3d(0,0,0,
extent = list(2*np.array([-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor])),
color = (1,0,1),
mode = 'sphere',
scale_factor = 2*node_scale_factor,
scale_mode = 'none',
figure = self.lineage_scene.mayavi_scene)
ol.actor.actor.visibility = 0
self.lineage_point_selection_outline.append(ol)
self.lineage_node_scale_factor = node_scale_factor
# Add axes outlines
self.lineage_extent = (0,np.max(nx.get_node_attributes(directed_graph,L_TCOORD).values()),
0,np.max(nx.get_node_attributes(directed_graph,L_YCOORD).values()),
0,0)
self.lineage_outline = mlab.pipeline.outline(self.lineage_node_collection,
extent = self.lineage_extent,
opacity = self.axes_opacity,
figure = self.lineage_scene.mayavi_scene)
t2 = time.clock()
logging.info("Computed layout (%.2f sec)"%(t2-t1))
else:
logging.info("Re-drawing lineage tree...")
if do_plots_need_updating["trajectories"]:
G = nx.convert_node_labels_to_integers(directed_graph,ordering="sorted")
edges = np.array([e for e in G.edges() if G.node[e[0]][VISIBLE] and G.node[e[1]][VISIBLE]])
self.lineage_edge_collection.mlab_source.dataset.lines = edges
self.lineage_edge_collection.mlab_source.update()
for key in connected_nodes.keys():
self.lineage_label_collection[key].actor.actor.visibility = self.parent.trajectory_selection[key]
if do_plots_need_updating["measurement"]:
self.lineage_node_collection.mlab_source.set(scalars = scalar_data)
if do_plots_need_updating["colormap"]:
# http://docs.enthought.com/mayavi/mayavi/auto/example_custom_colormap.html
self.lineage_node_collection.module_manager.scalar_lut_manager.lut_mode = selected_colormap
# Re-enable the rendering
self.lineage_scene.disable_render = False
def draw_trajectories(self, do_plots_need_updating, directed_graph = None, connected_nodes = None, selected_colormap=None, scalar_data = None):
# Rendering temporarily disabled
self.trajectory_scene.disable_render = True
# Draw the lineage tree if either (1) all the controls indicate that updating is needed (e.g., initial condition) or
# (2) if the dataset has been updated
if do_plots_need_updating["dataset"]:
self.directed_graph = directed_graph
self.connected_nodes = connected_nodes
logging.info("Drawing trajectories...")
# Clear the scene
if self.parent.plot_initialized:
self.clear_figures(self.trajectory_scene)
#mlab.title("Trajectory plot",size=2.0,figure=self.trajectory_scene.mayavi_scene)
t1 = time.clock()
G = nx.convert_node_labels_to_integers(directed_graph,ordering="sorted")
xyts = np.array([(directed_graph.node[key]["x"],
directed_graph.node[key]["y"],
directed_graph.node[key]["t"],
directed_graph.node[key][SCALAR_VAL],
directed_graph.node[key][VISIBLE])
for key in sorted(directed_graph)])
visible = xyts[:, -1]
# Compute reasonable scaling factor according to the data limits.
# We want the plot to be roughly square, to avoid nasty Mayavi axis scaling issues later.
# Unfortunately, adjusting the surface.actor.actor.scale seems to lead to more problems than solutions.
# See: http://stackoverflow.com/questions/13015097/how-do-i-scale-the-x-and-y-axes-in-mayavi2
t_scaling = np.mean( [(max(xyts[:,0])-min(xyts[:,0])), (max(xyts[:,1])-min(xyts[:,1]))] ) / (max(xyts[:,2])-min(xyts[:,2]))
xyts[:,2] *= t_scaling
self.trajectory_temporal_scaling = t_scaling
# Taken from http://docs.enthought.com/mayavi/mayavi/auto/example_plotting_many_lines.html
# Create the lines
self.trajectory_line_source = mlab.pipeline.scalar_scatter(xyts[:,0], xyts[:,1], xyts[:,2], xyts[:,3], \
figure = self.trajectory_scene.mayavi_scene)
# Connect them using the graph edge matrix
self.trajectory_line_source.mlab_source.dataset.lines = np.array(G.edges())
# Finally, display the set of lines by using the surface module. Using a wireframe
# representation allows to control the line-width.
self.trajectory_line_collection = mlab.pipeline.surface(mlab.pipeline.stripper(self.trajectory_line_source), # The stripper filter cleans up connected lines; it regularizes surfaces by creating triangle strips
line_width=1,
colormap=selected_colormap,
figure = self.trajectory_scene.mayavi_scene)
# Generate the corresponding set of nodes
dt = np.median(np.diff(np.unique(nx.get_node_attributes(directed_graph,"t").values())))
self.lineage_temporal_scaling = dt
# Try to scale the nodes in a reasonable way
# To inspect, see pts.glyph.glpyh.scale_factor
node_scale_factor = 0.5*dt
pts = mlab.points3d(xyts[:,0], xyts[:,1], xyts[:,2], xyts[:,3],
scale_factor = 0.0,
scale_mode = 'none',
colormap = selected_colormap,
figure = self.trajectory_scene.mayavi_scene)
pts.glyph.color_mode = 'color_by_scalar'
pts.mlab_source.dataset.lines = np.array(G.edges())
self.trajectory_node_collection = pts
# Add outline to be used later when selecting points
self.trajectory_selection_outline = mlab.outline(line_width = 3,
figure = self.trajectory_scene.mayavi_scene)
self.trajectory_selection_outline.outline_mode = 'cornered'
self.trajectory_selection_outline.bounds = (-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor)
self.trajectory_selection_outline.actor.actor.visibility = 0
# Add 2 more points to be used later when selecting points
self.trajectory_point_selection_outline = []
for i in range(2):
ol = mlab.points3d(0,0,0,
extent = [-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor,
-node_scale_factor,node_scale_factor],
color = (1,0,1),
figure = self.trajectory_scene.mayavi_scene)
ol.actor.actor.visibility = 0
self.trajectory_point_selection_outline.append(ol)
self.trajectory_node_scale_factor = node_scale_factor
# Using axes doesn't work until the scene is avilable:
# http://docs.enthought.com/mayavi/mayavi/building_applications.html#making-the-visualization-live
mlab.pipeline.outline(self.trajectory_line_source,
opacity = self.axes_opacity,
figure = self.trajectory_scene.mayavi_scene)
# Figure decorations
# Orientation axes
#mlab.orientation_axes(zlabel = "T",
#line_width = 5,
#figure = self.mayavi_view.trajectory_scene.mayavi_scene )
# Colormap
# TODO: Figure out how to scale colorbar to smaller size
#c = mlab.colorbar(orientation = "horizontal",
#title = self.selected_measurement,
#figure = self.mayavi_view.trajectory_scene.mayavi_scene)
#c.scalar_bar_representation.position2[1] = 0.05
#c.scalar_bar.height = 0.05
t2 = time.clock()
logging.info("Computed trajectory layout (%.2f sec)"%(t2-t1))
else:
logging.info("Re-drawing trajectories...")
if do_plots_need_updating["trajectories"]:
G = nx.convert_node_labels_to_integers(directed_graph,ordering="sorted")
edges = [e for e in G.edges() if G.node[e[0]][VISIBLE] and G.node[e[1]][VISIBLE]]
self.trajectory_line_collection.mlab_source.dataset.lines = self.trajectory_line_source.mlab_source.dataset.lines = \
np.array(edges)
self.trajectory_line_collection.mlab_source.update()
self.trajectory_line_source.mlab_source.update()
for key in connected_nodes.keys():
self.trajectory_label_collection[key].actor.actor.visibility = self.parent.trajectory_selection[key] != 0
if do_plots_need_updating["measurement"]:
self.trajectory_line_collection.mlab_source.set(scalars = scalar_data)
self.trajectory_node_collection.mlab_source.set(scalars = scalar_data)
if do_plots_need_updating["colormap"]:
self.trajectory_line_collection.module_manager.scalar_lut_manager.lut_mode = selected_colormap
self.trajectory_node_collection.module_manager.scalar_lut_manager.lut_mode = selected_colormap
# Re-enable the rendering
self.trajectory_scene.disable_render = False
################################################################################
class FigureFrame(wx.Frame, CPATool):
"""A wx.Frame with a figure inside"""
def __init__(self, parent=None, id=-1, title="",
pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.DEFAULT_FRAME_STYLE, name=wx.FrameNameStr,
subplots=None, on_close = None):
"""Initialize the frame:
parent - parent window to this one
id - window ID
title - title in title bar
pos - 2-tuple position on screen in pixels
size - 2-tuple size of frame in pixels
style - window style
name - searchable window name
subplots - 2-tuple indicating the layout of subplots inside the window
on_close - a function to run when the window closes
"""
super(FigureFrame,self).__init__(parent, id, title, pos, size, style, name)
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(sizer)
matplotlib.rcdefaults()
self.figure = figure = matplotlib.figure.Figure()
figure.set_facecolor((1,1,1))
figure.set_edgecolor((1,1,1))
self.panel = FigureCanvasWxAgg(self, -1, self.figure)
sizer.Add(self.panel, 1, wx.EXPAND)
#wx.EVT_CLOSE(self, self.on_close)
if subplots:
self.subplots = np.zeros(subplots,dtype=object)
self.Fit()
self.Show()
def on_close(self, event):
if self.close_fn is not None:
self.close_fn(event)
self.clf() # Free memory allocated by imshow
self.Destroy()
################################################################################
class TimeLapseTool(wx.Frame, CPATool):
'''
A time-lapse visual plot with its controls.
'''
def __init__(self, parent, size=(1000,600), **kwargs):
wx.Frame.__init__(self, parent, -1, size=size, title='Time-Lapse Tool', **kwargs)