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.
- waveorder/_version.py +16 -3
- waveorder/acq/__init__.py +0 -0
- waveorder/acq/acq_functions.py +166 -0
- waveorder/assets/HSV_legend.png +0 -0
- waveorder/assets/JCh_legend.png +0 -0
- waveorder/assets/waveorder_plugin_logo.png +0 -0
- waveorder/calib/Calibration.py +1512 -0
- waveorder/calib/Optimization.py +470 -0
- waveorder/calib/__init__.py +0 -0
- waveorder/calib/calibration_workers.py +464 -0
- waveorder/cli/apply_inverse_models.py +328 -0
- waveorder/cli/apply_inverse_transfer_function.py +379 -0
- waveorder/cli/compute_transfer_function.py +432 -0
- waveorder/cli/gui_widget.py +58 -0
- waveorder/cli/main.py +39 -0
- waveorder/cli/monitor.py +163 -0
- waveorder/cli/option_eat_all.py +47 -0
- waveorder/cli/parsing.py +122 -0
- waveorder/cli/printing.py +16 -0
- waveorder/cli/reconstruct.py +67 -0
- waveorder/cli/settings.py +187 -0
- waveorder/cli/utils.py +175 -0
- waveorder/filter.py +1 -2
- waveorder/focus.py +136 -25
- waveorder/io/__init__.py +0 -0
- waveorder/io/_reader.py +61 -0
- waveorder/io/core_functions.py +272 -0
- waveorder/io/metadata_reader.py +195 -0
- waveorder/io/utils.py +175 -0
- waveorder/io/visualization.py +160 -0
- waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
- waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
- waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
- waveorder/models/isotropic_thin_3d.py +73 -72
- waveorder/models/phase_thick_3d.py +103 -4
- waveorder/napari.yaml +36 -0
- waveorder/plugin/__init__.py +9 -0
- waveorder/plugin/gui.py +1094 -0
- waveorder/plugin/gui.ui +1440 -0
- waveorder/plugin/job_manager.py +42 -0
- waveorder/plugin/main_widget.py +1605 -0
- waveorder/plugin/tab_recon.py +3294 -0
- waveorder/scripts/__init__.py +0 -0
- waveorder/scripts/launch_napari.py +13 -0
- waveorder/scripts/repeat-cal-acq-rec.py +147 -0
- waveorder/scripts/repeat-calibration.py +31 -0
- waveorder/scripts/samples.py +85 -0
- waveorder/scripts/simulate_zarr_acq.py +204 -0
- waveorder/util.py +1 -1
- waveorder/visuals/napari_visuals.py +1 -1
- waveorder-3.0.0.dist-info/METADATA +350 -0
- waveorder-3.0.0.dist-info/RECORD +69 -0
- {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
- waveorder-3.0.0.dist-info/entry_points.txt +5 -0
- {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info/licenses}/LICENSE +13 -1
- waveorder-2.2.1b0.dist-info/METADATA +0 -187
- waveorder-2.2.1b0.dist-info/RECORD +0 -27
- {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)
|