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.
- microEye/__init__.py +47 -0
- microEye/_version.py +2 -0
- microEye/analysis/__init__.py +1 -0
- microEye/analysis/checklist_dialog.py +143 -0
- microEye/analysis/cmosMaps.py +228 -0
- microEye/analysis/filters/__init__.py +9 -0
- microEye/analysis/filters/base.py +21 -0
- microEye/analysis/filters/spatial.py +338 -0
- microEye/analysis/filters/temporal.py +76 -0
- microEye/analysis/fitting/__init__.py +0 -0
- microEye/analysis/fitting/fit.py +680 -0
- microEye/analysis/fitting/nena.py +375 -0
- microEye/analysis/fitting/phasor_fit.py +90 -0
- microEye/analysis/fitting/processing.py +317 -0
- microEye/analysis/fitting/psf/__init__.py +6 -0
- microEye/analysis/fitting/psf/extract.py +1129 -0
- microEye/analysis/fitting/psf/rubost_mean.py +150 -0
- microEye/analysis/fitting/psf/spline.py +167 -0
- microEye/analysis/fitting/psf/stats/__init__.py +12 -0
- microEye/analysis/fitting/psf/stats/core.py +295 -0
- microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
- microEye/analysis/fitting/psf/stats/io.py +104 -0
- microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
- microEye/analysis/fitting/psf/temp.py +147 -0
- microEye/analysis/fitting/psf/test.py +47 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
- microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
- microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
- microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
- microEye/analysis/fitting/results.py +917 -0
- microEye/analysis/fitting/results_stats.py +251 -0
- microEye/analysis/fitting/tardis.py +209 -0
- microEye/analysis/multi_viewer.py +568 -0
- microEye/analysis/processing/__init__.py +1 -0
- microEye/analysis/processing/frc.py +120 -0
- microEye/analysis/rendering/__init__.py +3 -0
- microEye/analysis/rendering/base.py +452 -0
- microEye/analysis/rendering/cloud.py +215 -0
- microEye/analysis/rendering/core.py +47 -0
- microEye/analysis/rendering/volumetric.py +198 -0
- microEye/analysis/tools/__init__.py +0 -0
- microEye/analysis/tools/kymograms.py +1079 -0
- microEye/analysis/tools/roi_selectors.py +556 -0
- microEye/analysis/utils/__init__.py +3 -0
- microEye/analysis/utils/coordinates.py +18 -0
- microEye/analysis/utils/images.py +68 -0
- microEye/analysis/utils/windows.py +19 -0
- microEye/analysis/viewer/__init__.py +3 -0
- microEye/analysis/viewer/image_options_widget.py +615 -0
- microEye/analysis/viewer/images.py +1225 -0
- microEye/analysis/viewer/layers_widget.py +415 -0
- microEye/analysis/viewer/localizations.py +1304 -0
- microEye/analysis/viewer/psf.py +1003 -0
- microEye/analysis/viewer/volume.py +456 -0
- microEye/hardware/__init__.py +3 -0
- microEye/hardware/cams/__init__.py +29 -0
- microEye/hardware/cams/camera_calibration.py +99 -0
- microEye/hardware/cams/camera_list.py +528 -0
- microEye/hardware/cams/camera_options.py +694 -0
- microEye/hardware/cams/camera_panel.py +941 -0
- microEye/hardware/cams/dummy/__init__.py +1 -0
- microEye/hardware/cams/dummy/dummy_panel.py +759 -0
- microEye/hardware/cams/jobs.py +497 -0
- microEye/hardware/cams/line_profiler.py +99 -0
- microEye/hardware/cams/linescan/IR_Cam.py +474 -0
- microEye/hardware/cams/linescan/__init__.py +1 -0
- microEye/hardware/cams/micam.py +524 -0
- microEye/hardware/cams/pco/__init__.py +58 -0
- microEye/hardware/cams/pco/enums.py +382 -0
- microEye/hardware/cams/pco/pco_cam.py +761 -0
- microEye/hardware/cams/pco/pco_panel.py +477 -0
- microEye/hardware/cams/shortcuts.py +312 -0
- microEye/hardware/cams/thorlabs/__init__.py +1 -0
- microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
- microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
- microEye/hardware/cams/ueye/__init__.py +1 -0
- microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
- microEye/hardware/cams/ueye/ueye_panel.py +861 -0
- microEye/hardware/cams/vimba/__init__.py +1 -0
- microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
- microEye/hardware/cams/vimba/vimba_panel.py +813 -0
- microEye/hardware/device.py +60 -0
- microEye/hardware/lasers/__init__.py +13 -0
- microEye/hardware/lasers/io_matchbox.py +791 -0
- microEye/hardware/lasers/io_params.py +85 -0
- microEye/hardware/lasers/io_single_laser.py +742 -0
- microEye/hardware/lasers/laser_relay.py +594 -0
- microEye/hardware/mieye/__init__.py +1 -0
- microEye/hardware/mieye/acquisition_manager.py +467 -0
- microEye/hardware/mieye/devices_manager.py +533 -0
- microEye/hardware/mieye/miEye.py +659 -0
- microEye/hardware/misc/__init__.py +0 -0
- microEye/hardware/misc/acquisition_view.py +71 -0
- microEye/hardware/misc/reglo.py +761 -0
- microEye/hardware/misc/temp.py +188 -0
- microEye/hardware/port_config.py +59 -0
- microEye/hardware/protocols/__init__.py +2 -0
- microEye/hardware/protocols/actions.py +402 -0
- microEye/hardware/protocols/actions_items.py +703 -0
- microEye/hardware/protocols/designer.py +244 -0
- microEye/hardware/protocols/scene_manager.py +191 -0
- microEye/hardware/protocols/serialization.py +97 -0
- microEye/hardware/pycromanager/__init__.py +16 -0
- microEye/hardware/pycromanager/core.py +1433 -0
- microEye/hardware/pycromanager/devices.py +461 -0
- microEye/hardware/pycromanager/enums.py +107 -0
- microEye/hardware/pycromanager/headless.py +153 -0
- microEye/hardware/pycromanager/utils.py +34 -0
- microEye/hardware/pycromanager/widgets/__init__.py +5 -0
- microEye/hardware/pycromanager/widgets/bridges.py +407 -0
- microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
- microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
- microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
- microEye/hardware/stages/__init__.py +18 -0
- microEye/hardware/stages/elliptec/__init__.py +5 -0
- microEye/hardware/stages/elliptec/baseDevice.py +314 -0
- microEye/hardware/stages/elliptec/device.py +384 -0
- microEye/hardware/stages/elliptec/deviceID.py +212 -0
- microEye/hardware/stages/elliptec/devicePort.py +379 -0
- microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
- microEye/hardware/stages/elliptec/devicesView.py +706 -0
- microEye/hardware/stages/elliptec/ellDevices.py +134 -0
- microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
- microEye/hardware/stages/elliptec/motorInfo.py +153 -0
- microEye/hardware/stages/elliptec/stage.py +62 -0
- microEye/hardware/stages/elliptec/test.py +139 -0
- microEye/hardware/stages/kinesis/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
- microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
- microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
- microEye/hardware/stages/kinesis/kinesis.py +776 -0
- microEye/hardware/stages/piezo_concept.py +607 -0
- microEye/hardware/stages/stabilizer.py +785 -0
- microEye/hardware/stages/stage.py +89 -0
- microEye/hardware/widgets/__init__.py +10 -0
- microEye/hardware/widgets/controller.py +246 -0
- microEye/hardware/widgets/devices.py +133 -0
- microEye/hardware/widgets/focusWidget.py +264 -0
- microEye/hardware/widgets/qlist_slider.py +113 -0
- microEye/hardware/widgets/scan_acquisition.py +424 -0
- microEye/icons/1024.png +0 -0
- microEye/icons/128.png +0 -0
- microEye/icons/16.png +0 -0
- microEye/icons/24.png +0 -0
- microEye/icons/256.png +0 -0
- microEye/icons/32.png +0 -0
- microEye/icons/48.png +0 -0
- microEye/icons/512.png +0 -0
- microEye/icons/64.png +0 -0
- microEye/icons/__init__.py +0 -0
- microEye/icons/close.svg +88 -0
- microEye/icons/mieye.png +0 -0
- microEye/icons/min.svg +83 -0
- microEye/icons/viewer.png +0 -0
- microEye/launcher.py +42 -0
- microEye/qt.py +181 -0
- microEye/utils/__init__.py +2 -0
- microEye/utils/enum_encoder.py +10 -0
- microEye/utils/expandable_groupbox.py +93 -0
- microEye/utils/gui_helper.py +457 -0
- microEye/utils/hid/__init__.py +8 -0
- microEye/utils/hid/controller.py +153 -0
- microEye/utils/hid/device.py +63 -0
- microEye/utils/hid/enums.py +140 -0
- microEye/utils/hid/utils.py +58 -0
- microEye/utils/labelled_slider.py +134 -0
- microEye/utils/metadata.py +599 -0
- microEye/utils/metadata_tree.py +718 -0
- microEye/utils/micro_launcher.py +245 -0
- microEye/utils/parameter_tree.py +325 -0
- microEye/utils/pyscripting.py +444 -0
- microEye/utils/retry_exec.py +35 -0
- microEye/utils/start_gui.py +112 -0
- microEye/utils/thread_worker.py +115 -0
- microEye/utils/uImage.py +1512 -0
- microeye-2.3.2.dist-info/METADATA +401 -0
- microeye-2.3.2.dist-info/RECORD +190 -0
- microeye-2.3.2.dist-info/WHEEL +5 -0
- microeye-2.3.2.dist-info/entry_points.txt +2 -0
- microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
- microeye-2.3.2.dist-info/top_level.txt +1 -0
microEye/utils/uImage.py
ADDED
|
@@ -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
|