microeye 2.3.2__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 (190) hide show
  1. microEye/__init__.py +47 -0
  2. microEye/_version.py +2 -0
  3. microEye/analysis/__init__.py +1 -0
  4. microEye/analysis/checklist_dialog.py +143 -0
  5. microEye/analysis/cmosMaps.py +228 -0
  6. microEye/analysis/filters/__init__.py +9 -0
  7. microEye/analysis/filters/base.py +21 -0
  8. microEye/analysis/filters/spatial.py +338 -0
  9. microEye/analysis/filters/temporal.py +76 -0
  10. microEye/analysis/fitting/__init__.py +0 -0
  11. microEye/analysis/fitting/fit.py +680 -0
  12. microEye/analysis/fitting/nena.py +375 -0
  13. microEye/analysis/fitting/phasor_fit.py +90 -0
  14. microEye/analysis/fitting/processing.py +317 -0
  15. microEye/analysis/fitting/psf/__init__.py +6 -0
  16. microEye/analysis/fitting/psf/extract.py +1129 -0
  17. microEye/analysis/fitting/psf/rubost_mean.py +150 -0
  18. microEye/analysis/fitting/psf/spline.py +167 -0
  19. microEye/analysis/fitting/psf/stats/__init__.py +12 -0
  20. microEye/analysis/fitting/psf/stats/core.py +295 -0
  21. microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
  22. microEye/analysis/fitting/psf/stats/io.py +104 -0
  23. microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
  24. microEye/analysis/fitting/psf/temp.py +147 -0
  25. microEye/analysis/fitting/psf/test.py +47 -0
  26. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
  27. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
  28. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
  29. microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
  30. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
  31. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
  32. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
  33. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
  34. microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
  35. microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
  36. microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
  37. microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
  38. microEye/analysis/fitting/results.py +917 -0
  39. microEye/analysis/fitting/results_stats.py +251 -0
  40. microEye/analysis/fitting/tardis.py +209 -0
  41. microEye/analysis/multi_viewer.py +568 -0
  42. microEye/analysis/processing/__init__.py +1 -0
  43. microEye/analysis/processing/frc.py +120 -0
  44. microEye/analysis/rendering/__init__.py +3 -0
  45. microEye/analysis/rendering/base.py +452 -0
  46. microEye/analysis/rendering/cloud.py +215 -0
  47. microEye/analysis/rendering/core.py +47 -0
  48. microEye/analysis/rendering/volumetric.py +198 -0
  49. microEye/analysis/tools/__init__.py +0 -0
  50. microEye/analysis/tools/kymograms.py +1079 -0
  51. microEye/analysis/tools/roi_selectors.py +556 -0
  52. microEye/analysis/utils/__init__.py +3 -0
  53. microEye/analysis/utils/coordinates.py +18 -0
  54. microEye/analysis/utils/images.py +68 -0
  55. microEye/analysis/utils/windows.py +19 -0
  56. microEye/analysis/viewer/__init__.py +3 -0
  57. microEye/analysis/viewer/image_options_widget.py +615 -0
  58. microEye/analysis/viewer/images.py +1225 -0
  59. microEye/analysis/viewer/layers_widget.py +415 -0
  60. microEye/analysis/viewer/localizations.py +1304 -0
  61. microEye/analysis/viewer/psf.py +1003 -0
  62. microEye/analysis/viewer/volume.py +456 -0
  63. microEye/hardware/__init__.py +3 -0
  64. microEye/hardware/cams/__init__.py +29 -0
  65. microEye/hardware/cams/camera_calibration.py +99 -0
  66. microEye/hardware/cams/camera_list.py +528 -0
  67. microEye/hardware/cams/camera_options.py +694 -0
  68. microEye/hardware/cams/camera_panel.py +941 -0
  69. microEye/hardware/cams/dummy/__init__.py +1 -0
  70. microEye/hardware/cams/dummy/dummy_panel.py +759 -0
  71. microEye/hardware/cams/jobs.py +497 -0
  72. microEye/hardware/cams/line_profiler.py +99 -0
  73. microEye/hardware/cams/linescan/IR_Cam.py +474 -0
  74. microEye/hardware/cams/linescan/__init__.py +1 -0
  75. microEye/hardware/cams/micam.py +524 -0
  76. microEye/hardware/cams/pco/__init__.py +58 -0
  77. microEye/hardware/cams/pco/enums.py +382 -0
  78. microEye/hardware/cams/pco/pco_cam.py +761 -0
  79. microEye/hardware/cams/pco/pco_panel.py +477 -0
  80. microEye/hardware/cams/shortcuts.py +312 -0
  81. microEye/hardware/cams/thorlabs/__init__.py +1 -0
  82. microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
  83. microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
  84. microEye/hardware/cams/ueye/__init__.py +1 -0
  85. microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
  86. microEye/hardware/cams/ueye/ueye_panel.py +861 -0
  87. microEye/hardware/cams/vimba/__init__.py +1 -0
  88. microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
  89. microEye/hardware/cams/vimba/vimba_panel.py +813 -0
  90. microEye/hardware/device.py +60 -0
  91. microEye/hardware/lasers/__init__.py +13 -0
  92. microEye/hardware/lasers/io_matchbox.py +791 -0
  93. microEye/hardware/lasers/io_params.py +85 -0
  94. microEye/hardware/lasers/io_single_laser.py +742 -0
  95. microEye/hardware/lasers/laser_relay.py +594 -0
  96. microEye/hardware/mieye/__init__.py +1 -0
  97. microEye/hardware/mieye/acquisition_manager.py +467 -0
  98. microEye/hardware/mieye/devices_manager.py +533 -0
  99. microEye/hardware/mieye/miEye.py +659 -0
  100. microEye/hardware/misc/__init__.py +0 -0
  101. microEye/hardware/misc/acquisition_view.py +71 -0
  102. microEye/hardware/misc/reglo.py +761 -0
  103. microEye/hardware/misc/temp.py +188 -0
  104. microEye/hardware/port_config.py +59 -0
  105. microEye/hardware/protocols/__init__.py +2 -0
  106. microEye/hardware/protocols/actions.py +402 -0
  107. microEye/hardware/protocols/actions_items.py +703 -0
  108. microEye/hardware/protocols/designer.py +244 -0
  109. microEye/hardware/protocols/scene_manager.py +191 -0
  110. microEye/hardware/protocols/serialization.py +97 -0
  111. microEye/hardware/pycromanager/__init__.py +16 -0
  112. microEye/hardware/pycromanager/core.py +1433 -0
  113. microEye/hardware/pycromanager/devices.py +461 -0
  114. microEye/hardware/pycromanager/enums.py +107 -0
  115. microEye/hardware/pycromanager/headless.py +153 -0
  116. microEye/hardware/pycromanager/utils.py +34 -0
  117. microEye/hardware/pycromanager/widgets/__init__.py +5 -0
  118. microEye/hardware/pycromanager/widgets/bridges.py +407 -0
  119. microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
  120. microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
  121. microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
  122. microEye/hardware/stages/__init__.py +18 -0
  123. microEye/hardware/stages/elliptec/__init__.py +5 -0
  124. microEye/hardware/stages/elliptec/baseDevice.py +314 -0
  125. microEye/hardware/stages/elliptec/device.py +384 -0
  126. microEye/hardware/stages/elliptec/deviceID.py +212 -0
  127. microEye/hardware/stages/elliptec/devicePort.py +379 -0
  128. microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
  129. microEye/hardware/stages/elliptec/devicesView.py +706 -0
  130. microEye/hardware/stages/elliptec/ellDevices.py +134 -0
  131. microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
  132. microEye/hardware/stages/elliptec/motorInfo.py +153 -0
  133. microEye/hardware/stages/elliptec/stage.py +62 -0
  134. microEye/hardware/stages/elliptec/test.py +139 -0
  135. microEye/hardware/stages/kinesis/__init__.py +1 -0
  136. microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
  137. microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
  138. microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
  139. microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
  140. microEye/hardware/stages/kinesis/kinesis.py +776 -0
  141. microEye/hardware/stages/piezo_concept.py +607 -0
  142. microEye/hardware/stages/stabilizer.py +785 -0
  143. microEye/hardware/stages/stage.py +89 -0
  144. microEye/hardware/widgets/__init__.py +10 -0
  145. microEye/hardware/widgets/controller.py +246 -0
  146. microEye/hardware/widgets/devices.py +133 -0
  147. microEye/hardware/widgets/focusWidget.py +264 -0
  148. microEye/hardware/widgets/qlist_slider.py +113 -0
  149. microEye/hardware/widgets/scan_acquisition.py +424 -0
  150. microEye/icons/1024.png +0 -0
  151. microEye/icons/128.png +0 -0
  152. microEye/icons/16.png +0 -0
  153. microEye/icons/24.png +0 -0
  154. microEye/icons/256.png +0 -0
  155. microEye/icons/32.png +0 -0
  156. microEye/icons/48.png +0 -0
  157. microEye/icons/512.png +0 -0
  158. microEye/icons/64.png +0 -0
  159. microEye/icons/__init__.py +0 -0
  160. microEye/icons/close.svg +88 -0
  161. microEye/icons/mieye.png +0 -0
  162. microEye/icons/min.svg +83 -0
  163. microEye/icons/viewer.png +0 -0
  164. microEye/launcher.py +42 -0
  165. microEye/qt.py +181 -0
  166. microEye/utils/__init__.py +2 -0
  167. microEye/utils/enum_encoder.py +10 -0
  168. microEye/utils/expandable_groupbox.py +93 -0
  169. microEye/utils/gui_helper.py +457 -0
  170. microEye/utils/hid/__init__.py +8 -0
  171. microEye/utils/hid/controller.py +153 -0
  172. microEye/utils/hid/device.py +63 -0
  173. microEye/utils/hid/enums.py +140 -0
  174. microEye/utils/hid/utils.py +58 -0
  175. microEye/utils/labelled_slider.py +134 -0
  176. microEye/utils/metadata.py +599 -0
  177. microEye/utils/metadata_tree.py +718 -0
  178. microEye/utils/micro_launcher.py +245 -0
  179. microEye/utils/parameter_tree.py +325 -0
  180. microEye/utils/pyscripting.py +444 -0
  181. microEye/utils/retry_exec.py +35 -0
  182. microEye/utils/start_gui.py +112 -0
  183. microEye/utils/thread_worker.py +115 -0
  184. microEye/utils/uImage.py +1512 -0
  185. microeye-2.3.2.dist-info/METADATA +401 -0
  186. microeye-2.3.2.dist-info/RECORD +190 -0
  187. microeye-2.3.2.dist-info/WHEEL +5 -0
  188. microeye-2.3.2.dist-info/entry_points.txt +2 -0
  189. microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
  190. microeye-2.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1512 @@
1
+ from typing import Union
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import tifffile as tf
6
+ import zarr
7
+ from numba import cuda, njit
8
+
9
+ BYTE = 256
10
+ TWELVE_BIT = 2**12
11
+ WORD = 2**16
12
+
13
+
14
+ # Numba-optimized functions
15
+ @njit
16
+ def normalize_image_single_channel(image):
17
+ image_min = np.min(image)
18
+ image_max = np.max(image)
19
+ if image_max == image_min:
20
+ return np.zeros_like(image, dtype=np.uint16)
21
+ return ((WORD - 1) * (image - image_min) / (image_max - image_min)).astype(
22
+ np.uint16
23
+ )
24
+
25
+
26
+ @njit
27
+ def normalize_image_multi_channel(image):
28
+ height, width, channels = image.shape
29
+ normalized_image = np.zeros((height, width, channels), dtype=np.uint16)
30
+ for c in range(channels):
31
+ channel = image[:, :, c]
32
+ image_min = np.min(channel)
33
+ image_max = np.max(channel)
34
+ if image_max != image_min:
35
+ normalized_image[..., c] = (
36
+ (WORD - 1) * (channel - image_min) / (image_max - image_min)
37
+ ).astype(np.uint16)
38
+ return normalized_image
39
+
40
+
41
+ def normalize_image_numba(image):
42
+ if image.ndim == 3:
43
+ return normalize_image_multi_channel(image)
44
+ else:
45
+ return normalize_image_single_channel(image)
46
+
47
+
48
+ # Numba-accelerated histogram calculation
49
+ @njit
50
+ def numba_histogram(image: np.ndarray, n_bins: int):
51
+ hist = np.zeros(n_bins, dtype=np.float32)
52
+ flat_image = image.ravel()
53
+ for value in flat_image:
54
+ if 0 <= value < n_bins:
55
+ hist[int(value)] += 1
56
+ return hist / flat_image.size
57
+
58
+
59
+ # Numba Histogram Function for RGB Images
60
+ @njit
61
+ def numba_histogram_rgb(image, n_bins):
62
+ channels = image.shape[2]
63
+ hist = np.zeros((n_bins, channels), dtype=np.float32)
64
+ for channel in range(channels):
65
+ for value in image[:, :, channel].ravel():
66
+ if value < n_bins:
67
+ hist[value, channel] += 1
68
+ return hist / (image.shape[0] * image.shape[1])
69
+
70
+
71
+ NUMBA_SIZES = {
72
+ BYTE: 512**2,
73
+ TWELVE_BIT: 768**2,
74
+ WORD: 2048**2,
75
+ }
76
+
77
+ NUMBA_RGB_SIZES = {
78
+ BYTE: 0,
79
+ TWELVE_BIT: 0,
80
+ WORD: 2048**2,
81
+ }
82
+
83
+
84
+ class uImage:
85
+ '''
86
+ Class for handling image processing and analysis.
87
+
88
+ Parameters
89
+ ----------
90
+ image : np.ndarray
91
+ Input image as a NumPy array.
92
+
93
+ Attributes
94
+ ----------
95
+ _image : np.ndarray
96
+ Processed image data.
97
+ _isfloat : bool
98
+ Flag indicating if the image data type is floating-point.
99
+ _norm : np.ndarray or None
100
+ Normalized image data.
101
+ _min : int
102
+ Minimum pixel value.
103
+ _max : int
104
+ Maximum pixel value.
105
+ _view : np.ndarray
106
+ View of the image data.
107
+ _hist : np.ndarray or None
108
+ Image histogram.
109
+ n_bins : int or None
110
+ Number of bins in the histogram.
111
+ _cdf : np.ndarray or None
112
+ Cumulative distribution function of the histogram.
113
+ _stats : dict
114
+ Image statistics.
115
+ _pixel_w : float
116
+ Width of a pixel.
117
+ _pixel_h : float
118
+ Height of a pixel.
119
+ '''
120
+
121
+ def __init__(self, image: np.ndarray, axis: str = 'CYX'):
122
+ '''
123
+ Initialize the uImage object.
124
+
125
+ Parameters
126
+ ----------
127
+ image : np.ndarray
128
+ Input image as a NumPy array.
129
+ '''
130
+ if axis == 'CYX' and image.ndim == 3:
131
+ # Swap axes for CYX configuration
132
+ image = np.transpose(image, (1, 2, 0))
133
+ elif axis != 'YXC' and image.ndim == 3:
134
+ raise ValueError("Invalid axis. Use 'YXC' or 'CYX'.")
135
+
136
+ self._axis = 'YXC' # Force the internal representation to 'YXC'
137
+
138
+ self.image = image
139
+ self._min = 0
140
+ self._max = BYTE - 1 if image.dtype == np.uint8 else WORD - 1
141
+ self._view = np.zeros(image.shape, dtype=np.uint8)
142
+ self._hist = None
143
+ self.n_bins = None
144
+ self._cdf = None
145
+ self._stats = {}
146
+ self._pixel_w = 1.0
147
+ self._pixel_h = 1.0
148
+
149
+ @property
150
+ def image(self):
151
+ '''
152
+ Get the image data.
153
+
154
+ Returns
155
+ -------
156
+ np.ndarray
157
+ Image data.
158
+ '''
159
+ return self._image
160
+
161
+ @image.setter
162
+ def image(self, value: np.ndarray):
163
+ '''
164
+ Set the image data.
165
+
166
+ Parameters
167
+ ----------
168
+ value : np.ndarray
169
+ New image data.
170
+ '''
171
+ if value.dtype not in [np.float64, np.float32, np.uint16, np.uint8]:
172
+ raise ValueError('Unsupported image dtype.')
173
+ if value.ndim > 3:
174
+ raise ValueError('Unexpected image dimensions. Expected 2D or 3D array.')
175
+
176
+ self._isfloat = np.issubdtype(value.dtype, np.floating)
177
+
178
+ self._image = value.astype(np.float32) if self._isfloat else value
179
+
180
+ if self._isfloat:
181
+ self._norm = normalize_image_numba(self._image)
182
+ else:
183
+ self._norm = None
184
+
185
+ @property
186
+ def width(self):
187
+ '''
188
+ Get the width of the image.
189
+
190
+ Returns
191
+ -------
192
+ int
193
+ Width of the image.
194
+ '''
195
+ return self._image.shape[2] if self._image.ndim == 3 else self._image.shape[1]
196
+
197
+ @property
198
+ def height(self):
199
+ '''
200
+ Get the height of the image.
201
+
202
+ Returns
203
+ -------
204
+ int
205
+ Height of the image.
206
+ '''
207
+ return self._image.shape[1] if self._image.ndim == 3 else self._image.shape[0]
208
+
209
+ @property
210
+ def channels(self):
211
+ '''
212
+ Get the number of channels in the image.
213
+
214
+ Returns
215
+ -------
216
+ int
217
+ Number of channels.
218
+ '''
219
+ return self._image.shape[2] if self._image.ndim == 3 else None
220
+
221
+ @property
222
+ def cdf_min(self) -> float:
223
+ '''
224
+ Get the minimum value for CDF calculation.
225
+
226
+ Returns
227
+ -------
228
+ float
229
+ Minimum value for CDF calculation.
230
+ '''
231
+ return 0.00001
232
+
233
+ @property
234
+ def cdf_max(self) -> float:
235
+ '''
236
+ Get the maximum value for CDF calculation.
237
+
238
+ Returns
239
+ -------
240
+ float
241
+ Maximum value for CDF calculation.
242
+ '''
243
+ return 0.999 if self._isfloat else 0.9999
244
+
245
+ def norm_to_float(self, value):
246
+ '''
247
+ Convert a value from _norm to the original float value.
248
+
249
+ Parameters
250
+ ----------
251
+ value : Union[int, np.ndarray]
252
+ The normalized value(s) from _norm.
253
+
254
+ Returns
255
+ -------
256
+ Union[float, np.ndarray]
257
+ The original float value(s).
258
+ '''
259
+ if self._isfloat:
260
+ image_min = self._image.min()
261
+ image_max = self._image.max()
262
+
263
+ if isinstance(value, (int, float, np.ndarray)):
264
+ return image_min + value * (image_max - image_min) / (WORD - 1)
265
+ else:
266
+ raise ValueError(
267
+ 'Input type not supported. Use int, float, or np.ndarray.'
268
+ )
269
+ else:
270
+ return value
271
+
272
+ def update_range(self):
273
+ '''
274
+ Update the minimum and maximum pixel values based
275
+ on the cumulative distribution function (CDF).
276
+ '''
277
+ if self._cdf.ndim == 1:
278
+ self._min, self._max = np.searchsorted(
279
+ self._cdf, [self.cdf_min, self.cdf_max]
280
+ )
281
+ elif self._cdf.ndim == 2:
282
+ # Considering the cumulative distribution function across all channels
283
+ min_indices = []
284
+ max_indices = []
285
+ for idx in range(self._cdf.shape[1]):
286
+ # Find indices for cdf_min and cdf_max for each channel
287
+ min_idx = np.searchsorted(self._cdf[:, idx], self.cdf_min, side='left')
288
+ max_idx = np.searchsorted(self._cdf[:, idx], self.cdf_max, side='left')
289
+
290
+ # Append the results
291
+ min_indices.append(min_idx)
292
+ max_indices.append(max_idx)
293
+
294
+ # Aggregate results across all channels
295
+ self._min = max(min_indices) if min_indices else 0
296
+ self._max = max(max_indices) if max_indices else self._cdf.shape[0] - 1
297
+
298
+ # Ensure _max is greater than _min
299
+ self._max = max(self._min + 1, self._max)
300
+
301
+ def calcHist_GPU(self):
302
+ '''
303
+ Calculate the image histogram using GPU acceleration.
304
+ '''
305
+ image_to_use = self._norm if self._isfloat else self.image
306
+
307
+ self.n_bins = BYTE if self._image.dtype == np.uint8 else WORD
308
+
309
+ # Configure kernel launch parameters
310
+ threads_per_block = (16, 16)
311
+ blocks_per_grid_x = (
312
+ image_to_use.shape[0] + threads_per_block[0] - 1
313
+ ) // threads_per_block[0]
314
+ blocks_per_grid_y = (
315
+ image_to_use.shape[1] + threads_per_block[1] - 1
316
+ ) // threads_per_block[1]
317
+ blocks_per_grid = (blocks_per_grid_x, blocks_per_grid_y)
318
+
319
+ if self.channels is None:
320
+ # Allocate memory for the LUT on the GPU
321
+ lut_device = cuda.to_device(np.zeros((self.n_bins,), dtype=np.int32))
322
+
323
+ # Single-channel image
324
+ generate_lut_single_channel_kernel[blocks_per_grid, threads_per_block](
325
+ image_to_use, lut_device, 0, 1, self.n_bins
326
+ )
327
+ else:
328
+ # Allocate memory for the LUT on the GPU
329
+ lut_device = cuda.to_device(
330
+ np.zeros((self.n_bins, self.channels), dtype=np.int32)
331
+ )
332
+ # Multi-channel image
333
+ generate_lut_multi_channel_kernel[blocks_per_grid, threads_per_block](
334
+ image_to_use, lut_device, 0, 1, self.n_bins
335
+ )
336
+
337
+ # Copy the LUT from GPU device to host memory
338
+ self._hist = lut_device.copy_to_host().astype(np.float64)
339
+
340
+ # Normalize the LUT values
341
+ self._hist = self._hist / float(np.prod(image_to_use.shape[:2]))
342
+
343
+ # calculate the cdf
344
+ self._cdf = self._hist.cumsum(axis=0)
345
+
346
+ self.update_range()
347
+
348
+ def calcHist(self):
349
+ '''
350
+ Calculate the image histogram using CPU.
351
+ '''
352
+ image_to_use = self._norm if self._isfloat else self.image
353
+
354
+ self.n_bins = BYTE if self._image.dtype == np.uint8 else WORD
355
+
356
+ if image_to_use.ndim == 3 and self.channels > 1:
357
+ if np.prod(image_to_use.shape[:2]) <= NUMBA_RGB_SIZES[self.n_bins]:
358
+ self._hist = numba_histogram_rgb(image_to_use, self.n_bins)
359
+ else:
360
+ self._hist = np.array(
361
+ [
362
+ cv2.calcHist(
363
+ [image_to_use[:, :, channel]],
364
+ [0],
365
+ None,
366
+ [self.n_bins],
367
+ [0, self.n_bins],
368
+ ).squeeze()
369
+ for channel in range(self.channels)
370
+ ]
371
+ ).T / float(np.prod(image_to_use.shape[:2]))
372
+ self._cdf = self._hist.cumsum(axis=0)
373
+ else:
374
+ # Calculate histogram for the entire image (single-channel)
375
+ if image_to_use.size <= NUMBA_SIZES[self.n_bins]:
376
+ self._hist = numba_histogram(image_to_use, self.n_bins)
377
+ else:
378
+ self._hist = cv2.calcHist(
379
+ [image_to_use], [0], None, [self.n_bins], [0, self.n_bins]
380
+ ).squeeze() / float(np.prod(image_to_use.shape))
381
+
382
+ # Calculate CDF for the single channel
383
+ self._cdf = self._hist.cumsum(axis=0).squeeze()
384
+
385
+ self.update_range()
386
+
387
+ def fastHIST(self):
388
+ '''
389
+ Calculate the image histogram using a fast method.
390
+ '''
391
+ image_to_use = self._norm if self._isfloat else self.image
392
+
393
+ self.n_bins = BYTE if self._image.dtype == np.uint8 else WORD
394
+ cv2.normalize(
395
+ src=image_to_use,
396
+ dst=self._view,
397
+ alpha=0,
398
+ beta=255,
399
+ norm_type=cv2.NORM_MINMAX,
400
+ dtype=cv2.CV_8U,
401
+ )
402
+
403
+ if image_to_use.ndim == 3 and self.channels > 1:
404
+ # Multi-channel image
405
+ self._hist = np.array(
406
+ [
407
+ cv2.calcHist(
408
+ [self._view[:, :, channel]],
409
+ [0],
410
+ None,
411
+ [self.n_bins],
412
+ [0, self.n_bins],
413
+ ).squeeze()
414
+ for channel in range(self.channels)
415
+ ]
416
+ ).T / float(np.prod(self._view.shape[:2]))
417
+ self._cdf = self._hist.cumsum(axis=0)
418
+ else:
419
+ # Single-channel image
420
+ # calculate image histogram
421
+ self._hist = cv2.calcHist(
422
+ [self._view], [0], None, [self.n_bins], [0, self.n_bins]
423
+ ).squeeze() / float(np.prod(self._view.shape))
424
+ # calculate the cdf
425
+ self._cdf = self._hist.cumsum()
426
+
427
+ self.update_range()
428
+
429
+ def equalizeLUT(self, range=None, nLUT=False):
430
+ '''
431
+ Equalize the image using a Look-Up Table (LUT).
432
+
433
+ Parameters
434
+ ----------
435
+ range : tuple or None, optional
436
+ Range of values to consider for equalization.
437
+ nLUT : bool, optional
438
+ Flag indicating whether to use the normalized histogram for LUT creation.
439
+ '''
440
+ if nLUT:
441
+ self.calcHist()
442
+
443
+ if range is not None:
444
+ self._min = min(max(range[0], 0), self.n_bins - 1)
445
+ self._max = min(max(range[1], 0), self.n_bins - 1)
446
+
447
+ self._LUT = np.zeros((self.n_bins), dtype=np.uint8)
448
+ self._LUT[self._min : self._max] = np.linspace(
449
+ 0, 255, self._max - self._min, dtype=np.uint8
450
+ )
451
+ self._LUT[self._max :] = 255
452
+ if not self._isfloat:
453
+ self._view = self._LUT[self._image]
454
+ else:
455
+ self._view = self._LUT[self._norm]
456
+ else:
457
+ self.fastHIST()
458
+
459
+ if range is not None:
460
+ self._min = min(max(range[0], 0), 255)
461
+ self._max = min(max(range[1], 0), 255)
462
+
463
+ self._LUT = np.zeros((BYTE), dtype=np.uint8)
464
+ self._LUT[self._min : self._max] = np.linspace(
465
+ 0, 255, self._max - self._min, dtype=np.uint8
466
+ )
467
+ self._LUT[self._max :] = 255
468
+
469
+ cv2.LUT(self._view, self._LUT, self._view)
470
+
471
+ def getStatistics(self):
472
+ '''
473
+ Calculate and update image statistics.
474
+ '''
475
+ if self._hist is None:
476
+ self.calcHist()
477
+
478
+ _sum = 0.0
479
+ _sum_of_sq = 0.0
480
+ _count = 0
481
+ for i, count in enumerate(self._hist):
482
+ _sum += float(i * count)
483
+ _sum_of_sq += (i**2) * count
484
+ _count += count
485
+
486
+ self._stats['Mean'] = _sum / _count
487
+ self._stats['Area'] = _count * self._pixel_w * self._pixel_h
488
+ self.calcStdDev(_count, _sum, _sum_of_sq)
489
+
490
+ def calcStdDev(self, n, sum, sum_of_sq):
491
+ '''
492
+ Calculate standard deviation based on the count, sum, and sum of squares.
493
+
494
+ Parameters
495
+ ----------
496
+ n : int
497
+ Count of pixels.
498
+ sum : float
499
+ Sum of pixel values.
500
+ sum_of_sq : float
501
+ Sum of squares of pixel values.
502
+ '''
503
+ if n > 0.0:
504
+ stdDev = sum_of_sq - (sum**2 / n)
505
+ if stdDev > 0:
506
+ self._stats['StdDev'] = np.sqrt(stdDev / (n - 1))
507
+ else:
508
+ self._stats['StdDev'] = 0.0
509
+ else:
510
+ self._stats['StdDev'] = 0.0
511
+
512
+ def hsplitData(self):
513
+ '''
514
+ Split the image data horizontally and create a new uImage object.
515
+
516
+ Returns
517
+ -------
518
+ uImage
519
+ uImage object containing the horizontally split image data.
520
+ '''
521
+ mid = self._image.shape[1] // 2
522
+ left_view, right_view = self._image[:, :mid], self._image[:, mid:]
523
+
524
+ RGB_img = np.zeros(left_view.shape[:2] + (3,), dtype=np.uint8)
525
+
526
+ RGB_img[..., 0] = left_view
527
+ RGB_img[..., 1] = np.fliplr(right_view)
528
+
529
+ return uImage(RGB_img)
530
+
531
+ def hsplitViewOverlay(self, RGB=True) -> np.ndarray:
532
+ '''
533
+ Create a horizontally split view overlay.
534
+
535
+ Parameters
536
+ ----------
537
+ RGB : bool, optional
538
+ Flag indicating whether to create an RGB overlay, by default True.
539
+
540
+ Returns
541
+ -------
542
+ np.ndarray
543
+ Horizontally split view overlay.
544
+ '''
545
+ mid = self._view.shape[1] // 2
546
+ left_view, right_view = self._view[:, :mid], self._view[:, mid:]
547
+
548
+ _img = np.zeros(left_view.shape[:2] + (3,), dtype=np.uint8)
549
+ if RGB:
550
+ _img[..., 1] = left_view
551
+ _img[..., 2] = np.fliplr(right_view)
552
+ else:
553
+ _img[..., 1] = left_view
554
+ _img[..., 0] = np.fliplr(right_view)
555
+ return _img
556
+
557
+ def hsplitView(self):
558
+ '''
559
+ Split the image horizontally and create new uImage objects.
560
+
561
+ Returns
562
+ -------
563
+ Tuple[uImage, uImage]
564
+ Two uImage objects containing the horizontally split image data.
565
+ '''
566
+ mid = self.image.shape[1] // 2
567
+
568
+ return uImage(self.image[:, :mid]), uImage(np.fliplr(self.image[:, mid:]))
569
+
570
+ @staticmethod
571
+ def fromUINT8(buffer, height, width):
572
+ '''
573
+ Create a uImage object from a UINT8 buffer.
574
+
575
+ Parameters
576
+ ----------
577
+ buffer : object
578
+ Buffer containing the image data.
579
+ height : int
580
+ Height of the image.
581
+ width : int
582
+ Width of the image.
583
+
584
+ Returns
585
+ -------
586
+ uImage
587
+ uImage object created from the UINT8 buffer.
588
+ '''
589
+ return uImage(np.frombuffer(buffer, dtype=np.uint8).reshape(height, width))
590
+
591
+ @staticmethod
592
+ def fromUINT16(buffer, height, width):
593
+ '''
594
+ Create a uImage object from a UINT16 buffer.
595
+
596
+ Parameters
597
+ ----------
598
+ buffer : object
599
+ Buffer containing the image data.
600
+ height : int
601
+ Height of the image.
602
+ width : int
603
+ Width of the image.
604
+
605
+ Returns
606
+ -------
607
+ uImage
608
+ uImage object created from the UINT16 buffer.
609
+ '''
610
+ return uImage(np.frombuffer(buffer, dtype='<u2').reshape(height, width))
611
+
612
+ @staticmethod
613
+ def fromBuffer(buffer, height, width, bytes_per_pixel):
614
+ '''
615
+ Create a uImage object from a buffer.
616
+
617
+ Parameters
618
+ ----------
619
+ buffer : object or None
620
+ Buffer containing the image data.
621
+ height : int
622
+ Height of the image.
623
+ width : int
624
+ Width of the image.
625
+ bytes_per_pixel : int
626
+ Number of bytes per pixel.
627
+
628
+ Returns
629
+ -------
630
+ uImage or np.ndarray
631
+ uImage object created from the buffer or a zero-filled array.
632
+ '''
633
+ if buffer is not None:
634
+ return (
635
+ uImage.fromUINT8(buffer, height, width)
636
+ if bytes_per_pixel == 1
637
+ else uImage.fromUINT16(buffer, height, width)
638
+ )
639
+ return np.zeros((height, width), dtype=np.uint16)
640
+
641
+
642
+ @cuda.jit
643
+ def generate_lut_single_channel_kernel(image, lut, min_value, bin_width, num_bins):
644
+ row, col = cuda.grid(2)
645
+ if row < image.shape[0] and col < image.shape[1]:
646
+ bin_index = min(int((image[row, col] - min_value) / bin_width), num_bins - 1)
647
+ cuda.atomic.add(lut, bin_index, 1)
648
+
649
+
650
+ @cuda.jit
651
+ def generate_lut_multi_channel_kernel(image, lut, min_value, bin_width, num_bins):
652
+ row, col = cuda.grid(2)
653
+ channels = lut.shape[1]
654
+ if row < image.shape[0] and col < image.shape[1]:
655
+ for channel in range(channels):
656
+ bin_index = min(
657
+ int((image[row, col, channel] - min_value) / bin_width), num_bins - 1
658
+ )
659
+ cuda.atomic.add(lut, (bin_index, channel), 1)
660
+
661
+
662
+ class ImageSequenceBase:
663
+ '''
664
+ A base class for handling image sequences.
665
+
666
+ Attributes
667
+ ----------
668
+ shape : tuple or None
669
+ Shape of the image sequence.
670
+ dtype : np.dtype or None
671
+ Data type of the image sequence.
672
+ '''
673
+
674
+ def __init__(self):
675
+ '''
676
+ Initializes the ImageSequenceBase object.
677
+ '''
678
+ self._shape = None
679
+ self._dtype = None
680
+ self._path = ''
681
+
682
+ @property
683
+ def path(self) -> str:
684
+ '''
685
+ Get the path of the image sequence.
686
+
687
+ Returns
688
+ -------
689
+ str
690
+ Path of the image sequence.
691
+ '''
692
+ return self._path
693
+
694
+ @path.setter
695
+ def path(self, value: str):
696
+ '''
697
+ Set the path of the image sequence.
698
+
699
+ Parameters
700
+ ----------
701
+ value : str
702
+ New path of the image sequence.
703
+ '''
704
+ self._path = value
705
+
706
+ def __getitem__(self, i):
707
+ '''
708
+ Retrieves a specific item or slice from the image sequence.
709
+
710
+ Parameters
711
+ ----------
712
+ i : Index or slice
713
+
714
+ Returns
715
+ -------
716
+ np.ndarray or None
717
+ Retrieved data.
718
+ '''
719
+ if isinstance(i, slice):
720
+ return self.getSlice(i)
721
+ elif isinstance(i, int):
722
+ return self.getSlice(slice(i, i + 1, 1))
723
+ else:
724
+ raise IndexError('Index must be an integer or a slice')
725
+
726
+ def getSlice(
727
+ self,
728
+ timeSlice=None,
729
+ channelSlice=None,
730
+ zSlice=None,
731
+ ySlice=None,
732
+ xSlice=None,
733
+ squeezed=True,
734
+ four='TCYX',
735
+ three='TYX',
736
+ ):
737
+ '''
738
+ Retrieves a slice from the image sequence based on specified indices.
739
+
740
+ Parameters
741
+ ----------
742
+ timeSlice : slice or None
743
+ Slice for the time dimension.
744
+ channelSlice : slice or None
745
+ Slice for the channel dimension.
746
+ zSlice : slice or None
747
+ Slice for the z dimension.
748
+ ySlice : slice or None
749
+ Slice for the y dimension.
750
+ xSlice : slice or None
751
+ Slice for the x dimension.
752
+ squeezed : bool (optional)
753
+ Squeeze returned slice, default is True.
754
+ four : str
755
+ String representing the axis configuration for four dimensions.
756
+ three : str
757
+ String representing the axis configuration for three dimensions.
758
+
759
+ Returns
760
+ -------
761
+ np.ndarray
762
+ Retrieved slice.
763
+ '''
764
+ raise NotImplementedError('This method should be implemented by subclasses.')
765
+
766
+ def open(self):
767
+ '''
768
+ Opens the image sequence and initializes the data structure.
769
+
770
+ Raises
771
+ ------
772
+ NotImplementedError
773
+ This method should be implemented by subclasses.
774
+ '''
775
+ raise NotImplementedError('This method should be implemented by subclasses.')
776
+
777
+ def close(self):
778
+ '''
779
+ Closes the image sequence and releases any resources.
780
+
781
+ Raises
782
+ ------
783
+ NotImplementedError
784
+ This method should be implemented by subclasses.
785
+ '''
786
+ raise NotImplementedError('This method should be implemented by subclasses.')
787
+
788
+ def __len__(self):
789
+ '''
790
+ Returns the length of the image sequence.
791
+
792
+ Returns
793
+ -------
794
+ int
795
+ Length of the image sequence.
796
+ '''
797
+ if self._shape is None:
798
+ return 0
799
+ return self._shape[0]
800
+
801
+ @property
802
+ def shape(self):
803
+ '''
804
+ Returns the shape of the image sequence.
805
+
806
+ Returns
807
+ -------
808
+ tuple or None
809
+ Shape of the image sequence.
810
+ '''
811
+ if self._shape is None:
812
+ return None
813
+ return self._shape
814
+
815
+ def shapeTCZYX(self, four='TCYX', three='TYX'):
816
+ '''
817
+ Returns the shape of the image sequence in a specific format.
818
+
819
+ Returns
820
+ -------
821
+ tuple or None
822
+ Shape of the image sequence.
823
+ '''
824
+ if self._shape:
825
+ if len(self._shape) == 5:
826
+ return self._shape
827
+ elif len(self._shape) == 4:
828
+ if four == 'TCYX':
829
+ return (
830
+ self._shape[0],
831
+ self._shape[1],
832
+ 1,
833
+ self._shape[2],
834
+ self._shape[3],
835
+ )
836
+ elif four == 'CZYX':
837
+ return (
838
+ 1,
839
+ self._shape[0],
840
+ self._shape[1],
841
+ self._shape[2],
842
+ self._shape[3],
843
+ )
844
+ elif four == 'TZYX':
845
+ return (
846
+ self._shape[0],
847
+ 1,
848
+ self._shape[1],
849
+ self._shape[2],
850
+ self._shape[3],
851
+ )
852
+ else:
853
+ raise ValueError(f'Unsupported dimensions format: {four}')
854
+ elif len(self._shape) == 3:
855
+ if three == 'TYX':
856
+ return (self._shape[0], 1, 1, self._shape[1], self._shape[2])
857
+ elif three == 'CYX':
858
+ return (1, self._shape[0], 1, self._shape[1], self._shape[2])
859
+ elif three == 'ZYX':
860
+ return (1, 1, self._shape[0], self._shape[1], self._shape[2])
861
+ else:
862
+ raise ValueError(f'Unsupported dimensions format: {three}')
863
+ elif len(self._shape) == 2:
864
+ return (1, 1, 1, self._shape[0], self._shape[1])
865
+ else:
866
+ raise ValueError(
867
+ f'Unsupported number of dimensions: {len(self._shape)}'
868
+ )
869
+ else:
870
+ return None
871
+
872
+ def __enter__(self):
873
+ '''
874
+ Enters the context manager.
875
+
876
+ Returns
877
+ -------
878
+ ImageSequenceBase
879
+ ImageSequenceBase object.
880
+ '''
881
+ return self
882
+
883
+ def __exit__(self, exc_type, exc_value, traceback):
884
+ '''
885
+ Exits the context manager.
886
+
887
+ Parameters
888
+ ----------
889
+ exc_type : type
890
+ Exception type.
891
+ exc_value : Exception
892
+ Exception instance.
893
+ traceback : Traceback
894
+ Traceback object.
895
+ '''
896
+ self.close()
897
+
898
+
899
+ class TiffSeqHandler(ImageSequenceBase):
900
+ '''
901
+ Class for handling TIFF sequences using tifffile and zarr libraries.
902
+
903
+ Parameters
904
+ ----------
905
+ tiff_seq : tf.TiffSequence
906
+ The TIFF sequence to be handled.
907
+
908
+ Methods
909
+ -------
910
+ open()
911
+ Opens the TIFF files and initializes necessary attributes.
912
+ close()
913
+ Closes the TIFF files and resets attributes.
914
+ __getitem__(i)
915
+ Gets an item or slice from the TIFF sequence.
916
+ get_slice(...)
917
+ Gets a slice from the TIFF sequence based on specified indices.
918
+ __len__()
919
+ Returns the total number of frames in the TIFF sequence.
920
+ __enter__()
921
+ Enters the context manager.
922
+ __exit__(exc_type, exc_value, traceback)
923
+ Exits the context manager.
924
+
925
+ Attributes
926
+ ----------
927
+ shape : tuple
928
+ Shape of the TIFF sequence.
929
+
930
+ Raises
931
+ ------
932
+ ValueError
933
+ If the shapes of TIFF files in the sequence do not match.
934
+ '''
935
+
936
+ def __init__(self, tiff_seq: tf.TiffSequence) -> None:
937
+ '''
938
+ Initializes the TiffSeqHandler object.
939
+
940
+ Parameters
941
+ ----------
942
+ tiff_seq : tf.TiffSequence
943
+ The TIFF sequence to be handled.
944
+ '''
945
+ super().__init__()
946
+
947
+ self._tiff_seq = tiff_seq
948
+ self.path = ','.join(self._tiff_seq.files)
949
+ self._initialize_arrays()
950
+
951
+ def _initialize_arrays(self):
952
+ '''Initializes arrays and attributes.'''
953
+ self._stores = [None] * len(self._tiff_seq.files)
954
+ self._zarr = [None] * len(self._tiff_seq.files)
955
+ self._frames = [None] * len(self._tiff_seq.files)
956
+ self._data = None
957
+ self._shape = None
958
+ self._dtype = None
959
+ self._cum_frames = None
960
+
961
+ def open(self):
962
+ '''
963
+ Opens the TIFF files and initializes necessary attributes.
964
+
965
+ Raises
966
+ ------
967
+ ValueError
968
+ If the shapes of TIFF files do not match.
969
+ '''
970
+ for idx, file in enumerate(self._tiff_seq.files):
971
+ self._stores[idx] = tf.imread(file, aszarr=True)
972
+ self._zarr[idx] = zarr.open(self._stores[idx], mode='r')
973
+ n_dim = len(self._zarr[idx].shape)
974
+ if n_dim > 2:
975
+ self._frames[idx] = self._zarr[idx].shape[0]
976
+ else:
977
+ self._zarr[idx] = self._zarr[idx][:, :][np.newaxis, ...]
978
+ self._frames[idx] = 1
979
+
980
+ # Check if shapes match
981
+ first_shape = self._zarr[0].shape[1:]
982
+ if any(
983
+ shape != first_shape for shape in [arr.shape[1:] for arr in self._zarr[1:]]
984
+ ):
985
+ raise ValueError('Shapes of TIFF files do not match.')
986
+
987
+ self._update_properties()
988
+
989
+ def close(self):
990
+ '''
991
+ Closes the TIFF files and resets attributes.
992
+ '''
993
+ for store in self._stores:
994
+ if store is not None:
995
+ store.close()
996
+ self._tiff_seq.close()
997
+ self._initialize_arrays()
998
+
999
+ def _update_properties(self):
1000
+ '''Updates properties such as shape, dtype, and cumulative frames.'''
1001
+ self._shape = (sum(self._frames),) + self._zarr[0].shape[1:]
1002
+ self._dtype = self._zarr[0].dtype
1003
+ self._cum_frames = np.cumsum(self._frames)
1004
+
1005
+ def __getitem__(self, i):
1006
+ '''
1007
+ Gets an item or slice from the TIFF sequence.
1008
+
1009
+ Parameters
1010
+ ----------
1011
+ i : int, slice
1012
+ Index or slice to retrieve.
1013
+
1014
+ Returns
1015
+ -------
1016
+ np.ndarray
1017
+ Retrieved data.
1018
+ '''
1019
+ if isinstance(i, slice):
1020
+ return self._get_slice(i)
1021
+ elif isinstance(i, int) or np.issubdtype(i, np.integer):
1022
+ return self._get_slice(slice(i, i + 1, 1))
1023
+ else:
1024
+ return self._get_slice(slice(None))
1025
+
1026
+ def _get_file_and_adjust_index(self, i):
1027
+ '''
1028
+ Determines the file index and adjusts the index for retrieval.
1029
+
1030
+ Parameters
1031
+ ----------
1032
+ i : int
1033
+ Index to adjust.
1034
+
1035
+ Returns
1036
+ -------
1037
+ tuple
1038
+ File index and adjusted index.
1039
+ '''
1040
+ file_idx = 0
1041
+ for idx, cum_frames in enumerate(self._cum_frames):
1042
+ if i <= cum_frames - 1:
1043
+ file_idx = idx
1044
+ i -= cum_frames - self._frames[idx]
1045
+ break
1046
+ return file_idx, i
1047
+
1048
+ def _get_slice(self, i):
1049
+ '''
1050
+ Retrieves a slice from the TIFF sequence.
1051
+
1052
+ Parameters
1053
+ ----------
1054
+ i : slice
1055
+ Slice to retrieve.
1056
+
1057
+ Returns
1058
+ -------
1059
+ np.ndarray
1060
+ Retrieved data.
1061
+ '''
1062
+ start = 0 if i.start is None else i.start
1063
+ stop = sum(self._frames) if start is None else i.stop
1064
+ if stop <= self._cum_frames[0]:
1065
+ indices = slice(start, stop)
1066
+ return self._zarr[0][indices]
1067
+ else:
1068
+ return self._get_concatenated_slice(i)
1069
+
1070
+ def _get_concatenated_slice(self, i):
1071
+ '''
1072
+ Retrieves a concatenated slice from multiple TIFF files.
1073
+
1074
+ Parameters
1075
+ ----------
1076
+ i : slice
1077
+ Slice to retrieve.
1078
+
1079
+ Returns
1080
+ -------
1081
+ np.ndarray
1082
+ Retrieved data.
1083
+ '''
1084
+ indices = np.arange(i.start or 0, i.stop)
1085
+ result = np.empty(shape=(0,) + self._zarr[0].shape[1:], dtype=self._dtype)
1086
+ for idx, cum_frames in enumerate(self._cum_frames):
1087
+ mask = np.logical_and(
1088
+ cum_frames - self._frames[idx] <= indices, indices < cum_frames
1089
+ )
1090
+ if np.sum(mask) > 0:
1091
+ r = indices[mask] - (cum_frames - self._frames[idx])
1092
+ result = np.concatenate(
1093
+ (result, self._zarr[idx][np.min(r) : np.max(r) + 1]), axis=0
1094
+ )
1095
+ return result
1096
+
1097
+ def getSlice(
1098
+ self,
1099
+ timeSlice=None,
1100
+ channelSlice=None,
1101
+ zSlice=None,
1102
+ ySlice=None,
1103
+ xSlice=None,
1104
+ squeezed=True,
1105
+ broadcasted=False,
1106
+ four='TCYX',
1107
+ three='TYX',
1108
+ ):
1109
+ '''
1110
+ Retrieves a slice from the Zarr array based on specified indices.
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ timeSlice : slice or None
1115
+ Slice for the time dimension.
1116
+ channelSlice : slice or None
1117
+ Slice for the channel dimension.
1118
+ zSlice : slice or None
1119
+ Slice for the z dimension.
1120
+ ySlice : slice or None
1121
+ Slice for the y dimension.
1122
+ xSlice : slice or None
1123
+ Slice for the x dimension.
1124
+ squeezed : bool (optional)
1125
+ Squeeze returned slice, default is True.
1126
+ broadcasted : bool (optional)
1127
+ Broad cast returned slice according to TCZYX, default is False.
1128
+ four : str
1129
+ String representing the axis configuration for four dimensions.
1130
+ three : str
1131
+ String representing the axis configuration for three dimensions.
1132
+
1133
+ Returns
1134
+ -------
1135
+ np.ndarray
1136
+ Retrieved slice.
1137
+ '''
1138
+ t = ifnone(timeSlice, slice(None))
1139
+ c = ifnone(channelSlice, slice(None))
1140
+ z = ifnone(zSlice, slice(None))
1141
+ y = ifnone(ySlice, slice(None))
1142
+ x = ifnone(xSlice, slice(None))
1143
+
1144
+ if self.shape is None:
1145
+ raise ValueError(f'The handler was not initializd correctly.')
1146
+
1147
+ ndim = len(self._shape)
1148
+ data = None
1149
+
1150
+ if ndim == 5:
1151
+ data = self[t][..., c, z, y, x]
1152
+ new_slice = (slice(None),) * 5
1153
+ elif ndim == 4:
1154
+ if four == 'TCYX':
1155
+ data = self[t][..., c, y, x]
1156
+ new_slice = (slice(None),) * 2 + (np.newaxis,) + (slice(None),) * 2
1157
+ elif four == 'CZYX':
1158
+ data = self[c][..., z, y, x]
1159
+ new_slice = (np.newaxis,) + (slice(None),) * 4
1160
+ elif four == 'TZYX':
1161
+ data = self[t][..., z, y, x]
1162
+ new_slice = (
1163
+ slice(None),
1164
+ np.newaxis,
1165
+ ) + (slice(None),) * 3
1166
+ else:
1167
+ raise ValueError(f'Unsupported dimensions format: {four}')
1168
+ elif ndim == 3:
1169
+ if three == 'TYX':
1170
+ data = self[t][..., y, x]
1171
+ new_slice = (
1172
+ slice(None),
1173
+ np.newaxis,
1174
+ np.newaxis,
1175
+ ) + (slice(None),) * 2
1176
+ elif three == 'CYX':
1177
+ data = self[c][..., y, x]
1178
+ new_slice = (
1179
+ np.newaxis,
1180
+ slice(None),
1181
+ np.newaxis,
1182
+ ) + (slice(None),) * 2
1183
+ elif three == 'ZYX':
1184
+ data = self[z][..., y, x]
1185
+ new_slice = (
1186
+ np.newaxis,
1187
+ np.newaxis,
1188
+ ) + (slice(None),) * 3
1189
+ else:
1190
+ raise ValueError(f'Unsupported dimensions format: {three}')
1191
+ elif ndim == 2:
1192
+ data = self[0][y, x]
1193
+ new_slice = (np.newaxis,) * 3 + (slice(None),) * 2
1194
+ else:
1195
+ raise ValueError(f'Unsupported number of dimensions: {len(self._shape)}')
1196
+
1197
+ if broadcasted:
1198
+ return data[new_slice]
1199
+ else:
1200
+ if squeezed:
1201
+ return data.squeeze()
1202
+ else:
1203
+ return data
1204
+
1205
+ def __len__(self):
1206
+ '''
1207
+ Returns the total number of frames in the TIFF sequence.
1208
+
1209
+ Returns
1210
+ -------
1211
+ int
1212
+ Total number of frames.
1213
+ '''
1214
+ return sum(self._frames)
1215
+
1216
+ @property
1217
+ def shape(self):
1218
+ '''
1219
+ Gets the shape of the TIFF sequence.
1220
+
1221
+ Returns
1222
+ -------
1223
+ tuple
1224
+ Shape of the TIFF sequence.
1225
+ '''
1226
+ return self._shape if self._zarr is not None else None
1227
+
1228
+
1229
+ class ZarrImageSequence(ImageSequenceBase):
1230
+ '''
1231
+ A class for handling image sequences stored in Zarr format.
1232
+
1233
+ Parameters
1234
+ ----------
1235
+ path : str
1236
+ The path to the Zarr store.
1237
+
1238
+ Attributes
1239
+ ----------
1240
+ path : str
1241
+ The path to the Zarr store.
1242
+ data : zarr.Array or None
1243
+ The Zarr array containing the image sequence data.
1244
+ '''
1245
+
1246
+ def __init__(self, path: str) -> None:
1247
+ '''
1248
+ Initializes the ZarrImageSequence object.
1249
+
1250
+ Parameters
1251
+ ----------
1252
+ path : str
1253
+ The path to the Zarr store.
1254
+ '''
1255
+ super().__init__()
1256
+ self.path = path
1257
+ self.data = None
1258
+
1259
+ def getSlice(
1260
+ self,
1261
+ timeSlice=None,
1262
+ channelSlice=None,
1263
+ zSlice=None,
1264
+ ySlice=None,
1265
+ xSlice=None,
1266
+ squeezed=True,
1267
+ four='TCYX',
1268
+ three='TYX',
1269
+ ):
1270
+ '''
1271
+ Retrieves a slice from the Zarr array based on specified indices.
1272
+
1273
+ Parameters
1274
+ ----------
1275
+ timeSlice : slice or None
1276
+ Slice for the time dimension.
1277
+ channelSlice : slice or None
1278
+ Slice for the channel dimension.
1279
+ zSlice : slice or None
1280
+ Slice for the z dimension.
1281
+ ySlice : slice or None
1282
+ Slice for the y dimension.
1283
+ xSlice : slice or None
1284
+ Slice for the x dimension.
1285
+ squeezed : bool (optional)
1286
+ Squeeze returned slice, default is True.
1287
+ four : str
1288
+ String representing the axis configuration for four dimensions.
1289
+ three : str
1290
+ String representing the axis configuration for three dimensions.
1291
+
1292
+ Returns
1293
+ -------
1294
+ np.ndarray
1295
+ Retrieved slice.
1296
+ '''
1297
+ za = zarr.open(self.path, 'r')
1298
+
1299
+ # Handle None values and replace with default slices
1300
+ timeSlice = ifnone(timeSlice, slice(None))
1301
+ channelSlice = ifnone(channelSlice, slice(None))
1302
+ zSlice = ifnone(zSlice, slice(None))
1303
+ ySlice = ifnone(ySlice, slice(None))
1304
+ xSlice = ifnone(xSlice, slice(None))
1305
+
1306
+ if len(za.shape) == 5:
1307
+ data = za[timeSlice, channelSlice, zSlice, ySlice, xSlice]
1308
+ elif len(za.shape) == 4:
1309
+ if four == 'TCYX':
1310
+ data = za[timeSlice, channelSlice, ySlice, xSlice]
1311
+ elif four == 'CZYX':
1312
+ data = za[channelSlice, zSlice, ySlice, xSlice]
1313
+ elif four == 'TZYX':
1314
+ data = za[timeSlice, zSlice, ySlice, xSlice]
1315
+ else:
1316
+ raise ValueError(f'Unsupported dimensions format: {four}')
1317
+ elif len(za.shape) == 3:
1318
+ if three == 'TYX':
1319
+ data = za[timeSlice, ySlice, xSlice]
1320
+ elif three == 'CYX':
1321
+ data = za[channelSlice, ySlice, xSlice]
1322
+ elif three == 'ZYX':
1323
+ data = za[zSlice, ySlice, xSlice]
1324
+ else:
1325
+ raise ValueError(f'Unsupported dimensions format: {three}')
1326
+ elif len(za.shape) == 2:
1327
+ data = za[ySlice, xSlice]
1328
+ else:
1329
+ raise ValueError(f'Unsupported number of dimensions: {len(za.shape)}')
1330
+
1331
+ del za
1332
+ if squeezed:
1333
+ return data.squeeze()
1334
+ else:
1335
+ return data
1336
+
1337
+ def open(self):
1338
+ '''
1339
+ Opens the zarr file.
1340
+ '''
1341
+ data = zarr.open(self.path, 'r')
1342
+ self._shape = data.shape
1343
+ self._dtype = data.dtype
1344
+
1345
+ def close(self):
1346
+ '''
1347
+ Closes the zarr file.
1348
+ '''
1349
+ pass
1350
+
1351
+
1352
+ def ifnone(a, b):
1353
+ return b if a is None else a
1354
+
1355
+
1356
+ def saveZarrImage(
1357
+ path: str,
1358
+ imgSeq: Union[TiffSeqHandler, ZarrImageSequence],
1359
+ timeSlice: slice = None,
1360
+ channelSlice: slice = None,
1361
+ zSlice: slice = None,
1362
+ ySlice: slice = None,
1363
+ xSlice: slice = None,
1364
+ ):
1365
+ '''
1366
+ Saves an image sequence represented by either a TiffSeqHandler
1367
+ or ZarrImageSequence to a Zarr store.
1368
+
1369
+ Parameters
1370
+ ----------
1371
+ path : str
1372
+ The path to the Zarr store.
1373
+ imgSeq : TiffSeqHandler or ZarrImageSequence
1374
+ The image sequence to save.
1375
+ timeSlice : slice or None
1376
+ Slice for the time dimension.
1377
+ channelSlice : slice or None
1378
+ Slice for the channel dimension.
1379
+ zSlice : slice or None
1380
+ Slice for the z dimension.
1381
+ ySlice : slice or None
1382
+ Slice for the y dimension.
1383
+ xSlice : slice or None
1384
+ Slice for the x dimension.
1385
+
1386
+ Returns
1387
+ -------
1388
+ bool
1389
+ True if the save operation is successful, False otherwise.
1390
+ '''
1391
+ # Handle None values and replace with default slices
1392
+ timeSlice = ifnone(timeSlice, slice(None))
1393
+ channelSlice = ifnone(channelSlice, slice(None))
1394
+ zSlice = ifnone(zSlice, slice(None))
1395
+ ySlice = ifnone(ySlice, slice(None))
1396
+ xSlice = ifnone(xSlice, slice(None))
1397
+
1398
+ if isinstance(imgSeq, TiffSeqHandler):
1399
+ ndim = len(imgSeq.shape)
1400
+ if ndim == 2:
1401
+ shape = (
1402
+ 1,
1403
+ 1,
1404
+ 1,
1405
+ ifnone(ySlice.stop, imgSeq.shape[0]) - ifnone(ySlice.start, 0),
1406
+ ifnone(xSlice.stop, imgSeq.shape[1]) - ifnone(xSlice.start, 0),
1407
+ )
1408
+ chunks = (1, 1, 1, shape[3], shape[4])
1409
+ elif ndim == 3:
1410
+ shape = (
1411
+ ifnone(timeSlice.stop, imgSeq.shape[0]) - ifnone(timeSlice.start, 0),
1412
+ 1,
1413
+ 1,
1414
+ ifnone(ySlice.stop, imgSeq.shape[1]) - ifnone(ySlice.start, 0),
1415
+ ifnone(xSlice.stop, imgSeq.shape[2]) - ifnone(xSlice.start, 0),
1416
+ )
1417
+ chunks = (min(10, shape[0]), 1, 1, shape[3], shape[4])
1418
+ elif ndim == 4:
1419
+ shape = (
1420
+ ifnone(timeSlice.stop, imgSeq.shape[0]) - ifnone(timeSlice.start, 0),
1421
+ ifnone(channelSlice.stop, imgSeq.shape[1])
1422
+ - ifnone(channelSlice.start, 0),
1423
+ 1,
1424
+ ifnone(ySlice.stop, imgSeq.shape[2]) - ifnone(ySlice.start, 0),
1425
+ ifnone(xSlice.stop, imgSeq.shape[3]) - ifnone(xSlice.start, 0),
1426
+ )
1427
+ chunks = (min(10, shape[0]), min(10, shape[1]), 1, shape[3], shape[4])
1428
+ elif ndim == 5:
1429
+ shape = (
1430
+ ifnone(timeSlice.stop, imgSeq.shape[0]) - ifnone(timeSlice.start, 0),
1431
+ ifnone(channelSlice.stop, imgSeq.shape[1])
1432
+ - ifnone(channelSlice.start, 0),
1433
+ ifnone(zSlice.stop, imgSeq.shape[2]) - ifnone(zSlice.start, 0),
1434
+ ifnone(ySlice.stop, imgSeq.shape[3]) - ifnone(ySlice.start, 0),
1435
+ ifnone(xSlice.stop, imgSeq.shape[4]) - ifnone(xSlice.start, 0),
1436
+ )
1437
+ chunks = (
1438
+ min(10, shape[0]),
1439
+ min(10, shape[1]),
1440
+ min(10, shape[2]),
1441
+ shape[3],
1442
+ shape[4],
1443
+ )
1444
+ else:
1445
+ raise ValueError(f'Unsupported number of dimensions: {ndim}')
1446
+
1447
+ zarrImg = zarr.open(
1448
+ path,
1449
+ mode='w-',
1450
+ shape=shape,
1451
+ chunks=chunks,
1452
+ compressor=None,
1453
+ dtype=imgSeq._dtype,
1454
+ )
1455
+
1456
+ timeSlice = slice(ifnone(timeSlice.start, 0), shape[0])
1457
+
1458
+ for idx in np.arange(len(imgSeq._zarr)):
1459
+ offset = imgSeq._cum_frames[idx] - imgSeq._frames[idx]
1460
+ zarrSlice = slice(
1461
+ max(
1462
+ timeSlice.start,
1463
+ offset,
1464
+ ),
1465
+ min(timeSlice.stop, imgSeq._cum_frames[idx]),
1466
+ )
1467
+
1468
+ # Adjust the tiffSlice based on the offset
1469
+ tiffSlice = slice(
1470
+ max(zarrSlice.start - offset, 0),
1471
+ min(zarrSlice.stop - offset, imgSeq._zarr[idx].shape[0]),
1472
+ )
1473
+
1474
+ # Use tuple unpacking to apply the slices to the image
1475
+ print('Saving ...', end='\r')
1476
+ zarrImg[zarrSlice, ...] = imgSeq.getSlice(
1477
+ tiffSlice, channelSlice, zSlice, ySlice, xSlice, broadcasted=True
1478
+ )
1479
+
1480
+ print('Done ...', end='\r')
1481
+ return True
1482
+ elif isinstance(imgSeq, ZarrImageSequence):
1483
+ print('Saving ...', end='\r')
1484
+ shape = (
1485
+ ifnone(timeSlice.stop, imgSeq.shape[0]) - ifnone(timeSlice.start, 0),
1486
+ ifnone(channelSlice.stop, imgSeq.shape[1]) - ifnone(channelSlice.start, 0),
1487
+ ifnone(zSlice.stop, imgSeq.shape[2]) - ifnone(zSlice.start, 0),
1488
+ ifnone(ySlice.stop, imgSeq.shape[3]) - ifnone(ySlice.start, 0),
1489
+ ifnone(xSlice.stop, imgSeq.shape[4]) - ifnone(xSlice.start, 0),
1490
+ )
1491
+ chunks = (
1492
+ min(10, shape[0]),
1493
+ min(10, shape[1]),
1494
+ min(10, shape[2]),
1495
+ shape[3],
1496
+ shape[4],
1497
+ )
1498
+ zarrImg = zarr.open(
1499
+ path,
1500
+ mode='w-',
1501
+ shape=shape,
1502
+ chunks=chunks,
1503
+ compressor=None,
1504
+ dtype=imgSeq._dtype,
1505
+ )
1506
+ zarrImg[:] = imgSeq.getSlice(timeSlice, channelSlice, zSlice, ySlice, xSlice)
1507
+
1508
+ print('Done ...', end='\r')
1509
+ return True
1510
+ else:
1511
+ print('Failed ...', end='\r')
1512
+ return False