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,1079 @@
1
+ import json
2
+ import traceback
3
+ from queue import Queue
4
+
5
+ import cv2
6
+ import matplotlib.pyplot as plt
7
+ import numba as nb
8
+ import numpy as np
9
+ import tifffile as tf
10
+
11
+ from microEye.qt import Qt, QtCore, QtWidgets, Signal, getOpenFileName, getSaveFileName
12
+ from microEye.utils.gui_helper import *
13
+ from microEye.utils.thread_worker import QThreadWorker
14
+ from microEye.utils.uImage import TiffSeqHandler, ZarrImageSequence, uImage
15
+
16
+
17
+ class MultiLineROISelector:
18
+ def __init__(self, image_data: np.ndarray, resize_factor=1.0):
19
+ '''
20
+ Initialize the MultiLineROISelector.
21
+
22
+ Parameters
23
+ ----------
24
+ image_data : np.ndarray
25
+ The image data as a NumPy array.
26
+ resize_factor : float, optional
27
+ The initial resize factor for displaying the image, by default 1.0.
28
+ '''
29
+ self.original = image_data.copy()
30
+ self.resize_factor = resize_factor
31
+ self.resized_empty: np.ndarray = cv2.resize(
32
+ self.original,
33
+ (0, 0),
34
+ fx=self.resize_factor,
35
+ fy=self.resize_factor,
36
+ interpolation=cv2.INTER_NEAREST,
37
+ )
38
+ self.resized_image = self.resized_empty.copy()
39
+
40
+ self.rois = [] # List store multiple ROIs, each represented as a list of points
41
+ self.active_roi_idx = -1 # Index of the currently active ROI
42
+ self.dragging = False
43
+ self.drag_idx = -1
44
+ self.modes = Queue()
45
+ for mode in ['Add', 'Append', 'Edit', 'Insert', 'Remove']:
46
+ self.modes.put(mode)
47
+ self.current_mode = self.modes.get()
48
+
49
+ def insert_index(self, cursor, threshold=1.005):
50
+ '''
51
+ Find the index to insert a new point based on cursor proximity to line segments.
52
+
53
+ Parameters
54
+ ----------
55
+ cursor : Tuple (x, y)
56
+ The cursor position represented as a tuple of (x, y) coordinates.
57
+ threshold : float, optional
58
+ Threshold distance for considering the cursor above a line segment,
59
+ by default 3.0.
60
+
61
+ Returns
62
+ -------
63
+ int
64
+ The index at which to add the point if the cursor is above a line segment,
65
+ otherwise -1.
66
+ '''
67
+ x, y = cursor
68
+ n_points = len(self.rois[self.active_roi_idx])
69
+
70
+ # Check if there are at least two points to form line segments
71
+ if n_points < 2:
72
+ return -1
73
+
74
+ distances = []
75
+ indices = []
76
+
77
+ # Iterate through existing points to calculate distances to line segments
78
+ for idx in range(n_points - 1):
79
+ x1, y1 = self.rois[self.active_roi_idx][idx]
80
+ x2, y2 = self.rois[self.active_roi_idx][idx + 1]
81
+
82
+ segment_length = np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
83
+ sum_distance = np.sqrt((x1 - x) ** 2 + (y1 - y) ** 2) + np.sqrt(
84
+ (x2 - x) ** 2 + (y2 - y) ** 2
85
+ )
86
+
87
+ # Calculate the distance from the cursor to the line segment
88
+ distance = sum_distance / segment_length
89
+
90
+ # Check if the distance is below the threshold
91
+ if distance < threshold:
92
+ distances.append(distance)
93
+ indices.append(idx + 1)
94
+
95
+ # If no suitable segment found, return -1
96
+ if len(distances) < 1:
97
+ return -1
98
+ else:
99
+ # Return the index of the closest line segment
100
+ return indices[distances.index(min(distances))]
101
+
102
+ def remove_point(self, x, y, threshold=5.0):
103
+ '''
104
+ Find the index of the point to remove based on cursor proximity.
105
+
106
+ Parameters
107
+ ----------
108
+ x : float
109
+ The x-coordinate of the cursor position.
110
+ y : float
111
+ The y-coordinate of the cursor position.
112
+ threshold : float, optional
113
+ Threshold distance for considering the cursor above a point, by default 5.0.
114
+
115
+ Returns
116
+ -------
117
+ int
118
+ The index of the point to remove if the cursor is above a point,
119
+ otherwise -1.
120
+ '''
121
+ if self.active_roi_idx >= 0 and self.active_roi_idx < len(self.rois):
122
+ for idx, point in enumerate(self.rois[self.active_roi_idx]):
123
+ dist = np.linalg.norm(np.array(point) - [x, y])
124
+ if dist < threshold:
125
+ return idx
126
+
127
+ return -1
128
+
129
+ def update_polyline(self, image, points, color):
130
+ '''
131
+ Update the image by drawing the polyline with the updated points.
132
+
133
+ Parameters
134
+ ----------
135
+ image : np.ndarray
136
+ The image data as a NumPy array.
137
+ points : list of Tuple (x, y)
138
+ List of points representing the polyline.
139
+ color : Tuple (B, G, R)
140
+ The color of polyline in BGR order.
141
+ '''
142
+ if len(points) > 1:
143
+ line_thickness = int(1 * self.resize_factor)
144
+ cv2.polylines(
145
+ image,
146
+ [(np.array(points) * self.resize_factor).astype(int)],
147
+ isClosed=False,
148
+ color=color,
149
+ thickness=line_thickness,
150
+ )
151
+
152
+ def redraw(self):
153
+ '''
154
+ Redraw the image with points and polyline.
155
+ '''
156
+ if self.resized_image.shape == self.resized_empty.shape:
157
+ self.resized_image[:] = self.resized_empty
158
+ else:
159
+ self.resized_image = self.resized_empty.copy()
160
+
161
+ for roi_idx, roi in enumerate(self.rois):
162
+ color = (0, 0, 255) if roi_idx != self.active_roi_idx else (0, 255, 255)
163
+
164
+ for _idx, point in enumerate(roi):
165
+ circle_radius = int(3 * self.resize_factor)
166
+ cv2.circle(
167
+ self.resized_image,
168
+ np.array(point * self.resize_factor).astype(int),
169
+ circle_radius,
170
+ color,
171
+ -1,
172
+ )
173
+ self.update_polyline(self.resized_image, roi, color)
174
+
175
+ # Add text annotation for ROI number/index
176
+ if roi:
177
+ text_position = tuple(np.array(roi[0]) * self.resize_factor)
178
+ text_position = (int(text_position[0]), int(text_position[1] - 10))
179
+ cv2.putText(
180
+ self.resized_image,
181
+ f'ROI {roi_idx}',
182
+ text_position,
183
+ cv2.FONT_HERSHEY_SIMPLEX,
184
+ 0.5,
185
+ color,
186
+ 1,
187
+ cv2.LINE_AA,
188
+ )
189
+
190
+ cv2.imshow('Select Line ROIs', self.resized_image)
191
+ cv2.setWindowTitle(
192
+ 'Select Line ROIs',
193
+ f'Select Line ROIs "Mode: {self.current_mode}"'
194
+ + ' (Space: change mode | Q: quit)',
195
+ )
196
+
197
+ def mouse_callback(self, event, x, y, flags, param):
198
+ '''
199
+ Mouse callback function for handling mouse events.
200
+
201
+ Parameters
202
+ ----------
203
+ event : int
204
+ The mouse event.
205
+ x : int
206
+ The x-coordinate of the mouse cursor.
207
+ y : int
208
+ The y-coordinate of the mouse cursor.
209
+ flags : int
210
+ Additional flags for the mouse event.
211
+ param : object
212
+ Additional parameters.
213
+ '''
214
+ if event == cv2.EVENT_LBUTTONDOWN:
215
+ if self.current_mode == 'Edit':
216
+ for roi_idx, roi in enumerate(self.rois):
217
+ for idx, point in enumerate(roi):
218
+ dist = np.linalg.norm(
219
+ np.array(point)
220
+ - [x / self.resize_factor, y / self.resize_factor]
221
+ )
222
+ if dist < 10:
223
+ self.dragging = True
224
+ self.drag_idx = idx
225
+ self.active_roi_idx = roi_idx
226
+ break
227
+ return
228
+ elif self.current_mode == 'Add':
229
+ self.rois.append([])
230
+ self.active_roi_idx = len(self.rois) - 1
231
+ self.rois[self.active_roi_idx].append(
232
+ np.array((x / self.resize_factor, y / self.resize_factor))
233
+ )
234
+
235
+ self.cycle_mode()
236
+ elif self.current_mode == 'Append':
237
+ if self.active_roi_idx != -1:
238
+ self.rois[self.active_roi_idx].append(
239
+ np.array((x / self.resize_factor, y / self.resize_factor))
240
+ )
241
+ elif self.current_mode == 'Remove':
242
+ if self.active_roi_idx != -1:
243
+ remove_idx = self.remove_point(
244
+ x / self.resize_factor, y / self.resize_factor
245
+ )
246
+ if remove_idx != -1:
247
+ del self.rois[self.active_roi_idx][remove_idx]
248
+
249
+ # Check if the ROI is empty after deletion
250
+ if not self.rois[self.active_roi_idx]:
251
+ del self.rois[self.active_roi_idx]
252
+
253
+ # Adjust active ROI index only if there are remaining ROIs
254
+ if self.rois:
255
+ self.active_roi_idx = max(0, self.active_roi_idx - 1)
256
+ else:
257
+ self.active_roi_idx = -1
258
+ elif self.current_mode == 'Insert':
259
+ if self.active_roi_idx != -1:
260
+ res = self.insert_index(
261
+ (x / self.resize_factor, y / self.resize_factor)
262
+ )
263
+ if res > 0:
264
+ self.rois[self.active_roi_idx].insert(
265
+ res,
266
+ np.array((x / self.resize_factor, y / self.resize_factor)),
267
+ )
268
+ self.redraw()
269
+ elif event == cv2.EVENT_LBUTTONUP:
270
+ self.dragging = False
271
+ elif event == cv2.EVENT_MOUSEMOVE:
272
+ if self.dragging and 0 <= self.drag_idx < len(
273
+ self.rois[self.active_roi_idx]
274
+ ):
275
+ self.rois[self.active_roi_idx][self.drag_idx] = np.array(
276
+ (x / self.resize_factor, y / self.resize_factor)
277
+ )
278
+ self.redraw()
279
+ elif event == cv2.EVENT_MOUSEWHEEL:
280
+ if flags > 0:
281
+ self.resize_factor += 0.05
282
+ else:
283
+ self.resize_factor = max(0.1, self.resize_factor - 0.05)
284
+ self.resized_empty = cv2.resize(
285
+ self.original,
286
+ (0, 0),
287
+ fx=self.resize_factor,
288
+ fy=self.resize_factor,
289
+ interpolation=cv2.INTER_NEAREST,
290
+ )
291
+ self.redraw()
292
+
293
+ def cycle_mode(self):
294
+ self.modes.put(self.current_mode)
295
+ self.current_mode = self.modes.get()
296
+
297
+ def select_line_rois(self, offset: tuple[int, int] = None):
298
+ '''
299
+ Start the interactive process of selecting multiple polyline ROIs.
300
+
301
+ Parameters
302
+ ----------
303
+ offset : tuple[int, int]
304
+ The points offset (x, y)
305
+
306
+ Returns
307
+ -------
308
+ list
309
+ List of arrays of points, where each array represents a polyline ROI.
310
+ '''
311
+ cv2.namedWindow('Select Line ROIs')
312
+ cv2.setMouseCallback('Select Line ROIs', self.mouse_callback)
313
+
314
+ while True:
315
+ self.redraw()
316
+ key = cv2.waitKeyEx(200)
317
+
318
+ if key == ord('q') or key == ord('Q'):
319
+ break
320
+ elif key == 32: # Space
321
+ self.cycle_mode()
322
+ elif key == 2490368: # Up arrow
323
+ if len(self.rois) > 0:
324
+ self.active_roi_idx = (self.active_roi_idx - 1) % len(self.rois)
325
+ self.redraw()
326
+ elif key == 2621440: # Down arrow
327
+ if len(self.rois) > 0:
328
+ self.active_roi_idx = (self.active_roi_idx + 1) % len(self.rois)
329
+ self.redraw()
330
+ elif key == 3014656: # Delete
331
+ if (
332
+ self.active_roi_idx != -1
333
+ and len(self.rois[self.active_roi_idx]) > 0
334
+ ):
335
+ del self.rois[self.active_roi_idx][-1]
336
+
337
+ # Check if the ROI is empty after deletion
338
+ if not self.rois[self.active_roi_idx]:
339
+ del self.rois[self.active_roi_idx]
340
+
341
+ # Adjust active ROI index only if there are remaining ROIs
342
+ if self.rois:
343
+ self.active_roi_idx = max(0, self.active_roi_idx - 1)
344
+ else:
345
+ self.active_roi_idx = -1
346
+ elif key == ord('a'):
347
+ while self.current_mode != 'Add':
348
+ self.cycle_mode()
349
+ elif key == ord('p'):
350
+ while self.current_mode != 'Append':
351
+ self.cycle_mode()
352
+ elif key == ord('e'):
353
+ while self.current_mode != 'Edit':
354
+ self.cycle_mode()
355
+ elif key == ord('i'):
356
+ while self.current_mode != 'Insert':
357
+ self.cycle_mode()
358
+ elif key == ord('r'):
359
+ while self.current_mode != 'Remove':
360
+ self.cycle_mode()
361
+ elif key == ord('c'):
362
+ self.rois = []
363
+ while self.current_mode != 'Add':
364
+ self.cycle_mode()
365
+
366
+ cv2.destroyAllWindows()
367
+
368
+ if offset is not None:
369
+ return [np.array(roi) + offset for roi in self.rois]
370
+ else:
371
+ return [np.array(roi) for roi in self.rois]
372
+
373
+ @staticmethod
374
+ def get_selector(image: np.ndarray, resize_factor=1.0):
375
+ if image is None:
376
+ raise ValueError('Parameter image is None.')
377
+
378
+ if image.ndim != 2:
379
+ raise ValueError(
380
+ f'Parameter image should be an array with ndim = 2, not {image.ndim}.'
381
+ )
382
+
383
+ # Convert monochromatic image to RGB
384
+ _image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
385
+
386
+ # Select a multi-point line ROI with the specified resize factor
387
+ return MultiLineROISelector(_image, resize_factor)
388
+
389
+
390
+ @nb.njit(cache=True)
391
+ def get_move_lines(linewidth: int):
392
+ '''
393
+ Generate an array representing movement lines for a given linewidth.
394
+
395
+ Parameters
396
+ ----------
397
+ linewidth : int
398
+ The width of the line.
399
+
400
+ Returns
401
+ -------
402
+ np.ndarray
403
+ An array of movement lines. For linewidth 0 or 1, no movement is required, and
404
+ the array contains only [0.0]. For other linewidths, the array includes
405
+ adjusted movement lines based on the linewidth. If the linewidth is even,
406
+ the central line is excluded.
407
+
408
+ Notes
409
+ -----
410
+ The function is optimized for use with Numba (nb.njit).
411
+
412
+ Examples
413
+ --------
414
+ >>> get_move_lines(1)
415
+ array([0.0])
416
+
417
+ >>> get_move_lines(3)
418
+ array([-1.0, 0.0, 1.0])
419
+
420
+ >>> get_move_lines(4)
421
+ array([-1.5, -0.5, 0.5, 1.5])
422
+ '''
423
+ if linewidth <= 1:
424
+ # For linewidth 0 or 1, no movement is required
425
+ return np.array([0.0])
426
+
427
+ half_width = linewidth // 2
428
+ width_indices = np.arange(-half_width, half_width + 1)
429
+
430
+ # Adjust for even linewidth to exclude the central line
431
+ if linewidth % 2 == 0:
432
+ width_indices = np.delete(width_indices, half_width)
433
+ return width_indices * 0.5
434
+ else:
435
+ return width_indices * 1.0 # Ensure it's a Numba-compatible float
436
+
437
+
438
+ @nb.njit(cache=True)
439
+ def get_kymogram_row(
440
+ Data: np.ndarray, X: np.ndarray, Y: np.ndarray, linewidth=1, method='average'
441
+ ):
442
+ """
443
+ Generate a polyline ROI data to construct a kymogram from the given data
444
+ along specified X and Y coordinates.
445
+
446
+ Parameters
447
+ ----------
448
+ Data : np.ndarray
449
+ 2D array representing the data.
450
+ X : np.ndarray
451
+ 1D array of X coordinates.
452
+ Y : np.ndarray
453
+ 1D array of Y coordinates.
454
+ linewidth : int, optional
455
+ Width of the kymogram lines, by default 1.
456
+ method : str, optional
457
+ Method to generate the kymogram, either 'average' (default) or 'maximum'.
458
+
459
+ Returns
460
+ -------
461
+ np.ndarray
462
+ 1D array representing a kymogram row.
463
+ """
464
+ diffX, diffY = np.diff(X), np.diff(Y)
465
+ lengths = np.hypot(diffX, diffY)
466
+
467
+ dX = -diffY / lengths
468
+ dY = diffX / lengths
469
+ move_lines = get_move_lines(linewidth)
470
+
471
+ n_points = lengths.astype(np.int64)
472
+ lut = np.concatenate((np.array([0]), np.cumsum(n_points))).astype(np.int64)
473
+
474
+ Roi = np.zeros(np.sum(n_points))
475
+
476
+ for idx in range(X.shape[0] - 1):
477
+ x = np.linspace(X[idx], X[idx + 1], n_points[idx])
478
+ y = np.linspace(Y[idx], Y[idx + 1], n_points[idx])
479
+
480
+ dx = dX[idx]
481
+ dy = dY[idx]
482
+
483
+ temp = np.zeros(n_points[idx])
484
+ for move_line in move_lines:
485
+ x_idx = np.round(x + move_line * dx).astype(np.int64)
486
+ y_idx = np.round(y + move_line * dy).astype(np.int64)
487
+
488
+ # Manually implement np.clip for x_idx and y_idx
489
+ x_idx = np.minimum(np.maximum(x_idx, 0), Data.shape[1] - 1)
490
+ y_idx = np.minimum(np.maximum(y_idx, 0), Data.shape[0] - 1)
491
+
492
+ # Iterating to fill temp with appropriate values from Data
493
+ for i in range(len(x_idx)):
494
+ if method == 'maximum':
495
+ temp[i] = max(temp[i], Data[y_idx[i], x_idx[i]])
496
+ else:
497
+ temp[i] += Data[y_idx[i], x_idx[i]]
498
+
499
+ if method == 'average' and linewidth > 1:
500
+ Roi[lut[idx] : lut[idx + 1]] = temp / len(move_lines)
501
+ else:
502
+ Roi[lut[idx] : lut[idx + 1]] = temp
503
+
504
+ return Roi
505
+
506
+
507
+ class Kymogram:
508
+ def __init__(
509
+ self,
510
+ data: np.ndarray,
511
+ rois: list[np.ndarray],
512
+ linewidth: int,
513
+ method: str,
514
+ window: int,
515
+ at_index: int,
516
+ window_location: str,
517
+ ) -> None:
518
+ '''
519
+ Initialize a Kymogram object.
520
+
521
+ Parameters
522
+ ----------
523
+ data : np.ndarray
524
+ The input data for kymogram generation.
525
+ rois : np.ndarray
526
+ The rois including points used for kymogram extraction.
527
+ linewidth : int
528
+ The linewidth for kymogram extraction.
529
+ method : str
530
+ The method used for kymogram generation.
531
+ window : int
532
+ The window size for kymogram extraction.
533
+ at_index: int
534
+ The frame index at which data was extracted.
535
+ window_location: str
536
+ The window location relative to at_index.
537
+ '''
538
+ self.data = data
539
+ self.rois = rois
540
+ self.linewidth = linewidth
541
+ self.method = method
542
+ self.window = window
543
+ self.at_index = at_index
544
+ self.window_location = window_location
545
+
546
+ def get_metadata(self):
547
+ '''
548
+ Get metadata of the Kymogram object.
549
+
550
+ Returns
551
+ -------
552
+ dict
553
+ A dictionary containing metadata, including the original
554
+ attributes and generated kymogram data.
555
+ '''
556
+ kymogram_dict = {
557
+ 'Kymogram': {
558
+ 'points': [points.tolist() for points in self.rois],
559
+ 'linewidth': self.linewidth,
560
+ 'method': self.method,
561
+ 'window': self.window,
562
+ 'window_location': self.window_location,
563
+ 'at_index': self.at_index,
564
+ }
565
+ }
566
+ return kymogram_dict
567
+
568
+ def export_to_json(self, filename):
569
+ '''
570
+ Export Kymogram metadata to a JSON file.
571
+
572
+ Parameters
573
+ ----------
574
+ filename : str
575
+ The name of the JSON file.
576
+ '''
577
+ metadata = self.get_metadata()
578
+ json_string = json.dumps(metadata, indent=2)
579
+
580
+ with open(filename, 'w') as json_file:
581
+ json_file.write(json_string)
582
+
583
+ def to_tiff(self, filename):
584
+ '''
585
+ Export the kymogram data as a TIFF image with metadata.
586
+
587
+ Parameters
588
+ ----------
589
+ filename : str
590
+ The name of the TIFF file to be created.
591
+ '''
592
+ if self.data is None:
593
+ raise ValueError('Kymogram data is not available.')
594
+
595
+ # Create a dictionary containing metadata
596
+ metadata = self.get_metadata()
597
+
598
+ # Write the metadata to the TIFF file
599
+ tf.imwrite(filename, self.data, bigtiff=True, metadata=metadata)
600
+
601
+ @classmethod
602
+ def from_json(cls, filename):
603
+ """
604
+ Create a Kymogram object from a JSON file.
605
+
606
+ Parameters
607
+ ----------
608
+ filename : str
609
+ The name of the JSON file.
610
+
611
+ Returns
612
+ -------
613
+ Kymogram or None
614
+ A Kymogram object created from the metadata in the JSON file,
615
+ or None if the 'Kymogram' key is not present.
616
+ """
617
+ with open(filename) as json_file:
618
+ metadata = json.load(json_file)
619
+
620
+ kymogram_metadata = metadata.get('Kymogram', None)
621
+ if kymogram_metadata is None:
622
+ # 'Kymogram' key is not present
623
+ return None
624
+
625
+ rois = [np.array(roi) for roi in kymogram_metadata.get('points', [])]
626
+ linewidth = kymogram_metadata.get('linewidth', 0)
627
+ method = kymogram_metadata.get('method', 'N/A')
628
+ window = kymogram_metadata.get('window', -1)
629
+ window_location = kymogram_metadata.get('window_location', 'From Current')
630
+ at_index = kymogram_metadata.get('at_index', 0)
631
+
632
+ kymogram = cls(None, rois, linewidth, method, window, at_index, window_location)
633
+
634
+ return kymogram
635
+
636
+ @classmethod
637
+ def from_tiff(cls, filename):
638
+ """
639
+ Create a Kymogram object from a TIFF file with metadata.
640
+
641
+ Parameters
642
+ ----------
643
+ filename : str
644
+ The name of the TIFF file.
645
+
646
+ Returns
647
+ -------
648
+ Kymogram or None
649
+ A Kymogram object created from the metadata in the TIFF file,
650
+ or None if the 'Kymogram' key is not present.
651
+ """
652
+ try:
653
+ # Read TIFF file and extract metadata
654
+ with tf.TiffFile(filename) as tiff:
655
+ data = tiff.asarray()
656
+
657
+ metadata = tf.tiffcomment(filename)
658
+
659
+ # Parse JSON metadata
660
+ metadata_dict = json.loads(metadata)
661
+
662
+ kymogram_metadata = metadata_dict.get('Kymogram', None)
663
+ if kymogram_metadata is None:
664
+ # 'Kymogram' key is not present
665
+ kymogram = cls(data, np.array([]), 1, 'N/A', data.shape[0], 0, 'N/A')
666
+
667
+ return kymogram
668
+ else:
669
+ rois = [np.array(roi) for roi in kymogram_metadata.get('points', [])]
670
+ linewidth = kymogram_metadata.get('linewidth', 0)
671
+ method = kymogram_metadata.get('method', 'N/A')
672
+ window = kymogram_metadata.get('window', -1)
673
+ window_location = kymogram_metadata.get(
674
+ 'window_location', 'From Current'
675
+ )
676
+ at_index = kymogram_metadata.get('at_index', 0)
677
+
678
+ kymogram = cls(
679
+ data, rois, linewidth, method, window, at_index, window_location
680
+ )
681
+
682
+ return kymogram
683
+
684
+ except Exception as e:
685
+ print(f'Error reading TIFF file: {e}')
686
+ return None
687
+
688
+
689
+ class KymogramWidget(QtWidgets.QWidget):
690
+ extractClicked = Signal()
691
+ displayClicked = Signal(np.ndarray)
692
+
693
+ controls_description = [
694
+ '<b>Q:</b> Quit',
695
+ '<b>Space:</b> Cycle modes',
696
+ '<b>A:</b> Add new ROI at cursor',
697
+ '<b>P:</b> Append to active ROI',
698
+ '<b>E:</b> Edit points (dragging)',
699
+ '<b>I:</b> Insert at segment',
700
+ '<b>R:</b> Remove points',
701
+ '<b>C:</b> Clear all ROIs',
702
+ '<b>Delete:</b> Delete the last added point',
703
+ '<b>Up/Down Arrows:</b> Select active ROI.',
704
+ ]
705
+
706
+ def __init__(self, parent=None, **kwargs):
707
+ super().__init__(parent)
708
+ self._threadpool = QtCore.QThreadPool.globalInstance()
709
+
710
+ self.kymogram_temporal_window = create_spin_box(
711
+ 0, kwargs.get('max_window', 1000), 1, 50
712
+ )
713
+ self.kymogram_linewidth = create_spin_box(1, 1000, 1, 1)
714
+
715
+ self.temporal_window_location = QtWidgets.QComboBox()
716
+ self.temporal_window_location.addItems(
717
+ ['From Current', 'Current Central', 'To Current']
718
+ )
719
+
720
+ self.kymogram_roi_selector = QtWidgets.QComboBox()
721
+ self.kymogram_roi_selector.addItems(
722
+ ['current', 'average', 'maximum', 'sum', 'std']
723
+ )
724
+
725
+ self.kymogram_roi_method = QtWidgets.QComboBox()
726
+ self.kymogram_roi_method.addItems(['average', 'maximum', 'sum'])
727
+
728
+ self.kymogram_btn = QtWidgets.QPushButton(
729
+ 'Extract', clicked=self.kymogram_btn_clicked
730
+ )
731
+ self.kymogram_display_btn = QtWidgets.QPushButton(
732
+ 'Display', clicked=self.kymogram_display_clicked
733
+ )
734
+ self.kymogram_save_btn = QtWidgets.QPushButton(
735
+ 'Save', clicked=self.kymogram_save_clicked
736
+ )
737
+ self.kymogram_load_btn = QtWidgets.QPushButton(
738
+ 'Load', clicked=self.kymogram_load_clicked
739
+ )
740
+
741
+ self.previous_selector = create_check_box('Use previous ROI selector?', True)
742
+
743
+ kymogram_btns = create_hbox_layout(
744
+ self.kymogram_btn,
745
+ self.kymogram_display_btn,
746
+ self.kymogram_save_btn,
747
+ self.kymogram_load_btn,
748
+ )
749
+
750
+ kymo_layout = QtWidgets.QFormLayout(self)
751
+ kymo_layout.addRow(
752
+ QtWidgets.QLabel('Temporal Window [frames]:'), self.kymogram_temporal_window
753
+ )
754
+ kymo_layout.addRow(
755
+ QtWidgets.QLabel('Window Range:'), self.temporal_window_location
756
+ )
757
+ kymo_layout.addRow(
758
+ QtWidgets.QLabel('Selector Displays:'), self.kymogram_roi_selector
759
+ )
760
+ kymo_layout.addRow(
761
+ QtWidgets.QLabel('ROI Linewidth [pixels]:'), self.kymogram_linewidth
762
+ )
763
+ kymo_layout.addRow(QtWidgets.QLabel('ROI Extracts:'), self.kymogram_roi_method)
764
+ kymo_layout.addWidget(self.previous_selector)
765
+ kymo_layout.addRow(kymogram_btns)
766
+
767
+ description_html = '; '.join(KymogramWidget.controls_description)
768
+ wrapped_label = QtWidgets.QLabel(
769
+ '<b>ROI Selector Controls:</b><br>' + description_html
770
+ )
771
+ wrapped_label.setAlignment(Qt.AlignmentFlag.AlignTop)
772
+ wrapped_label.setWordWrap(True)
773
+ kymo_layout.addRow(wrapped_label)
774
+
775
+ # Create a QTableWidget
776
+ self.tableWidget = QtWidgets.QTableWidget()
777
+
778
+ # Set the column count
779
+ self.tableWidget.setColumnCount(3)
780
+
781
+ # Set the headers
782
+ self.tableWidget.setHorizontalHeaderLabels(['ROI index', 'X', 'Y'])
783
+ self.tableWidget.setVerticalScrollBarPolicy(
784
+ Qt.ScrollBarPolicy.ScrollBarAlwaysOn
785
+ )
786
+ kymo_layout.addRow(self.tableWidget)
787
+
788
+ self._kymogram: Kymogram = None # Assuming you have a Kymogram instance
789
+
790
+ def setMaximum(self, value: int):
791
+ self.kymogram_temporal_window.setMaximum(value)
792
+
793
+ def extract_kymogram(
794
+ self,
795
+ tiffSeq_Handler: Union[ZarrImageSequence, TiffSeqHandler],
796
+ current_frame: int,
797
+ max_frame: int,
798
+ roi_info: tuple[int, int] = None,
799
+ channel: int = 0,
800
+ zSlice: int = 0,
801
+ ):
802
+ if tiffSeq_Handler is None:
803
+ return
804
+
805
+ try:
806
+ window_location = self.temporal_window_location.currentText()
807
+ window_range = self.get_kymogram_window(current_frame, max_frame)
808
+
809
+ linewidth = self.kymogram_linewidth.value()
810
+
811
+ roi_extracts = self.kymogram_roi_method.currentText()
812
+
813
+ previous_selector = self.previous_selector.isChecked()
814
+ if previous_selector and self._kymogram is not None:
815
+ if roi_info is not None:
816
+ origin, _ = roi_info
817
+ else:
818
+ origin = (0, 0)
819
+ old_rois = [
820
+ [np.array(point) - origin for point in roi.tolist()]
821
+ for roi in self._kymogram.rois
822
+ ]
823
+ else:
824
+ old_rois = None
825
+
826
+ def work_func(**kwargs):
827
+ try:
828
+ image = uImage(
829
+ self.get_selector_image(
830
+ tiffSeq_Handler, current_frame, window_range
831
+ )
832
+ )
833
+
834
+ image.equalizeLUT()
835
+
836
+ if roi_info is not None:
837
+ origin, dim = roi_info
838
+ view = image._view[
839
+ int(origin[1]) : int(origin[1] + dim[1]),
840
+ int(origin[0]) : int(origin[0] + dim[0]),
841
+ ]
842
+ else:
843
+ origin = None
844
+ view = image._view
845
+
846
+ scale_factor = get_scaling_factor(view.shape[0], view.shape[1])
847
+
848
+ selector = MultiLineROISelector.get_selector(view, scale_factor)
849
+ if old_rois:
850
+ selector.rois = old_rois
851
+
852
+ rois = selector.select_line_rois(origin)
853
+
854
+ if rois:
855
+ res = None
856
+ zero = np.zeros(1)
857
+ t_points = window_range[1] - window_range[0] + 1
858
+ for frame_idx in range(window_range[0], window_range[1] + 1):
859
+ data = tiffSeq_Handler.getSlice(frame_idx, channel, zSlice)
860
+ rois_row = None
861
+ for points in rois:
862
+ if len(points) > 1:
863
+ row = get_kymogram_row(
864
+ data,
865
+ points[:, 0].flatten(), # X points
866
+ points[:, 1].flatten(), # Y points
867
+ linewidth,
868
+ roi_extracts,
869
+ )
870
+ rois_row = (
871
+ row
872
+ if rois_row is None
873
+ else np.concatenate([rois_row, zero, row])
874
+ )
875
+ res = (
876
+ rois_row if res is None else np.vstack([res, rois_row])
877
+ )
878
+ print(
879
+ 'Generating Kymogram ... {:.2f}%'.format(
880
+ 100
881
+ * (res.shape[0] if res.ndim == 2 else 1)
882
+ / t_points
883
+ ),
884
+ end='\r',
885
+ )
886
+ return Kymogram(
887
+ res,
888
+ rois,
889
+ linewidth,
890
+ roi_extracts,
891
+ t_points,
892
+ current_frame,
893
+ window_location,
894
+ )
895
+ else:
896
+ return None
897
+ except Exception:
898
+ traceback.print_exc()
899
+ return None
900
+
901
+ def done(results: Kymogram):
902
+ self.kymogram_btn.setDisabled(False)
903
+ if results is not None:
904
+ self._kymogram = results
905
+ self.displayClicked.emit(results.data)
906
+
907
+ self.worker = QThreadWorker(work_func)
908
+ self.worker.signals.result.connect(done)
909
+ # Execute
910
+ self.kymogram_btn.setDisabled(True)
911
+ self._threadpool.start(self.worker)
912
+ except Exception:
913
+ traceback.print_exc()
914
+
915
+ def kymogram_display_clicked(self):
916
+ if self._kymogram is None:
917
+ return
918
+
919
+ self.displayClicked.emit(self._kymogram.data)
920
+
921
+ def kymogram_btn_clicked(self):
922
+ self.extractClicked.emit()
923
+
924
+ def kymogram_save_clicked(self):
925
+ if self._kymogram is None:
926
+ return
927
+
928
+ filename, _ = getSaveFileName(
929
+ self, 'Export Kymogram', filter='Tiff files (*.tif);;'
930
+ )
931
+
932
+ if len(filename) > 0:
933
+ self._kymogram.to_tiff(filename)
934
+
935
+ def kymogram_load_clicked(self):
936
+ filename, _ = getOpenFileName(
937
+ self, 'Load Kymogram', filter='Tiff files (*.tif);;'
938
+ )
939
+
940
+ if len(filename) > 0:
941
+ kymogram = Kymogram.from_tiff(filename)
942
+ if kymogram is not None:
943
+ self._kymogram = kymogram
944
+ self.kymogram_linewidth.setValue(kymogram.linewidth)
945
+ self.kymogram_temporal_window.setValue(kymogram.window)
946
+
947
+ index = self.kymogram_roi_method.findText(kymogram.method)
948
+
949
+ if index != -1:
950
+ self.kymogram_roi_method.setCurrentIndex(index)
951
+
952
+ index = self.temporal_window_location.findText(kymogram.window_location)
953
+
954
+ if index != -1:
955
+ self.temporal_window_location.setCurrentIndex(index)
956
+
957
+ self.displayClicked.emit(self._kymogram.data)
958
+
959
+ def get_kymogram_window(self, current_frame: int, max_frame: int):
960
+ '''
961
+ Get the temporal window for generating a kymogram based on the selected option.
962
+
963
+ Parameters
964
+ ----------
965
+ current_frame : int
966
+ The current frame index of the stack.
967
+ max_frame : int
968
+ The maximum frame index of the stack.
969
+ Returns
970
+ -------
971
+ Tuple[int, int]:
972
+ A tuple representing the start and end frames
973
+ of the kymogram window.
974
+ '''
975
+ selected_option = self.temporal_window_location.currentText()
976
+
977
+ n_frames = self.kymogram_temporal_window.value()
978
+
979
+ if max_frame + 1 <= n_frames:
980
+ # return the entire range.
981
+ return 0, max_frame
982
+ else:
983
+ if selected_option == 'To Current':
984
+ # Return frames from (current_frame - n_frames) to current_frame
985
+ start_frame = max(0, current_frame - n_frames)
986
+ return start_frame, current_frame
987
+ elif selected_option == 'Current Central':
988
+ # Return frames centered around current_frame (window size = n_frames)
989
+ half_window = n_frames // 2
990
+ start_frame = max(0, current_frame - half_window)
991
+ end_frame = min(max_frame, current_frame + half_window)
992
+ return start_frame, end_frame
993
+ else: # Assuming the third option is 'From Current'
994
+ # Return frames from current_frame to (current_frame + n_frames)
995
+ start_frame = current_frame
996
+ end_frame = min(max_frame, current_frame + n_frames)
997
+ return start_frame, end_frame
998
+
999
+ def get_selector_image(
1000
+ self,
1001
+ tiffSeq_Handler: Union[ZarrImageSequence, TiffSeqHandler],
1002
+ current_frame: int,
1003
+ window_range: tuple[int, int],
1004
+ **kwargs,
1005
+ ):
1006
+ condition = self.kymogram_roi_selector.currentText()
1007
+ c = kwargs.get('channel', 0)
1008
+ z = kwargs.get('slice', 0)
1009
+
1010
+ if condition == 'current':
1011
+ return tiffSeq_Handler.getSlice(current_frame, c, z)
1012
+ elif condition == 'average':
1013
+ return np.mean(
1014
+ tiffSeq_Handler.getSlice(
1015
+ slice(window_range[0], window_range[1] + 1), c, z
1016
+ ),
1017
+ axis=0,
1018
+ )
1019
+ elif condition == 'maximum':
1020
+ return np.max(
1021
+ tiffSeq_Handler.getSlice(
1022
+ slice(window_range[0], window_range[1] + 1), c, z
1023
+ ),
1024
+ axis=0,
1025
+ )
1026
+ elif condition == 'sum':
1027
+ return np.sum(
1028
+ tiffSeq_Handler.getSlice(
1029
+ slice(window_range[0], window_range[1] + 1), c, z
1030
+ ),
1031
+ axis=0,
1032
+ )
1033
+ elif condition == 'std':
1034
+ return np.std(
1035
+ tiffSeq_Handler.getSlice(
1036
+ slice(window_range[0], window_range[1] + 1), c, z
1037
+ ),
1038
+ axis=0,
1039
+ )
1040
+
1041
+ return image
1042
+
1043
+
1044
+ if __name__ == '__main__':
1045
+ # Example usage
1046
+ image = np.random.choice(range(256), (512, 512)).astype(np.uint8)
1047
+
1048
+ resize_factor = 1
1049
+
1050
+ selector = MultiLineROISelector.get_selector(image, resize_factor)
1051
+
1052
+ # Select a multi-point line ROI with the specified resize factor
1053
+ roi_points = selector.select_line_rois()
1054
+
1055
+ # Draw the selected line on the resized image
1056
+ if len(roi_points) > 1:
1057
+ for roi in roi_points:
1058
+ if len(roi) > 1:
1059
+ # -- Plot...
1060
+ fig, axes = plt.subplots(nrows=2)
1061
+ axes[0].imshow(image)
1062
+ axes[0].plot(roi[:, 0], roi[:, 1], 'ro-')
1063
+ axes[0].axis('image')
1064
+
1065
+ # linewidth
1066
+ linewidth = 1
1067
+ axes[1].plot(
1068
+ get_kymogram_row(
1069
+ image,
1070
+ roi[:, 0].flatten(), # X points
1071
+ roi[:, 1].flatten(), # Y points
1072
+ linewidth,
1073
+ ),
1074
+ label=f'linewidth={linewidth}',
1075
+ )
1076
+ plt.legend()
1077
+ plt.show()
1078
+ else:
1079
+ print('Error: Please select at least two points.')