forked from SainsburyWellcomeCentre/lasagna
-
Notifications
You must be signed in to change notification settings - Fork 0
/
lasagna.py
executable file
·1327 lines (1019 loc) · 53.5 KB
/
lasagna.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
#! /usr/bin/env python3
"""
Show coronal, transverse and saggital plots in different panels
Depends on:
vtk
pyqtgraph (0.9.10 and above 0.9.8 is known not to work)
numpy
tifffile
argparse
tempfile
urllib
"""
__author__ = "Rob Campbell"
__license__ = "GPL v3"
__maintainer__ = "Rob Campbell"
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
import pyqtgraph as pg
import numpy as np
import sys
import signal
import os.path
# lasagna modules
import ingredients # A set of classes for handling loaded data
import imageStackLoader # To load TIFF and MHD files
import lasagna_axis # The class that runs the axes
import imageProcessing # A potentially temporary module that houses general-purpose image processing code
import pluginHandler # Deals with finding plugins in the path, etc
import lasagna_mainWindow # Derived from designer .ui files built by pyuic
import lasagna_helperFunctions as lasHelp # Module the provides a variety of import functions (e.g. preference file handling)
from alert import alert # Class used to bring up a warning box
# The following imports are made here in order to ensure Lasagna builds as a standlone
# application on the Mac with py2app
import json, ara_json, tree # For handling ARA labels files
import lasagna_plugin # Needed here to build a standalone version
#import tifffile # Used to load tiff and LSM files
#import nrrd
# Parse command-line input arguments
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-D", help="Load demo images", action="store_true") # Store true makes it zero by default
parser.add_argument("-im", nargs='+', help="file name(s) of image stacks to load")
parser.add_argument("-S", nargs='+', help="file names of sparse points file(s) to load")
parser.add_argument("-L", nargs='+', help="file names of lines file(s) to load")
parser.add_argument("-T", nargs='+', help="file names of tree file(s) to load")
parser.add_argument("-C", help="start a ipython console", action='store_true')
parser.add_argument("-P", help="start plugin of this name. use string from plugins menu as the argument")
args = parser.parse_args()
pluginToStart = args.P
sparsePointsToLoad = args.S
linesToLoad = args.L
treesToLoad = args.T
# Either load the demo stacks or a user-specified stacks
if args.D == True:
import tempfile
import urllib.request, urllib.parse, urllib.error
imStackFnamesToLoad = [tempfile.gettempdir()+os.path.sep+'reference.tiff',
tempfile.gettempdir()+os.path.sep+'sample.tiff']
loadUrl = 'http://mouse.vision/lasagna/'
for fname in imStackFnamesToLoad:
if not os.path.exists(fname):
url = loadUrl + fname.split(os.path.sep)[-1]
print(('Downloading %s to %s' % (url,fname)))
urllib.request.urlretrieve(url,fname)
else:
imStackFnamesToLoad = args.im
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Set up the figure window
class lasagna(QtGui.QMainWindow, lasagna_mainWindow.Ui_lasagna_mainWindow):
def __init__(self, parent=None):
"""
Create default values for properties then call initialiseUI to set up main window
"""
super(lasagna, self).__init__(parent)
# Create widgets defined in the designer file
# self.win = QtGui.QMainWindow()
self.setupUi(self)
self.show()
self.app=None # The QApplication handle kept here
# Misc. window set up
self.setWindowTitle("Lasagna - 3D sectioning volume visualiser")
self.recentLoadActions = []
self.updateRecentlyOpenedFiles()
# We will maintain a list of classes of loaded items that can be added to plots
self.ingredientList = []
# Set up GUI based on preferences
self.view1Z_spinBox.setValue(lasHelp.readPreference('defaultPointZSpread')[0])
self.view2Z_spinBox.setValue(lasHelp.readPreference('defaultPointZSpread')[1])
self.view3Z_spinBox.setValue(lasHelp.readPreference('defaultPointZSpread')[2])
self.markerSize_spinBox.setValue(lasHelp.readPreference('defaultSymbolSize'))
self.lineWidth_spinBox.setValue(lasHelp.readPreference('defaultLineWidth'))
self.markerAlpha_spinBox.setValue(lasHelp.readPreference('defaultSymbolOpacity'))
# Set up axes
# Turn axisRatioLineEdit_x elements into a list to allow functions to iterate across them
self.axisRatioLineEdits = [self.axisRatioLineEdit_1, self.axisRatioLineEdit_2, self.axisRatioLineEdit_3]
self.graphicsViews = [self.graphicsView_1, self.graphicsView_2, self.graphicsView_3] # These are the graphics_views from the UI file
self.axes2D=[]
print("")
for ii in range(len(self.graphicsViews)):
self.axes2D.append(lasagna_axis.projection2D(self.graphicsViews[ii], self, axisRatio=float(self.axisRatioLineEdits[ii].text()), axisToPlot=ii))
print("")
# Establish links between projections for panning and zooming using lasagna_viewBox.linkedAxis
self.axes2D[0].view.getViewBox().linkedAxis = {
self.axes2D[1].view.getViewBox(): {'linkX': None, 'linkY': 'y', 'linkZoom': True},
self.axes2D[2].view.getViewBox(): {'linkX': 'x', 'linkY': None, 'linkZoom': True}
}
self.axes2D[1].view.getViewBox().linkedAxis = {
self.axes2D[0].view.getViewBox(): {'linkX': None, 'linkY': 'y', 'linkZoom': True},
self.axes2D[2].view.getViewBox(): {'linkX': 'y', 'linkY': None, 'linkZoom': True}
}
self.axes2D[2].view.getViewBox().linkedAxis = {
self.axes2D[0].view.getViewBox(): {'linkX': 'x', 'linkY': None, 'linkZoom': True},
self.axes2D[1].view.getViewBox(): {'linkX': None, 'linkY': 'x', 'linkZoom': True}
}
# Establish links between projections for scrolling through slices [implemented by signals in main() after the GUI is instantiated]
self.axes2D[0].linkedXprojection = self.axes2D[2]
self.axes2D[0].linkedYprojection = self.axes2D[1]
self.axes2D[2].linkedXprojection = self.axes2D[0]
self.axes2D[2].linkedYprojection = self.axes2D[1]
self.axes2D[1].linkedXprojection = self.axes2D[2]
self.axes2D[1].linkedYprojection = self.axes2D[0]
# UI elements updated during mouse moves over an axis
self.crossHairVLine = None
self.crossHairHLine = None
self.showCrossHairs = lasHelp.readPreference('showCrossHairs')
self.mouseX = None
self.mouseY = None
self.inAxis = 0 # The axis the mouse is currently in [see mouseMoved()]
self.mousePositionInStack = [] # A list defining voxel (Z,X,Y) in which the mouse cursor is currently positioned [see mouseMoved()]
self.statusBarText = None
#Ensure that the menu on OS X appears the same as in Linux and Windows
self.menuBar.setNativeMenuBar(False)
# Lists of functions that are used as hooks for plugins to modify the behavior of built-in methods.
# Hooks are named using the following convention: <lasagnaMethodName_[Start|End]>
# So:
# 1. It's obvious which method will call a given hook list.
# 2. _Start indicates the hook will run at the top of the method, potentially modifying all
# subsequent behavior of the method.
# 3. _End indicates that the hook will run at the end of the method, appending its functionality
# to whatever the method normally does.
self.hooks = {
'updateStatusBar_End' : [],
'loadImageStack_Start' : [],
'loadImageStack_End' : [],
'showStackLoadDialog_Start' : [],
'showStackLoadDialog_End' : [],
'removeCrossHairs_Start' : [],
'showFileLoadDialog_Start' : [],
'showFileLoadDialog_End' : [],
'loadRecentFileSlot_Start' : [],
'updateMainWindowOnMouseMove_Start' : [],
'updateMainWindowOnMouseMove_End' : [],
'changeImageStackColorMap_Slot_End' : [],
'deleteLayerStack_Slot_End' : [],
'axisClicked' : [],
}
#Handle IO plugins. For instance these are the loaders that handle different data types
#and different loading actions.
lasagna_path = os.path.dirname(os.path.realpath(sys.argv[0]))
builtInIOPath = os.path.join(lasagna_path,'IO')
IO_Paths = lasHelp.readPreference('IO_modulePaths') #directories containing IO modules
IO_Paths.append(builtInIOPath)
IO_Paths = list(set(IO_Paths)) #remove duplicate paths
print("Adding IO module paths to Python path")
IO_plugins, IO_pluginPaths = pluginHandler.findPlugins(IO_Paths)
for p in IO_Paths:
sys.path.append(p) # append to system path
print(p)
# Add *load actions* to the Load ingredients sub-menu and add loader modules here
# TODO: currently we only have code to handle load actions as no save actions are available
self.loadActions = {} # actions must be attached to the lasagna object or they won't function
for thisIOmodule in IO_plugins:
IOclass, IOname = pluginHandler.getPluginInstanceFromFileName(thisIOmodule,attributeToImport='loaderClass')
thisInstance = IOclass(self)
self.loadActions[thisInstance.objectName] = thisInstance
print(("Added %s to load menu as object name %s" % (thisIOmodule, thisInstance.objectName)))
print("")
# Link other menu signals to slots
self.actionOpen.triggered.connect(self.showStackLoadDialog)
self.actionQuit.triggered.connect(self.quitLasagna)
self.actionAbout.triggered.connect(self.about_slot)
# Link toolbar signals to slots
self.actionResetAxes.triggered.connect(self.resetAxes)
# Link tabbed view items to slots
# Image tab stuff
# ImageStack QTreeView (see lasagna_ingredient.addToList for where the model is updated upon ingredient addition)
self.logYcheckBox.clicked.connect(self.plotImageStackHistogram)
self.imageAlpha_horizontalSlider.valueChanged.connect(self.imageAlpha_horizontalSlider_slot)
self.imageStackLayers_Model = QtGui.QStandardItemModel(self.imageStackLayers_TreeView)
self.imageStackLayers_Model.setHorizontalHeaderLabels(["Name"])
self.imageStackLayers_TreeView.setModel(self.imageStackLayers_Model)
self.imageStackLayers_TreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.imageStackLayers_TreeView.customContextMenuRequested.connect(self.layersMenuStacks)
# self.imageStackLayers_TreeView.setColumnWidth(0,200)
self.imageStackLayers_TreeView.selectionModel().selectionChanged[QtCore.QItemSelection, QtCore.QItemSelection].connect(self.imageStackLayers_TreeView_slot)
# Points tab stuff. (The points tab deals with sparse data types like points, lines, and trees)
# Points QTreeView (see lasagna_ingredient.addToList for where the model is updated upon ingredient addition)
self.points_Model = QtGui.QStandardItemModel(self.points_TreeView)
self.points_Model.setHorizontalHeaderLabels(["Name"])
self.points_TreeView.setModel(self.points_Model)
self.points_TreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.points_TreeView.customContextMenuRequested.connect(self.layersMenuPoints)
self.points_TreeView.selectionModel().selectionChanged[QtCore.QItemSelection, QtCore.QItemSelection].connect(self.pointsLayers_TreeView_slot)
# Settings boxes, etc, for the points (sparse data) ingredients
[self.markerSymbol_comboBox.addItem(pointType) for pointType in lasHelp.readPreference('symbolOrder')] # populate with markers
self.markerSymbol_comboBox.activated.connect(self.markerSymbol_comboBox_slot)
self.markerSize_spinBox.valueChanged.connect(self.markerSize_spinBox_slot)
self.markerAlpha_spinBox.valueChanged.connect(self.markerAlpha_spinBox_slot)
self.markerAlpha_spinBox.valueChanged.connect(self.markerAlpha_spinBox_slot)
self.markerColor_pushButton.released.connect(self.markerColor_pushButton_slot)
self.lineWidth_spinBox.valueChanged.connect(self.lineWidth_spinBox_slot)
# add the z-points spinboxes to a list to make them indexable
self.viewZ_spinBoxes = [self.view1Z_spinBox, self.view2Z_spinBox, self.view3Z_spinBox]
# create a slot to force a re-draw of the screen when the spinbox value changes
self.view1Z_spinBox.valueChanged.connect(self.viewZ_spinBoxes_slot)
self.view2Z_spinBox.valueChanged.connect(self.viewZ_spinBoxes_slot)
self.view3Z_spinBox.valueChanged.connect(self.viewZ_spinBoxes_slot)
# Axis tab stuff
# TODO: set up as one slot that receives an argument telling it which axis ratio was changed
self.axisRatioLineEdit_1.textChanged.connect(self.axisRatio1Slot)
self.axisRatioLineEdit_2.textChanged.connect(self.axisRatio2Slot)
self.axisRatioLineEdit_3.textChanged.connect(self.axisRatio3Slot)
# Flip axis
self.pushButton_FlipView1.released.connect(lambda: self.flipAxis_Slot(0))
self.pushButton_FlipView2.released.connect(lambda: self.flipAxis_Slot(1))
self.pushButton_FlipView3.released.connect(lambda: self.flipAxis_Slot(2))
# Plugins menu and initialisation
# 1. Get a list of all plugins in the plugins path and add their directories to the Python path
pluginPaths = lasHelp.readPreference('pluginPaths')
plugins, pluginPaths = pluginHandler.findPlugins(pluginPaths)
print("Adding plugin paths to Python path:")
self.pluginSubMenus = {}
for p in pluginPaths: # print plugin paths to screen, add to path, add as sub-dir names in Plugins menu
print(p)
sys.path.append(p)
dirName = p.split(os.path.sep)[-1]
self.pluginSubMenus[dirName] = QtGui.QMenu(self.menuPlugins)
self.pluginSubMenus[dirName].setObjectName(dirName)
self.pluginSubMenus[dirName].setTitle(dirName)
self.menuPlugins.addAction(self.pluginSubMenus[dirName].menuAction())
# 2. Add each plugin to a dictionary where the keys are plugin name and values are instances of the plugin.
print("")
self.plugins = {} # A dictionary where keys are plugin names and values are plugin classes or plugin instances
self.pluginActions = {} # A dictionary where keys are plugin names and values are QActions associated with a plugin
for thisPlugin in plugins:
# Get the module name and class
pluginClass, pluginName = pluginHandler.getPluginInstanceFromFileName(thisPlugin,None)
# Get the name of the directory in which the plugin resides so we can add it to the right sub-menu
dirName = os.path.dirname(pluginClass.__file__).split(os.path.sep)[-1]
# create instance of the plugin object and add to the self.plugins dictionary
print(("Creating reference to class " + pluginName + ".plugin"))
self.plugins[pluginName] = pluginClass.plugin
# create an action associated with the plugin and add to the self.pluginActions dictionary
print(("Creating menu QAction for " + pluginName))
self.pluginActions[pluginName] = QtGui.QAction(pluginName, self)
self.pluginActions[pluginName].setObjectName(pluginName)
self.pluginActions[pluginName].setCheckable(True) # so we have a checkbox next to the menu entry
self.pluginSubMenus[dirName].addAction(self.pluginActions[pluginName]) # add action to the correct plugins sub-menu
self.pluginActions[pluginName].triggered.connect(self.startStopPlugin) # Connect this action's signal to the slot
print("")
self.statusBar.showMessage("Initialised")
# - - - - - - - - - - - - - - - - - -
def about_slot(self):
"""
A simple about box
"""
msg = "Lasagna - Rob Campbell<br>Basel - 2015"
reply = QtGui.QMessageBox.question(self, 'Message', msg)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Plugin-related methods
def startStopPlugin(self):
pluginName = str(self.sender().objectName()) # Get the name of the action that sent this signal
if self.pluginActions[pluginName].isChecked():
self.startPlugin(pluginName)
else:
self.stopPlugin(pluginName)
def startPlugin(self,pluginName):
print(("Starting " + pluginName))
self.plugins[pluginName] = self.plugins[pluginName](self) # Create an instance of the plugin object
def stopPlugin(self, pluginName):
print(("Stopping " + pluginName))
try:
self.plugins[pluginName].closePlugin() # tidy up the plugin
except:
print(("failed to properly close plugin " + pluginName))
# delete the plugin instance and replace it in the dictionary with a reference (that what it is?) to the class
# NOTE: plugins with a window do not run the following code when the window is closed. They should, however,
# detach hooks (unless the plugin author forgot to do this)
del(self.plugins[pluginName])
pluginClass, pluginName = pluginHandler.getPluginInstanceFromFileName(pluginName+".py", None)
self.plugins[pluginName] = pluginClass.plugin
def runHook(self, hookArray, *args):
"""
loops through list of functions and runs them
"""
if len(hookArray) == 0 :
return
for thisHook in hookArray:
try:
if thisHook is None:
print("Skipping empty hook in hook list")
continue
else:
thisHook(*args)
except:
print(("Error running plugin method " + str(thisHook)))
raise
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# File menu and methods associated with loading the base image stack.
def loadImageStack(self,fnameToLoad):
"""
Loads an image image stack.
"""
self.runHook(self.hooks['loadImageStack_Start'])
if not os.path.isfile(fnameToLoad):
msg = 'Unable to find ' + fnameToLoad
print(msg)
self.statusBar.showMessage(msg)
return False
print(("Loading image stack " + fnameToLoad))
# TODO: The axis swap likely shouldn't be hard-coded here
loadedImageStack = imageStackLoader.loadStack(fnameToLoad)
if len(loadedImageStack) == 0 and loadedImageStack == False:
return False
# Set up default values in tabs
# It's ok to load images of different sizes but their voxel sizes need to be the same
axRatio = imageStackLoader.getVoxelSpacing(fnameToLoad)
for ii in range(len(axRatio)):
self.axisRatioLineEdits[ii].setText(str(axRatio[ii]))
# Add to the ingredients list
objName=fnameToLoad.split(os.path.sep)[-1]
self.addIngredient(objectName=objName ,
kind='imagestack' ,
data=loadedImageStack ,
fname=fnameToLoad)
self.returnIngredientByName(objName).addToPlots() # Add item to all three 2D plots
# If only one stack is present, we will display it as gray (see imagestack class)
# if more than one stack has been added, we will colour successive stacks according
# to the colorOrder preference in the parameter file
stacks = self.stacksInTreeList()
colorOrder = lasHelp.readPreference('colorOrder')
if len(stacks) == 2:
self.returnIngredientByName(stacks[0]).lut = colorOrder[0]
self.returnIngredientByName(stacks[1]).lut = colorOrder[1]
elif len(stacks) > 2:
self.returnIngredientByName(stacks[len(stacks)-1]).lut = colorOrder[len(stacks)-1]
# remove any existing range highlighter on the histogram. We do this because different images
# will likely have different default ranges
if hasattr(self, 'plottedIntensityRegionObj'):
del self.plottedIntensityRegionObj
self.runHook(self.hooks['loadImageStack_End'])
def showStackLoadDialog(self, triggered=None, fileFilter=imageStackLoader.imageFilter()):
"""
This slot brings up the file load dialog and gets the file name.
If the file name is valid, it loads the base stack using the loadImageStack method.
We split things up so that the base stack can be loaded from the command line,
or from a plugin without going via the load dialog.
triggered - just catches the input from the signal so we can set fileFilter
"""
self.runHook(self.hooks['showStackLoadDialog_Start'])
fname = self.showFileLoadDialog(fileFilter=fileFilter) # TODO: this way the recently loaded files are updated before we succesfully loaded
if fname is None:
return
if os.path.isfile(fname):
self.loadImageStack(str(fname))
self.initialiseAxes()
else:
self.statusBar.showMessage("Unable to find " + str(fname))
self.runHook(self.hooks['showStackLoadDialog_End'])
# - - - - - - - - - - - - - - - - - - - - -
# Code to handle generic file loading, dialogs, etc
def showFileLoadDialog(self, fileFilter="All files (*)"):
"""
Bring up the file load dialog. Return the file name. Update the last used path.
"""
self.runHook(self.hooks['showFileLoadDialog_Start'])
fname = QtGui.QFileDialog.getOpenFileName(self, 'Open file', lasHelp.readPreference('lastLoadDir'), fileFilter)[0]
fname = str(fname)
if len(fname) == 0:
return None
# Update last loaded directory
lasHelp.preferenceWriter('lastLoadDir', lasHelp.stripTrailingFileFromPath(fname))
# Keep a track of the last loaded files
recentlyLoaded = lasHelp.readPreference('recentlyLoadedFiles')
n = lasHelp.readPreference('numRecentFiles')
# Add to start of list
recentlyLoaded.reverse()
recentlyLoaded.append(fname)
recentlyLoaded.reverse()
while len(recentlyLoaded) > n:
recentlyLoaded.pop(-1)
# TODO: list will no longer have the most recent item first
recentlyLoaded = list(set(recentlyLoaded)) # get remove repeats (i.e. keep only unique values)
lasHelp.preferenceWriter('recentlyLoadedFiles', recentlyLoaded)
self.updateRecentlyOpenedFiles()
self.runHook(self.hooks['showFileLoadDialog_End'])
return fname
def updateRecentlyOpenedFiles(self):
"""
Updates the list of recently opened files
"""
recentlyLoadedFiles = lasHelp.readPreference('recentlyLoadedFiles')
#Remove existing actions if present
if len(self.recentLoadActions)>0 and len(recentlyLoadedFiles)>0:
for thisAction in self.recentLoadActions:
self.menuOpen_recent.removeAction(thisAction)
self.recentLoadActions = []
for thisFile in recentlyLoadedFiles:
self.recentLoadActions.append(self.menuOpen_recent.addAction(thisFile)) #add action to list
self.recentLoadActions[-1].triggered.connect(self.loadRecentFileSlot) #link it to a slot
#NOTE: tried the lambda approach but it always assigns the last file name to the list to all signals
# http://stackoverflow.com/questions/940555/pyqt-sending-parameter-to-slot-when-connecting-to-a-signal
def loadRecentFileSlot(self):
"""
load a file from recently opened list
"""
self.runHook(self.hooks['loadRecentFileSlot_Start'])
fname = str(self.sender().text())
self.loadImageStack(fname)
self.initialiseAxes()
def quitLasagna(self):
"""
Neatly shut down the GUI
"""
# Loop through and shut plugins.
for thisPlugin in list(self.pluginActions.keys()):
if self.pluginActions[thisPlugin].isChecked():
if not self.plugins[thisPlugin].confirmOnClose: # TODO: handle cases where plugins want confirmation to close
self.stopPlugin(thisPlugin)
qApp.quit()
sys.exit(0) # without this we get a big horrible error report on the Mac
def closeEvent(self, event):
self.quitLasagna()
#------------------------------------------------------------------------
# Ingredient handling methods
def addIngredient(self, kind='', objectName='', data=None, fname=''):
"""
Adds an ingredient to the list of ingredients.
Scans the list of ingredients to see if an ingredient is already present.
If so, it removes it before adding a new one with the same name.
ingredients are classes that are defined in the ingredients package
"""
print(("\nlasanga.addIngredient - Adding %s ingredient: %s" % (kind, objectName)))
if len(kind)==0:
print(("ERROR: no ingredient kind %s is defined by Lasagna" % (kind)))
return
# Do not attempt to add an ingredient if it's class is not defined
if not hasattr(ingredients,kind):
print(("ERROR: ingredients module has no class '%s'" % kind))
return
# If an ingredient with this object name is already present we delete it
self.removeIngredientByName(objectName)
# Get ingredient of this class from the ingredients package
ingredientClassObj = getattr(getattr(ingredients, kind), kind) # make an ingredient of type "kind"
self.ingredientList.append(ingredientClassObj(
parent=self,
fnameAbsPath=fname,
data=data,
objectName=objectName
)
)
def removeIngredient(self, ingredientInstance):
"""
Removes the ingredient "ingredientInstance" from self.ingredientList
This method is called by the two following methods that remove based on
ingredient name or type
"""
ingredientInstance.removePlotItem() # remove from axes
self.ingredientList.remove(ingredientInstance) # Remove ingredient from the list of ingredients
ingredientInstance.removeFromList() # remove ingredient from the list with which it is associated
self.selectedStackName() # Ensures something is highlighted
# TODO: The following two lines fail to clear the image data from RAM. Somehow there are other references to the object...
ingredientInstance._data = None
del(ingredientInstance)
self.initialiseAxes()
def removeIngredientByName(self,objectName):
"""
Finds ingredient by name and removes it from the list
"""
verbose = False
if len(self.ingredientList)==0:
if verbose:
print("lasagna.removeIngredientByType finds no ingredients in list!")
return
removedIngredient=False
for thisIngredient in self.ingredientList[:]:
if thisIngredient.objectName == objectName:
if verbose:
print(('Removing ingredient ' + objectName))
self.removeIngredient(thisIngredient)
self.selectedStackName() # Ensures something is highlighted
removedIngredient=True
if removedIngredient == False & verbose==True:
print(("** Failed to remove ingredient %s **" % objectName))
return False
return True
def removeIngredientByType(self,ingredientType):
"""
Finds ingredients of one type (e.g. all imagestacks) and removes them all
"""
verbose = False
if len(self.ingredientList)==0:
if verbose:
print("removeIngredientByType finds no ingredients in list!")
return
for thisIngredient in self.ingredientList[:]:
if thisIngredient.__module__.endswith(ingredientType): # TODO: fix this so we look for it by instance not name
if verbose:
print(('Removing ingredient ' + thisIngredient.objectName))
self.selectedStackName() # Ensures something is highlighted
self.removeIngredient(thisIngredient)
def listIngredients(self):
"""
Return a list of ingredient objectNames
"""
ingredientNames = []
for thisIngredient in self.ingredientList:
ingredientNames.append(thisIngredient.objectName)
return ingredientNames
def returnIngredientByType(self,ingredientType):
"""
Return a list of ingredients based upon their type. e.g. imagestack, sparsepoints, etc
"""
verbose = False
if len(self.ingredientList)==0:
if verbose:
print("returnIngredientByType finds no ingredients in list!")
return False
returnedIngredients=[]
for thisIngredient in self.ingredientList:
if thisIngredient.__module__.endswith(ingredientType): # TODO: fix this so we look for it by instance not name
returnedIngredients.append(thisIngredient)
if verbose and len(returnedIngredients)==0:
print(("returnIngredientByType finds no ingredients with type " + ingredientType))
return False
else:
return returnedIngredients
def returnIngredientByName(self,objectName):
"""
Return a specific ingredient object based upon its object name.
Returns False if the ingredient was not found
"""
verbose = False
if len(self.ingredientList)==0:
if verbose:
print("returnIngredientByName finds no ingredients in list!")
return False
for thisIngredient in self.ingredientList:
if thisIngredient.objectName == objectName:
return thisIngredient
if verbose:
print(("returnIngredientByName finds no ingredient called " + objectName))
return False
# - - - - - - - - - - - - - - - - - - - - -
# Functions involved in the display of plots on the screen
def resetAxes(self):
"""
Set X and Y limit of each axes to fit the data
"""
if self.stacksInTreeList()==False:
return
[axis.resetAxes() for axis in self.axes2D]
def initialiseAxes(self,resetAxes=False):
"""
Initial display of images in axes and also update other parts of the GUI.
"""
if self.stacksInTreeList()==False:
self.plotImageStackHistogram() # wipes the histogram
return
# show default images (snap to middle layer of each axis)
[axis.updatePlotItems_2D(self.ingredientList, sliceToPlot=axis.currentSlice, resetToMiddleLayer=resetAxes) for axis in self.axes2D]
# initialize cross hair
if self.showCrossHairs:
if self.crossHairVLine is None:
self.crossHairVLine = pg.InfiniteLine(pen=(220,200,0,180),angle=90, movable=False)
self.crossHairVLine.objectName = 'crossHairVLine'
if self.crossHairHLine is None:
self.crossHairHLine = pg.InfiniteLine(pen=(220,200,0,180),angle=0, movable=False)
self.crossHairHLine.objectName = 'crossHairHLine'
self.plotImageStackHistogram()
for ii in range(len(self.axisRatioLineEdits)):
self.axes2D[ii].view.setAspectLocked(True, float(self.axisRatioLineEdits[ii].text()))
if resetAxes:
self.resetAxes()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Slots for image stack tab
# In each case, we set the values of the currently selected ingredient using the spinbox value
# TODO: this is an example of code that is not flexible. These UI elements should be created by the ingredient
def imageAlpha_horizontalSlider_slot(self,value):
"""
Get the value of the slider and assign it to the currently selected imagestack ingredient.
This is read back, and the slider assigned to the currently selected imagestack value in
the slot: imageStackLayers_TreeView_slot
"""
ingredient = self.selectedStackName()
if ingredient==False:
return
self.returnIngredientByName(ingredient).alpha = int(value)
self.initialiseAxes()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Slots for points tab
# In each case, we set the values of the currently selected ingredient using the spinbox value
# TODO: this is an example of code that is not flexible. These UI elements should be created by the ingredient
def viewZ_spinBoxes_slot(self):
self.initialiseAxes()
def markerSymbol_comboBox_slot(self,index):
symbol = str(self.markerSymbol_comboBox.currentText())
ingredient = self.returnIngredientByName(self.selectedPointsName())
if ingredient==False:
return
ingredient.symbol = symbol
self.initialiseAxes()
def markerSize_spinBox_slot(self,spinBoxValue):
ingredient = self.returnIngredientByName(self.selectedPointsName())
if ingredient==False:
return
ingredient.symbolSize = spinBoxValue
self.initialiseAxes()
def markerAlpha_spinBox_slot(self,spinBoxValue):
ingredient = self.returnIngredientByName(self.selectedPointsName())
if ingredient==False:
return
ingredient.alpha = spinBoxValue
self.initialiseAxes()
def lineWidth_spinBox_slot(self,spinBoxValue):
ingredient = self.returnIngredientByName(self.selectedPointsName())
if ingredient==False:
return
ingredient.lineWidth = spinBoxValue
self.initialiseAxes()
def markerColor_pushButton_slot(self):
ingredient = self.returnIngredientByName(self.selectedPointsName())
if ingredient==False:
return
col = QtGui.QColorDialog.getColor()
rgb = [col.toRgb().red(), col.toRgb().green(), col.toRgb().blue()]
ingredient.color =rgb
self.initialiseAxes()
def selectedPointsName(self):
"""
Return the name of the selected points ingredient. If none are selected, returns the first in the list
"""
if self.points_Model.rowCount()==0:
print("lasagna.selectedPointsName finds no image stacks in list")
return False
# Highlight the first row if nothing is selected (which shouldn't ever happen)
if len(self.points_TreeView.selectedIndexes())==0:
firstItem = self.points_Model.index(0,0)
self.points_TreeView.setCurrentIndex(firstItem)
print("lasagna.selectedStackName forced highlighting of first image stack")
return self.points_TreeView.selectedIndexes()[0].data()
# The remaining methods for this tab are involved in building a context menu on right-click
def layersMenuPoints(self, position):
"""
Defines a pop-up menu that appears when the user right-clicks on a points ingredient
in the points QTreeView
"""
menu = QtGui.QMenu()
action = QtGui.QAction("Delete",self)
action.triggered.connect(self.deleteLayerPoints_Slot)
menu.addAction(action)
menu.exec_(self.points_TreeView.viewport().mapToGlobal(position))
def deleteLayerPoints_Slot(self):
"""
Remove a points ingredient and list item
"""
objName = self.selectedPointsName()
self.removeIngredientByName(objName)
print(("removed " + objName))
def pointsLayers_TreeView_slot(self):
"""
Runs when the user selects one of the points ingredients in the list.
"""
if len(self.ingredientList) == 0:
return
name = self.selectedPointsName()
ingredient = self.returnIngredientByName(name)
if not ingredient:
return
# Assign GUI values based on what is stored in the ingredient
if isinstance(ingredient.symbolSize,int):
self.markerSize_spinBox.setValue(ingredient.symbolSize)
if isinstance(ingredient.alpha,int):
self.markerAlpha_spinBox.setValue(ingredient.alpha)
if isinstance(ingredient.lineWidth,int):
self.lineWidth_spinBox.setValue(ingredient.lineWidth)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Slots for axis tab
# TODO: incorporate these three slots into one
def axisRatio1Slot(self):
"""
Set axis ratio on plot 1
"""
self.axes2D[0].view.setAspectLocked(True, float(self.axisRatioLineEdit_1.text()) )
def axisRatio2Slot(self):
"""
Set axis ratio on plot 2
"""
self.axes2D[1].view.setAspectLocked(True, float(self.axisRatioLineEdit_2.text()))
def axisRatio3Slot(self):
"""
Set axis ratio on plot 3
"""
self.axes2D[2].view.setAspectLocked(True, float(self.axisRatioLineEdit_3.text()))
def flipAxis_Slot(self,axisToFlip):
"""
Loops through all displayed image stacks and flips the axes
"""
imageStacks = self.returnIngredientByType('imagestack')
if imageStacks==False:
return
for thisStack in imageStacks:
thisStack.flipAlongAxis(axisToFlip)
self.initialiseAxes()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Methods that are run during navigation
def removeCrossHairs(self):
"""
Remove the cross hairs from all plots
"""
# NOTE: I'm a little unhappy about this as I don't understand what's going on.
# I've noticed that removing the cross hairs from any one plot is sufficient to remove
# them from the other two. However, if all three axes are not explicitly removed I've
# seen peculiar behavior with plugins that query the PlotWidgets. RAAC 21/07/2015
self.runHook(self.hooks['removeCrossHairs_Start']) # This will be run each time a plot is updated
if not self.showCrossHairs:
return
[axis.removeItemFromPlotWidget(self.crossHairVLine) for axis in self.axes2D]
[axis.removeItemFromPlotWidget(self.crossHairHLine) for axis in self.axes2D]
def updateCrossHairs(self,highlightCrossHairs=False):
"""
Update the drawn cross hairs on the current image.
Highlight cross hairs in red if caller says so
"""
if not self.showCrossHairs:
return
# make cross hairs red if control key is pressed
if QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier and highlightCrossHairs:
self.crossHairVLine.setPen(240,0,0,200)
self.crossHairHLine.setPen(240,0,0,200)
else:
self.crossHairVLine.setPen(220,200,0,180)
self.crossHairHLine.setPen(220,200,0,180)
self.crossHairVLine.setPos(self.mouseX+0.5) # Add 0.5 to add line to middle of pixel
self.crossHairHLine.setPos(self.mouseY+0.5)
def updateStatusBar(self):
"""
Update the text on the status bar based on the current mouse position
"""
X = self.mouseX
Y = self.mouseY
# get pixels under image
imageItems = self.axes2D[self.inAxis].getPlotItemByType('ImageItem')
pixelValues=[]
# Get the pixel intensity of all displayed image layers under the mouse
# The following assumes that images have their origin at (0,0)
for thisImageItem in imageItems:
imShape = thisImageItem.image.shape
if X<0 or Y<0:
pixelValues.append(0)
elif X>=imShape[0] or Y>=imShape[1]:
pixelValues.append(0)
else:
pixelValues.append(thisImageItem.image[X, Y])
# Build a text string to house these values
valueStr = ''
while len(pixelValues)>0:
valueStr = valueStr + '%d,' % pixelValues.pop()