phasor-handler 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasor_handler/__init__.py +9 -0
- phasor_handler/app.py +249 -0
- phasor_handler/img/icons/chevron-down.svg +3 -0
- phasor_handler/img/icons/chevron-up.svg +3 -0
- phasor_handler/img/logo.ico +0 -0
- phasor_handler/models/dir_manager.py +100 -0
- phasor_handler/scripts/contrast.py +131 -0
- phasor_handler/scripts/convert.py +155 -0
- phasor_handler/scripts/meta_reader.py +467 -0
- phasor_handler/scripts/plot.py +110 -0
- phasor_handler/scripts/register.py +86 -0
- phasor_handler/themes/__init__.py +8 -0
- phasor_handler/themes/dark_theme.py +330 -0
- phasor_handler/tools/__init__.py +1 -0
- phasor_handler/tools/check_stylesheet.py +15 -0
- phasor_handler/tools/misc.py +20 -0
- phasor_handler/widgets/__init__.py +5 -0
- phasor_handler/widgets/analysis/components/__init__.py +9 -0
- phasor_handler/widgets/analysis/components/bnc.py +426 -0
- phasor_handler/widgets/analysis/components/circle_roi.py +850 -0
- phasor_handler/widgets/analysis/components/image_view.py +667 -0
- phasor_handler/widgets/analysis/components/meta_info.py +481 -0
- phasor_handler/widgets/analysis/components/roi_list.py +659 -0
- phasor_handler/widgets/analysis/components/trace_plot.py +621 -0
- phasor_handler/widgets/analysis/view.py +1735 -0
- phasor_handler/widgets/conversion/view.py +83 -0
- phasor_handler/widgets/registration/view.py +110 -0
- phasor_handler/workers/__init__.py +2 -0
- phasor_handler/workers/analysis_worker.py +0 -0
- phasor_handler/workers/histogram_worker.py +55 -0
- phasor_handler/workers/registration_worker.py +242 -0
- phasor_handler-2.2.0.dist-info/METADATA +134 -0
- phasor_handler-2.2.0.dist-info/RECORD +37 -0
- phasor_handler-2.2.0.dist-info/WHEEL +5 -0
- phasor_handler-2.2.0.dist-info/entry_points.txt +5 -0
- phasor_handler-2.2.0.dist-info/licenses/LICENSE.md +21 -0
- phasor_handler-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TraceplotWidget - A standalone widget for trace plotting functionality.
|
|
3
|
+
|
|
4
|
+
This widget encapsulates all trace plotting logic including:
|
|
5
|
+
- Y-limit controls
|
|
6
|
+
- Formula selection dropdown
|
|
7
|
+
- Time display toggle
|
|
8
|
+
- Matplotlib figure and canvas
|
|
9
|
+
- Signal extraction and plotting methods
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# TODO Make options only raw Fg and Fg - Fog / Fog for single channel recordings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
from PyQt6.QtWidgets import (
|
|
17
|
+
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
|
18
|
+
QComboBox, QSizePolicy, QSpinBox
|
|
19
|
+
)
|
|
20
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
21
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TraceplotWidget(QWidget):
|
|
25
|
+
"""A widget that handles all trace plotting functionality."""
|
|
26
|
+
|
|
27
|
+
# Signal emitted when trace plot needs to update due to user controls
|
|
28
|
+
traceUpdateRequested = pyqtSignal()
|
|
29
|
+
|
|
30
|
+
def __init__(self, parent=None):
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
self.main_window = None # Will be set by parent
|
|
33
|
+
self._show_time_in_seconds = False # Track current display mode
|
|
34
|
+
self._frame_vline = None # Reference to the current frame line
|
|
35
|
+
|
|
36
|
+
self._init_ui()
|
|
37
|
+
|
|
38
|
+
def _init_ui(self):
|
|
39
|
+
"""Initialize the UI components for trace plotting."""
|
|
40
|
+
# Main layout
|
|
41
|
+
main_layout = QHBoxLayout()
|
|
42
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
43
|
+
|
|
44
|
+
# Left side: Controls
|
|
45
|
+
controls_layout = QVBoxLayout()
|
|
46
|
+
|
|
47
|
+
# Y limits inputs
|
|
48
|
+
ylim_layout = QVBoxLayout()
|
|
49
|
+
ylim_layout.setSpacing(2)
|
|
50
|
+
ylim_layout.setContentsMargins(0, 0, 0, 0)
|
|
51
|
+
|
|
52
|
+
ylim_label_inner = QHBoxLayout()
|
|
53
|
+
ylim_label_inner.setContentsMargins(0, 0, 0, 0)
|
|
54
|
+
|
|
55
|
+
ylim_label = QLabel("Y limits:")
|
|
56
|
+
ylim_label.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed))
|
|
57
|
+
ylim_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
|
58
|
+
|
|
59
|
+
self.ylim_min_edit = QLineEdit()
|
|
60
|
+
self.ylim_min_edit.setFixedWidth(50)
|
|
61
|
+
self.ylim_min_edit.setFixedHeight(20)
|
|
62
|
+
self.ylim_min_edit.setPlaceholderText("Min")
|
|
63
|
+
self.ylim_min_edit.editingFinished.connect(self._update_trace_from_roi)
|
|
64
|
+
|
|
65
|
+
self.ylim_max_edit = QLineEdit()
|
|
66
|
+
self.ylim_max_edit.setFixedWidth(50)
|
|
67
|
+
self.ylim_max_edit.setFixedHeight(20)
|
|
68
|
+
self.ylim_max_edit.setPlaceholderText("Max")
|
|
69
|
+
self.ylim_max_edit.editingFinished.connect(self._update_trace_from_roi)
|
|
70
|
+
|
|
71
|
+
ylim_label_inner.addWidget(self.ylim_min_edit)
|
|
72
|
+
ylim_label_inner.addWidget(self.ylim_max_edit)
|
|
73
|
+
ylim_label_inner.addStretch()
|
|
74
|
+
|
|
75
|
+
self.reset_ylim_button = QPushButton("Reset")
|
|
76
|
+
self.reset_ylim_button.setFixedWidth(50)
|
|
77
|
+
self.reset_ylim_button.setFixedHeight(20)
|
|
78
|
+
self.reset_ylim_button.clicked.connect(self._reset_ylim)
|
|
79
|
+
|
|
80
|
+
ylim_layout.addWidget(ylim_label)
|
|
81
|
+
ylim_layout.addLayout(ylim_label_inner)
|
|
82
|
+
ylim_layout.addSpacing(4)
|
|
83
|
+
ylim_layout.addWidget(self.reset_ylim_button)
|
|
84
|
+
controls_layout.addLayout(ylim_layout)
|
|
85
|
+
|
|
86
|
+
# Baseline percentage spinbox
|
|
87
|
+
self.base_spinbox = QSpinBox()
|
|
88
|
+
self.base_spinbox.setRange(1, 99)
|
|
89
|
+
self.base_spinbox.setValue(10)
|
|
90
|
+
self.base_spinbox.setFixedWidth(60)
|
|
91
|
+
self.base_spinbox.setFixedHeight(25)
|
|
92
|
+
self.base_spinbox.valueChanged.connect(self._update_trace_from_roi)
|
|
93
|
+
controls_layout.addWidget(QLabel("Baseline %:"))
|
|
94
|
+
controls_layout.addWidget(self.base_spinbox)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Formula dropdown
|
|
98
|
+
self.formula_dropdown = QComboBox()
|
|
99
|
+
self.formula_dropdown.setFixedWidth(100)
|
|
100
|
+
self.formula_dropdown.setStyleSheet("QComboBox { font-size: 8pt; }")
|
|
101
|
+
self.formula_dropdown.addItem("Fg - Fog / Fr")
|
|
102
|
+
self.formula_dropdown.addItem("Fg - Fog / Fog")
|
|
103
|
+
self.formula_dropdown.addItem("Fg only")
|
|
104
|
+
self.formula_dropdown.addItem("Fr only")
|
|
105
|
+
self.formula_dropdown.setContentsMargins(0, 0, 0, 0)
|
|
106
|
+
self.formula_dropdown.currentIndexChanged.connect(self._update_trace_from_roi)
|
|
107
|
+
controls_layout.addWidget(self.formula_dropdown)
|
|
108
|
+
|
|
109
|
+
controls_layout.addSpacing(15)
|
|
110
|
+
|
|
111
|
+
# Time display toggle button
|
|
112
|
+
self.time_display_button = QPushButton("Frames")
|
|
113
|
+
self.time_display_button.setFixedWidth(100)
|
|
114
|
+
self.time_display_button.setFixedHeight(20)
|
|
115
|
+
self.time_display_button.setCheckable(True)
|
|
116
|
+
self.time_display_button.setChecked(False) # Default to frames
|
|
117
|
+
self.time_display_button.setStyleSheet("QPushButton { font-size: 8pt; }")
|
|
118
|
+
self.time_display_button.setToolTip("Toggle between frame numbers and time in seconds")
|
|
119
|
+
self.time_display_button.clicked.connect(self._toggle_time_display)
|
|
120
|
+
controls_layout.addWidget(self.time_display_button)
|
|
121
|
+
|
|
122
|
+
main_layout.addLayout(controls_layout, 0)
|
|
123
|
+
|
|
124
|
+
# Right side: Figure and canvas
|
|
125
|
+
self.trace_fig, self.trace_ax = plt.subplots(figsize=(12, 6), dpi=100)
|
|
126
|
+
self.trace_ax.set_xticks([])
|
|
127
|
+
self.trace_ax.set_yticks([])
|
|
128
|
+
self.trace_ax.set_xlabel("")
|
|
129
|
+
self.trace_ax.set_ylabel("")
|
|
130
|
+
for spine in self.trace_ax.spines.values():
|
|
131
|
+
spine.set_visible(True)
|
|
132
|
+
self.trace_fig.patch.set_alpha(0.0)
|
|
133
|
+
self.trace_ax.set_facecolor('none')
|
|
134
|
+
self.trace_canvas = FigureCanvas(self.trace_fig)
|
|
135
|
+
self.trace_canvas.setStyleSheet("background:transparent; border: 1px solid #888;")
|
|
136
|
+
self.trace_canvas.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
137
|
+
self.trace_canvas.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
138
|
+
self.trace_ax.xaxis.label.set_color('white')
|
|
139
|
+
self.trace_ax.yaxis.label.set_color('white')
|
|
140
|
+
self.trace_ax.tick_params(axis='x', colors='white')
|
|
141
|
+
self.trace_ax.tick_params(axis='y', colors='white')
|
|
142
|
+
for spine in self.trace_ax.spines.values():
|
|
143
|
+
spine.set_color('white')
|
|
144
|
+
self.trace_fig.tight_layout()
|
|
145
|
+
|
|
146
|
+
main_layout.addWidget(self.trace_canvas, 1) # Give stretch factor of 1 to make it expand
|
|
147
|
+
|
|
148
|
+
self.setLayout(main_layout)
|
|
149
|
+
|
|
150
|
+
def set_main_window(self, main_window):
|
|
151
|
+
"""Set reference to the main window for accessing data."""
|
|
152
|
+
self.main_window = main_window
|
|
153
|
+
|
|
154
|
+
# Store user overrides on main window for compatibility
|
|
155
|
+
if not hasattr(main_window, '_ylim_min_user'):
|
|
156
|
+
main_window._ylim_min_user = None
|
|
157
|
+
if not hasattr(main_window, '_ylim_max_user'):
|
|
158
|
+
main_window._ylim_max_user = None
|
|
159
|
+
|
|
160
|
+
def get_widgets_for_compatibility(self):
|
|
161
|
+
"""Return a dict of widgets for backward compatibility."""
|
|
162
|
+
return {
|
|
163
|
+
'ylim_min_edit': self.ylim_min_edit,
|
|
164
|
+
'ylim_max_edit': self.ylim_max_edit,
|
|
165
|
+
'reset_ylim_button': self.reset_ylim_button,
|
|
166
|
+
'formula_dropdown': self.formula_dropdown,
|
|
167
|
+
'time_display_button': self.time_display_button,
|
|
168
|
+
'trace_fig': self.trace_fig,
|
|
169
|
+
'trace_ax': self.trace_ax,
|
|
170
|
+
'trace_canvas': self.trace_canvas
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def _update_trace_from_roi(self, index=None):
|
|
174
|
+
"""Update the trace plot based on current ROI selection."""
|
|
175
|
+
print(f"DEBUG: TraceplotWidget._update_trace_from_roi called with index={index}")
|
|
176
|
+
|
|
177
|
+
# Check conditions
|
|
178
|
+
has_main_window = self.main_window is not None
|
|
179
|
+
has_current_tif = self.main_window._current_tif is not None if has_main_window else False
|
|
180
|
+
has_roi_xyxy = getattr(self.main_window, '_last_roi_xyxy', None) is not None if has_main_window else False
|
|
181
|
+
|
|
182
|
+
print(f"DEBUG: has_main_window={has_main_window}, has_current_tif={has_current_tif}, has_roi_xyxy={has_roi_xyxy}")
|
|
183
|
+
|
|
184
|
+
if not has_main_window or not has_current_tif or not has_roi_xyxy:
|
|
185
|
+
print("DEBUG: Early return - missing required data")
|
|
186
|
+
# clear your plot if you want
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
print(f"DEBUG: ROI xyxy: {self.main_window._last_roi_xyxy}")
|
|
190
|
+
print(f"DEBUG: Image shape: {self.main_window._current_tif.shape}")
|
|
191
|
+
|
|
192
|
+
# Get the ellipse mask from the ROI tool (handles rotation correctly)
|
|
193
|
+
mask_result = None
|
|
194
|
+
try:
|
|
195
|
+
if hasattr(self.main_window, 'roi_tool'):
|
|
196
|
+
mask_result = self.main_window.roi_tool.get_ellipse_mask()
|
|
197
|
+
print(f"DEBUG: Got ellipse mask result: {mask_result is not None}")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"Warning: Could not get ellipse mask: {e}")
|
|
200
|
+
|
|
201
|
+
if mask_result is None:
|
|
202
|
+
# Fallback to rectangular region
|
|
203
|
+
x0, y0, x1, y1 = self.main_window._last_roi_xyxy
|
|
204
|
+
def stack3d(a):
|
|
205
|
+
a = np.asarray(a).squeeze()
|
|
206
|
+
return a[None, ...] if a.ndim == 2 else a
|
|
207
|
+
|
|
208
|
+
ch1 = stack3d(self.main_window._current_tif)
|
|
209
|
+
sig1 = ch1[:, y0:y1, x0:x1].mean(axis=(1,2)) if (x1>x0 and y1>y0) else np.zeros((ch1.shape[0],), dtype=np.float32)
|
|
210
|
+
|
|
211
|
+
ch2 = getattr(self.main_window, "_current_tif_chan2", None)
|
|
212
|
+
sig2 = None
|
|
213
|
+
if ch2 is not None:
|
|
214
|
+
ch2 = stack3d(ch2)
|
|
215
|
+
sig2 = ch2[:, y0:y1, x0:x1].mean(axis=(1,2))
|
|
216
|
+
else:
|
|
217
|
+
# Use ellipse mask for proper signal extraction
|
|
218
|
+
X0, Y0, X1, Y1, mask = mask_result
|
|
219
|
+
|
|
220
|
+
def stack3d(a):
|
|
221
|
+
a = np.asarray(a).squeeze()
|
|
222
|
+
return a[None, ...] if a.ndim == 2 else a
|
|
223
|
+
|
|
224
|
+
ch1 = stack3d(self.main_window._current_tif)
|
|
225
|
+
|
|
226
|
+
# Extract signal using the ellipse mask
|
|
227
|
+
if mask.size > 0 and np.any(mask):
|
|
228
|
+
# Apply mask to each frame
|
|
229
|
+
sig1_frames = []
|
|
230
|
+
for frame_idx in range(ch1.shape[0]):
|
|
231
|
+
frame = ch1[frame_idx, Y0:Y1, X0:X1]
|
|
232
|
+
if frame.shape == mask.shape:
|
|
233
|
+
masked_values = frame[mask]
|
|
234
|
+
sig1_frames.append(np.mean(masked_values) if len(masked_values) > 0 else 0.0)
|
|
235
|
+
else:
|
|
236
|
+
print(f"Warning: Frame shape {frame.shape} doesn't match mask shape {mask.shape}")
|
|
237
|
+
sig1_frames.append(0.0)
|
|
238
|
+
sig1 = np.array(sig1_frames, dtype=np.float32)
|
|
239
|
+
else:
|
|
240
|
+
sig1 = np.zeros((ch1.shape[0],), dtype=np.float32)
|
|
241
|
+
|
|
242
|
+
ch2 = getattr(self.main_window, "_current_tif_chan2", None)
|
|
243
|
+
sig2 = None
|
|
244
|
+
if ch2 is not None:
|
|
245
|
+
ch2 = stack3d(ch2)
|
|
246
|
+
if mask.size > 0 and np.any(mask):
|
|
247
|
+
# Apply mask to each frame of channel 2
|
|
248
|
+
sig2_frames = []
|
|
249
|
+
for frame_idx in range(ch2.shape[0]):
|
|
250
|
+
frame = ch2[frame_idx, Y0:Y1, X0:X1]
|
|
251
|
+
if frame.shape == mask.shape:
|
|
252
|
+
masked_values = frame[mask]
|
|
253
|
+
sig2_frames.append(np.mean(masked_values) if len(masked_values) > 0 else 0.0)
|
|
254
|
+
else:
|
|
255
|
+
sig2_frames.append(0.0)
|
|
256
|
+
sig2 = np.array(sig2_frames, dtype=np.float32)
|
|
257
|
+
else:
|
|
258
|
+
sig2 = np.zeros((ch2.shape[0],), dtype=np.float32)
|
|
259
|
+
|
|
260
|
+
# Compute Fo (baseline) as mean over first 10% of frames of sig1
|
|
261
|
+
nframes = sig1.shape[0]
|
|
262
|
+
if nframes <= 0:
|
|
263
|
+
return
|
|
264
|
+
# Determine baseline fraction from base_spinbox (percent).
|
|
265
|
+
try:
|
|
266
|
+
pct = int(self.base_spinbox.value()) if hasattr(self, 'base_spinbox') else 10
|
|
267
|
+
# Clamp between 1 and 99
|
|
268
|
+
pct = max(1, min(99, pct))
|
|
269
|
+
frac = float(pct) / 100.0
|
|
270
|
+
except Exception:
|
|
271
|
+
frac = 0.10
|
|
272
|
+
|
|
273
|
+
baseline_count = max(1, int(np.ceil(nframes * frac)))
|
|
274
|
+
Fog = float(np.mean(sig1[:baseline_count]))
|
|
275
|
+
|
|
276
|
+
self.trace_ax.cla()
|
|
277
|
+
|
|
278
|
+
# Compute metric depending on available channels and selected formula
|
|
279
|
+
# If red channel missing, switch to (Fg - Fo)/Fo (index 1) and disable other choices
|
|
280
|
+
if sig2 is None:
|
|
281
|
+
# Single-channel data: use (Fg - Fo)/Fo
|
|
282
|
+
try:
|
|
283
|
+
# set dropdown to index 1 (Fg - Fo / Fo) but do not allow changing it
|
|
284
|
+
if self.formula_dropdown.count() > 1:
|
|
285
|
+
# If index argument was provided override, respect it, otherwise set selection
|
|
286
|
+
if index is None:
|
|
287
|
+
self.formula_dropdown.setCurrentIndex(1)
|
|
288
|
+
self.formula_dropdown.setEnabled(False)
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Safe denom: avoid division by zero
|
|
293
|
+
denom_val = Fog if (Fog is not None and Fog != 0) else 1e-6
|
|
294
|
+
metric = (sig1 - Fog) / denom_val
|
|
295
|
+
else:
|
|
296
|
+
# Two-channel data: enable formula selection
|
|
297
|
+
self.formula_dropdown.setEnabled(True)
|
|
298
|
+
formula_index = self.formula_dropdown.currentIndex() if index is None else index
|
|
299
|
+
if formula_index == 0:
|
|
300
|
+
denom = sig2.copy().astype(np.float32)
|
|
301
|
+
denom[denom == 0] = 1e-6
|
|
302
|
+
metric = (sig1 - Fog) / denom
|
|
303
|
+
elif formula_index == 1:
|
|
304
|
+
denom_val = Fog if (Fog is not None and Fog != 0) else 1e-6
|
|
305
|
+
metric = (sig1 - Fog) / denom_val
|
|
306
|
+
elif formula_index == 2:
|
|
307
|
+
metric = sig1
|
|
308
|
+
elif formula_index == 3:
|
|
309
|
+
metric = sig2 if sig2 is not None else np.full_like(sig1, 0)
|
|
310
|
+
else:
|
|
311
|
+
# Default fallback for any unexpected formula_index
|
|
312
|
+
denom_val = Fog if (Fog is not None and Fog != 0) else 1e-6
|
|
313
|
+
metric = (sig1 - Fog) / denom_val
|
|
314
|
+
|
|
315
|
+
current_frame = 0
|
|
316
|
+
if hasattr(self.main_window, 'tif_slider'):
|
|
317
|
+
try:
|
|
318
|
+
current_frame = int(self.main_window.tif_slider.value())
|
|
319
|
+
except Exception:
|
|
320
|
+
current_frame = 0
|
|
321
|
+
|
|
322
|
+
# Determine x-axis values and labels based on time display mode
|
|
323
|
+
show_time = getattr(self, '_show_time_in_seconds', False)
|
|
324
|
+
x_values = None
|
|
325
|
+
x_label = "Frame"
|
|
326
|
+
current_x_pos = current_frame
|
|
327
|
+
|
|
328
|
+
if show_time:
|
|
329
|
+
# Try to get time stamps from experiment data
|
|
330
|
+
try:
|
|
331
|
+
ed = getattr(self.main_window, '_exp_data', None)
|
|
332
|
+
time_stamps = None
|
|
333
|
+
|
|
334
|
+
if ed is not None:
|
|
335
|
+
# Try different possible attribute names for time stamps
|
|
336
|
+
# Handle both dictionary and object metadata formats
|
|
337
|
+
for attr_name in ['time_stamps', 'timeStamps', 'timestamps', 'ElapsedTimes']:
|
|
338
|
+
if isinstance(ed, dict):
|
|
339
|
+
if attr_name in ed:
|
|
340
|
+
time_stamps = ed[attr_name]
|
|
341
|
+
print(f"DEBUG: Found time stamps in dict key '{attr_name}', length: {len(time_stamps) if hasattr(time_stamps, '__len__') else 'unknown'}")
|
|
342
|
+
if hasattr(time_stamps, '__len__') and len(time_stamps) > 0:
|
|
343
|
+
print(f"DEBUG: First few time stamps: {time_stamps[:min(5, len(time_stamps))]}")
|
|
344
|
+
break
|
|
345
|
+
else:
|
|
346
|
+
if hasattr(ed, attr_name):
|
|
347
|
+
time_stamps = getattr(ed, attr_name)
|
|
348
|
+
print(f"DEBUG: Found time stamps in attribute '{attr_name}', length: {len(time_stamps) if hasattr(time_stamps, '__len__') else 'unknown'}")
|
|
349
|
+
if hasattr(time_stamps, '__len__') and len(time_stamps) > 0:
|
|
350
|
+
print(f"DEBUG: First few time stamps: {time_stamps[:min(5, len(time_stamps))]}")
|
|
351
|
+
break
|
|
352
|
+
|
|
353
|
+
if time_stamps is not None and len(time_stamps) >= len(metric):
|
|
354
|
+
# Use time stamps as x-axis (convert from ms to seconds)
|
|
355
|
+
x_values = np.array(time_stamps[:len(metric)]) / 1000.0
|
|
356
|
+
x_label = "Time (s)"
|
|
357
|
+
# Convert current frame position to time
|
|
358
|
+
if current_frame < len(time_stamps):
|
|
359
|
+
current_x_pos = time_stamps[current_frame] / 1000.0
|
|
360
|
+
else:
|
|
361
|
+
current_x_pos = time_stamps[-1] / 1000.0 if len(time_stamps) > 0 else current_frame
|
|
362
|
+
print(f"DEBUG: Using time stamps for x-axis (converted from ms), current position: {current_x_pos}s")
|
|
363
|
+
else:
|
|
364
|
+
# Fallback: estimate time based on frame rate (if available)
|
|
365
|
+
frame_rate = getattr(ed, 'frame_rate', None) if ed else None
|
|
366
|
+
if frame_rate and frame_rate > 0:
|
|
367
|
+
x_values = np.arange(len(metric)) / frame_rate
|
|
368
|
+
x_label = "Time (s)"
|
|
369
|
+
current_x_pos = current_frame / frame_rate
|
|
370
|
+
print(f"DEBUG: Using estimated time from frame rate {frame_rate} Hz, current position: {current_x_pos}s")
|
|
371
|
+
else:
|
|
372
|
+
# No time data available, fall back to frames
|
|
373
|
+
show_time = False
|
|
374
|
+
print("DEBUG: No time stamp data or frame rate found, showing frames instead")
|
|
375
|
+
except Exception as e:
|
|
376
|
+
print(f"DEBUG: Error getting time stamps: {e}")
|
|
377
|
+
show_time = False
|
|
378
|
+
|
|
379
|
+
# Plot metric with appropriate x-axis
|
|
380
|
+
if x_values is not None:
|
|
381
|
+
self.trace_ax.plot(x_values, metric, label="(F green - Fo green)/F red", color='white')
|
|
382
|
+
else:
|
|
383
|
+
self.trace_ax.plot(metric, label="(F green - Fo green)/F red", color='white')
|
|
384
|
+
|
|
385
|
+
self.trace_ax.set_xlabel(x_label, color='white', labelpad=2)
|
|
386
|
+
self.trace_ax.tick_params(axis='x', pad=1, labelsize=9)
|
|
387
|
+
self._frame_vline = self.trace_ax.axvline(current_x_pos, color='yellow', linestyle='-', zorder=20, linewidth=2)
|
|
388
|
+
|
|
389
|
+
# Store frame vline reference on main window for compatibility
|
|
390
|
+
if self.main_window:
|
|
391
|
+
self.main_window._frame_vline = self._frame_vline
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
stims = []
|
|
395
|
+
ed = getattr(self.main_window, '_exp_data', None)
|
|
396
|
+
if ed is None:
|
|
397
|
+
stims = []
|
|
398
|
+
else:
|
|
399
|
+
# Handle both dictionary and object metadata formats
|
|
400
|
+
if isinstance(ed, dict):
|
|
401
|
+
stims = ed.get('stimulation_timeframes', [])
|
|
402
|
+
else:
|
|
403
|
+
stims = getattr(ed, 'stimulation_timeframes', [])
|
|
404
|
+
|
|
405
|
+
print(f"DEBUG: Found {len(stims)} stimulation timeframes: {stims}")
|
|
406
|
+
|
|
407
|
+
# Convert stimulation timeframes to appropriate x-axis units
|
|
408
|
+
if show_time and x_values is not None:
|
|
409
|
+
# Convert stim frames to time positions (from ms to seconds)
|
|
410
|
+
for stim in stims:
|
|
411
|
+
stim_frame = int(stim)
|
|
412
|
+
if stim_frame < len(time_stamps):
|
|
413
|
+
stim_x_pos = time_stamps[stim_frame] / 1000.0
|
|
414
|
+
print(f"DEBUG: Adding stimulation vline at time {stim_x_pos:.2f}s (frame {stim_frame})")
|
|
415
|
+
self.trace_ax.axvline(stim_x_pos, color='red', linestyle='--', zorder=15, linewidth=2)
|
|
416
|
+
else:
|
|
417
|
+
# Use frame numbers
|
|
418
|
+
for stim in stims:
|
|
419
|
+
stim_frame = int(stim)
|
|
420
|
+
print(f"DEBUG: Adding stimulation vline at frame {stim_frame}")
|
|
421
|
+
self.trace_ax.axvline(stim_frame, color='red', linestyle='--', zorder=15, linewidth=2)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
# keep plotting even if stim drawing fails
|
|
424
|
+
print(f"DEBUG: Error adding stimulation vlines: {e}")
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
# Parse y-limits from the QLineEdits (if present) and apply them
|
|
428
|
+
try:
|
|
429
|
+
def _parse(txt):
|
|
430
|
+
try:
|
|
431
|
+
s = str(txt).strip()
|
|
432
|
+
return float(s) if s != '' else None
|
|
433
|
+
except Exception:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
ymin = None
|
|
437
|
+
ymax = None
|
|
438
|
+
if hasattr(self, 'ylim_min_edit'):
|
|
439
|
+
ymin = _parse(self.ylim_min_edit.text())
|
|
440
|
+
if hasattr(self, 'ylim_max_edit'):
|
|
441
|
+
ymax = _parse(self.ylim_max_edit.text())
|
|
442
|
+
|
|
443
|
+
# If both provided and inverted, swap
|
|
444
|
+
if ymin is not None and ymax is not None and ymin > ymax:
|
|
445
|
+
ymin, ymax = ymax, ymin
|
|
446
|
+
|
|
447
|
+
if ymin is not None or ymax is not None:
|
|
448
|
+
# If one side missing, keep current autoscaled value for that side
|
|
449
|
+
cur = self.trace_ax.get_ylim()
|
|
450
|
+
if ymin is None:
|
|
451
|
+
ymin = cur[0]
|
|
452
|
+
if ymax is None:
|
|
453
|
+
ymax = cur[1]
|
|
454
|
+
self.trace_ax.set_ylim(ymin, ymax)
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
self.trace_fig.tight_layout()
|
|
459
|
+
self.trace_canvas.draw_idle()
|
|
460
|
+
|
|
461
|
+
def _update_trace_vline(self):
|
|
462
|
+
"""Lightweight: update only the vertical frame line on the existing trace."""
|
|
463
|
+
if self.main_window is None:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# If the axes are empty, don't try to add a vline (use full update instead)
|
|
467
|
+
try:
|
|
468
|
+
current_frame = 0
|
|
469
|
+
if hasattr(self.main_window, 'tif_slider'):
|
|
470
|
+
current_frame = int(self.main_window.tif_slider.value())
|
|
471
|
+
except Exception:
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# Determine current position based on time display mode
|
|
475
|
+
show_time = getattr(self, '_show_time_in_seconds', False)
|
|
476
|
+
current_x_pos = current_frame
|
|
477
|
+
|
|
478
|
+
if show_time:
|
|
479
|
+
try:
|
|
480
|
+
ed = getattr(self.main_window, '_exp_data', None)
|
|
481
|
+
time_stamps = None
|
|
482
|
+
|
|
483
|
+
if ed is not None:
|
|
484
|
+
# Try different possible attribute names for time stamps
|
|
485
|
+
# Handle both dictionary and object metadata formats
|
|
486
|
+
for attr_name in ['time_stamps', 'timeStamps', 'timestamps', 'ElapsedTimes']:
|
|
487
|
+
if isinstance(ed, dict):
|
|
488
|
+
if attr_name in ed:
|
|
489
|
+
time_stamps = ed[attr_name]
|
|
490
|
+
break
|
|
491
|
+
else:
|
|
492
|
+
if hasattr(ed, attr_name):
|
|
493
|
+
time_stamps = getattr(ed, attr_name)
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
if time_stamps is not None and current_frame < len(time_stamps):
|
|
497
|
+
current_x_pos = time_stamps[current_frame] / 1000.0
|
|
498
|
+
elif ed is not None:
|
|
499
|
+
# Fallback: estimate time based on frame rate
|
|
500
|
+
if isinstance(ed, dict):
|
|
501
|
+
frame_rate = ed.get('frame_rate', None)
|
|
502
|
+
else:
|
|
503
|
+
frame_rate = getattr(ed, 'frame_rate', None)
|
|
504
|
+
if frame_rate and frame_rate > 0:
|
|
505
|
+
current_x_pos = current_frame / frame_rate
|
|
506
|
+
except Exception:
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
# If there's no existing metric plotted, set sensible x-limits so a
|
|
510
|
+
# standalone vline will be visible (use number of frames when available).
|
|
511
|
+
if not self.trace_ax.lines:
|
|
512
|
+
try:
|
|
513
|
+
nframes = 1
|
|
514
|
+
if (hasattr(self.main_window, '_current_tif') and
|
|
515
|
+
self.main_window._current_tif is not None and
|
|
516
|
+
self.main_window._current_tif.ndim >= 3):
|
|
517
|
+
nframes = self.main_window._current_tif.shape[0]
|
|
518
|
+
|
|
519
|
+
# Set x-limits based on display mode
|
|
520
|
+
if show_time:
|
|
521
|
+
# Try to get max time value
|
|
522
|
+
try:
|
|
523
|
+
ed = getattr(self.main_window, '_exp_data', None)
|
|
524
|
+
time_stamps = None
|
|
525
|
+
|
|
526
|
+
if ed is not None:
|
|
527
|
+
for attr_name in ['time_stamps', 'timeStamps', 'timestamps', 'ElapsedTimes']:
|
|
528
|
+
if hasattr(ed, attr_name):
|
|
529
|
+
time_stamps = getattr(ed, attr_name)
|
|
530
|
+
break
|
|
531
|
+
|
|
532
|
+
if time_stamps is not None and len(time_stamps) > 0:
|
|
533
|
+
xmax = max(np.array(time_stamps[:min(nframes, len(time_stamps))]) / 1000.0)
|
|
534
|
+
elif ed is not None:
|
|
535
|
+
frame_rate = getattr(ed, 'frame_rate', None)
|
|
536
|
+
if frame_rate and frame_rate > 0:
|
|
537
|
+
xmax = (nframes - 1) / frame_rate
|
|
538
|
+
else:
|
|
539
|
+
xmax = max(1, nframes - 1)
|
|
540
|
+
else:
|
|
541
|
+
xmax = max(1, nframes - 1)
|
|
542
|
+
except Exception:
|
|
543
|
+
xmax = max(1, nframes - 1)
|
|
544
|
+
else:
|
|
545
|
+
xmax = max(1, nframes - 1)
|
|
546
|
+
|
|
547
|
+
self.trace_ax.set_xlim(0, xmax)
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
# Ensure we have a persistent vline and move it (create if missing)
|
|
552
|
+
if not hasattr(self, '_frame_vline') or self._frame_vline is None:
|
|
553
|
+
self._frame_vline = self.trace_ax.axvline(current_x_pos, color='yellow', linestyle='-', zorder=10, linewidth=2)
|
|
554
|
+
if self.main_window:
|
|
555
|
+
self.main_window._frame_vline = self._frame_vline
|
|
556
|
+
else:
|
|
557
|
+
try:
|
|
558
|
+
self._frame_vline.set_xdata([current_x_pos, current_x_pos])
|
|
559
|
+
except Exception:
|
|
560
|
+
# recreate fallback
|
|
561
|
+
self._frame_vline = self.trace_ax.axvline(current_x_pos, color='yellow', linestyle='-', zorder=10, linewidth=2)
|
|
562
|
+
if self.main_window:
|
|
563
|
+
self.main_window._frame_vline = self._frame_vline
|
|
564
|
+
|
|
565
|
+
# Redraw canvas (fast)
|
|
566
|
+
try:
|
|
567
|
+
self.trace_canvas.draw_idle()
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
def _reset_ylim(self):
|
|
572
|
+
"""Clear any user-set y-limits and revert to autoscaling."""
|
|
573
|
+
if hasattr(self, 'ylim_min_edit'):
|
|
574
|
+
self.ylim_min_edit.setText("")
|
|
575
|
+
if hasattr(self, 'ylim_max_edit'):
|
|
576
|
+
self.ylim_max_edit.setText("")
|
|
577
|
+
self._update_trace_from_roi()
|
|
578
|
+
|
|
579
|
+
def _toggle_time_display(self):
|
|
580
|
+
"""Toggle between showing frame numbers and time in seconds on the trace plot."""
|
|
581
|
+
self._show_time_in_seconds = not getattr(self, '_show_time_in_seconds', False)
|
|
582
|
+
|
|
583
|
+
# Update button text to show current mode
|
|
584
|
+
if self._show_time_in_seconds:
|
|
585
|
+
self.time_display_button.setText("Seconds")
|
|
586
|
+
else:
|
|
587
|
+
self.time_display_button.setText("Frames")
|
|
588
|
+
|
|
589
|
+
# Update the trace plot with new x-axis
|
|
590
|
+
self._update_trace_from_roi()
|
|
591
|
+
|
|
592
|
+
def clear_trace(self):
|
|
593
|
+
"""Clear the trace plot and reset it to initial state."""
|
|
594
|
+
if hasattr(self, 'trace_ax') and self.trace_ax is not None:
|
|
595
|
+
self.trace_ax.cla()
|
|
596
|
+
|
|
597
|
+
# Reset the plot appearance
|
|
598
|
+
self.trace_ax.set_xticks([])
|
|
599
|
+
self.trace_ax.set_yticks([])
|
|
600
|
+
self.trace_ax.set_xlabel("")
|
|
601
|
+
self.trace_ax.set_ylabel("")
|
|
602
|
+
for spine in self.trace_ax.spines.values():
|
|
603
|
+
spine.set_visible(True)
|
|
604
|
+
self.trace_ax.set_facecolor('none')
|
|
605
|
+
self.trace_ax.xaxis.label.set_color('white')
|
|
606
|
+
self.trace_ax.yaxis.label.set_color('white')
|
|
607
|
+
self.trace_ax.tick_params(axis='x', colors='white')
|
|
608
|
+
self.trace_ax.tick_params(axis='y', colors='white')
|
|
609
|
+
for spine in self.trace_ax.spines.values():
|
|
610
|
+
spine.set_color('white')
|
|
611
|
+
|
|
612
|
+
# Clear the frame vline reference
|
|
613
|
+
if hasattr(self, '_frame_vline'):
|
|
614
|
+
self._frame_vline = None
|
|
615
|
+
if self.main_window and hasattr(self.main_window, '_frame_vline'):
|
|
616
|
+
self.main_window._frame_vline = None
|
|
617
|
+
|
|
618
|
+
# Redraw the trace canvas
|
|
619
|
+
if hasattr(self, 'trace_canvas') and self.trace_canvas is not None:
|
|
620
|
+
self.trace_fig.tight_layout()
|
|
621
|
+
self.trace_canvas.draw()
|