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
|
@@ -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.')
|