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
waveorder/focus.py CHANGED
@@ -3,11 +3,53 @@ from typing import Literal, Optional
3
3
 
4
4
  import matplotlib.pyplot as plt
5
5
  import numpy as np
6
+ import torch
6
7
  from scipy.signal import peak_widths
7
8
 
8
9
  from waveorder import util
9
10
 
10
11
 
12
+ def compute_midband_power(
13
+ yx_array: torch.Tensor,
14
+ NA_det: float,
15
+ lambda_ill: float,
16
+ pixel_size: float,
17
+ midband_fractions: tuple[float, float] = (0.125, 0.25),
18
+ ) -> torch.Tensor:
19
+ """Compute midband spatial frequency power by summing over a 2D midband donut.
20
+
21
+ Parameters
22
+ ----------
23
+ yx_array : torch.Tensor
24
+ 2D tensor in (Y, X) order.
25
+ NA_det : float
26
+ Detection NA.
27
+ lambda_ill : float
28
+ Illumination wavelength.
29
+ Units are arbitrary, but must match [pixel_size].
30
+ pixel_size : float
31
+ Object-space pixel size = camera pixel size / magnification.
32
+ Units are arbitrary, but must match [lambda_ill].
33
+ midband_fractions : tuple[float, float], optional
34
+ The minimum and maximum fraction of the cutoff frequency that define the midband.
35
+ Default is (0.125, 0.25).
36
+
37
+ Returns
38
+ -------
39
+ torch.Tensor
40
+ Sum of absolute FFT values in the midband region.
41
+ """
42
+ _, _, fxx, fyy = util.gen_coordinate(yx_array.shape, pixel_size)
43
+ frr = torch.tensor(np.sqrt(fxx**2 + fyy**2))
44
+ xy_abs_fft = torch.abs(torch.fft.fftn(yx_array))
45
+ cutoff = 2 * NA_det / lambda_ill
46
+ mask = torch.logical_and(
47
+ frr > cutoff * midband_fractions[0],
48
+ frr < cutoff * midband_fractions[1],
49
+ )
50
+ return torch.sum(xy_abs_fft[mask])
51
+
52
+
11
53
  def focus_from_transverse_band(
12
54
  zyx_array,
13
55
  NA_det,
@@ -15,8 +57,11 @@ def focus_from_transverse_band(
15
57
  pixel_size,
16
58
  midband_fractions=(0.125, 0.25),
17
59
  mode: Literal["min", "max"] = "max",
60
+ polynomial_fit_order: Optional[int] = None,
18
61
  plot_path: Optional[str] = None,
19
62
  threshold_FWHM: float = 0,
63
+ return_statistics: bool = False,
64
+ enable_subpixel_precision: bool = False,
20
65
  ):
21
66
  """Estimates the in-focus slice from a 3D stack by optimizing a transverse spatial frequency band.
22
67
 
@@ -36,8 +81,10 @@ def focus_from_transverse_band(
36
81
  midband_fractions: Tuple[float, float], optional
37
82
  The minimum and maximum fraction of the cutoff frequency that define the midband.
38
83
  Requires: 0 <= midband_fractions[0] < midband_fractions[1] <= 1.
39
- mode: {'max', 'min'}, optional
40
- Option to choose the in-focus slice by minimizing or maximizing the midband frequency.
84
+ mode: {'min', 'max'}, optional
85
+ Option to choose the in-focus slice by minimizing or maximizing the midband power. By default 'max'.
86
+ polynomial_fit_order: int, optional
87
+ Default None is no fit. If integer, a polynomial of that degree is fit to the midband power before choosing the extreme point as the in-focus slice.
41
88
  plot_path: str or None, optional
42
89
  File name for a diagnostic plot (supports matplotlib filetypes .png, .pdf, .svg, etc.).
43
90
  Use None to skip.
@@ -46,14 +93,22 @@ def focus_from_transverse_band(
46
93
  The default value, 0, applies no threshold, and the maximum midband power is always considered in focus.
47
94
  For values > 0, the peak's FWHM must be greater than the threshold for the slice to be considered in focus.
48
95
  If the peak does not meet this threshold, the function returns None.
96
+ return_statistics: bool, optional
97
+ If True, returns a tuple (in_focus_index, peak_stats) instead of just in_focus_index.
98
+ Default is False for backward compatibility.
99
+ enable_subpixel_precision: bool, optional
100
+ If True and polynomial_fit_order is provided, enables sub-pixel precision focus detection
101
+ by finding the continuous extremum of the polynomial fit. Default is False for backward compatibility.
49
102
 
50
103
  Returns
51
- ------
52
- slice : int or None
53
- If peak's FWHM > peak_width_threshold:
54
- return the index of the in-focus slice
55
- else:
56
- return None
104
+ -------
105
+ slice : int, float, None, or tuple
106
+ If return_statistics is False (default):
107
+ Returns in_focus_index (int if enable_subpixel_precision=False,
108
+ float if enable_subpixel_precision=True and polynomial_fit_order is not None, or None).
109
+ If return_statistics is True:
110
+ Returns tuple (in_focus_index, peak_stats) where peak_stats is a dict
111
+ containing 'peak_index' and 'peak_FWHM'.
57
112
 
58
113
  Example
59
114
  ------
@@ -64,6 +119,7 @@ def focus_from_transverse_band(
64
119
  >>> in_focus_data = data[slice,:,:]
65
120
  """
66
121
  minmaxfunc = _mode_to_minmaxfunc(mode)
122
+ peak_stats = {"peak_index": None, "peak_FWHM": None}
67
123
 
68
124
  _check_focus_inputs(
69
125
  zyx_array, NA_det, lambda_ill, pixel_size, midband_fractions
@@ -74,29 +130,71 @@ def focus_from_transverse_band(
74
130
  warnings.warn(
75
131
  "The dataset only contained a single slice. Returning trivial slice index = 0."
76
132
  )
133
+ if return_statistics:
134
+ return 0, peak_stats
77
135
  return 0
78
136
 
79
- # Calculate coordinates
80
- _, Y, X = zyx_array.shape
81
- _, _, fxx, fyy = util.gen_coordinate((Y, X), pixel_size)
82
- frr = np.sqrt(fxx**2 + fyy**2)
137
+ # Calculate midband power for each slice
138
+ midband_sum = np.array(
139
+ [
140
+ compute_midband_power(
141
+ torch.from_numpy(zyx_array[z]),
142
+ NA_det,
143
+ lambda_ill,
144
+ pixel_size,
145
+ midband_fractions,
146
+ ).numpy()
147
+ for z in range(zyx_array.shape[0])
148
+ ]
149
+ )
83
150
 
84
- # Calculate fft
85
- xy_abs_fft = np.abs(np.fft.fftn(zyx_array, axes=(1, 2)))
151
+ if polynomial_fit_order is None:
152
+ peak_index = minmaxfunc(midband_sum)
153
+ else:
154
+ x = np.arange(len(midband_sum))
155
+ coeffs = np.polyfit(x, midband_sum, polynomial_fit_order)
156
+ poly_func = np.poly1d(coeffs)
86
157
 
87
- # Calculate midband mask
88
- cutoff = 2 * NA_det / lambda_ill
89
- midband_mask = np.logical_and(
90
- frr > cutoff * midband_fractions[0],
91
- frr < cutoff * midband_fractions[1],
92
- )
158
+ if enable_subpixel_precision:
159
+ # Find the continuous extremum using derivative
160
+ poly_deriv = np.polyder(coeffs)
161
+ # Find roots of the derivative (critical points)
162
+ critical_points = np.roots(poly_deriv)
163
+
164
+ # Filter for real roots within the data range
165
+ real_critical_points = []
166
+ for cp in critical_points:
167
+ if np.isreal(cp) and 0 <= cp.real < len(midband_sum):
168
+ real_critical_points.append(cp.real)
169
+
170
+ if real_critical_points:
171
+ # Evaluate the polynomial at critical points to find extremum
172
+ critical_values = [
173
+ poly_func(cp) for cp in real_critical_points
174
+ ]
175
+ if mode == "max":
176
+ best_idx = np.argmax(critical_values)
177
+ else: # mode == "min"
178
+ best_idx = np.argmin(critical_values)
179
+ peak_index = real_critical_points[best_idx]
180
+ else:
181
+ # Fall back to discrete maximum if no valid critical points
182
+ peak_index = float(minmaxfunc(poly_func(x)))
183
+ else:
184
+ peak_index = minmaxfunc(poly_func(x))
93
185
 
94
- # Find slice index with min/max power in midband
95
- midband_sum = np.sum(xy_abs_fft[:, midband_mask], axis=1)
96
- peak_index = minmaxfunc(midband_sum)
186
+ # For peak width calculation, use integer peak index
187
+ if enable_subpixel_precision and polynomial_fit_order is not None:
188
+ # Use the closest integer index for peak width calculation
189
+ integer_peak_index = int(np.round(peak_index))
190
+ else:
191
+ integer_peak_index = int(peak_index)
97
192
 
98
- peak_results = peak_widths(midband_sum, [peak_index])
193
+ peak_results = peak_widths(midband_sum, [integer_peak_index])
99
194
  peak_FWHM = peak_results[0][0]
195
+ peak_stats.update(
196
+ {"peak_index": int(peak_index), "peak_FWHM": float(peak_FWHM)}
197
+ )
100
198
 
101
199
  if peak_FWHM >= threshold_FWHM:
102
200
  in_focus_index = peak_index
@@ -114,6 +212,9 @@ def focus_from_transverse_band(
114
212
  threshold_FWHM,
115
213
  )
116
214
 
215
+ if return_statistics:
216
+ return in_focus_index, peak_stats
217
+
117
218
  return in_focus_index
118
219
 
119
220
 
@@ -168,9 +269,19 @@ def _plot_focus_metric(
168
269
  ):
169
270
  _, ax = plt.subplots(1, 1, figsize=(4, 4))
170
271
  ax.plot(midband_sum, "-k")
272
+
273
+ # Handle floating-point peak_index for plotting
274
+ if isinstance(peak_index, float) and not peak_index.is_integer():
275
+ # Use interpolation to get the y-value at the floating-point x-position
276
+ peak_y_value = np.interp(
277
+ peak_index, np.arange(len(midband_sum)), midband_sum
278
+ )
279
+ else:
280
+ peak_y_value = midband_sum[int(peak_index)]
281
+
171
282
  ax.plot(
172
283
  peak_index,
173
- midband_sum[peak_index],
284
+ peak_y_value,
174
285
  "go" if in_focus_index is not None else "ro",
175
286
  )
176
287
  ax.hlines(*peak_results[1:], color="k", linestyles="dashed")
File without changes
@@ -0,0 +1,61 @@
1
+ from typing import Dict, List, Tuple, Union
2
+
3
+ import zarr
4
+ from iohub import read_micromanager
5
+ from napari_ome_zarr._reader import napari_get_reader as fallback_reader
6
+
7
+
8
+ def napari_get_reader(path):
9
+ if isinstance(path, str):
10
+ if ".zarr" in path:
11
+ with zarr.open(path) as root:
12
+ if "plate" in root.attrs:
13
+ return hcs_zarr_reader
14
+ else:
15
+ return fallback_reader(path)
16
+ else:
17
+ return ome_tif_reader
18
+ else:
19
+ return None
20
+
21
+
22
+ def hcs_zarr_reader(
23
+ path: Union[str, List[str]],
24
+ ) -> List[Tuple[zarr.Array, Dict]]:
25
+ reader = read_micromanager(path)
26
+ results = list()
27
+
28
+ zs = zarr.open(path, "r")
29
+ names = []
30
+
31
+ dict_ = zs.attrs.asdict()
32
+ wells = dict_["plate"]["wells"]
33
+ for well in wells:
34
+ path = well["path"]
35
+ well_dict = zs[path].attrs.asdict()
36
+ for name in well_dict["well"]["images"]:
37
+ names.append(name["path"])
38
+ for pos in range(reader.get_num_positions()):
39
+ meta = dict()
40
+ name = names[pos]
41
+ meta["name"] = name
42
+ results.append((reader.get_zarr(pos), meta))
43
+ return results
44
+
45
+
46
+ def ome_tif_reader(
47
+ path: Union[str, List[str]],
48
+ ) -> List[Tuple[zarr.Array, Dict]]:
49
+ reader = read_micromanager(path)
50
+ results = list()
51
+
52
+ npos = reader.get_num_positions()
53
+ for pos in range(npos):
54
+ meta = dict()
55
+ if npos == 1:
56
+ meta["name"] = "Pos000_000"
57
+ else:
58
+ meta["name"] = reader.stage_positions[pos]["Label"][2:]
59
+ results.append((reader.get_zarr(pos), meta))
60
+
61
+ return results
@@ -0,0 +1,272 @@
1
+ import time
2
+ from contextlib import contextmanager
3
+
4
+ import numpy as np
5
+
6
+
7
+ @contextmanager
8
+ def suspend_live_sm(snap_manager):
9
+ """Context manager that suspends/unsuspends MM live mode for `SnapLiveManager`.
10
+
11
+ Parameters
12
+ ----------
13
+ snap_manager : object
14
+ `org.micromanager.internal.SnapLiveManager` object via pycromanager
15
+
16
+ Yields
17
+ ------
18
+ object
19
+ `org.micromanager.internal.SnapLiveManager` object via pycromanager
20
+
21
+ Usage
22
+ -----
23
+ ```py
24
+ with suspend_live_sm(snap_manager) as sm:
25
+ pass # do something with MM that can't be done in live mode
26
+ ```
27
+ """
28
+ snap_manager.setSuspended(True)
29
+ try:
30
+ yield snap_manager
31
+ finally:
32
+ snap_manager.setSuspended(False)
33
+
34
+
35
+ def snap_and_get_image(snap_manager):
36
+ """
37
+ Snap and get image using Snap Live Window Manager + transfer of ZMQ
38
+
39
+ Parameters
40
+ ----------
41
+ snap_manager: (object) MM Snap Live Window object
42
+
43
+ Returns
44
+ -------
45
+ image: (array) 2D array of size (Y, X)
46
+
47
+ """
48
+ snap_manager.snap(True)
49
+ time.sleep(
50
+ 0.3
51
+ ) # sleep after snap to make sure the image we grab is the correct one
52
+
53
+ # get pixels + dimensions
54
+ height = snap_manager.getDisplay().getDisplayedImages().get(0).getHeight()
55
+ width = snap_manager.getDisplay().getDisplayedImages().get(0).getWidth()
56
+ array = (
57
+ snap_manager.getDisplay().getDisplayedImages().get(0).getRawPixels()
58
+ )
59
+
60
+ return np.reshape(array, (height, width))
61
+
62
+
63
+ def snap_and_average(snap_manager, display=True):
64
+ """
65
+ Snap an image with Snap Live manager + grab only the mean (computed in java)
66
+
67
+ Parameters
68
+ ----------
69
+ snap_manager: (object) MM Snap Live Window object
70
+ display: (bool) Whether to show the snap on the Snap Live Window in MM
71
+
72
+ Returns
73
+ -------
74
+ mean: (float) mean of snapped image
75
+
76
+ """
77
+
78
+ snap_manager.snap(display)
79
+ time.sleep(
80
+ 0.3
81
+ ) # sleep after snap to make sure the image we grab is the correct one
82
+
83
+ return snap_manager.getDisplay().getImagePlus().getStatistics().umean
84
+
85
+
86
+ def set_lc_waves(mmc, device_property: tuple, value: float):
87
+ """
88
+ Set retardance in waves for LC in device_property
89
+
90
+ Parameters
91
+ ----------
92
+ mmc : object
93
+ MM Core object
94
+ device_property : tuple
95
+ (device_name, property_name) set
96
+ value : float
97
+ Retardance to set as fraction of a wavelength
98
+
99
+ Returns
100
+ -------
101
+
102
+ """
103
+ device_name = device_property[0]
104
+ prop_name = device_property[1]
105
+
106
+ if value > 1.6 or value < 0.001:
107
+ raise ValueError(
108
+ f"Requested retardance value is {value} waves. "
109
+ f"Retardance must be greater than 0.001 and less than 1.6 waves."
110
+ )
111
+
112
+ mmc.setProperty(device_name, prop_name, str(value))
113
+ time.sleep(20 / 1000)
114
+
115
+
116
+ def set_lc_voltage(mmc, device_property: tuple, value: float):
117
+ """
118
+ Set LC retardance by specifying LC voltage
119
+
120
+ Parameters
121
+ ----------
122
+ mmc : object
123
+ MM Core object
124
+ device_property : tuple
125
+ (device_name, property_name) set
126
+ value : float
127
+ LC voltage in volts. Applied voltage is limited to 20V
128
+
129
+ Returns
130
+ -------
131
+
132
+ """
133
+ device_name = device_property[0]
134
+ prop_name = device_property[1]
135
+
136
+ if value > 20.0 or value < 0.0:
137
+ raise ValueError(
138
+ f"Requested LC voltage is {value} V. "
139
+ f"LC voltage must be greater than 0.0 and less than 20.0 V."
140
+ )
141
+
142
+ mmc.setProperty(device_name, prop_name, str(value))
143
+ time.sleep(20 / 1000)
144
+
145
+
146
+ def set_lc_daq(mmc, device_property: tuple, value: float):
147
+ """
148
+ Set LC retardance based on DAQ output
149
+
150
+ Parameters
151
+ ----------
152
+ mmc : object
153
+ MM Core object
154
+ device_property : tuple
155
+ (device_name, property_name) set
156
+ value : float
157
+ DAQ output voltage in volts. DAQ output must be in 0-5V range
158
+
159
+ Returns
160
+ -------
161
+
162
+ """
163
+ device_name = device_property[0]
164
+ prop_name = device_property[1]
165
+
166
+ if value > 5.0 or value < 0.0:
167
+ raise ValueError(
168
+ "DAC voltage must be greater than 0.0 and less than 5.0"
169
+ )
170
+
171
+ mmc.setProperty(device_name, prop_name, str(value))
172
+ time.sleep(20 / 1000)
173
+
174
+
175
+ def get_lc(mmc, device_property: tuple):
176
+ """
177
+ Get LC state in the native units of the device property
178
+
179
+ Parameters
180
+ ----------
181
+ mmc : object
182
+ MM Core object
183
+ device_property : tuple
184
+ (device_name, property_name) set
185
+
186
+ Returns
187
+ -------
188
+
189
+ """
190
+
191
+ device_name = device_property[0]
192
+ prop_name = device_property[1]
193
+
194
+ val = float(mmc.getProperty(device_name, prop_name))
195
+ return val
196
+
197
+
198
+ def define_meadowlark_state(mmc, device_property: tuple):
199
+ """
200
+ Defines pallet element in the Meadowlark device adapter for the given state.
201
+ Make sure LC values for this state are set before calling this function
202
+
203
+ Parameters
204
+ ----------
205
+ mmc : object
206
+ MM Core object
207
+ device_property : tuple
208
+ (device_name, property_name) set, e.g.
209
+ ('MeadowlarkLC', 'Pal. elem. 00; enter 0 to define; 1 to activate')
210
+
211
+ Returns
212
+ -------
213
+
214
+ """
215
+
216
+ device_name = device_property[0]
217
+ prop_name = device_property[1]
218
+
219
+ # define LC state
220
+ # setting pallet elements to 0 defines LC state
221
+ mmc.setProperty(device_name, prop_name, 0)
222
+ mmc.waitForDevice(device_name)
223
+
224
+
225
+ def define_config_state(
226
+ mmc, group: str, config: str, device_properties: list, values: list
227
+ ):
228
+ """
229
+ Define config state by specifying the values for all device properties in this config
230
+
231
+ Parameters
232
+ ----------
233
+ mmc : object
234
+ MM Core object
235
+ group : str
236
+ Name of config group
237
+ config : str
238
+ Name of config, e.g. State0
239
+ device_properties: list
240
+ List of (device_name, property_name) tuples in config
241
+ values: list
242
+ List of matching device property values
243
+
244
+ Returns
245
+ -------
246
+
247
+ """
248
+
249
+ for device_property, value in zip(device_properties, values):
250
+ device_name = device_property[0]
251
+ prop_name = device_property[1]
252
+ mmc.defineConfig(group, config, device_name, prop_name, str(value))
253
+ mmc.waitForConfig(group, config)
254
+
255
+
256
+ def set_lc_state(mmc, group: str, config: str):
257
+ """
258
+ Change to the specific LC State
259
+
260
+ Parameters
261
+ ----------
262
+ mmc : object
263
+ MM Core object
264
+ group : str
265
+ Name of config group
266
+ config : str
267
+ Name of config, e.g. State0
268
+
269
+ """
270
+
271
+ mmc.setConfig(group, config)
272
+ time.sleep(20 / 1000) # delay for LC settle time