waveorder 3.0.0a2__py3-none-any.whl → 3.0.0a3__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.
@@ -1,650 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import shutil
5
- from pathlib import Path
6
-
7
- # type hint/check
8
- from typing import TYPE_CHECKING
9
-
10
- import numpy as np
11
- from iohub import open_ome_zarr
12
- from iohub.convert import TIFFConverter
13
- from napari.qt.threading import WorkerBase, WorkerBaseSignals
14
- from napari.utils.notifications import show_warning
15
- from qtpy.QtCore import Signal
16
-
17
- from waveorder.acq.acq_functions import (
18
- acquire_from_settings,
19
- generate_acq_settings,
20
- )
21
- from waveorder.cli import settings
22
- from waveorder.cli.apply_inverse_transfer_function import (
23
- _apply_inverse_transfer_function_cli,
24
- )
25
- from waveorder.cli.compute_transfer_function import (
26
- _compute_transfer_function_cli,
27
- )
28
- from waveorder.io.utils import add_index_to_path, model_to_yaml, ram_message
29
-
30
- # avoid runtime import error
31
- if TYPE_CHECKING:
32
- from waveorder.calib.Calibration import QLIPP_Calibration
33
- from waveorder.plugin.main_widget import MainWidget
34
-
35
-
36
- def _check_scale_mismatch(
37
- recon_scale: np.array,
38
- ngff_scale: tuple[float, float, float, float, float],
39
- ) -> None:
40
- if not np.allclose(np.array(ngff_scale[2:]), recon_scale, rtol=1e-2):
41
- show_warning(
42
- f"Requested reconstruction scale = {recon_scale} "
43
- f"and OME-Zarr metadata scale = {ngff_scale[2:]} are not equal. "
44
- "waveorder's reconstruction uses the GUI's "
45
- "Z-step, pixel size, and magnification, "
46
- "while napari's viewer uses the input array's metadata."
47
- )
48
-
49
-
50
- def _generate_reconstruction_config_from_gui(
51
- reconstruction_config_path,
52
- mode,
53
- calib_window,
54
- input_channel_names,
55
- ):
56
- if mode == "birefringence" or mode == "all":
57
- if calib_window.bg_option == "None":
58
- background_path = ""
59
- remove_estimated_background = False
60
- elif calib_window.bg_option == "Measured":
61
- background_path = str(calib_window.acq_bg_directory)
62
- remove_estimated_background = False
63
- elif calib_window.bg_option == "Estimated":
64
- background_path = ""
65
- remove_estimated_background = True
66
- elif calib_window.bg_option == "Measured + Estimated":
67
- background_path = str(calib_window.acq_bg_directory)
68
- remove_estimated_background = True
69
-
70
- birefringence_transfer_function_settings = (
71
- settings.BirefringenceTransferFunctionSettings(
72
- swing=calib_window.swing,
73
- )
74
- )
75
- birefringence_apply_inverse_settings = (
76
- settings.BirefringenceApplyInverseSettings(
77
- wavelength_illumination=calib_window.recon_wavelength
78
- / 1000, # convert from um to nm
79
- background_path=background_path,
80
- remove_estimated_background=remove_estimated_background,
81
- flip_orientation=calib_window.flip_orientation,
82
- rotate_orientation=calib_window.rotate_orientation,
83
- )
84
- )
85
- birefringence_settings = settings.BirefringenceSettings(
86
- transfer_function=birefringence_transfer_function_settings,
87
- apply_inverse=birefringence_apply_inverse_settings,
88
- )
89
- else:
90
- birefringence_settings = None
91
-
92
- if mode == "phase" or mode == "all":
93
- phase_transfer_function_settings = (
94
- settings.PhaseTransferFunctionSettings(
95
- wavelength_illumination=calib_window.recon_wavelength
96
- / 1000, # um
97
- yx_pixel_size=calib_window.ps / calib_window.mag, # um
98
- z_pixel_size=calib_window.z_step, # um
99
- z_padding=calib_window.pad_z,
100
- index_of_refraction_media=calib_window.n_media,
101
- numerical_aperture_detection=calib_window.obj_na,
102
- numerical_aperture_illumination=calib_window.cond_na,
103
- invert_phase_contrast=calib_window.invert_phase_contrast,
104
- )
105
- )
106
- phase_apply_inverse_settings = settings.FourierApplyInverseSettings(
107
- reconstruction_algorithm=calib_window.phase_regularizer,
108
- regularization_strength=calib_window.ui.le_phase_strength.text(),
109
- TV_rho_strength=calib_window.ui.le_rho.text(),
110
- TV_iterations=calib_window.ui.le_itr.text(),
111
- )
112
- phase_settings = settings.PhaseSettings(
113
- transfer_function=phase_transfer_function_settings,
114
- apply_inverse=phase_apply_inverse_settings,
115
- )
116
- else:
117
- phase_settings = None
118
-
119
- reconstruction_settings = settings.ReconstructionSettings(
120
- input_channel_names=input_channel_names,
121
- reconstruction_dimension=int(calib_window.acq_mode[0]),
122
- birefringence=birefringence_settings,
123
- phase=phase_settings,
124
- )
125
-
126
- model_to_yaml(reconstruction_settings, reconstruction_config_path)
127
-
128
-
129
- class PolarizationAcquisitionSignals(WorkerBaseSignals):
130
- """
131
- Custom Signals class that includes napari native signals
132
- """
133
-
134
- phase_image_emitter = Signal(tuple)
135
- bire_image_emitter = Signal(tuple)
136
- phase_reconstructor_emitter = Signal(object)
137
- aborted = Signal()
138
-
139
-
140
- class BFAcquisitionSignals(WorkerBaseSignals):
141
- """
142
- Custom Signals class that includes napari native signals
143
- """
144
-
145
- phase_image_emitter = Signal(tuple)
146
- phase_reconstructor_emitter = Signal(object)
147
- aborted = Signal()
148
-
149
-
150
- class BFAcquisitionWorker(WorkerBase):
151
- """
152
- Class to execute a brightfield acquisition. First step is to snap the images follow by a second
153
- step of reconstructing those images.
154
- """
155
-
156
- def __init__(self, calib_window: MainWidget):
157
- super().__init__(SignalsClass=BFAcquisitionSignals)
158
-
159
- # Save current state of GUI window
160
- self.calib_window = calib_window
161
-
162
- # Init Properties
163
- self.prefix = "snap"
164
- self.dm = self.calib_window.mm.displays()
165
- self.dim = (
166
- "2D"
167
- if self.calib_window.ui.cb_acq_mode.currentIndex() == 0
168
- else "3D"
169
- )
170
- self.img_dim = None
171
-
172
- save_dir = (
173
- self.calib_window.save_directory
174
- if self.calib_window.save_directory
175
- else self.calib_window.directory
176
- )
177
-
178
- if save_dir is None:
179
- raise ValueError(
180
- "save directory is empty, please specify a directory in the plugin"
181
- )
182
-
183
- if self.calib_window.save_name is None:
184
- self.snap_dir = Path(save_dir) / "snap"
185
- else:
186
- self.snap_dir = Path(save_dir) / (
187
- self.calib_window.save_name + "_snap"
188
- )
189
- self.snap_dir = add_index_to_path(self.snap_dir)
190
- self.snap_dir.mkdir()
191
-
192
- def _check_abort(self):
193
- if self.abort_requested:
194
- self.aborted.emit()
195
- raise TimeoutError("Stop Requested")
196
-
197
- def _check_ram(self):
198
- """
199
- Show a warning if RAM < 32 GB.
200
- """
201
- is_warning, msg = ram_message()
202
- if is_warning:
203
- show_warning(msg)
204
- else:
205
- logging.info(msg)
206
-
207
- def work(self):
208
- """
209
- Function that runs the 2D or 3D acquisition and reconstructs the data
210
- """
211
- self._check_ram()
212
- logging.info("Running Acquisition...")
213
- self._check_abort()
214
-
215
- channel_idx = self.calib_window.ui.cb_acq_channel.currentIndex()
216
- channel = self.calib_window.ui.cb_acq_channel.itemText(channel_idx)
217
- channel_group = None
218
-
219
- groups = self.calib_window.mmc.getAvailableConfigGroups()
220
- group_list = []
221
- for i in range(groups.size()):
222
- group_list.append(groups.get(i))
223
-
224
- for group in group_list:
225
- config = self.calib_window.mmc.getAvailableConfigs(group)
226
- for idx in range(config.size()):
227
- if channel in config.get(idx):
228
- channel_group = group
229
- break
230
-
231
- # Create and validate reconstruction settings
232
- self.config_path = self.snap_dir / "reconstruction_settings.yml"
233
-
234
- _generate_reconstruction_config_from_gui(
235
- self.config_path,
236
- "phase",
237
- self.calib_window,
238
- input_channel_names=["BF"],
239
- )
240
-
241
- # Acquire 3D stack
242
- logging.debug("Acquiring 3D stack")
243
-
244
- # Generate MDA Settings
245
- settings = generate_acq_settings(
246
- self.calib_window.mm,
247
- channel_group=channel_group,
248
- channels=[channel],
249
- zstart=self.calib_window.z_start,
250
- zend=self.calib_window.z_end,
251
- zstep=self.calib_window.z_step,
252
- save_dir=str(self.snap_dir),
253
- prefix=self.prefix,
254
- keep_shutter_open_slices=True,
255
- )
256
-
257
- self._check_abort()
258
-
259
- # Acquire from MDA settings uses MM MDA GUI
260
- # Returns (1, 4/5, Z, Y, X) array
261
- stack = acquire_from_settings(
262
- self.calib_window.mm,
263
- settings,
264
- grab_images=True,
265
- restore_settings=True,
266
- )
267
- self._check_abort()
268
-
269
- # Cleanup acquisition by closing window, converting to zarr, and deleting temp directory
270
- self._cleanup_acq()
271
-
272
- # Reconstruct snapped images
273
- self.n_slices = stack.shape[2]
274
-
275
- phase, scale = self._reconstruct()
276
- self._check_abort()
277
-
278
- # Warn the user about axial
279
- if self.calib_window.invert_phase_contrast:
280
- show_warning(
281
- "Inverting the phase contrast. This affects the visualization and saved reconstruction."
282
- )
283
-
284
- # Warn user about mismatched scales
285
- recon_scale = np.array(
286
- (self.calib_window.z_step,)
287
- + 2 * (self.calib_window.ps / self.calib_window.mag,)
288
- )
289
- _check_scale_mismatch(recon_scale, scale)
290
-
291
- logging.info("Finished Acquisition")
292
- logging.debug("Finished Acquisition")
293
-
294
- # Emit the images and let thread know function is finished
295
- self.phase_image_emitter.emit((phase, scale))
296
-
297
- def _reconstruct(self):
298
- """
299
- Method to reconstruct
300
- """
301
- self._check_abort()
302
-
303
- # Create i/o paths
304
- transfer_function_path = Path(self.snap_dir) / "transfer_function.zarr"
305
- reconstruction_path = Path(self.snap_dir) / "reconstruction.zarr"
306
- input_data_path = Path(self.latest_out_path) / "0" / "0" / "0"
307
-
308
- # TODO: skip if config files match
309
- _compute_transfer_function_cli(
310
- input_position_dirpath=input_data_path,
311
- config_filepath=self.config_path,
312
- output_dirpath=transfer_function_path,
313
- )
314
-
315
- _apply_inverse_transfer_function_cli(
316
- input_position_dirpaths=[input_data_path],
317
- transfer_function_dirpath=transfer_function_path,
318
- config_filepath=self.config_path,
319
- output_dirpath=reconstruction_path,
320
- )
321
-
322
- # Read reconstruction to pass to emitters
323
- with open_ome_zarr(reconstruction_path, mode="r") as dataset:
324
- phase = dataset["0/0/0/0"][0]
325
- scale = dataset["0/0/0"].scale
326
-
327
- return phase, scale
328
-
329
- def _cleanup_acq(self):
330
- # Get display windows
331
- disps = self.dm.getAllDataViewers()
332
-
333
- # loop through display window and find one with matching prefix
334
- for i in range(disps.size()):
335
- disp = disps.get(i)
336
-
337
- # close the datastore and grab the path to where the data is saved
338
- if self.prefix in disp.getName():
339
- dp = disp.getDataProvider()
340
- dir_ = dp.getSummaryMetadata().getDirectory()
341
- prefix = dp.getSummaryMetadata().getPrefix()
342
- closed = False
343
- disp.close()
344
- while not closed:
345
- closed = disp.isClosed()
346
- dp.close()
347
-
348
- # Try to delete the data, sometime it isn't cleaned up quickly enough and will
349
- # return an error. In this case, catch the error and then try to close again (seems to work).
350
- try:
351
- self.latest_out_path = self.snap_dir / "raw_data.zarr"
352
- converter = TIFFConverter(
353
- str(Path(dir_) / prefix),
354
- str(self.latest_out_path),
355
- data_type="ometiff",
356
- grid_layout=False,
357
- )
358
- converter.run()
359
- shutil.rmtree(Path(dir_) / prefix)
360
- except PermissionError as ex:
361
- dp.close()
362
- break
363
- else:
364
- continue
365
-
366
-
367
- # TODO: Cache common OTF's on local computers and use those for reconstruction
368
- class PolarizationAcquisitionWorker(WorkerBase):
369
- """
370
- Class to execute a birefringence/phase acquisition. First step is to snap the images follow by a second
371
- step of reconstructing those images.
372
- """
373
-
374
- def __init__(
375
- self, calib_window: MainWidget, calib: QLIPP_Calibration, mode: str
376
- ):
377
- super().__init__(SignalsClass=PolarizationAcquisitionSignals)
378
-
379
- # Save current state of GUI window
380
- self.calib_window = calib_window
381
-
382
- # Init properties
383
- self.calib = calib
384
- self.mode = mode
385
- self.n_slices = None
386
- self.prefix = "waveorderPluginSnap"
387
- self.dm = self.calib_window.mm.displays()
388
- self.channel_group = self.calib_window.config_group
389
-
390
- # Determine whether 2D or 3D acquisition is needed
391
- if self.mode == "birefringence" and self.calib_window.acq_mode == "2D":
392
- self.dim = "2D"
393
- else:
394
- self.dim = "3D"
395
-
396
- save_dir = (
397
- self.calib_window.save_directory
398
- if self.calib_window.save_directory
399
- else self.calib_window.directory
400
- )
401
-
402
- if save_dir is None:
403
- raise ValueError(
404
- "save directory is empty, please specify a directory in the plugin"
405
- )
406
-
407
- if self.calib_window.save_name is None:
408
- self.snap_dir = Path(save_dir) / "snap"
409
- else:
410
- self.snap_dir = Path(save_dir) / (
411
- self.calib_window.save_name + "_snap"
412
- )
413
- self.snap_dir = add_index_to_path(self.snap_dir)
414
- self.snap_dir.mkdir()
415
-
416
- def _check_abort(self):
417
- if self.abort_requested:
418
- self.aborted.emit()
419
- raise TimeoutError("Stop Requested")
420
-
421
- def _check_ram(self):
422
- """
423
- Show a warning if RAM < 32 GB.
424
- """
425
- is_warning, msg = ram_message()
426
- if is_warning:
427
- show_warning(msg)
428
- else:
429
- logging.info(msg)
430
-
431
- def work(self):
432
- """
433
- Function that runs the 2D or 3D acquisition and reconstructs the data
434
- """
435
- self._check_ram()
436
- logging.info("Running Acquisition...")
437
-
438
- # List the Channels to acquire, if 5-state then append 5th channel
439
- channels = ["State0", "State1", "State2", "State3"]
440
- if self.calib.calib_scheme == "5-State":
441
- channels.append("State4")
442
-
443
- self._check_abort()
444
-
445
- # Create and validate reconstruction settings
446
- self.config_path = self.snap_dir / "reconstruction_settings.yml"
447
- _generate_reconstruction_config_from_gui(
448
- self.config_path,
449
- self.mode,
450
- self.calib_window,
451
- input_channel_names=channels,
452
- )
453
-
454
- # Acquire 2D stack
455
- if self.dim == "2D":
456
- logging.debug("Acquiring 2D stack")
457
-
458
- # Generate MDA Settings
459
- self.settings = generate_acq_settings(
460
- self.calib_window.mm,
461
- channel_group=self.channel_group,
462
- channels=channels,
463
- save_dir=str(self.snap_dir),
464
- prefix=self.prefix,
465
- keep_shutter_open_channels=True,
466
- )
467
- self._check_abort()
468
- # acquire images
469
- stack = self._acquire()
470
-
471
- # Acquire 3D stack
472
- else:
473
- logging.debug("Acquiring 3D stack")
474
-
475
- # Generate MDA Settings
476
- self.settings = generate_acq_settings(
477
- self.calib_window.mm,
478
- channel_group=self.channel_group,
479
- channels=channels,
480
- zstart=self.calib_window.z_start,
481
- zend=self.calib_window.z_end,
482
- zstep=self.calib_window.z_step,
483
- save_dir=str(self.snap_dir),
484
- prefix=self.prefix,
485
- keep_shutter_open_channels=True,
486
- keep_shutter_open_slices=True,
487
- )
488
-
489
- self._check_abort()
490
-
491
- # set acquisition order to channel-first
492
- self.settings["slicesFirst"] = False
493
- self.settings["acqOrderMode"] = 0 # TIME_POS_SLICE_CHANNEL
494
-
495
- # acquire images
496
- stack = self._acquire()
497
-
498
- # Cleanup acquisition by closing window, converting to zarr, and deleting temp directory
499
- self._cleanup_acq()
500
-
501
- # Reconstruct snapped images
502
- self._check_abort()
503
- self.n_slices = stack.shape[2]
504
- birefringence, phase, scale = self._reconstruct()
505
- self._check_abort()
506
-
507
- # Warn the user about rotations and flips
508
- if self.calib_window.rotate_orientation:
509
- show_warning(
510
- "Applying a +90 degree rotation to the orientation channel. This affects the visualization and saved reconstruction."
511
- )
512
- if self.calib_window.flip_orientation:
513
- show_warning(
514
- "Applying a flip to orientation channel. This affects the visualization and saved reconstruction."
515
- )
516
-
517
- # Warn user about mismatched scales
518
- recon_scale = np.array(
519
- (self.calib_window.z_step,)
520
- + 2 * (self.calib_window.ps / self.calib_window.mag,)
521
- )
522
- _check_scale_mismatch(recon_scale, scale)
523
-
524
- logging.info("Finished Acquisition")
525
- logging.debug("Finished Acquisition")
526
-
527
- # Emit the images and let thread know function is finished
528
- self.bire_image_emitter.emit((birefringence, scale))
529
- self.phase_image_emitter.emit((phase, scale))
530
-
531
- def _check_exposure(self) -> None:
532
- """
533
- Check that all LF channels have the same exposure settings. If not, abort Acquisition.
534
- """
535
- # parse exposure times
536
- channel_exposures = []
537
- for channel in self.settings["channels"]:
538
- channel_exposures.append(channel["exposure"])
539
- logging.debug(f"Verifying exposure times: {channel_exposures}")
540
- channel_exposures = np.array(channel_exposures)
541
- # check if exposure times are equal
542
- if not np.all(channel_exposures == channel_exposures[0]):
543
- error_exposure_msg = (
544
- f"The MDA exposure times are not equal! Aborting Acquisition.\n"
545
- f"Please manually set the exposure times to the same value from the MDA menu."
546
- )
547
-
548
- raise ValueError(error_exposure_msg)
549
-
550
- self._check_abort()
551
-
552
- def _acquire(self) -> np.ndarray:
553
- """
554
- Acquire images.
555
-
556
- Returns
557
- -------
558
- stack: (nd-array) Dimensions are (C, Z, Y, X). Z=1 for 2D acquisition.
559
- """
560
- # check if exposure times are the same
561
- self._check_exposure()
562
-
563
- # Acquire from MDA settings uses MM MDA GUI
564
- # Returns (1, 4/5, Z, Y, X) array
565
- stack = acquire_from_settings(
566
- self.calib_window.mm,
567
- self.settings,
568
- grab_images=True,
569
- restore_settings=True,
570
- )
571
- self._check_abort()
572
-
573
- return stack
574
-
575
- def _reconstruct(self):
576
- """
577
- Method to reconstruct. First need to initialize the reconstructor given
578
- what type of acquisition it is (birefringence only skips a lot of heavy compute needed for phase).
579
- This function also checks to see if the reconstructor needs to be updated from previous acquisitions
580
-
581
- """
582
- self._check_abort()
583
-
584
- # Create config and i/o paths
585
- transfer_function_path = Path(self.snap_dir) / "transfer_function.zarr"
586
- reconstruction_path = Path(self.snap_dir) / "reconstruction.zarr"
587
- input_data_path = Path(self.latest_out_path) / "0" / "0" / "0"
588
-
589
- # TODO: skip if config files match
590
- _compute_transfer_function_cli(
591
- input_position_dirpath=input_data_path,
592
- config_filepath=self.config_path,
593
- output_dirpath=transfer_function_path,
594
- )
595
-
596
- _apply_inverse_transfer_function_cli(
597
- input_position_dirpaths=[input_data_path],
598
- transfer_function_dirpath=transfer_function_path,
599
- config_filepath=self.config_path,
600
- output_dirpath=reconstruction_path,
601
- )
602
-
603
- # Read reconstruction to pass to emitters
604
- with open_ome_zarr(reconstruction_path, mode="r") as dataset:
605
- czyx_data = dataset["0/0/0/0"][0]
606
- birefringence = czyx_data[0:4]
607
- try:
608
- phase = czyx_data[4]
609
- except:
610
- phase = None
611
- scale = dataset["0/0/0"].scale
612
-
613
- return birefringence, phase, scale
614
-
615
- def _cleanup_acq(self):
616
- # Get display windows
617
- disps = self.dm.getAllDataViewers()
618
-
619
- # loop through display window and find one with matching prefix
620
- for i in range(disps.size()):
621
- disp = disps.get(i)
622
-
623
- # close the datastore and grab the path to where the data is saved
624
- if self.prefix in disp.getName():
625
- dp = disp.getDataProvider()
626
- dir_ = dp.getSummaryMetadata().getDirectory()
627
- prefix = dp.getSummaryMetadata().getPrefix()
628
- closed = False
629
- disp.close()
630
- while not closed:
631
- closed = disp.isClosed()
632
- dp.close()
633
-
634
- # Try to delete the data, sometime it isn't cleaned up quickly enough and will
635
- # return an error. In this case, catch the error and then try to close again (seems to work).
636
- try:
637
- self.latest_out_path = self.snap_dir / "raw_data.zarr"
638
- converter = TIFFConverter(
639
- str(Path(dir_) / prefix),
640
- str(self.latest_out_path),
641
- data_type="ometiff",
642
- grid_layout=False,
643
- )
644
- converter.run()
645
- shutil.rmtree(Path(dir_) / prefix)
646
- except PermissionError as ex:
647
- dp.close()
648
- break
649
- else:
650
- continue