waveorder 2.2.1b0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. waveorder/_version.py +16 -3
  2. waveorder/acq/__init__.py +0 -0
  3. waveorder/acq/acq_functions.py +166 -0
  4. waveorder/assets/HSV_legend.png +0 -0
  5. waveorder/assets/JCh_legend.png +0 -0
  6. waveorder/assets/waveorder_plugin_logo.png +0 -0
  7. waveorder/calib/Calibration.py +1512 -0
  8. waveorder/calib/Optimization.py +470 -0
  9. waveorder/calib/__init__.py +0 -0
  10. waveorder/calib/calibration_workers.py +464 -0
  11. waveorder/cli/apply_inverse_models.py +328 -0
  12. waveorder/cli/apply_inverse_transfer_function.py +379 -0
  13. waveorder/cli/compute_transfer_function.py +432 -0
  14. waveorder/cli/gui_widget.py +58 -0
  15. waveorder/cli/main.py +39 -0
  16. waveorder/cli/monitor.py +163 -0
  17. waveorder/cli/option_eat_all.py +47 -0
  18. waveorder/cli/parsing.py +122 -0
  19. waveorder/cli/printing.py +16 -0
  20. waveorder/cli/reconstruct.py +67 -0
  21. waveorder/cli/settings.py +187 -0
  22. waveorder/cli/utils.py +175 -0
  23. waveorder/filter.py +1 -2
  24. waveorder/focus.py +136 -25
  25. waveorder/io/__init__.py +0 -0
  26. waveorder/io/_reader.py +61 -0
  27. waveorder/io/core_functions.py +272 -0
  28. waveorder/io/metadata_reader.py +195 -0
  29. waveorder/io/utils.py +175 -0
  30. waveorder/io/visualization.py +160 -0
  31. waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
  32. waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
  33. waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
  34. waveorder/models/isotropic_thin_3d.py +73 -72
  35. waveorder/models/phase_thick_3d.py +103 -4
  36. waveorder/napari.yaml +36 -0
  37. waveorder/plugin/__init__.py +9 -0
  38. waveorder/plugin/gui.py +1094 -0
  39. waveorder/plugin/gui.ui +1440 -0
  40. waveorder/plugin/job_manager.py +42 -0
  41. waveorder/plugin/main_widget.py +1605 -0
  42. waveorder/plugin/tab_recon.py +3294 -0
  43. waveorder/scripts/__init__.py +0 -0
  44. waveorder/scripts/launch_napari.py +13 -0
  45. waveorder/scripts/repeat-cal-acq-rec.py +147 -0
  46. waveorder/scripts/repeat-calibration.py +31 -0
  47. waveorder/scripts/samples.py +85 -0
  48. waveorder/scripts/simulate_zarr_acq.py +204 -0
  49. waveorder/util.py +1 -1
  50. waveorder/visuals/napari_visuals.py +1 -1
  51. waveorder-3.0.0.dist-info/METADATA +350 -0
  52. waveorder-3.0.0.dist-info/RECORD +69 -0
  53. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
  54. waveorder-3.0.0.dist-info/entry_points.txt +5 -0
  55. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info/licenses}/LICENSE +13 -1
  56. waveorder-2.2.1b0.dist-info/METADATA +0 -187
  57. waveorder-2.2.1b0.dist-info/RECORD +0 -27
  58. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1605 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import textwrap
7
+ from os.path import dirname
8
+ from pathlib import Path, PurePath
9
+
10
+ # type hint/check
11
+ from typing import TYPE_CHECKING
12
+
13
+ import dask.array as da
14
+ import numpy as np
15
+ from numpy.typing import NDArray
16
+ from numpydoc.docscrape import NumpyDocString
17
+ from packaging import version
18
+ from qtpy.QtCore import Qt, Signal, Slot
19
+ from qtpy.QtGui import QColor, QPixmap
20
+ from qtpy.QtWidgets import QFileDialog, QSizePolicy, QSlider, QWidget
21
+ from superqt import QDoubleRangeSlider, QRangeSlider
22
+
23
+ from waveorder.waveorder_reconstructor import waveorder_microscopy
24
+
25
+ try:
26
+ from pycromanager import Core, Studio, zmq_bridge
27
+ except:
28
+ pass
29
+
30
+ try:
31
+ from napari import Viewer
32
+ from napari.components import LayerList
33
+ from napari.utils.events import Event
34
+ from napari.utils.notifications import show_warning
35
+ except:
36
+ pass
37
+
38
+ from waveorder.calib import Calibration
39
+ from waveorder.calib.Calibration import LC_DEVICE_NAME, QLIPP_Calibration
40
+ from waveorder.calib.calibration_workers import (
41
+ BackgroundCaptureWorker,
42
+ CalibrationWorker,
43
+ load_calibration,
44
+ )
45
+ from waveorder.io.core_functions import set_lc_state, snap_and_average
46
+ from waveorder.io.metadata_reader import MetadataReader
47
+ from waveorder.io.visualization import ret_ori_overlay
48
+ from waveorder.plugin import gui
49
+
50
+ # avoid runtime import error
51
+ if TYPE_CHECKING:
52
+ pass
53
+
54
+
55
+ class MainWidget(QWidget):
56
+ """
57
+ This is the main waveorder widget that houses all of the GUI components of waveorder.
58
+ The GUI is designed in QT Designer in /waveorder/plugin/gui.ui and converted to a python file
59
+ with the pyuic5 command.
60
+ """
61
+
62
+ # Initialize Custom Signals
63
+ log_changed = Signal(str)
64
+
65
+ # Initialize class attributes
66
+ disabled_button_style = "border: 1px solid rgb(65,72,81);"
67
+
68
+ def __init__(self, napari_viewer: Viewer):
69
+ super().__init__()
70
+ self.viewer = napari_viewer
71
+
72
+ # Setup GUI elements
73
+ self.ui = gui.Ui_Form()
74
+ self.ui.setupUi(self)
75
+ self.ui.tab_reconstruction.set_viewer(napari_viewer)
76
+
77
+ # Override initial tab focus
78
+ self.ui.tabWidget.setCurrentIndex(0)
79
+
80
+ # Disable buttons until connected to MM
81
+ self._set_buttons_enabled(False)
82
+
83
+ # Set up overlay sliders (Commenting for 0.3.0. Consider debugging or deleting for 1.0.0.)
84
+ # self._promote_slider_init()
85
+
86
+ ## Connect GUI elements to functions
87
+ # Top bar
88
+ self.ui.qbutton_connect_to_mm.clicked[bool].connect(
89
+ self.toggle_mm_connection
90
+ )
91
+
92
+ # Calibration tab
93
+ self.ui.qbutton_browse.clicked[bool].connect(self.browse_dir_path)
94
+ self.ui.le_directory.editingFinished.connect(self.enter_dir_path)
95
+ self.ui.le_directory.setText(str(Path.cwd()))
96
+
97
+ self.ui.le_swing.editingFinished.connect(self.enter_swing)
98
+ self.ui.le_swing.setText("0.1")
99
+ self.enter_swing()
100
+
101
+ self.ui.le_wavelength.editingFinished.connect(self.enter_wavelength)
102
+ self.ui.le_wavelength.setText("532")
103
+ self.enter_wavelength()
104
+
105
+ self.ui.cb_calib_scheme.currentIndexChanged[int].connect(
106
+ self.enter_calib_scheme
107
+ )
108
+ self.ui.cb_calib_mode.currentIndexChanged[int].connect(
109
+ self.enter_calib_mode
110
+ )
111
+ self.ui.cb_lca.currentIndexChanged[int].connect(self.enter_dac_lca)
112
+ self.ui.cb_lcb.currentIndexChanged[int].connect(self.enter_dac_lcb)
113
+ self.ui.qbutton_calibrate.clicked[bool].connect(self.run_calibration)
114
+ self.ui.qbutton_load_calib.clicked[bool].connect(self.load_calibration)
115
+ self.ui.qbutton_calc_extinction.clicked[bool].connect(
116
+ self.calc_extinction
117
+ )
118
+ self.ui.cb_config_group.currentIndexChanged[int].connect(
119
+ self.enter_config_group
120
+ )
121
+
122
+ self.ui.le_bg_folder.editingFinished.connect(self.enter_bg_folder_name)
123
+ self.ui.le_n_avg.editingFinished.connect(self.enter_n_avg)
124
+ self.ui.qbutton_capture_bg.clicked[bool].connect(self.capture_bg)
125
+
126
+ # Advanced tab
127
+ self.ui.cb_loglevel.currentIndexChanged[int].connect(
128
+ self.enter_log_level
129
+ )
130
+ self.ui.qbutton_push_note.clicked[bool].connect(self.push_note)
131
+
132
+ # hook to render overlay
133
+ # acquistion updates existing layers and moves them to the top which triggers this event
134
+ self.viewer.layers.events.moved.connect(self.handle_layers_updated)
135
+ self.viewer.layers.events.inserted.connect(self.handle_layers_updated)
136
+
137
+ # Birefringence overlay controls
138
+ self.ui.retMaxSlider.sliderMoved[int].connect(
139
+ self.handle_ret_max_slider_move
140
+ )
141
+
142
+ ## Initialize logging
143
+ log_box = QtLogger(self.ui.te_log)
144
+ log_box.setFormatter(logging.Formatter("%(levelname)s - %(message)s"))
145
+ logging.getLogger().addHandler(log_box)
146
+ logging.getLogger().setLevel(logging.INFO)
147
+
148
+ ## Initialize attributes
149
+ self.connected_to_mm = False
150
+ self.bridge = None
151
+ self.mm = None
152
+ self.mmc = None
153
+ self.calib = None
154
+ self.current_dir_path = str(Path.cwd())
155
+ self.directory = str(Path.cwd())
156
+ self.calib_scheme = "4-State"
157
+ self.calib_mode = "MM-Retardance"
158
+ self.interp_method = "schnoor_fit"
159
+ self.config_group = "Channel"
160
+ self.calib_channels = [
161
+ "State0",
162
+ "State1",
163
+ "State2",
164
+ "State3",
165
+ "State4",
166
+ ]
167
+ self.last_calib_meta_file = None
168
+ self.use_cropped_roi = False
169
+ self.bg_folder_name = "bg"
170
+ self.n_avg = 5
171
+ self.intensity_monitor = []
172
+ self.auto_shutter = True
173
+ self.lca_dac = None
174
+ self.lcb_dac = None
175
+ self.pause_updates = False
176
+ self.method = "QLIPP"
177
+ self.mode = "3D"
178
+ self.calib_path = str(Path.cwd())
179
+ self.data_dir = str(Path.cwd())
180
+ self.config_path = str(Path.cwd())
181
+ self.save_config_path = str(Path.cwd())
182
+ self.colormap = "HSV"
183
+ self.use_full_volume = False
184
+ self.display_slice = 0
185
+ self.last_p = 0
186
+ self.reconstruction_data_path = None
187
+ self.reconstruction_data = None
188
+ self.calib_assessment_level = None
189
+ self.ret_max = 25
190
+ waveorder_dir = dirname(dirname(dirname(os.path.abspath(__file__))))
191
+ self.worker = None
192
+
193
+ ## Initialize calibration plot
194
+ self.plot_item = self.ui.plot_widget.getPlotItem()
195
+ self.plot_item.enableAutoRange()
196
+ self.plot_item.setLabel("left", "Intensity")
197
+ self.ui.plot_widget.setBackground((32, 34, 40))
198
+ self.plot_sequence = "Coarse"
199
+
200
+ ## Initialize visuals
201
+ # Initialize GUI Images (plotting legends, waveorder logo)
202
+ assets_dir = Path(__file__).parent.parent / "assets"
203
+ jch_legend_path = assets_dir / "JCh_legend.png"
204
+ hsv_legend_path = assets_dir / "HSV_legend.png"
205
+ logo_path = assets_dir / "waveorder_plugin_logo.png"
206
+
207
+ self.jch_pixmap = QPixmap(str(jch_legend_path))
208
+ self.hsv_pixmap = QPixmap(str(hsv_legend_path))
209
+ self.ui.label_orientation_image.setPixmap(self.hsv_pixmap)
210
+ logo_pixmap = QPixmap(str(logo_path))
211
+ self.ui.label_logo.setPixmap(logo_pixmap)
212
+
213
+ # Hide UI elements for popups
214
+ # DAC mode popups
215
+ self.ui.label_lca.hide()
216
+ self.ui.label_lcb.hide()
217
+ self.ui.cb_lca.hide()
218
+ self.ui.cb_lcb.hide()
219
+
220
+ # Hide temporarily unsupported "Overlay" functions
221
+ self.ui.tabWidget.setTabText(
222
+ self.ui.tabWidget.indexOf(self.ui.Display), "Visualization"
223
+ )
224
+ self.ui.label_orientation_legend.setHidden(True)
225
+ self.ui.DisplayOptions.setHidden(True)
226
+
227
+ # Set initial UI Properties
228
+ self.ui.label_extinction.setText("Extinction Ratio")
229
+ self.ui.le_mm_status.setStyleSheet(
230
+ "border: 1px solid rgb(200,0,0); color: rgb(200,0,0);"
231
+ )
232
+ self.ui.te_log.setStyleSheet("background-color: rgb(32,34,40);")
233
+ self.ui.le_sat_min.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
234
+ self.ui.le_sat_max.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
235
+ self.ui.le_val_min.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
236
+ self.ui.le_val_max.setStyleSheet("background-color: rgba(0, 0, 0, 0);")
237
+ self.setStyleSheet("QTabWidget::tab-bar {alignment: center;}")
238
+ self.red_text = QColor(200, 0, 0, 255)
239
+
240
+ # Populate calibration modes from docstring
241
+ cal_docs = NumpyDocString(
242
+ Calibration.QLIPP_Calibration.__init__.__doc__
243
+ )
244
+ mode_docs = " ".join(cal_docs["Parameters"][3].desc).split("* ")[1:]
245
+ for i, mode_doc in enumerate(mode_docs):
246
+ mode_name, mode_tooltip = mode_doc.split(": ")
247
+ wrapped_tooltip = "\n".join(textwrap.wrap(mode_tooltip, width=70))
248
+ self.ui.cb_calib_mode.addItem(mode_name)
249
+ self.ui.cb_calib_mode.setItemData(
250
+ i, wrapped_tooltip, Qt.ToolTipRole
251
+ )
252
+
253
+ # make sure the top says waveorder and not 'Form'
254
+ self.ui.tabWidget.parent().setObjectName("waveorder")
255
+
256
+ ## Set GUI behaviors
257
+ # set focus to "Plot" tab by default
258
+ self.ui.tabWidget_2.setCurrentIndex(0)
259
+
260
+ # disable wheel events for combo boxes
261
+ for attr_name in dir(self.ui):
262
+ if "cb_" in attr_name:
263
+ attr = getattr(self.ui, attr_name)
264
+ attr.wheelEvent = lambda event: None
265
+
266
+ # Display GUI using maximum resolution
267
+ self.showMaximized()
268
+
269
+ def _demote_slider_offline(self, ui_slider, range_):
270
+ """
271
+ This function converts a promoted superqt.QRangeSlider to a QSlider element
272
+
273
+ Parameters
274
+ ----------
275
+ ui_slider (superqt.QRangeSlider) QSlider UI element to demote
276
+ range_ (tuple) initial range to set for the slider
277
+
278
+ Returns
279
+ -------
280
+
281
+ """
282
+ sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
283
+ sizePolicy.setHorizontalStretch(0)
284
+ sizePolicy.setVerticalStretch(0)
285
+
286
+ # Get positioning information from regular sliders
287
+ slider_idx = self.ui.gridLayout_26.indexOf(ui_slider)
288
+ slider_position = self.ui.gridLayout_26.getItemPosition(slider_idx)
289
+ slider_parent = ui_slider.parent().objectName()
290
+ slider_name = ui_slider.objectName()
291
+
292
+ # Remove regular sliders from the UI
293
+ self.ui.gridLayout_26.removeWidget(ui_slider)
294
+
295
+ # Add back the sliders as range sliders with the same properties
296
+ ui_slider = QSlider(getattr(self.ui, slider_parent))
297
+ sizePolicy.setHeightForWidth(
298
+ ui_slider.sizePolicy().hasHeightForWidth()
299
+ )
300
+ ui_slider.setSizePolicy(sizePolicy)
301
+ ui_slider.setOrientation(Qt.Horizontal)
302
+ ui_slider.setObjectName(slider_name)
303
+ self.ui.gridLayout_26.addWidget(
304
+ ui_slider,
305
+ slider_position[0],
306
+ slider_position[1],
307
+ slider_position[2],
308
+ slider_position[3],
309
+ )
310
+ ui_slider.setRange(range_[0], range_[1])
311
+
312
+ def _promote_slider_offline(self, ui_slider, range_):
313
+ """
314
+ This function converts a a QSlider element to a promoted superqt.QRangeSlider
315
+
316
+ Parameters
317
+ ----------
318
+ ui_slider (QT.Slider) QSlider UI element to demote
319
+ range_ (tuple) initial range to set for the slider
320
+
321
+ Returns
322
+ -------
323
+
324
+ """
325
+
326
+ sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
327
+ sizePolicy.setHorizontalStretch(0)
328
+ sizePolicy.setVerticalStretch(0)
329
+
330
+ # Get Information from regular sliders
331
+ slider_idx = self.ui.gridLayout_26.indexOf(ui_slider)
332
+ slider_position = self.ui.gridLayout_26.getItemPosition(slider_idx)
333
+ slider_parent = ui_slider.parent().objectName()
334
+ slider_name = ui_slider.objectName()
335
+
336
+ # Remove regular sliders from the UI
337
+ self.ui.gridLayout_26.removeWidget(ui_slider)
338
+
339
+ # Add back the sliders as range sliders with the same properties
340
+ ui_slider = QRangeSlider(getattr(self.ui, slider_parent))
341
+ sizePolicy.setHeightForWidth(
342
+ ui_slider.sizePolicy().hasHeightForWidth()
343
+ )
344
+ ui_slider.setSizePolicy(sizePolicy)
345
+ ui_slider.setOrientation(Qt.Horizontal)
346
+ ui_slider.setObjectName(slider_name)
347
+ self.ui.gridLayout_26.addWidget(
348
+ ui_slider,
349
+ slider_position[0],
350
+ slider_position[1],
351
+ slider_position[2],
352
+ slider_position[3],
353
+ )
354
+ ui_slider.setRange(range_[0], range_[1])
355
+
356
+ def _promote_slider_init(self):
357
+ """
358
+ Used to promote the Display Tab sliders from QSlider to QDoubeRangeSlider with superqt
359
+ Returns
360
+ -------
361
+
362
+ """
363
+
364
+ sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
365
+ sizePolicy.setHorizontalStretch(0)
366
+ sizePolicy.setVerticalStretch(0)
367
+
368
+ # Get Information from regular sliders
369
+ value_slider_idx = self.ui.gridLayout_17.indexOf(self.ui.slider_value)
370
+ value_slider_position = self.ui.gridLayout_17.getItemPosition(
371
+ value_slider_idx
372
+ )
373
+ value_slider_parent = self.ui.slider_value.parent().objectName()
374
+ saturation_slider_idx = self.ui.gridLayout_17.indexOf(
375
+ self.ui.slider_saturation
376
+ )
377
+ saturation_slider_position = self.ui.gridLayout_17.getItemPosition(
378
+ saturation_slider_idx
379
+ )
380
+ saturation_slider_parent = (
381
+ self.ui.slider_saturation.parent().objectName()
382
+ )
383
+
384
+ # Remove regular sliders from the UI
385
+ self.ui.gridLayout_17.removeWidget(self.ui.slider_value)
386
+ self.ui.gridLayout_17.removeWidget(self.ui.slider_saturation)
387
+
388
+ # Add back the sliders as range sliders with the same properties
389
+ self.ui.slider_saturation = QDoubleRangeSlider(
390
+ getattr(self.ui, saturation_slider_parent)
391
+ )
392
+ sizePolicy.setHeightForWidth(
393
+ self.ui.slider_saturation.sizePolicy().hasHeightForWidth()
394
+ )
395
+ self.ui.slider_saturation.setSizePolicy(sizePolicy)
396
+ self.ui.slider_saturation.setOrientation(Qt.Horizontal)
397
+ self.ui.slider_saturation.setObjectName("slider_saturation")
398
+ self.ui.gridLayout_17.addWidget(
399
+ self.ui.slider_saturation,
400
+ saturation_slider_position[0],
401
+ saturation_slider_position[1],
402
+ saturation_slider_position[2],
403
+ saturation_slider_position[3],
404
+ )
405
+ self.ui.slider_saturation.setRange(0, 100)
406
+
407
+ self.ui.slider_value = QDoubleRangeSlider(
408
+ getattr(self.ui, value_slider_parent)
409
+ )
410
+ sizePolicy.setHeightForWidth(
411
+ self.ui.slider_value.sizePolicy().hasHeightForWidth()
412
+ )
413
+ self.ui.slider_value.setSizePolicy(sizePolicy)
414
+ self.ui.slider_value.setOrientation(Qt.Horizontal)
415
+ self.ui.slider_value.setObjectName("slider_value")
416
+ self.ui.gridLayout_17.addWidget(
417
+ self.ui.slider_value,
418
+ value_slider_position[0],
419
+ value_slider_position[1],
420
+ value_slider_position[2],
421
+ value_slider_position[3],
422
+ )
423
+ self.ui.slider_value.setRange(0, 100)
424
+
425
+ def _set_buttons_enabled(self, val):
426
+ """
427
+ enables/disables buttons that require a connection to MM
428
+ """
429
+ action_buttons = [
430
+ self.ui.qbutton_calibrate,
431
+ self.ui.qbutton_capture_bg,
432
+ self.ui.qbutton_calc_extinction,
433
+ self.ui.qbutton_load_calib,
434
+ self.ui.qbutton_create_overlay,
435
+ ]
436
+
437
+ for action_button in action_buttons:
438
+ action_button.setEnabled(val)
439
+ if val:
440
+ action_button.setToolTip("")
441
+ action_button.setStyleSheet(self.disabled_button_style)
442
+ else:
443
+ action_button.setToolTip(
444
+ "Action temporarily disabled. Connect to MM or wait for acquisition to finish."
445
+ )
446
+ action_button.setStyleSheet(self.disabled_button_style)
447
+
448
+ def _enable_buttons(self):
449
+ self._set_buttons_enabled(True)
450
+
451
+ def _disable_buttons(self):
452
+ self._set_buttons_enabled(False)
453
+
454
+ def _handle_error(self, exc):
455
+ """
456
+ Handles errors from calibration and restores Micro-Manager to its state prior to the start of calibration
457
+ Parameters
458
+ ----------
459
+ exc: (Error) Propogated error message to display
460
+
461
+ Returns
462
+ -------
463
+
464
+ """
465
+
466
+ self.ui.tb_calib_assessment.setText(f"Error: {str(exc)}")
467
+ self.ui.tb_calib_assessment.setStyleSheet(
468
+ "border: 1px solid rgb(200,0,0);"
469
+ )
470
+
471
+ # Reset ROI if it was cropped down during reconstruction
472
+ if self.use_cropped_roi:
473
+ self.mmc.clearROI()
474
+
475
+ # Reset the autoshutter setting if errored during blacklevel calculation
476
+ self.mmc.setAutoShutter(self.auto_shutter)
477
+
478
+ # Reset the progress bar to 0
479
+ self.ui.progress_bar.setValue(0)
480
+
481
+ # Raise the error
482
+ raise exc
483
+
484
+ def _handle_calib_abort(self):
485
+ if self.use_cropped_roi:
486
+ self.mmc.clearROI()
487
+ self.mmc.setAutoShutter(self.auto_shutter)
488
+ self.ui.progress_bar.setValue(0)
489
+
490
+ def _handle_acq_error(self, exc):
491
+ raise exc
492
+
493
+ def _handle_load_finished(self):
494
+ """
495
+ Updates the calibration assessment when the user loads a previous calibration metadata file.
496
+
497
+ Returns
498
+ -------
499
+
500
+ """
501
+ self.ui.tb_calib_assessment.setText(
502
+ "Previous calibration successfully loaded"
503
+ )
504
+ self.ui.tb_calib_assessment.setStyleSheet("border: 1px solid green;")
505
+ self.ui.progress_bar.setValue(100)
506
+
507
+ def _update_calib(self, val):
508
+ self.calib = val
509
+
510
+ def _check_line_edit(self, name):
511
+ """
512
+ Convenience function used in checking whether a line edit is present or missing. Will place a red border
513
+ around the line edit if it is empty, otherwise it will remove the red border.
514
+
515
+ Parameters
516
+ ----------
517
+ name: (str) name of the LineEdit element as specified in QT Designer file.
518
+
519
+ Returns
520
+ -------
521
+
522
+ """
523
+ le = getattr(self.ui, f"le_{name}")
524
+ text = le.text()
525
+
526
+ if text == "":
527
+ le.setStyleSheet("border: 1px solid rgb(200,0,0);")
528
+ return False
529
+ else:
530
+ le.setStyleSheet("")
531
+ return True
532
+
533
+ @Slot(bool)
534
+ def toggle_mm_connection(self):
535
+ """
536
+ Toggles MM connection and updates the corresponding GUI elements.
537
+ """
538
+ if self.connected_to_mm:
539
+ self.ui.qbutton_connect_to_mm.setText("Connect to MM")
540
+ self.ui.le_mm_status.setText("Disconnected")
541
+ self.ui.le_mm_status.setStyleSheet(
542
+ "border: 1px solid rgb(200,0,0); color: rgb(200,0,0);"
543
+ )
544
+ self.connected_to_mm = False
545
+ self._set_buttons_enabled(False)
546
+ self.ui.cb_config_group.clear()
547
+
548
+ else:
549
+ try:
550
+ self.connect_to_mm()
551
+ self.ui.qbutton_connect_to_mm.setText("Disconnect from MM")
552
+ self.ui.le_mm_status.setText("Connected")
553
+ self.ui.le_mm_status.setStyleSheet(
554
+ "border: 1px solid green; color: green;"
555
+ )
556
+ self.connected_to_mm = True
557
+ self._set_buttons_enabled(True)
558
+ except:
559
+ self.ui.le_mm_status.setText("Failed")
560
+ self.ui.le_mm_status.setStyleSheet(
561
+ "border: 1px solid yellow; color: yellow;"
562
+ )
563
+
564
+ @Slot(bool)
565
+ def connect_to_mm(self):
566
+ """
567
+ Establishes the python/java bridge to Micro-Manager. Micro-Manager must be open with a config loaded
568
+ in order for the connection to be successful. On connection, it will populate all of the available config
569
+ groups. Config group choice is used to establish which config group the Polarization states live in.
570
+
571
+ Returns
572
+ -------
573
+
574
+ """
575
+ RECOMMENDED_MM = "20230426"
576
+ ZMQ_TARGET_VERSION = "4.2.0"
577
+ try:
578
+ self.mmc = Core(convert_camel_case=False)
579
+ # Check it works
580
+ self.mmc.getAvailableConfigGroups()
581
+ self.mm = Studio(convert_camel_case=False)
582
+ # Order is important: If the bridge is created before Core, Core will not work
583
+ self.bridge = zmq_bridge._bridge._Bridge()
584
+ logging.debug("Established ZMQ Bridge and found Core and Studio")
585
+ except NameError:
586
+ print("Is pycromanager package installed?")
587
+ except Exception as ex:
588
+ print(
589
+ "Could not establish pycromanager bridge.\n"
590
+ "Is Micro-Manager open?\n"
591
+ "Is Tools > Options > Run server on port 4827 checked?\n"
592
+ f"Are you using nightly build {RECOMMENDED_MM}?\n"
593
+ )
594
+ template = "An exception of type {0} occurred. Arguments:\n{1!r}"
595
+ message = template.format(type(ex).__name__, ", ".join(ex.args))
596
+ print(message)
597
+ raise EnvironmentError(
598
+ "Could not establish pycromanager bridge.\n"
599
+ "Is Micro-Manager open?\n"
600
+ "Is Tools > Options > Run server on port 4827 checked?\n"
601
+ f"Are you using nightly build {RECOMMENDED_MM}?"
602
+ )
603
+
604
+ # Warn the user if there is a Micro-Manager/ZMQ version mismatch
605
+ # NS: Not quite sure what this is good for, we already know the Core works
606
+ # This code uses undocumented PycroManager features, so may well break in the future
607
+ self.bridge._main_socket.send({"command": "connect", "debug": False})
608
+ reply_json = self.bridge._main_socket.receive(timeout=500)
609
+ zmq_mm_version = reply_json["version"]
610
+ if zmq_mm_version != ZMQ_TARGET_VERSION:
611
+ upgrade_str = (
612
+ "upgrade"
613
+ if version.parse(zmq_mm_version)
614
+ < version.parse(ZMQ_TARGET_VERSION)
615
+ else "downgrade"
616
+ )
617
+ logging.warning(
618
+ (
619
+ "This version of Micro-Manager has not been tested with waveorder.\n"
620
+ f"Please {upgrade_str} to Micro-Manager nightly build {RECOMMENDED_MM}."
621
+ )
622
+ )
623
+
624
+ logging.debug("Confirmed correct ZMQ bridge----")
625
+
626
+ # Find config group containing calibration channels
627
+ # calib_channels is typically ['State0', 'State1', 'State2', ...]
628
+ # config_list may be something line ['GFP', 'RFP', 'State0', 'State1', 'State2', ...]
629
+ # config_list may also be of the form ['GFP', 'RFP', 'LF-State0', 'LF-State1', 'LF-State2', ...]
630
+ # in this version of the code we correctly parse 'LF-State0', but these channels cannot be used
631
+ # by the Calibration class.
632
+ # A valid config group contains all channels in calib_channels
633
+ # self.ui.cb_config_group.clear() # This triggers the enter config we will clear when switching off
634
+ groups = self.mmc.getAvailableConfigGroups()
635
+ config_group_found = False
636
+ logging.debug("Checking MM config group")
637
+ for i in range(groups.size()):
638
+ group = groups.get(i)
639
+ configs = self.mmc.getAvailableConfigs(group)
640
+ config_list = []
641
+ for j in range(configs.size()):
642
+ config_list.append(configs.get(j))
643
+ if np.all(
644
+ [
645
+ np.any([ch in config for config in config_list])
646
+ for ch in self.calib_channels
647
+ ]
648
+ ):
649
+ if not config_group_found:
650
+ self.config_group = (
651
+ group # set to first config group found
652
+ )
653
+ config_group_found = True
654
+ self.ui.cb_config_group.addItem(group)
655
+
656
+ logging.debug("Checked configs.")
657
+ if not config_group_found:
658
+ msg = (
659
+ f"No config group contains channels {self.calib_channels}. "
660
+ "Please refer to the waveorder docs on how to set up the config properly."
661
+ )
662
+ self.ui.cb_config_group.setStyleSheet(
663
+ "border: 1px solid rgb(200,0,0);"
664
+ )
665
+ raise KeyError(msg)
666
+
667
+ # set startup LC control mode
668
+ logging.debug("Setting startup LC control mode...")
669
+ _devices = self.mmc.getLoadedDevices()
670
+ loaded_devices = [_devices.get(i) for i in range(_devices.size())]
671
+ if LC_DEVICE_NAME in loaded_devices:
672
+ config_desc = self.mmc.getConfigData(
673
+ "Channel", "State0"
674
+ ).getVerbose()
675
+ if "String send to" in config_desc:
676
+ self.calib_mode = "MM-Retardance"
677
+ self.ui.cb_calib_mode.setCurrentIndex(0)
678
+ if "Voltage (V)" in config_desc:
679
+ self.calib_mode = "MM-Voltage"
680
+ self.ui.cb_calib_mode.setCurrentIndex(1)
681
+ else:
682
+ self.calib_mode = "DAC"
683
+ self.ui.cb_calib_mode.setCurrentIndex(2)
684
+
685
+ logging.debug("Finished connecting to MM.")
686
+
687
+ @Slot(tuple)
688
+ def handle_progress_update(self, value):
689
+ self.ui.progress_bar.setValue(value[0])
690
+ self.ui.label_progress.setText("Progress: " + value[1])
691
+
692
+ @Slot(str)
693
+ def handle_extinction_update(self, value):
694
+ self.ui.le_extinction.setText(value)
695
+
696
+ @Slot(object)
697
+ def handle_plot_update(self, value):
698
+ """
699
+ handles the plotting of the intensity values during calibration. Calibration class will emit a signal
700
+ depending on which stage of the calibration process it is in and then we limit the scaling / range of the plot
701
+ accordingly. After the coarse search of extinction is done, the plot will shift the viewing range to only be
702
+ that of the convex optimization. Full plot will still exist if the user uses their mouse to zoom out.
703
+
704
+ Parameters
705
+ ----------
706
+ value: (float) new intensity value from calibration
707
+
708
+ Returns
709
+ -------
710
+
711
+ """
712
+ self.intensity_monitor.append(value)
713
+ self.ui.plot_widget.plot(self.intensity_monitor)
714
+
715
+ if self.plot_sequence[0] == "Coarse":
716
+ self.plot_item.autoRange()
717
+ else:
718
+ self.plot_item.setRange(
719
+ xRange=(self.plot_sequence[1], len(self.intensity_monitor)),
720
+ yRange=(
721
+ 0,
722
+ np.max(self.intensity_monitor[self.plot_sequence[1] :]),
723
+ ),
724
+ padding=0.1,
725
+ )
726
+
727
+ @Slot(str)
728
+ def handle_calibration_assessment_update(self, value):
729
+ self.calib_assessment_level = value
730
+
731
+ @Slot(str)
732
+ def handle_calibration_assessment_msg_update(self, value):
733
+ self.ui.tb_calib_assessment.setText(value)
734
+
735
+ if self.calib_assessment_level == "good":
736
+ self.ui.tb_calib_assessment.setStyleSheet(
737
+ "border: 1px solid green;"
738
+ )
739
+ elif self.calib_assessment_level == "okay":
740
+ self.ui.tb_calib_assessment.setStyleSheet(
741
+ "border: 1px solid rgb(252,190,3);"
742
+ )
743
+ elif self.calib_assessment_level == "bad":
744
+ self.ui.tb_calib_assessment.setStyleSheet(
745
+ "border: 1px solid rgb(200,0,0);"
746
+ )
747
+ else:
748
+ pass
749
+
750
+ @Slot(tuple)
751
+ def handle_lc_states_emit(self, value: tuple[tuple, dict[str, list]]):
752
+ """Receive and plot polarization state and calibrated LC retardance values from the calibration worker.
753
+
754
+ Parameters
755
+ ----------
756
+ value : tuple[tuple, dict[str, list]]
757
+ 2-tuple consisting of a tuple of polarization state names and a dictionary of LC retardance values.
758
+ """
759
+ pol_states, lc_values = value
760
+
761
+ # Calculate circle
762
+ theta = np.linspace(0, 2 * np.pi, 100)
763
+ x_circ = self.swing * np.cos(theta) + lc_values["LCA"][0]
764
+ y_circ = self.swing * np.sin(theta) + lc_values["LCB"][0]
765
+
766
+ import matplotlib.pyplot as plt
767
+
768
+ plt.close("all")
769
+ with (
770
+ plt.rc_context(
771
+ {
772
+ "axes.spines.right": False,
773
+ "axes.spines.top": False,
774
+ }
775
+ )
776
+ and plt.ion()
777
+ ):
778
+ plt.figure("Calibrated LC States")
779
+ plt.scatter(lc_values["LCA"], lc_values["LCB"], c="r")
780
+ plt.plot(x_circ, y_circ, "k--", alpha=0.25)
781
+ plt.axis("equal")
782
+ plt.xlabel("LCA retardance")
783
+ plt.ylabel("LCB retardance")
784
+ for i, pol in enumerate(pol_states):
785
+ plt.annotate(
786
+ pol,
787
+ xy=(lc_values["LCA"][i], lc_values["LCB"][i]),
788
+ xycoords="data",
789
+ xytext=(10, 10), # annotation offset
790
+ textcoords="offset points",
791
+ )
792
+
793
+ def _add_or_update_image_layer(
794
+ self,
795
+ image: NDArray,
796
+ name: str,
797
+ cmap: str = "gray",
798
+ move_to_top: bool = True,
799
+ scale: tuple = 5 * (1,),
800
+ ):
801
+ """Add image layer of the given name if it does not exist, update existing layer otherwise.
802
+
803
+ Parameters
804
+ ----------
805
+ image : NDArray
806
+ image intensity values
807
+ name : str
808
+ layer key name in napari layers list
809
+ cmap : str, optional
810
+ colormap to render in, by default "gray", use "rgb" for RGB images
811
+ move_to_top : bool, optional
812
+ whether to move the updated layer to the top of layers list, by default True
813
+ """
814
+ if image.shape[0] == 1:
815
+ image = image.squeeze(axis=0)
816
+ scale = scale[1:]
817
+
818
+ scale = scale[-image.ndim :] # match shapes
819
+
820
+ if name in self.viewer.layers:
821
+ self.viewer.layers[name].data = image
822
+ if move_to_top:
823
+ logging.debug(f"Moving layer {name} to the top.")
824
+ src_index = self.viewer.layers.index(name)
825
+ self.viewer.layers.move(src_index, dest_index=-1)
826
+ else:
827
+ if cmap == "rgb":
828
+ self.viewer.add_image(
829
+ image,
830
+ name=name,
831
+ rgb=True,
832
+ scale=scale,
833
+ cache=False,
834
+ )
835
+ else:
836
+ self.viewer.add_image(
837
+ image,
838
+ name=name,
839
+ colormap=cmap,
840
+ scale=scale,
841
+ cache=False,
842
+ )
843
+
844
+ @Slot(tuple)
845
+ def handle_bg_image_update(self, value):
846
+ data, scale = value
847
+ self._add_or_update_image_layer(data, "Raw Background", scale=scale)
848
+
849
+ @Slot(tuple)
850
+ def handle_bg_bire_image_update(self, value):
851
+ data, scale = value
852
+ self._add_or_update_image_layer(
853
+ data[0], "Retardance Background", scale=scale
854
+ )
855
+ self._add_or_update_image_layer(
856
+ data[1], "Orientation Background", cmap="hsv", scale=scale
857
+ )
858
+
859
+ def handle_layers_updated(self, event: Event):
860
+ """Whenever a layer is inserted or moved, we check if the top layer
861
+ starts with 'Orientation*'. If it is, we search for a layer that starts
862
+ with 'Retardance*' and has the same suffix as 'Orientation*', then use the
863
+ 'Orientation*'-'Retardance*' pair to generate a 'BirefringenceOverlay*'
864
+ layer.
865
+
866
+ We also color the 'Orientation*' layer in an HSV colormap.
867
+ """
868
+
869
+ layers: LayerList = event.source
870
+ # if the first channel starts with "Orientation"
871
+ if layers[-1].name.startswith("Orientation"):
872
+ orientation_name = layers[-1].name
873
+ suffix = orientation_name.replace("Orientation", "")
874
+ retardance_name = "Retardance" + suffix
875
+ overlay_name = "Birefringence Overlay" + suffix
876
+ # if the matching retardance layer is present, generate an overlay
877
+ if retardance_name in layers:
878
+ logging.info(
879
+ "Detected updated birefringence layers: "
880
+ f"'{retardance_name}', '{orientation_name}'"
881
+ )
882
+ self._draw_bire_overlay(
883
+ retardance_name,
884
+ orientation_name,
885
+ overlay_name,
886
+ scale=layers[-1].scale,
887
+ )
888
+
889
+ # always display layers that start with "Orientation" in hsv
890
+ logging.info(
891
+ "Detected orientation layer in updated layer list."
892
+ "Setting its colormap to HSV."
893
+ )
894
+ self.viewer.layers[orientation_name].colormap = "hsv"
895
+
896
+ def _draw_bire_overlay(
897
+ self,
898
+ retardance_name: str,
899
+ orientation_name: str,
900
+ overlay_name: str,
901
+ scale: tuple,
902
+ ):
903
+ def _layer_data(name: str):
904
+ data = self.viewer.layers[name].data
905
+ if isinstance(data, da.Array):
906
+ # the ome-zarr reader will read HCS plates/wells as nested dask graph
907
+ # which will contain 'get_tile' or 'get_field' in its graph
908
+ # this object will remain a dask `Array` after calling `compute()`
909
+ if any([("get_" in k) for k in data.dask.keys()]):
910
+ data: da.Array = data.compute()
911
+ else:
912
+ chunks = (data.ndim - 2) * (1,) + data.shape[
913
+ -2:
914
+ ] # needs to match
915
+ data = da.from_array(data, chunks=chunks)
916
+ return data
917
+
918
+ self.overlay_scale = scale
919
+ self.overlay_name = overlay_name
920
+ self.overlay_retardance = _layer_data(retardance_name)
921
+ self.overlay_orientation = _layer_data(orientation_name)
922
+ self.update_overlay_dask_array()
923
+
924
+ def update_overlay_dask_array(self):
925
+ self.rgb_chunks = (
926
+ (3,)
927
+ + (self.overlay_retardance.ndim - 2) * (1,)
928
+ + self.overlay_retardance.shape[-2:]
929
+ )
930
+ overlay = da.map_blocks(
931
+ ret_ori_overlay,
932
+ np.stack((self.overlay_retardance, self.overlay_orientation)),
933
+ ret_max=self.ret_max,
934
+ cmap=self.colormap,
935
+ chunks=self.rgb_chunks,
936
+ dtype=np.float32,
937
+ drop_axis=0,
938
+ new_axis=0,
939
+ )
940
+
941
+ overlay = da.moveaxis(overlay, source=0, destination=-1)
942
+
943
+ self._add_or_update_image_layer(
944
+ overlay, self.overlay_name, cmap="rgb", scale=self.overlay_scale
945
+ )
946
+
947
+ @Slot(tuple)
948
+ def handle_bire_image_update(self, value):
949
+ data, scale = value
950
+
951
+ # generate overlay in a separate thread
952
+ for i, channel in enumerate(("Retardance", "Orientation")):
953
+ name = channel
954
+ cmap = "gray" if channel != "Orientation" else "hsv"
955
+ self._add_or_update_image_layer(
956
+ data[i], name, cmap=cmap, scale=scale
957
+ )
958
+
959
+ @Slot(tuple)
960
+ def handle_phase_image_update(self, value):
961
+ phase, scale = value
962
+ # Determine name based on data dimensionality
963
+ name = (
964
+ "Phase2D"
965
+ if phase.ndim == 2 or (phase.ndim > 2 and phase.shape[0] == 1)
966
+ else "Phase3D"
967
+ )
968
+
969
+ # Add new layer if none exists, otherwise update layer data
970
+ self._add_or_update_image_layer(phase, name, scale=scale)
971
+
972
+ if "Phase" not in [
973
+ self.ui.cb_saturation.itemText(i)
974
+ for i in range(self.ui.cb_saturation.count())
975
+ ]:
976
+ self.ui.cb_saturation.addItem("Retardance")
977
+ if "Phase" not in [
978
+ self.ui.cb_value.itemText(i)
979
+ for i in range(self.ui.cb_value.count())
980
+ ]:
981
+ self.ui.cb_value.addItem("Retardance")
982
+
983
+ @Slot(object)
984
+ def handle_qlipp_reconstructor_update(self, value: waveorder_microscopy):
985
+ # Saves phase reconstructor to be re-used if possible
986
+ self.phase_reconstructor = value
987
+
988
+ @Slot(Path)
989
+ def handle_calib_file_update(self, value):
990
+ self.last_calib_meta_file = value
991
+
992
+ @Slot(str)
993
+ def handle_plot_sequence_update(self, value):
994
+ current_idx = len(self.intensity_monitor)
995
+ self.plot_sequence = (value, current_idx)
996
+
997
+ @Slot(tuple)
998
+ def handle_sat_slider_move(self, value):
999
+ self.ui.le_sat_min.setText(str(np.round(value[0], 3)))
1000
+ self.ui.le_sat_max.setText(str(np.round(value[1], 3)))
1001
+
1002
+ @Slot(tuple)
1003
+ def handle_val_slider_move(self, value):
1004
+ self.ui.le_val_min.setText(str(np.round(value[0], 3)))
1005
+ self.ui.le_val_max.setText(str(np.round(value[1], 3)))
1006
+
1007
+ @Slot(str)
1008
+ def handle_reconstruction_store_update(self, value):
1009
+ self.reconstruction_data_path = value
1010
+
1011
+ @Slot(bool)
1012
+ def browse_dir_path(self):
1013
+ result = self._open_file_dialog(self.current_dir_path, "dir")
1014
+ self.directory = result
1015
+ self.current_dir_path = result
1016
+ self.ui.le_directory.setText(result)
1017
+
1018
+ @Slot(bool)
1019
+ def browse_data_dir(self):
1020
+ path = self._open_file_dialog(self.data_dir, "dir")
1021
+ self.data_dir = path
1022
+ self.ui.le_data_dir.setText(self.data_dir)
1023
+
1024
+ @Slot(bool)
1025
+ def browse_calib_meta(self):
1026
+ path = self._open_file_dialog(self.calib_path, "file")
1027
+ self.calib_path = path
1028
+ self.ui.le_calibration_metadata.setText(self.calib_path)
1029
+
1030
+ @Slot()
1031
+ def enter_dir_path(self):
1032
+ path = self.ui.le_directory.text()
1033
+ if os.path.exists(path):
1034
+ self.directory = path
1035
+ else:
1036
+ self.ui.le_directory.setText("Path Does Not Exist")
1037
+
1038
+ @Slot()
1039
+ def enter_swing(self):
1040
+ self.swing = float(self.ui.le_swing.text())
1041
+
1042
+ @Slot()
1043
+ def enter_wavelength(self):
1044
+ self.wavelength = int(self.ui.le_wavelength.text())
1045
+
1046
+ @Slot()
1047
+ def enter_calib_scheme(self):
1048
+ index = self.ui.cb_calib_scheme.currentIndex()
1049
+ if index == 0:
1050
+ self.calib_scheme = "4-State"
1051
+ else:
1052
+ self.calib_scheme = "5-State"
1053
+
1054
+ @Slot()
1055
+ def enter_calib_mode(self):
1056
+ index = self.ui.cb_calib_mode.currentIndex()
1057
+ if index == 0:
1058
+ self.calib_mode = "MM-Retardance"
1059
+ self.ui.label_lca.hide()
1060
+ self.ui.label_lcb.hide()
1061
+ self.ui.cb_lca.hide()
1062
+ self.ui.cb_lcb.hide()
1063
+ elif index == 1:
1064
+ self.calib_mode = "MM-Voltage"
1065
+ self.ui.label_lca.hide()
1066
+ self.ui.label_lcb.hide()
1067
+ self.ui.cb_lca.hide()
1068
+ self.ui.cb_lcb.hide()
1069
+ elif index == 2:
1070
+ self.calib_mode = "DAC"
1071
+ self.ui.cb_lca.clear()
1072
+ self.ui.cb_lcb.clear()
1073
+ self.ui.cb_lca.show()
1074
+ self.ui.cb_lcb.show()
1075
+ self.ui.label_lca.show()
1076
+ self.ui.label_lcb.show()
1077
+
1078
+ cfg = self.mmc.getConfigData(self.config_group, "State0")
1079
+
1080
+ # Update the DAC combo boxes with available DAC's from the config. Necessary for the user
1081
+ # to specify which DAC output corresponds to which LC for voltage-space calibration
1082
+ memory = set()
1083
+ for i in range(cfg.size()):
1084
+ prop = cfg.getSetting(i)
1085
+ if "TS_DAC" in prop.getDeviceLabel():
1086
+ dac = prop.getDeviceLabel()[-2:]
1087
+ if dac not in memory:
1088
+ self.ui.cb_lca.addItem("DAC" + dac)
1089
+ self.ui.cb_lcb.addItem("DAC" + dac)
1090
+ memory.add(dac)
1091
+ else:
1092
+ continue
1093
+ self.ui.cb_lca.setCurrentIndex(0)
1094
+ self.ui.cb_lcb.setCurrentIndex(1)
1095
+
1096
+ @Slot()
1097
+ def enter_dac_lca(self):
1098
+ dac = self.ui.cb_lca.currentText()
1099
+ self.lca_dac = dac
1100
+
1101
+ @Slot()
1102
+ def enter_dac_lcb(self):
1103
+ dac = self.ui.cb_lcb.currentText()
1104
+ self.lcb_dac = dac
1105
+
1106
+ @Slot()
1107
+ def enter_config_group(self):
1108
+ """
1109
+ callback for changing the config group combo box. User needs to specify a config group that has the
1110
+ hardcoded states 'State0', 'State1', ... , 'State4'. Calibration will not work unless a proper config
1111
+ group is specific
1112
+
1113
+ Returns
1114
+ -------
1115
+
1116
+ """
1117
+ # if/else takes care of the clearing of config
1118
+ if self.ui.cb_config_group.count() != 0:
1119
+ self.mmc = Core(convert_camel_case=False)
1120
+ self.mm = Studio(convert_camel_case=False)
1121
+
1122
+ # Gather config groups and their children
1123
+ self.config_group = self.ui.cb_config_group.currentText()
1124
+ config = self.mmc.getAvailableConfigs(self.config_group)
1125
+
1126
+ channels = []
1127
+ for i in range(config.size()):
1128
+ channels.append(config.get(i))
1129
+
1130
+ # Check to see if any states are missing
1131
+ states = ["State0", "State1", "State2", "State3", "State4"]
1132
+ missing = []
1133
+ for state in states:
1134
+ if state not in channels:
1135
+ missing.append(state)
1136
+
1137
+ # if states are missing, set the combo box red and alert the user
1138
+ if len(missing) != 0:
1139
+ msg = (
1140
+ f"The chosen config group ({self.config_group}) is missing states: {missing}. "
1141
+ "Please refer to the waveorder wiki on how to set up the config properly."
1142
+ )
1143
+
1144
+ self.ui.cb_config_group.setStyleSheet(
1145
+ "border: 1px solid rgb(200,0,0);"
1146
+ )
1147
+ raise KeyError(msg)
1148
+ else:
1149
+ self.ui.cb_config_group.setStyleSheet("")
1150
+
1151
+ @Slot()
1152
+ def enter_bg_folder_name(self):
1153
+ self.bg_folder_name = self.ui.le_bg_folder.text()
1154
+
1155
+ @Slot()
1156
+ def enter_n_avg(self):
1157
+ self.n_avg = int(self.ui.le_n_avg.text())
1158
+
1159
+ @Slot()
1160
+ def enter_log_level(self):
1161
+ index = self.ui.cb_loglevel.currentIndex()
1162
+ if index == 0:
1163
+ logging.getLogger().setLevel(logging.INFO)
1164
+ else:
1165
+ logging.getLogger().setLevel(logging.DEBUG)
1166
+
1167
+ @Slot()
1168
+ def enter_pause_updates(self):
1169
+ """
1170
+ pauses the updating of the dimension slider for offline reconstruction or live listening mode.
1171
+
1172
+ Returns
1173
+ -------
1174
+
1175
+ """
1176
+ state = self.ui.chb_pause_updates.checkState()
1177
+ if state == 2:
1178
+ self.pause_updates = True
1179
+ elif state == 0:
1180
+ self.pause_updates = False
1181
+
1182
+ @Slot(int)
1183
+ def enter_method(self):
1184
+ """
1185
+ Handles the updating of UI elements depending on the method of offline reconstruction.
1186
+
1187
+ Returns
1188
+ -------
1189
+
1190
+ """
1191
+
1192
+ idx = self.ui.cb_method.currentIndex()
1193
+
1194
+ if idx == 0:
1195
+ self.method = "QLIPP"
1196
+ self.ui.label_bf_chan.hide()
1197
+ self.ui.le_bf_chan.hide()
1198
+ self.ui.label_chan_desc.setText(
1199
+ "Retardance, Orientation, BF, Phase3D, Phase2D, S0, S1, S2, S3"
1200
+ )
1201
+
1202
+ elif idx == 1:
1203
+ self.method = "PhaseFromBF"
1204
+ self.ui.label_bf_chan.show()
1205
+ self.ui.le_bf_chan.show()
1206
+ self.ui.label_bf_chan.setText("Brightfield Channel Index")
1207
+ self.ui.le_bf_chan.setPlaceholderText("int")
1208
+ self.ui.label_chan_desc.setText("Phase3D, Phase2D")
1209
+
1210
+ @Slot(int)
1211
+ def enter_mode(self):
1212
+ idx = self.ui.cb_mode.currentIndex()
1213
+
1214
+ if idx == 0:
1215
+ self.mode = "3D"
1216
+ self.ui.label_focus_zidx.hide()
1217
+ self.ui.le_focus_zidx.hide()
1218
+ else:
1219
+ self.mode = "2D"
1220
+ self.ui.label_focus_zidx.show()
1221
+ self.ui.le_focus_zidx.show()
1222
+
1223
+ @Slot()
1224
+ def enter_data_dir(self):
1225
+ entry = self.ui.le_data_dir.text()
1226
+ if not os.path.exists(entry):
1227
+ self.ui.le_data_dir.setStyleSheet(
1228
+ "border: 1px solid rgb(200,0,0);"
1229
+ )
1230
+ self.ui.le_data_dir.setText("Path Does Not Exist")
1231
+ else:
1232
+ self.ui.le_data_dir.setStyleSheet("")
1233
+ self.data_dir = entry
1234
+
1235
+ @Slot()
1236
+ def enter_calib_meta(self):
1237
+ entry = self.ui.le_calibration_metadata.text()
1238
+ if not os.path.exists(entry):
1239
+ self.ui.le_calibration_metadata.setStyleSheet(
1240
+ "border: 1px solid rgb(200,0,0);"
1241
+ )
1242
+ self.ui.le_calibration_metadata.setText("Path Does Not Exist")
1243
+ else:
1244
+ self.ui.le_calibration_metadata.setStyleSheet("")
1245
+ self.calib_path = entry
1246
+
1247
+ @Slot(bool)
1248
+ def push_note(self):
1249
+ """
1250
+ Pushes a note to the last calibration metadata file.
1251
+
1252
+ Returns
1253
+ -------
1254
+
1255
+ """
1256
+
1257
+ # make sure the user has performed a calibration in this session (or loaded a previous one)
1258
+ if not self.last_calib_meta_file:
1259
+ raise ValueError(
1260
+ "No calibration has been performed yet so there is no previous metadata file"
1261
+ )
1262
+ else:
1263
+ note = self.ui.le_notes_field.text()
1264
+
1265
+ # Open the existing calibration metadata file and append the notes
1266
+ with open(self.last_calib_meta_file, "r") as file:
1267
+ current_json = json.load(file)
1268
+
1269
+ # Append note to the end of the old note (so we don't overwrite previous notes) or write a new
1270
+ # note in the blank notes field
1271
+ old_note = current_json["Notes"]
1272
+ if old_note is None or old_note == "" or old_note == note:
1273
+ current_json["Notes"] = note
1274
+ else:
1275
+ current_json["Notes"] = old_note + ", " + note
1276
+
1277
+ # dump the contents into the metadata file
1278
+ with open(self.last_calib_meta_file, "w") as file:
1279
+ json.dump(current_json, file, indent=1)
1280
+
1281
+ @Slot(bool)
1282
+ def calc_extinction(self):
1283
+ """
1284
+ Calculates the extinction when the user uses the Load Calibration functionality. This if performed
1285
+ because the calibration file could be loaded in a different FOV which may require recalibration
1286
+ depending on the extinction quality.
1287
+
1288
+ Returns
1289
+ -------
1290
+
1291
+ """
1292
+
1293
+ # Snap images from the extinction state and first elliptical state
1294
+ set_lc_state(self.mmc, self.config_group, "State0")
1295
+ extinction = snap_and_average(self.calib.snap_manager)
1296
+ set_lc_state(self.mmc, self.config_group, "State1")
1297
+ state1 = snap_and_average(self.calib.snap_manager)
1298
+
1299
+ # Calculate extinction based off captured intensities
1300
+ extinction = self.calib.calculate_extinction(
1301
+ self.swing, self.calib.I_Black, extinction, state1
1302
+ )
1303
+ self.ui.le_extinction.setText(str(extinction))
1304
+
1305
+ @Slot(bool)
1306
+ def load_calibration(self):
1307
+ """
1308
+ Uses previous JSON calibration metadata to load previous calibration
1309
+ """
1310
+
1311
+ metadata_path = self._open_file_dialog(self.current_dir_path, "file")
1312
+ metadata = MetadataReader(metadata_path)
1313
+
1314
+ # Update Properties
1315
+ self.wavelength = metadata.Wavelength
1316
+ self.swing = metadata.Swing
1317
+
1318
+ # Initialize calibration class
1319
+ self.calib = QLIPP_Calibration(
1320
+ self.mmc,
1321
+ self.mm,
1322
+ group=self.config_group,
1323
+ lc_control_mode=self.calib_mode,
1324
+ interp_method=self.interp_method,
1325
+ wavelength=self.wavelength,
1326
+ )
1327
+ self.calib.swing = self.swing
1328
+ self.ui.le_swing.setText(str(self.swing))
1329
+ self.calib.wavelength = self.wavelength
1330
+ self.ui.le_wavelength.setText(str(self.wavelength))
1331
+
1332
+ # Update Calibration Scheme Combo Box
1333
+ if metadata.Calibration_scheme == "4-State":
1334
+ self.ui.cb_calib_scheme.setCurrentIndex(0)
1335
+ else:
1336
+ self.ui.cb_calib_scheme.setCurrentIndex(1)
1337
+
1338
+ self.last_calib_meta_file = metadata_path
1339
+
1340
+ # Move the load calibration function to a separate thread
1341
+ self.worker = load_calibration(self.calib, metadata)
1342
+
1343
+ def update_extinction(extinction):
1344
+ self.calib.extinction_ratio = float(extinction)
1345
+
1346
+ # FIXME: for 1.0.0 we'd like to avoid MM call in the main thread
1347
+ # Make sure Live Mode is off
1348
+ if self.calib.snap_manager.getIsLiveModeOn():
1349
+ self.calib.snap_manager.setLiveModeOn(False)
1350
+
1351
+ # initialize worker properties for multi-threading
1352
+ self.ui.qbutton_stop_calib.clicked.connect(self.worker.quit)
1353
+ self.worker.yielded.connect(self.ui.le_extinction.setText)
1354
+ self.worker.yielded.connect(update_extinction)
1355
+ self.worker.returned.connect(self._update_calib)
1356
+ self.worker.errored.connect(self._handle_error)
1357
+ self.worker.started.connect(self._disable_buttons)
1358
+ self.worker.finished.connect(self._enable_buttons)
1359
+ self.worker.finished.connect(self._handle_load_finished)
1360
+ self.worker.start()
1361
+
1362
+ @Slot(bool)
1363
+ def run_calibration(self):
1364
+ """
1365
+ Wrapper function to create calibration worker and move that worker to a thread.
1366
+ Calibration is then executed by the calibration worker
1367
+ """
1368
+
1369
+ self._check_MM_config_setup()
1370
+
1371
+ self.calib = QLIPP_Calibration(
1372
+ self.mmc,
1373
+ self.mm,
1374
+ group=self.config_group,
1375
+ lc_control_mode=self.calib_mode,
1376
+ interp_method=self.interp_method,
1377
+ wavelength=self.wavelength,
1378
+ )
1379
+
1380
+ if self.calib_mode == "DAC":
1381
+ self.calib.set_dacs(self.lca_dac, self.lcb_dac)
1382
+
1383
+ # Reset Styling
1384
+ self.ui.tb_calib_assessment.setText("")
1385
+ self.ui.tb_calib_assessment.setStyleSheet("")
1386
+
1387
+ # Save initial autoshutter state for when we set it back later
1388
+ self.auto_shutter = self.mmc.getAutoShutter()
1389
+
1390
+ logging.info("Starting Calibration")
1391
+
1392
+ # Initialize displays + parameters for calibration
1393
+ self.ui.progress_bar.setValue(0)
1394
+ self.plot_item.clear()
1395
+ self.intensity_monitor = []
1396
+ self.calib.swing = self.swing
1397
+ self.calib.wavelength = self.wavelength
1398
+ self.calib.meta_file = os.path.join(
1399
+ self.directory, "polarization_calibration.txt"
1400
+ )
1401
+
1402
+ # FIXME: for 1.0.0 we'd like to avoid MM call in the main thread
1403
+ # Make sure Live Mode is off
1404
+ if self.calib.snap_manager.getIsLiveModeOn():
1405
+ self.calib.snap_manager.setLiveModeOn(False)
1406
+
1407
+ # Init Worker and Thread
1408
+ self.worker = CalibrationWorker(self, self.calib)
1409
+
1410
+ # Connect Handlers
1411
+ self.worker.progress_update.connect(self.handle_progress_update)
1412
+ self.worker.extinction_update.connect(self.handle_extinction_update)
1413
+ self.worker.intensity_update.connect(self.handle_plot_update)
1414
+ self.worker.calib_assessment.connect(
1415
+ self.handle_calibration_assessment_update
1416
+ )
1417
+ self.worker.calib_assessment_msg.connect(
1418
+ self.handle_calibration_assessment_msg_update
1419
+ )
1420
+ self.worker.calib_file_emit.connect(self.handle_calib_file_update)
1421
+ self.worker.plot_sequence_emit.connect(
1422
+ self.handle_plot_sequence_update
1423
+ )
1424
+ self.worker.lc_states.connect(self.handle_lc_states_emit)
1425
+ self.worker.started.connect(self._disable_buttons)
1426
+ self.worker.finished.connect(self._enable_buttons)
1427
+ self.worker.errored.connect(self._handle_error)
1428
+ self.ui.qbutton_stop_calib.clicked.connect(self.worker.quit)
1429
+
1430
+ self.worker.start()
1431
+
1432
+ @property
1433
+ def _channel_descriptions(self):
1434
+ return [
1435
+ self.mmc.getConfigData(
1436
+ self.config_group, calib_channel
1437
+ ).getVerbose()
1438
+ for calib_channel in self.calib_channels
1439
+ ]
1440
+
1441
+ def _check_MM_config_setup(self):
1442
+ # Warns the user if the MM configuration is not correctly set up.
1443
+ desc = self._channel_descriptions
1444
+ if self.calib_mode == "MM-Retardance":
1445
+ if all("String send to" in s for s in desc) and not any(
1446
+ "Voltage (V)" in s for s in desc
1447
+ ):
1448
+ return
1449
+ else:
1450
+ msg = " \n".join(
1451
+ textwrap.wrap(
1452
+ "In 'MM-Retardance' mode each preset must include the "
1453
+ "'String send to' property, and no 'Voltage' properties.",
1454
+ width=40,
1455
+ )
1456
+ )
1457
+ show_warning(msg)
1458
+
1459
+ elif self.calib_mode == "MM-Voltage":
1460
+ if (
1461
+ all("Voltage (V) LC-A" in s for s in desc)
1462
+ and all("Voltage (V) LC-B" in s for s in desc)
1463
+ and not any("String send to" in s for s in desc)
1464
+ ):
1465
+ return
1466
+ else:
1467
+ msg = " \n".join(
1468
+ textwrap.wrap(
1469
+ "In 'MM-Voltage' mode each preset must include the 'Voltage (V) LC-A' "
1470
+ "property, the 'Voltage (V) LC-B' property, and no 'String send to' properties.",
1471
+ width=40,
1472
+ )
1473
+ )
1474
+ show_warning(msg)
1475
+
1476
+ elif self.calib_mode == "DAC":
1477
+ _devices = self.mmc.getLoadedDevices()
1478
+ loaded_devices = [_devices.get(i) for i in range(_devices.size())]
1479
+ if LC_DEVICE_NAME in loaded_devices:
1480
+ show_warning(
1481
+ "In 'DAC' mode the MeadowLarkLC device adapter must not be loaded in MM."
1482
+ )
1483
+
1484
+ else:
1485
+ raise ValueError(
1486
+ f"self.calib_mode = {self.calib_mode} is an unrecognized state."
1487
+ )
1488
+
1489
+ @Slot(bool)
1490
+ def capture_bg(self):
1491
+ """
1492
+ Wrapper function to capture a set of background images. Will snap images and display reconstructed
1493
+ birefringence. Check connected handlers for napari display.
1494
+
1495
+ Returns
1496
+ -------
1497
+
1498
+ """
1499
+
1500
+ if self.calib is None:
1501
+ no_calibration_message = """Capturing a background requires calibrated liquid crystals. \
1502
+ Please either run a calibration or load a calibration from file."""
1503
+ raise RuntimeError(no_calibration_message)
1504
+
1505
+ # Init worker and thread
1506
+ self.worker = BackgroundCaptureWorker(self, self.calib)
1507
+
1508
+ # Connect Handlers
1509
+ self.worker.bg_image_emitter.connect(self.handle_bg_image_update)
1510
+ self.worker.bire_image_emitter.connect(
1511
+ self.handle_bg_bire_image_update
1512
+ )
1513
+
1514
+ self.worker.started.connect(self._disable_buttons)
1515
+ self.worker.finished.connect(self._enable_buttons)
1516
+ self.worker.errored.connect(self._handle_error)
1517
+ self.ui.qbutton_stop_calib.clicked.connect(self.worker.quit)
1518
+ self.worker.aborted.connect(self._handle_calib_abort)
1519
+
1520
+ # Start Capture Background Thread
1521
+ self.worker.start()
1522
+
1523
+ @Slot(bool)
1524
+ def save_config(self):
1525
+ path = self._open_file_dialog(self.save_config_path, "save")
1526
+ self.save_config_path = path
1527
+ name = PurePath(self.save_config_path).name
1528
+ dir_ = self.save_config_path.strip(name)
1529
+ self._populate_config_from_app()
1530
+
1531
+ if isinstance(self.config_reader.positions, tuple):
1532
+ pos = self.config_reader.positions
1533
+ self.config_reader.positions = (
1534
+ f"[!!python/tuple [{pos[0]},{pos[1]}]]"
1535
+ )
1536
+ if isinstance(self.config_reader.timepoints, tuple):
1537
+ t = self.config_reader.timepoints
1538
+ self.config_reader.timepoints = f"[!!python/tuple [{t[0]},{t[1]}]]"
1539
+
1540
+ self.config_reader.save_yaml(dir_=dir_, name=name)
1541
+
1542
+ @Slot(int)
1543
+ def handle_ret_max_slider_move(self, value):
1544
+ self.ret_max = value
1545
+ self.update_overlay_dask_array()
1546
+
1547
+ @Slot(tuple)
1548
+ def update_dims(self, dims):
1549
+ if not self.pause_updates:
1550
+ self.viewer.dims.set_current_step(0, dims[0])
1551
+ self.viewer.dims.set_current_step(1, dims[1])
1552
+ self.viewer.dims.set_current_step(3, dims[2])
1553
+ else:
1554
+ pass
1555
+
1556
+ def _open_file_dialog(self, default_path, type):
1557
+ return self._open_dialog("select a directory", str(default_path), type)
1558
+
1559
+ def _open_dialog(self, title, ref, type):
1560
+ """
1561
+ opens pop-up dialogue for the user to choose a specific file or directory.
1562
+
1563
+ Parameters
1564
+ ----------
1565
+ title: (str) message to display at the top of the pop up
1566
+ ref: (str) reference path to start the search at
1567
+ type: (str) type of file the user is choosing (dir, file, or save)
1568
+
1569
+ Returns
1570
+ -------
1571
+
1572
+ """
1573
+
1574
+ options = QFileDialog.DontUseNativeDialog
1575
+ if type == "dir":
1576
+ path = QFileDialog.getExistingDirectory(
1577
+ None, title, ref, options=options
1578
+ )
1579
+ elif type == "file":
1580
+ path = QFileDialog.getOpenFileName(
1581
+ None, title, ref, options=options
1582
+ )[0]
1583
+ elif type == "save":
1584
+ path = QFileDialog.getSaveFileName(
1585
+ None, "Choose a save name", ref, options=options
1586
+ )[0]
1587
+ else:
1588
+ raise ValueError("Did not understand file dialogue type")
1589
+
1590
+ return path
1591
+
1592
+
1593
+ class QtLogger(logging.Handler):
1594
+ """
1595
+ Class to changing logging handler to the napari log output display
1596
+ """
1597
+
1598
+ def __init__(self, widget):
1599
+ super().__init__()
1600
+ self.widget = widget
1601
+
1602
+ # emit function necessary to be considered a logging handler
1603
+ def emit(self, record):
1604
+ msg = self.format(record)
1605
+ self.widget.appendPlainText(msg)