flimkit 0.12.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.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit-0.12.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib
|
|
13
|
+
import matplotlib.image as mpimg
|
|
14
|
+
from matplotlib.figure import Figure
|
|
15
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
16
|
+
from flimkit.utils import display
|
|
17
|
+
from flimkit.utils.roi import RoiManager
|
|
18
|
+
from flimkit.UI.roi_tools import RoiAnalysisPanel
|
|
19
|
+
|
|
20
|
+
class FOVPreviewPanel:
|
|
21
|
+
def __init__(self, parent):
|
|
22
|
+
self.frame = ttk.Frame(parent)
|
|
23
|
+
self.frame.columnconfigure(0, weight=1)
|
|
24
|
+
self.frame.rowconfigure(0, weight=1)
|
|
25
|
+
from matplotlib.gridspec import GridSpec
|
|
26
|
+
self._fig = Figure(figsize=(10, 8), dpi=100, facecolor='black')
|
|
27
|
+
self._decay_visible = True
|
|
28
|
+
self._display_mode = 'flim'
|
|
29
|
+
gs = GridSpec(3, 3, figure=self._fig, height_ratios=[1, 0.6, 0.3], width_ratios=[1, 1, 0.05], hspace=0.38, wspace=0.15)
|
|
30
|
+
self._ax_img = self._fig.add_subplot(gs[0, 0])
|
|
31
|
+
self._ax_flim = self._fig.add_subplot(gs[0, 1])
|
|
32
|
+
self._ax_cbar = self._fig.add_subplot(gs[0, 2])
|
|
33
|
+
self._ax_decay = self._fig.add_subplot(gs[1, :])
|
|
34
|
+
self._ax_resid = self._fig.add_subplot(gs[2, :], sharex=self._ax_decay)
|
|
35
|
+
for _ax in (self._ax_img, self._ax_flim):
|
|
36
|
+
_ax.set_facecolor('black')
|
|
37
|
+
self._ax_decay.set_facecolor('white')
|
|
38
|
+
self._ax_decay.tick_params(colors='white')
|
|
39
|
+
self._ax_decay.xaxis.label.set_color('white')
|
|
40
|
+
self._ax_decay.yaxis.label.set_color('white')
|
|
41
|
+
self._ax_decay.title.set_color('white')
|
|
42
|
+
self._ax_resid.set_facecolor('white')
|
|
43
|
+
self._ax_resid.tick_params(colors='white')
|
|
44
|
+
self._ax_resid.xaxis.label.set_color('white')
|
|
45
|
+
self._ax_resid.yaxis.label.set_color('white')
|
|
46
|
+
self._strip_image_axes(self._ax_img)
|
|
47
|
+
self._strip_image_axes(self._ax_flim)
|
|
48
|
+
self._canvas_mpl = FigureCanvasTkAgg(self._fig, master=self.frame)
|
|
49
|
+
self._canvas_mpl.get_tk_widget().grid(row=0, column=0, sticky='nsew')
|
|
50
|
+
self._status = tk.StringVar(value='No FOV loaded')
|
|
51
|
+
ttk.Label(self.frame, textvariable=self._status, foreground='grey', font=('Courier', 8)).grid(
|
|
52
|
+
row=1, column=0, sticky='w', padx=4, pady=(2, 4))
|
|
53
|
+
ctrl_frame = ttk.LabelFrame(self.frame, text='FLIM Color Scale', padding=4)
|
|
54
|
+
ctrl_frame.grid(row=2, column=0, sticky='ew', padx=4, pady=(0, 4))
|
|
55
|
+
ctrl_frame.columnconfigure(1, weight=1)
|
|
56
|
+
ctrl_frame.grid_remove()
|
|
57
|
+
self._ctrl_frame = ctrl_frame
|
|
58
|
+
zbar = ttk.Frame(self.frame)
|
|
59
|
+
zbar.grid(row=3, column=0, sticky='ew', padx=4, pady=(0, 4))
|
|
60
|
+
zbar.columnconfigure(1, weight=1)
|
|
61
|
+
self._z_label = tk.StringVar(value='z 1/1')
|
|
62
|
+
ttk.Label(zbar, textvariable=self._z_label, width=12).grid(row=0, column=0, sticky='w')
|
|
63
|
+
self._z_slider_updating = False
|
|
64
|
+
self._z_slider = ttk.Scale(zbar, from_=0, to=0, orient='horizontal',
|
|
65
|
+
command=self._on_z_slider)
|
|
66
|
+
self._z_slider.grid(row=0, column=1, sticky='ew', padx=4)
|
|
67
|
+
self._zbar = zbar
|
|
68
|
+
zbar.grid_remove()
|
|
69
|
+
self._zstack = None
|
|
70
|
+
self._series = None
|
|
71
|
+
self._z_i = 0
|
|
72
|
+
ttk.Label(ctrl_frame, text='τ range (ns):').grid(row=0, column=0, sticky='w')
|
|
73
|
+
ttk.Label(ctrl_frame, text='Min:').grid(row=0, column=1, sticky='w', padx=(10, 2))
|
|
74
|
+
self._sv_tau_min = tk.StringVar()
|
|
75
|
+
ttk.Entry(ctrl_frame, textvariable=self._sv_tau_min, width=6).grid(row=0, column=2, sticky='w', padx=2)
|
|
76
|
+
ttk.Label(ctrl_frame, text='Max:').grid(row=0, column=3, sticky='w', padx=(10, 2))
|
|
77
|
+
self._sv_tau_max = tk.StringVar()
|
|
78
|
+
ttk.Entry(ctrl_frame, textvariable=self._sv_tau_max, width=6).grid(row=0, column=4, sticky='w', padx=2)
|
|
79
|
+
ttk.Button(ctrl_frame, text='Auto', width=6, command=self._auto_detect_scale).grid(row=0, column=5, sticky='w', padx=2)
|
|
80
|
+
ttk.Label(ctrl_frame, text='Γ:').grid(row=1, column=0, sticky='w')
|
|
81
|
+
self._sv_gamma = tk.StringVar(value='1.0')
|
|
82
|
+
ttk.Entry(ctrl_frame, textvariable=self._sv_gamma, width=6).grid(row=1, column=2, sticky='w', padx=2)
|
|
83
|
+
ttk.Label(ctrl_frame, text='Colormap:').grid(row=1, column=3, sticky='w', padx=(10, 2))
|
|
84
|
+
self._sv_cmap = tk.StringVar(value='viridis')
|
|
85
|
+
self._cmap_combo = ttk.Combobox(ctrl_frame, textvariable=self._sv_cmap,
|
|
86
|
+
state='readonly', width=10)
|
|
87
|
+
self._cmap_combo.grid(row=1, column=4, sticky='w', padx=2)
|
|
88
|
+
self._cmap_combo['values'] = list(display.COLORMAPS.keys())
|
|
89
|
+
ttk.Button(ctrl_frame, text='Update', width=8, command=self._update_flim_display).grid(row=1, column=5, sticky='w', padx=2)
|
|
90
|
+
self._bv_show_decay = tk.BooleanVar(value=True)
|
|
91
|
+
ttk.Checkbutton(ctrl_frame, text='Show Decay Plot',
|
|
92
|
+
variable=self._bv_show_decay,
|
|
93
|
+
command=self._toggle_decay).grid(
|
|
94
|
+
row=2, column=0, columnspan=3, sticky='w', pady=(4, 0))
|
|
95
|
+
self._sv_display_mode = tk.StringVar(value='flim')
|
|
96
|
+
dm_frame = ttk.Frame(ctrl_frame)
|
|
97
|
+
dm_frame.grid(row=2, column=3, columnspan=3, sticky='w', pady=(4, 0))
|
|
98
|
+
ttk.Label(dm_frame, text='View:').pack(side='left', padx=(0, 4))
|
|
99
|
+
ttk.Radiobutton(dm_frame, text='FLIM', variable=self._sv_display_mode,
|
|
100
|
+
value='flim', command=self._on_display_mode_changed).pack(side='left')
|
|
101
|
+
ttk.Radiobutton(dm_frame, text='Intensity', variable=self._sv_display_mode,
|
|
102
|
+
value='intensity', command=self._on_display_mode_changed).pack(side='left')
|
|
103
|
+
self._sv_tau_weighting = tk.StringVar(value='amplitude')
|
|
104
|
+
wt_frame = ttk.Frame(ctrl_frame)
|
|
105
|
+
wt_frame.grid(row=3, column=0, columnspan=6, sticky='w', pady=(4, 0))
|
|
106
|
+
ttk.Label(wt_frame, text='τ weighting:').pack(side='left', padx=(0, 4))
|
|
107
|
+
ttk.Radiobutton(wt_frame, text='Amplitude', variable=self._sv_tau_weighting,
|
|
108
|
+
value='amplitude', command=self._on_weighting_changed).pack(side='left')
|
|
109
|
+
ttk.Radiobutton(wt_frame, text='Intensity', variable=self._sv_tau_weighting,
|
|
110
|
+
value='intensity', command=self._on_weighting_changed).pack(side='left')
|
|
111
|
+
self._ptu_path = None
|
|
112
|
+
self._lifetime_map = None
|
|
113
|
+
self._pixel_maps = None
|
|
114
|
+
self._intensity_map = None
|
|
115
|
+
self._flim_cbar = None
|
|
116
|
+
self._flim_color_scale = {
|
|
117
|
+
'vmin': None,
|
|
118
|
+
'vmax': None,
|
|
119
|
+
'gamma': 1.0,
|
|
120
|
+
'cmap': 'viridis',
|
|
121
|
+
}
|
|
122
|
+
self._n_exp = 1
|
|
123
|
+
self._irf_prompt = None
|
|
124
|
+
self._roi_manager = RoiManager()
|
|
125
|
+
self._roi_patches = {}
|
|
126
|
+
self._drawing_mode = tk.StringVar(value='select')
|
|
127
|
+
self._is_drawing = False
|
|
128
|
+
self._draw_coords = []
|
|
129
|
+
self._temp_line = None
|
|
130
|
+
self._mouse_press_event = None
|
|
131
|
+
self._roi_analysis_panel = None
|
|
132
|
+
self._roi_drag = None
|
|
133
|
+
self._setup_drawing_events()
|
|
134
|
+
self._cached_decay_lines = []
|
|
135
|
+
self._cached_decay_title = ''
|
|
136
|
+
self._cached_decay_yscale = 'log'
|
|
137
|
+
self._cached_resid_data = None
|
|
138
|
+
self._setup_zoom()
|
|
139
|
+
def load_fov(self, ptu_path: Optional[str], _keep_zstack=False):
|
|
140
|
+
if not _keep_zstack:
|
|
141
|
+
self._hide_zstack()
|
|
142
|
+
self._lifetime_map = None
|
|
143
|
+
self._pixel_maps = None
|
|
144
|
+
self._cached_resid_data = None
|
|
145
|
+
if not ptu_path or not Path(ptu_path).exists():
|
|
146
|
+
self._clear()
|
|
147
|
+
self._status.set('Invalid PTU file')
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
self._ptu_path = ptu_path
|
|
151
|
+
from flimkit.formats import FLIMFile
|
|
152
|
+
import numpy as np
|
|
153
|
+
ptu = FLIMFile(ptu_path, verbose=False)
|
|
154
|
+
is_image = getattr(ptu, 'is_image', True)
|
|
155
|
+
decay = ptu.summed_decay(channel=None)
|
|
156
|
+
time_ns = ptu.time_ns
|
|
157
|
+
self._ax_img.clear()
|
|
158
|
+
if is_image:
|
|
159
|
+
stack = ptu.pixel_stack(channel=None, binning=1)
|
|
160
|
+
intensity = stack.sum(axis=2)
|
|
161
|
+
intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
|
|
162
|
+
self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
|
|
163
|
+
else:
|
|
164
|
+
intensity = None
|
|
165
|
+
self._ax_img.text(0.5, 0.5, 'Point measurement\nno image to reconstruct',
|
|
166
|
+
ha='center', va='center', transform=self._ax_img.transAxes,
|
|
167
|
+
fontsize=9, color='white')
|
|
168
|
+
self._ax_img.set_title('Intensity', fontsize=9, fontweight='bold', color='white')
|
|
169
|
+
self._strip_image_axes(self._ax_img)
|
|
170
|
+
self._ax_flim.clear()
|
|
171
|
+
self._ax_flim.text(0.5, 0.5,
|
|
172
|
+
'Waiting for fit...' if is_image else 'No FLIM image\n(point measurement)',
|
|
173
|
+
ha='center', va='center',
|
|
174
|
+
transform=self._ax_flim.transAxes, fontsize=9, color='white')
|
|
175
|
+
self._ax_flim.set_title('FLIM Lifetime', fontsize=10, fontweight='bold', color='white')
|
|
176
|
+
self._ax_decay.clear()
|
|
177
|
+
self._ax_decay.set_facecolor('white')
|
|
178
|
+
self._ax_decay.semilogy(time_ns, decay, color='steelblue', linewidth=1.5)
|
|
179
|
+
self._ax_decay.set_title('Summed Decay', fontsize=10, fontweight='bold', color='white')
|
|
180
|
+
self._ax_decay.set_xlabel('Time (ns)', color='white')
|
|
181
|
+
self._ax_decay.set_ylabel('Photon Count', color='white')
|
|
182
|
+
self._ax_decay.grid(True, alpha=0.3)
|
|
183
|
+
self._ax_decay.tick_params(labelsize=8, colors='white')
|
|
184
|
+
self._ax_resid.clear()
|
|
185
|
+
self._ax_resid.set_facecolor('white')
|
|
186
|
+
self._ax_resid.tick_params(labelsize=7, colors='white')
|
|
187
|
+
self._ax_resid.grid(True, alpha=0.3)
|
|
188
|
+
self._cached_resid_data = None
|
|
189
|
+
self._redraw_region_overlays()
|
|
190
|
+
self._canvas_mpl.draw_idle()
|
|
191
|
+
n_photons = int(decay.sum())
|
|
192
|
+
if is_image:
|
|
193
|
+
img_shape = intensity.shape
|
|
194
|
+
self._status.set(f"✓ {Path(ptu_path).name} | {img_shape[0]}×{img_shape[1]}px | {n_photons} photons")
|
|
195
|
+
else:
|
|
196
|
+
self._status.set(f"✓ {Path(ptu_path).name} | point measurement, no image | {n_photons} photons")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self._clear()
|
|
199
|
+
self._status.set(f"Error loading FOV: {str(e)[:50]}")
|
|
200
|
+
def display_fit_results(self, ptu_path: str, fit_result: dict, _keep_zstack=False):
|
|
201
|
+
if not _keep_zstack:
|
|
202
|
+
self._hide_zstack()
|
|
203
|
+
try:
|
|
204
|
+
from flimkit.formats import FLIMFile
|
|
205
|
+
import numpy as np
|
|
206
|
+
global_summary = fit_result.get('global_summary', {})
|
|
207
|
+
global_popt = fit_result.get('global_popt')
|
|
208
|
+
irf_prompt = fit_result.get('irf_prompt')
|
|
209
|
+
if irf_prompt is not None:
|
|
210
|
+
self._irf_prompt = irf_prompt
|
|
211
|
+
time_ns_from_result = fit_result.get('time_ns')
|
|
212
|
+
decay_from_result = fit_result.get('decay')
|
|
213
|
+
canvas = fit_result.get('canvas')
|
|
214
|
+
if decay_from_result is not None and time_ns_from_result is not None:
|
|
215
|
+
decay = decay_from_result
|
|
216
|
+
time_ns = time_ns_from_result
|
|
217
|
+
else:
|
|
218
|
+
if ptu_path and Path(ptu_path).exists():
|
|
219
|
+
ptu = FLIMFile(ptu_path, verbose=False)
|
|
220
|
+
decay = ptu.summed_decay(channel=None)
|
|
221
|
+
time_ns = ptu.time_ns
|
|
222
|
+
else:
|
|
223
|
+
decay = None
|
|
224
|
+
time_ns = None
|
|
225
|
+
intensity = None
|
|
226
|
+
if canvas is not None and 'intensity' in canvas:
|
|
227
|
+
intensity = canvas['intensity']
|
|
228
|
+
elif 'intensity' in fit_result:
|
|
229
|
+
intensity = fit_result['intensity']
|
|
230
|
+
elif ptu_path and Path(ptu_path).exists():
|
|
231
|
+
ptu = FLIMFile(ptu_path, verbose=False)
|
|
232
|
+
if getattr(ptu, 'is_image', True):
|
|
233
|
+
stack = ptu.pixel_stack(channel=None, binning=1)
|
|
234
|
+
intensity = stack.sum(axis=2)
|
|
235
|
+
if intensity is None:
|
|
236
|
+
intensity = np.ones((512, 512), dtype=np.float32)
|
|
237
|
+
from flimkit.utils.display import compute_weighted_lifetime
|
|
238
|
+
pixel_maps = fit_result.get('pixel_maps')
|
|
239
|
+
if pixel_maps is None and canvas is not None:
|
|
240
|
+
pixel_maps = {k: v for k, v in canvas.items()
|
|
241
|
+
if k not in ('intensity', 'coverage')}
|
|
242
|
+
nexp = global_summary.get('n_exp', len(global_summary.get('taus_ns', [])))
|
|
243
|
+
if nexp == 0:
|
|
244
|
+
nexp = sum(1 for k in range(1, 4) if f'tau{k}_mean_ns' in global_summary)
|
|
245
|
+
lifetime_map = None
|
|
246
|
+
if pixel_maps and nexp > 0:
|
|
247
|
+
try:
|
|
248
|
+
lifetime_map = compute_weighted_lifetime(
|
|
249
|
+
pixel_maps, intensity, n_exp=nexp,
|
|
250
|
+
weighting=self._sv_tau_weighting.get(),
|
|
251
|
+
)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f" - Warning: Could not compute lifetime map: {e}")
|
|
254
|
+
lifetime_map = None
|
|
255
|
+
if (lifetime_map is not None and intensity is not None
|
|
256
|
+
and lifetime_map.shape != intensity.shape[:2]):
|
|
257
|
+
try:
|
|
258
|
+
import cv2 as _cv2
|
|
259
|
+
th, tw = intensity.shape[:2]
|
|
260
|
+
lifetime_map = _cv2.resize(
|
|
261
|
+
lifetime_map.astype(np.float32), (tw, th),
|
|
262
|
+
interpolation=_cv2.INTER_NEAREST)
|
|
263
|
+
except Exception as _upe:
|
|
264
|
+
print(f" - Could not upsample lifetime_map: {_upe}")
|
|
265
|
+
self._lifetime_map = lifetime_map
|
|
266
|
+
self._pixel_maps = pixel_maps
|
|
267
|
+
self._intensity_map = intensity
|
|
268
|
+
self._n_exp = nexp
|
|
269
|
+
if 'intensity' not in fit_result and intensity is not None:
|
|
270
|
+
fit_result['intensity'] = intensity
|
|
271
|
+
if lifetime_map is not None:
|
|
272
|
+
fit_result['lifetime'] = lifetime_map
|
|
273
|
+
if pixel_maps:
|
|
274
|
+
fit_result['pixel_maps'] = pixel_maps
|
|
275
|
+
taus_fit = global_summary.get('taus_ns', [])
|
|
276
|
+
model = global_summary.get('model')
|
|
277
|
+
self._ax_img.clear()
|
|
278
|
+
intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
|
|
279
|
+
self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
|
|
280
|
+
self._ax_img.set_title('Intensity', fontsize=9, fontweight='bold', color='white')
|
|
281
|
+
self._strip_image_axes(self._ax_img)
|
|
282
|
+
self._ax_flim.clear()
|
|
283
|
+
if self._lifetime_map is not None and np.any(~np.isnan(self._lifetime_map)):
|
|
284
|
+
scaled = display.apply_color_scale(
|
|
285
|
+
self._lifetime_map,
|
|
286
|
+
vmin=self._flim_color_scale['vmin'],
|
|
287
|
+
vmax=self._flim_color_scale['vmax'],
|
|
288
|
+
gamma=self._flim_color_scale['gamma'],
|
|
289
|
+
)
|
|
290
|
+
cmap = display.get_colormap(self._flim_color_scale['cmap'])
|
|
291
|
+
cmap.set_bad(color='black')
|
|
292
|
+
im = self._ax_flim.imshow(scaled, cmap=cmap, origin='upper', vmin=0, vmax=1)
|
|
293
|
+
self._ax_flim.set_title('FLIM Lifetime (ns)', fontsize=9, fontweight='bold', color='white')
|
|
294
|
+
self._strip_image_axes(self._ax_flim)
|
|
295
|
+
valid_data = self._lifetime_map[~np.isnan(self._lifetime_map)]
|
|
296
|
+
if valid_data.size > 0:
|
|
297
|
+
data_min = np.min(valid_data)
|
|
298
|
+
data_max = np.max(valid_data)
|
|
299
|
+
self._ax_cbar.clear()
|
|
300
|
+
cbar = self._fig.colorbar(im, cax=self._ax_cbar)
|
|
301
|
+
cbar.set_label(f"τ (ns)", fontsize=8, color='white')
|
|
302
|
+
self._flim_cbar = cbar
|
|
303
|
+
n_ticks = 5
|
|
304
|
+
tick_positions = np.linspace(0, 1, n_ticks)
|
|
305
|
+
tick_values = data_min + tick_positions * (data_max - data_min)
|
|
306
|
+
cbar.set_ticks(tick_positions)
|
|
307
|
+
cbar.set_ticklabels([f"{v:.2f}" for v in tick_values], fontsize=7, color='white')
|
|
308
|
+
cbar.ax.tick_params(colors='white')
|
|
309
|
+
else:
|
|
310
|
+
self._ax_cbar.clear()
|
|
311
|
+
else:
|
|
312
|
+
self._ax_flim.text(0.5, 0.6, 'No FLIM data', ha='center', va='center',
|
|
313
|
+
transform=self._ax_flim.transAxes, fontsize=9, color='white')
|
|
314
|
+
self._ax_flim.text(0.5, 0.35, '(enable per-pixel fitting)', ha='center', va='center',
|
|
315
|
+
transform=self._ax_flim.transAxes, fontsize=8, color='white', style='italic')
|
|
316
|
+
self._ax_flim.set_title('FLIM Lifetime', fontsize=10, fontweight='bold', color='white')
|
|
317
|
+
self._redraw_region_overlays()
|
|
318
|
+
self._ax_decay.clear()
|
|
319
|
+
self._ax_decay.set_facecolor('white')
|
|
320
|
+
if decay is None or len(decay) == 0:
|
|
321
|
+
self._ax_decay.text(0.5, 0.5, 'No decay data', ha='center', va='center',
|
|
322
|
+
transform=self._ax_decay.transAxes)
|
|
323
|
+
else:
|
|
324
|
+
self._ax_decay.semilogy(time_ns, decay, 'o-', color='steelblue',
|
|
325
|
+
linewidth=1.5, markersize=3, label='Measured', alpha=0.7)
|
|
326
|
+
if irf_prompt is not None and len(irf_prompt) > 0:
|
|
327
|
+
irf_max = irf_prompt.max()
|
|
328
|
+
if irf_max > 0:
|
|
329
|
+
irf_scaled = (irf_prompt / irf_max) * decay.max() * 0.2
|
|
330
|
+
irf_time = time_ns[:len(irf_prompt)]
|
|
331
|
+
self._ax_decay.semilogy(irf_time, np.maximum(irf_scaled, 1e-2),
|
|
332
|
+
linewidth=2.0, color='orange', label='IRF', alpha=0.8)
|
|
333
|
+
model = global_summary.get('model')
|
|
334
|
+
if model is not None and len(model) > 0:
|
|
335
|
+
self._ax_decay.semilogy(time_ns, model, linewidth=2.0,
|
|
336
|
+
color='red', label='Fitted', alpha=0.8)
|
|
337
|
+
self._ax_decay.set_title(f"Summed Decay{f' ({nexp}-exp fit)' if nexp > 0 else ''}",
|
|
338
|
+
fontsize=10, fontweight='bold', color='white')
|
|
339
|
+
self._ax_decay.set_xlabel('Time (ns)', color='white')
|
|
340
|
+
self._ax_decay.set_ylabel('Photon Count', color='white')
|
|
341
|
+
if decay is not None and len(decay) > 0:
|
|
342
|
+
self._ax_decay.legend(fontsize=8, loc='upper right', labelcolor='black')
|
|
343
|
+
self._ax_decay.grid(True, alpha=0.3)
|
|
344
|
+
self._ax_decay.tick_params(labelsize=8, colors='white')
|
|
345
|
+
self._ax_resid.clear()
|
|
346
|
+
self._ax_resid.set_facecolor('white')
|
|
347
|
+
model_arr = global_summary.get('model')
|
|
348
|
+
if (decay is not None and len(decay) > 0
|
|
349
|
+
and model_arr is not None
|
|
350
|
+
and len(model_arr) == len(decay)):
|
|
351
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
352
|
+
resid = np.where(model_arr > 0,
|
|
353
|
+
(decay - model_arr) / np.sqrt(model_arr),
|
|
354
|
+
0.0)
|
|
355
|
+
self._cached_resid_data = (time_ns.copy(), resid)
|
|
356
|
+
self._ax_resid.plot(time_ns, resid, color='steelblue', linewidth=1.0)
|
|
357
|
+
self._ax_resid.axhline(0, color='red', linewidth=1.0,
|
|
358
|
+
linestyle='--', alpha=0.8)
|
|
359
|
+
self._ax_resid.set_ylabel('Resid. (σ)', fontsize=7, color='white')
|
|
360
|
+
chi2_r = global_summary.get('reduced_chi2_tail')
|
|
361
|
+
if chi2_r is not None:
|
|
362
|
+
self._ax_resid.annotate(
|
|
363
|
+
f"χ²_r = {chi2_r:.3f}",
|
|
364
|
+
xy=(0.98, 0.85), xycoords='axes fraction',
|
|
365
|
+
ha='right', va='top', fontsize=7,
|
|
366
|
+
color='white',
|
|
367
|
+
bbox=dict(boxstyle='round,pad=0.2', fc='#333333', alpha=0.7),
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
self._cached_resid_data = None
|
|
371
|
+
self._ax_resid.set_xlabel('Time (ns)', color='white')
|
|
372
|
+
self._ax_resid.tick_params(labelsize=7, colors='white')
|
|
373
|
+
self._ax_resid.grid(True, alpha=0.3)
|
|
374
|
+
self._ctrl_frame.grid()
|
|
375
|
+
self._canvas_mpl.draw_idle()
|
|
376
|
+
status = f"✓ Fit complete"
|
|
377
|
+
chi2_tail = global_summary.get('reduced_chi2_tail')
|
|
378
|
+
if chi2_tail is not None:
|
|
379
|
+
status += f" | χ²_r(tail)={chi2_tail:.3f}"
|
|
380
|
+
if nexp > 0:
|
|
381
|
+
taus = [global_summary.get(f'taus_ns', [])[i] if i < len(global_summary.get('taus_ns', [])) else None
|
|
382
|
+
for i in range(nexp)]
|
|
383
|
+
taus_str = ', '.join([f"{t:.3f}" for t in taus if t is not None])
|
|
384
|
+
status += f" | τ=[{taus_str}] ns"
|
|
385
|
+
self._status.set(status)
|
|
386
|
+
print(f" - Status: {status}")
|
|
387
|
+
except Exception as e:
|
|
388
|
+
import traceback
|
|
389
|
+
print(f"[FOV Preview] Error displaying fit results:")
|
|
390
|
+
traceback.print_exc()
|
|
391
|
+
self._status.set(f"Error: {str(e)[:60]}")
|
|
392
|
+
self._status.set(f"Error displaying fit: {str(e)[:50]}")
|
|
393
|
+
def display_zstack(self, slices, ptu_path=None):
|
|
394
|
+
self._zstack = list(slices) if slices else None
|
|
395
|
+
if not self._zstack:
|
|
396
|
+
self._hide_zstack()
|
|
397
|
+
return
|
|
398
|
+
n = len(self._zstack)
|
|
399
|
+
self._z_slider.configure(to=max(n - 1, 0))
|
|
400
|
+
self._zbar.grid()
|
|
401
|
+
self._z_i = 0
|
|
402
|
+
self._sync_z_slider()
|
|
403
|
+
self._show_zstack_slice(0)
|
|
404
|
+
|
|
405
|
+
def _hide_zstack(self):
|
|
406
|
+
self._zstack = None
|
|
407
|
+
self._series = None
|
|
408
|
+
try:
|
|
409
|
+
self._zbar.grid_remove()
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
def display_series(self, planes, output_dir):
|
|
414
|
+
self._zstack = None
|
|
415
|
+
self._series = [dict(p, _root=str(output_dir)) for p in planes] if planes else None
|
|
416
|
+
if not self._series:
|
|
417
|
+
self._hide_zstack()
|
|
418
|
+
return
|
|
419
|
+
self._z_slider.configure(to=max(len(self._series) - 1, 0))
|
|
420
|
+
self._zbar.grid()
|
|
421
|
+
self._z_i = 0
|
|
422
|
+
self._sync_z_slider()
|
|
423
|
+
self._show_series_plane(0)
|
|
424
|
+
|
|
425
|
+
def _series_plane_label(self, desc, i):
|
|
426
|
+
parts = []
|
|
427
|
+
if desc.get('t') is not None:
|
|
428
|
+
parts.append(f"t{desc['t']}")
|
|
429
|
+
if desc.get('z') is not None:
|
|
430
|
+
parts.append(f"z{desc['z']}")
|
|
431
|
+
head = ' '.join(parts) or desc.get('name', '')
|
|
432
|
+
return f"{head} ({i + 1}/{len(self._series)})"
|
|
433
|
+
|
|
434
|
+
def _show_series_plane(self, i):
|
|
435
|
+
desc = self._series[i]
|
|
436
|
+
self._z_label.set(self._series_plane_label(desc, i))
|
|
437
|
+
plane_dir = Path(desc['_root']) / desc.get('dir', desc.get('name', ''))
|
|
438
|
+
keep = self._series
|
|
439
|
+
self.load_stitched_roi(str(plane_dir))
|
|
440
|
+
self._series = keep
|
|
441
|
+
self._zbar.grid()
|
|
442
|
+
|
|
443
|
+
def _sync_z_slider(self):
|
|
444
|
+
self._z_slider_updating = True
|
|
445
|
+
try:
|
|
446
|
+
self._z_slider.set(self._z_i)
|
|
447
|
+
finally:
|
|
448
|
+
self._z_slider_updating = False
|
|
449
|
+
|
|
450
|
+
def _on_z_slider(self, value):
|
|
451
|
+
if self._z_slider_updating:
|
|
452
|
+
return
|
|
453
|
+
stack = self._series or self._zstack
|
|
454
|
+
if not stack:
|
|
455
|
+
return
|
|
456
|
+
i = max(0, min(int(round(float(value))), len(stack) - 1))
|
|
457
|
+
if i == self._z_i:
|
|
458
|
+
return
|
|
459
|
+
self._z_i = i
|
|
460
|
+
if self._series:
|
|
461
|
+
self._show_series_plane(i)
|
|
462
|
+
else:
|
|
463
|
+
self._show_zstack_slice(i)
|
|
464
|
+
|
|
465
|
+
def _show_zstack_slice(self, i):
|
|
466
|
+
desc = self._zstack[i]
|
|
467
|
+
self._z_label.set(f"z {desc.get('z', i + 1)} ({i + 1}/{len(self._zstack)})")
|
|
468
|
+
fit_result = desc.get('fit_result')
|
|
469
|
+
if fit_result is not None:
|
|
470
|
+
self.display_fit_results(desc.get('ptu_path'), fit_result, _keep_zstack=True)
|
|
471
|
+
else:
|
|
472
|
+
self.load_fov(desc.get('ptu_path'), _keep_zstack=True)
|
|
473
|
+
if self._display_mode != 'flim' or not self._decay_visible:
|
|
474
|
+
self._rebuild_layout()
|
|
475
|
+
|
|
476
|
+
def load_stitched_roi(self, output_dir: str):
|
|
477
|
+
if not output_dir:
|
|
478
|
+
self._clear()
|
|
479
|
+
self._status.set('No output directory')
|
|
480
|
+
return
|
|
481
|
+
try:
|
|
482
|
+
from pathlib import Path
|
|
483
|
+
import numpy as np
|
|
484
|
+
import tifffile
|
|
485
|
+
out_path = Path(output_dir)
|
|
486
|
+
intensity_files = sorted(out_path.glob('*_stitched_intensity.tif'))
|
|
487
|
+
if not intensity_files:
|
|
488
|
+
intensity_files = sorted(out_path.glob('*_intensity.tif'))
|
|
489
|
+
if not intensity_files:
|
|
490
|
+
self._clear()
|
|
491
|
+
self._status.set('No stitched image found')
|
|
492
|
+
return
|
|
493
|
+
intensity = tifffile.imread(str(intensity_files[0]))
|
|
494
|
+
self._ax_img.clear()
|
|
495
|
+
intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
|
|
496
|
+
self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
|
|
497
|
+
self._ax_img.set_title('Stitched ROI', fontsize=9, fontweight='bold', color='white')
|
|
498
|
+
self._strip_image_axes(self._ax_img)
|
|
499
|
+
lifetime_data = None
|
|
500
|
+
lifetime_min, lifetime_max = None, None
|
|
501
|
+
lifetime_full = sorted(out_path.glob('*_tau_intensity_weighted_fullrange.tif'))
|
|
502
|
+
if lifetime_full:
|
|
503
|
+
try:
|
|
504
|
+
lifetime_data = tifffile.imread(str(lifetime_full[0])).astype(np.float32)
|
|
505
|
+
valid = np.isfinite(lifetime_data)
|
|
506
|
+
if valid.any():
|
|
507
|
+
lifetime_min = float(np.nanmin(lifetime_data[valid]))
|
|
508
|
+
lifetime_max = float(np.nanpercentile(lifetime_data[valid], 98))
|
|
509
|
+
print(f" ✓ Loaded full-range lifetime: {lifetime_min:.2f}-{lifetime_max:.2f} ns")
|
|
510
|
+
except Exception as e:
|
|
511
|
+
print(f" - Could not load full-range lifetime: {e}")
|
|
512
|
+
if lifetime_data is None:
|
|
513
|
+
lifetime_disp = sorted(out_path.glob('*_tau_intensity_weighted.tif'))
|
|
514
|
+
if lifetime_disp:
|
|
515
|
+
try:
|
|
516
|
+
lifetime_data = tifffile.imread(str(lifetime_disp[0])).astype(np.float32)
|
|
517
|
+
lifetime_data = lifetime_data / 65535.0 * 5.0
|
|
518
|
+
lifetime_min, lifetime_max = 0.0, 5.0
|
|
519
|
+
print(f" ✓ Loaded display-scaled lifetime: 0-5 ns")
|
|
520
|
+
except Exception as e:
|
|
521
|
+
print(f" - Could not load display-scaled lifetime: {e}")
|
|
522
|
+
if lifetime_data is not None:
|
|
523
|
+
self._ax_flim.clear()
|
|
524
|
+
if lifetime_min is None or lifetime_max is None or lifetime_max <= lifetime_min:
|
|
525
|
+
lifetime_min = 0.0
|
|
526
|
+
lifetime_max = 5.0
|
|
527
|
+
if lifetime_max <= lifetime_min:
|
|
528
|
+
lifetime_max = lifetime_min + 0.1
|
|
529
|
+
lifetime_norm = np.clip((lifetime_data - lifetime_min) / (lifetime_max - lifetime_min), 0, 1)
|
|
530
|
+
im = self._ax_flim.imshow(lifetime_norm, cmap='viridis', origin='upper', vmin=0, vmax=1)
|
|
531
|
+
self._ax_flim.set_title(f"FLIM Lifetime ({lifetime_min:.2f}-{lifetime_max:.2f} ns)",
|
|
532
|
+
fontsize=9, fontweight='bold', color='white')
|
|
533
|
+
self._strip_image_axes(self._ax_flim)
|
|
534
|
+
self._ax_cbar.clear()
|
|
535
|
+
cbar = self._fig.colorbar(im, cax=self._ax_cbar, label='τ (ns)')
|
|
536
|
+
_min, _max = lifetime_min, lifetime_max
|
|
537
|
+
def _fmt_ns(x, pos):
|
|
538
|
+
return f"{_min + x * (_max - _min):.1f}"
|
|
539
|
+
from matplotlib.ticker import FuncFormatter
|
|
540
|
+
cbar.ax.yaxis.set_major_formatter(FuncFormatter(_fmt_ns))
|
|
541
|
+
cbar.ax.tick_params(labelsize=7)
|
|
542
|
+
else:
|
|
543
|
+
self._ax_flim.clear()
|
|
544
|
+
self._ax_flim.text(0.5, 0.5, 'Lifetime map not available', ha='center', va='center',
|
|
545
|
+
transform=self._ax_flim.transAxes, fontsize=9, color='white')
|
|
546
|
+
self._ax_flim.set_title('FLIM Lifetime', fontsize=10, fontweight='bold', color='white')
|
|
547
|
+
self._ax_decay.clear()
|
|
548
|
+
self._ax_decay.set_facecolor('white')
|
|
549
|
+
self._ax_decay.text(0.5, 0.5, 'Per-tile fit complete ✓',
|
|
550
|
+
ha='center', va='center', transform=self._ax_decay.transAxes,
|
|
551
|
+
fontsize=10, color='forestgreen', fontweight='bold')
|
|
552
|
+
self._canvas_mpl.draw_idle()
|
|
553
|
+
img_shape = intensity.shape
|
|
554
|
+
self._status.set(f"✓ Tile fit | {img_shape[0]}×{img_shape[1]}px")
|
|
555
|
+
except Exception as e:
|
|
556
|
+
import traceback
|
|
557
|
+
traceback.print_exc()
|
|
558
|
+
self._clear()
|
|
559
|
+
self._status.set(f"Error loading stitched: {str(e)[:50]}")
|
|
560
|
+
def _clear(self):
|
|
561
|
+
self._ax_img.clear()
|
|
562
|
+
self._ax_flim.clear()
|
|
563
|
+
self._ax_decay.clear()
|
|
564
|
+
self._ax_decay.set_facecolor('white')
|
|
565
|
+
self._ax_cbar.clear()
|
|
566
|
+
self._flim_cbar = None
|
|
567
|
+
self._ax_img.set_title('No FOV loaded', color='white')
|
|
568
|
+
self._ax_flim.set_title('FLIM Lifetime', color='white')
|
|
569
|
+
self._ax_decay.text(0.5, 0.5, 'Load a PTU file →',
|
|
570
|
+
ha='center', va='center', transform=self._ax_decay.transAxes,
|
|
571
|
+
fontsize=10, color='#888')
|
|
572
|
+
self._ctrl_frame.grid_remove()
|
|
573
|
+
self._canvas_mpl.draw_idle()
|
|
574
|
+
def _on_weighting_changed(self):
|
|
575
|
+
import numpy as np
|
|
576
|
+
if self._pixel_maps is None or self._intensity_map is None:
|
|
577
|
+
return
|
|
578
|
+
from flimkit.utils.display import compute_weighted_lifetime
|
|
579
|
+
try:
|
|
580
|
+
lifetime_map = compute_weighted_lifetime(
|
|
581
|
+
self._pixel_maps, self._intensity_map, n_exp=self._n_exp,
|
|
582
|
+
weighting=self._sv_tau_weighting.get(),
|
|
583
|
+
)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f" - Could not recompute lifetime map: {e}")
|
|
586
|
+
return
|
|
587
|
+
if (lifetime_map is not None
|
|
588
|
+
and lifetime_map.shape != self._intensity_map.shape[:2]):
|
|
589
|
+
try:
|
|
590
|
+
import cv2 as _cv2
|
|
591
|
+
th, tw = self._intensity_map.shape[:2]
|
|
592
|
+
lifetime_map = _cv2.resize(lifetime_map.astype(np.float32), (tw, th),
|
|
593
|
+
interpolation=_cv2.INTER_NEAREST)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
self._lifetime_map = lifetime_map
|
|
597
|
+
self._update_flim_display()
|
|
598
|
+
def _auto_detect_scale(self):
|
|
599
|
+
import numpy as np
|
|
600
|
+
if self._lifetime_map is None:
|
|
601
|
+
return
|
|
602
|
+
valid_data = self._lifetime_map[~np.isnan(self._lifetime_map)]
|
|
603
|
+
if valid_data.size > 0:
|
|
604
|
+
vmin = np.percentile(valid_data, 2)
|
|
605
|
+
vmax = np.percentile(valid_data, 98)
|
|
606
|
+
self._sv_tau_min.set(f"{vmin:.2f}")
|
|
607
|
+
self._sv_tau_max.set(f"{vmax:.2f}")
|
|
608
|
+
self._update_flim_display()
|
|
609
|
+
def _update_flim_display(self):
|
|
610
|
+
import numpy as np
|
|
611
|
+
if self._lifetime_map is None or not np.any(~np.isnan(self._lifetime_map)):
|
|
612
|
+
return
|
|
613
|
+
try:
|
|
614
|
+
try:
|
|
615
|
+
vmin = float(self._sv_tau_min.get()) if self._sv_tau_min.get() else None
|
|
616
|
+
except ValueError:
|
|
617
|
+
vmin = None
|
|
618
|
+
try:
|
|
619
|
+
vmax = float(self._sv_tau_max.get()) if self._sv_tau_max.get() else None
|
|
620
|
+
except ValueError:
|
|
621
|
+
vmax = None
|
|
622
|
+
try:
|
|
623
|
+
gamma = float(self._sv_gamma.get())
|
|
624
|
+
if gamma <= 0:
|
|
625
|
+
gamma = 1.0
|
|
626
|
+
except ValueError:
|
|
627
|
+
gamma = 1.0
|
|
628
|
+
cmap_name = self._sv_cmap.get()
|
|
629
|
+
self._flim_color_scale['vmin'] = vmin
|
|
630
|
+
self._flim_color_scale['vmax'] = vmax
|
|
631
|
+
self._flim_color_scale['gamma'] = gamma
|
|
632
|
+
self._flim_color_scale['cmap'] = cmap_name
|
|
633
|
+
self._save_color_scale_update()
|
|
634
|
+
scaled = display.apply_color_scale(
|
|
635
|
+
self._lifetime_map, vmin=vmin, vmax=vmax, gamma=gamma
|
|
636
|
+
)
|
|
637
|
+
self._ax_flim.clear()
|
|
638
|
+
self._ax_cbar.clear()
|
|
639
|
+
self._flim_cbar = None
|
|
640
|
+
cmap = display.get_colormap(cmap_name)
|
|
641
|
+
cmap.set_bad(color='black')
|
|
642
|
+
im = self._ax_flim.imshow(scaled, cmap=cmap, origin='upper', vmin=0, vmax=1)
|
|
643
|
+
self._ax_flim.set_title('FLIM Lifetime (ns)', fontsize=9, fontweight='bold', color='white')
|
|
644
|
+
self._strip_image_axes(self._ax_flim)
|
|
645
|
+
valid_data = self._lifetime_map[~np.isnan(self._lifetime_map)]
|
|
646
|
+
if valid_data.size > 0:
|
|
647
|
+
data_min = vmin if vmin is not None else np.min(valid_data)
|
|
648
|
+
data_max = vmax if vmax is not None else np.max(valid_data)
|
|
649
|
+
self._ax_cbar.clear()
|
|
650
|
+
cbar = self._fig.colorbar(im, cax=self._ax_cbar)
|
|
651
|
+
cbar.set_label('τ (ns)', fontsize=8, color='white')
|
|
652
|
+
self._flim_cbar = cbar
|
|
653
|
+
n_ticks = 5
|
|
654
|
+
tick_positions = np.linspace(0, 1, n_ticks)
|
|
655
|
+
tick_values = data_min + tick_positions * (data_max - data_min)
|
|
656
|
+
cbar.set_ticks(tick_positions)
|
|
657
|
+
cbar.set_ticklabels([f"{v:.2f}" for v in tick_values], fontsize=7, color='white')
|
|
658
|
+
cbar.ax.tick_params(colors='white')
|
|
659
|
+
else:
|
|
660
|
+
self._ax_cbar.clear()
|
|
661
|
+
self._redraw_region_overlays()
|
|
662
|
+
self._canvas_mpl.draw_idle()
|
|
663
|
+
except Exception as e:
|
|
664
|
+
print(f"Error updating FLIM display: {e}")
|
|
665
|
+
def _save_color_scale_update(self):
|
|
666
|
+
try:
|
|
667
|
+
if not self._ptu_path:
|
|
668
|
+
return
|
|
669
|
+
from pathlib import Path
|
|
670
|
+
import json
|
|
671
|
+
import numpy as np
|
|
672
|
+
ptu_path = Path(self._ptu_path)
|
|
673
|
+
session_file = ptu_path.parent / f"{ptu_path.stem}.roi_session.npz"
|
|
674
|
+
if not session_file.exists():
|
|
675
|
+
return
|
|
676
|
+
existing_data = np.load(session_file, allow_pickle=True)
|
|
677
|
+
session_data = {key: existing_data[key].item() if existing_data[key].ndim == 0 else existing_data[key]
|
|
678
|
+
for key in existing_data.files}
|
|
679
|
+
session_data['fov_color_scale'] = json.dumps(self._flim_color_scale)
|
|
680
|
+
np.savez_compressed(session_file, **session_data)
|
|
681
|
+
print(f"[Color Scale] ✓ Saved to {session_file.name}")
|
|
682
|
+
except Exception as e:
|
|
683
|
+
print(f"[Color Scale] Could not save update: {e}")
|
|
684
|
+
def _save_regions_update(self):
|
|
685
|
+
try:
|
|
686
|
+
if not self._ptu_path:
|
|
687
|
+
return
|
|
688
|
+
from pathlib import Path
|
|
689
|
+
import json
|
|
690
|
+
import numpy as np
|
|
691
|
+
from datetime import datetime
|
|
692
|
+
ptu_path = Path(self._ptu_path)
|
|
693
|
+
session_file = ptu_path.parent / f"{ptu_path.stem}.roi_session.npz"
|
|
694
|
+
if session_file.exists():
|
|
695
|
+
existing_data = np.load(session_file, allow_pickle=True)
|
|
696
|
+
session_data = {key: existing_data[key].item() if existing_data[key].ndim == 0 else existing_data[key]
|
|
697
|
+
for key in existing_data.files}
|
|
698
|
+
else:
|
|
699
|
+
session_data = {
|
|
700
|
+
'timestamp': datetime.now().isoformat(),
|
|
701
|
+
'source': str(self._ptu_path),
|
|
702
|
+
'form_state_json': json.dumps({}, default=str),
|
|
703
|
+
}
|
|
704
|
+
if self._lifetime_map is not None:
|
|
705
|
+
session_data['fov_lifetime_map'] = self._lifetime_map
|
|
706
|
+
if self._intensity_map is not None:
|
|
707
|
+
session_data['fov_intensity_map'] = self._intensity_map
|
|
708
|
+
session_data['fov_color_scale'] = json.dumps(self._flim_color_scale)
|
|
709
|
+
session_data['fov_n_exp'] = self._n_exp
|
|
710
|
+
if self._ptu_path:
|
|
711
|
+
session_data['fov_ptu_path'] = self._ptu_path
|
|
712
|
+
session_data['fov_regions'] = self._roi_manager.to_json()
|
|
713
|
+
np.savez_compressed(session_file, **session_data)
|
|
714
|
+
print(f"[ROI Manager] ✓ Saved {len(self._roi_manager.regions)} region(s) to {session_file.name}")
|
|
715
|
+
except Exception as e:
|
|
716
|
+
print(f"[ROI Manager] Could not save regions: {e}")
|
|
717
|
+
def _load_regions_from_json(self, json_str: str):
|
|
718
|
+
try:
|
|
719
|
+
self._roi_manager = RoiManager.from_json(json_str)
|
|
720
|
+
print(f"[ROI Manager] ✓ Loaded {len(self._roi_manager.regions)} region(s)")
|
|
721
|
+
self._redraw_region_overlays()
|
|
722
|
+
except Exception as e:
|
|
723
|
+
print(f"[ROI Manager] Could not load regions: {e}")
|
|
724
|
+
def _redraw_region_overlays(self):
|
|
725
|
+
import matplotlib.patches as mpatches
|
|
726
|
+
from flimkit.utils.roi import get_rectangle_patch, get_ellipse_patch, get_polygon_patch
|
|
727
|
+
target_axes = [ax for ax in (self._ax_flim, self._ax_img) if ax.get_visible()]
|
|
728
|
+
for patches in self._roi_patches.values():
|
|
729
|
+
for patch in (patches if isinstance(patches, list) else [patches]):
|
|
730
|
+
try:
|
|
731
|
+
patch.remove()
|
|
732
|
+
except (ValueError, NotImplementedError):
|
|
733
|
+
pass
|
|
734
|
+
self._roi_patches = {}
|
|
735
|
+
for region in self._roi_manager.get_all_regions():
|
|
736
|
+
region_id = region['id']
|
|
737
|
+
tool_type = region['tool']
|
|
738
|
+
coords = region['coords']
|
|
739
|
+
color = self._roi_manager.get_color(region_id)
|
|
740
|
+
linewidth = 2.5 if region_id == self._roi_manager.get_selected_id() else 1.5
|
|
741
|
+
patches_for_region = []
|
|
742
|
+
for ax in target_axes:
|
|
743
|
+
try:
|
|
744
|
+
if tool_type == 'rect':
|
|
745
|
+
patch = get_rectangle_patch(coords, edgecolor=color, linewidth=linewidth)
|
|
746
|
+
elif tool_type == 'ellipse':
|
|
747
|
+
patch = get_ellipse_patch(coords, edgecolor=color, linewidth=linewidth)
|
|
748
|
+
elif tool_type in ('polygon', 'freehand'):
|
|
749
|
+
patch = get_polygon_patch(coords, edgecolor=color, linewidth=linewidth)
|
|
750
|
+
else:
|
|
751
|
+
continue
|
|
752
|
+
ax.add_patch(patch)
|
|
753
|
+
patches_for_region.append(patch)
|
|
754
|
+
except Exception as e:
|
|
755
|
+
print(f"[ROI] Could not draw region {region_id}: {e}")
|
|
756
|
+
if patches_for_region:
|
|
757
|
+
self._roi_patches[region_id] = patches_for_region
|
|
758
|
+
self._canvas_mpl.draw_idle()
|
|
759
|
+
@staticmethod
|
|
760
|
+
def _strip_image_axes(ax):
|
|
761
|
+
ax.set_xlabel('')
|
|
762
|
+
ax.set_ylabel('')
|
|
763
|
+
ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
|
|
764
|
+
def _setup_zoom(self):
|
|
765
|
+
self._zoom_cid = self._canvas_mpl.mpl_connect('scroll_event', self._on_scroll_zoom)
|
|
766
|
+
self._pan_press_cid = self._canvas_mpl.mpl_connect('button_press_event', self._on_pan_press)
|
|
767
|
+
self._pan_release_cid = self._canvas_mpl.mpl_connect('button_release_event', self._on_pan_release)
|
|
768
|
+
self._pan_motion_cid = self._canvas_mpl.mpl_connect('motion_notify_event', self._on_pan_motion)
|
|
769
|
+
self._pan_origin = None
|
|
770
|
+
def _on_scroll_zoom(self, event):
|
|
771
|
+
ax = event.inaxes
|
|
772
|
+
if ax is None or ax not in (self._ax_img, self._ax_flim):
|
|
773
|
+
return
|
|
774
|
+
if event.xdata is None or event.ydata is None:
|
|
775
|
+
return
|
|
776
|
+
base_scale = 1.3
|
|
777
|
+
if event.button == 'up':
|
|
778
|
+
scale_factor = 1 / base_scale
|
|
779
|
+
elif event.button == 'down':
|
|
780
|
+
scale_factor = base_scale
|
|
781
|
+
else:
|
|
782
|
+
return
|
|
783
|
+
xlim = ax.get_xlim()
|
|
784
|
+
ylim = ax.get_ylim()
|
|
785
|
+
x_range = (xlim[1] - xlim[0]) * scale_factor
|
|
786
|
+
y_range = (ylim[1] - ylim[0]) * scale_factor
|
|
787
|
+
ax.set_xlim([event.xdata - x_range * (event.xdata - xlim[0]) / (xlim[1] - xlim[0]),
|
|
788
|
+
event.xdata + x_range * (xlim[1] - event.xdata) / (xlim[1] - xlim[0])])
|
|
789
|
+
ax.set_ylim([event.ydata - y_range * (event.ydata - ylim[0]) / (ylim[1] - ylim[0]),
|
|
790
|
+
event.ydata + y_range * (ylim[1] - event.ydata) / (ylim[1] - ylim[0])])
|
|
791
|
+
self._canvas_mpl.draw_idle()
|
|
792
|
+
def _on_pan_press(self, event):
|
|
793
|
+
if event.button == 1 and self._drawing_mode.get() != 'select':
|
|
794
|
+
return
|
|
795
|
+
ax = event.inaxes
|
|
796
|
+
if ax is None or ax not in self._active_image_axes():
|
|
797
|
+
return
|
|
798
|
+
if event.xdata is None:
|
|
799
|
+
return
|
|
800
|
+
if event.button == 3:
|
|
801
|
+
selected_id = self._roi_manager.get_selected_id()
|
|
802
|
+
if selected_id is None:
|
|
803
|
+
selected_id = self._hit_test_roi(event.xdata, event.ydata, ax)
|
|
804
|
+
if selected_id is not None:
|
|
805
|
+
self._start_roi_drag(selected_id, event.xdata, event.ydata)
|
|
806
|
+
return
|
|
807
|
+
self._pan_origin = (event.xdata, event.ydata, ax)
|
|
808
|
+
def _on_pan_release(self, event):
|
|
809
|
+
if self._roi_drag is not None:
|
|
810
|
+
self._finish_roi_drag()
|
|
811
|
+
self._pan_origin = None
|
|
812
|
+
def _on_pan_motion(self, event):
|
|
813
|
+
if self._roi_drag is not None:
|
|
814
|
+
if event.xdata is not None and event.ydata is not None:
|
|
815
|
+
self._update_roi_drag(event.xdata, event.ydata)
|
|
816
|
+
return
|
|
817
|
+
if self._pan_origin is None:
|
|
818
|
+
return
|
|
819
|
+
ox, oy, ax = self._pan_origin
|
|
820
|
+
if event.inaxes != ax or event.xdata is None:
|
|
821
|
+
return
|
|
822
|
+
dx = ox - event.xdata
|
|
823
|
+
dy = oy - event.ydata
|
|
824
|
+
xlim = ax.get_xlim()
|
|
825
|
+
ylim = ax.get_ylim()
|
|
826
|
+
ax.set_xlim(xlim[0] + dx, xlim[1] + dx)
|
|
827
|
+
ax.set_ylim(ylim[0] + dy, ylim[1] + dy)
|
|
828
|
+
self._canvas_mpl.draw_idle()
|
|
829
|
+
def _hit_test_roi(self, x, y, ax):
|
|
830
|
+
for region_id, patches in self._roi_patches.items():
|
|
831
|
+
for patch in (patches if isinstance(patches, list) else [patches]):
|
|
832
|
+
if patch.axes is ax and patch.contains_point(ax.transData.transform((x, y))):
|
|
833
|
+
return region_id
|
|
834
|
+
return None
|
|
835
|
+
def _start_roi_drag(self, region_id, x, y):
|
|
836
|
+
self._roi_drag = {'id': region_id, 'ox': x, 'oy': y}
|
|
837
|
+
self._roi_manager.select_region(region_id)
|
|
838
|
+
self._redraw_region_overlays()
|
|
839
|
+
if self._roi_analysis_panel:
|
|
840
|
+
self._roi_analysis_panel._refresh_region_list()
|
|
841
|
+
def _update_roi_drag(self, x, y):
|
|
842
|
+
drag = self._roi_drag
|
|
843
|
+
dx = x - drag['ox']
|
|
844
|
+
dy = y - drag['oy']
|
|
845
|
+
region = self._roi_manager.get_region(drag['id'])
|
|
846
|
+
if region is None:
|
|
847
|
+
self._roi_drag = None
|
|
848
|
+
return
|
|
849
|
+
new_coords = [[c[0] + dx, c[1] + dy] for c in region['coords']]
|
|
850
|
+
self._roi_manager.update_region(drag['id'], coords=new_coords)
|
|
851
|
+
drag['ox'] = x
|
|
852
|
+
drag['oy'] = y
|
|
853
|
+
self._redraw_region_overlays()
|
|
854
|
+
def _finish_roi_drag(self):
|
|
855
|
+
self._roi_drag = None
|
|
856
|
+
self._save_regions_update()
|
|
857
|
+
if self._roi_analysis_panel:
|
|
858
|
+
self._roi_analysis_panel._refresh_region_list()
|
|
859
|
+
def _on_display_mode_changed(self):
|
|
860
|
+
new_mode = self._sv_display_mode.get()
|
|
861
|
+
if new_mode == self._display_mode:
|
|
862
|
+
return
|
|
863
|
+
self._display_mode = new_mode
|
|
864
|
+
self._rebuild_layout()
|
|
865
|
+
def _toggle_decay(self):
|
|
866
|
+
show = self._bv_show_decay.get()
|
|
867
|
+
if show == self._decay_visible:
|
|
868
|
+
return
|
|
869
|
+
self._decay_visible = show
|
|
870
|
+
self._rebuild_layout()
|
|
871
|
+
def _rebuild_layout(self):
|
|
872
|
+
from matplotlib.gridspec import GridSpec
|
|
873
|
+
flim_title = self._ax_flim.get_title() if self._ax_flim.get_visible() else 'FLIM Lifetime (ns)'
|
|
874
|
+
img_title = self._ax_img.get_title() if self._ax_img.get_visible() else 'Intensity'
|
|
875
|
+
current_lines = []
|
|
876
|
+
for line in self._ax_decay.get_lines():
|
|
877
|
+
current_lines.append({
|
|
878
|
+
'x': line.get_xdata().copy(),
|
|
879
|
+
'y': line.get_ydata().copy(),
|
|
880
|
+
'color': line.get_color(),
|
|
881
|
+
'lw': line.get_linewidth(),
|
|
882
|
+
'label': line.get_label(),
|
|
883
|
+
'alpha': line.get_alpha(),
|
|
884
|
+
'marker': line.get_marker(),
|
|
885
|
+
'ms': line.get_markersize(),
|
|
886
|
+
})
|
|
887
|
+
if current_lines:
|
|
888
|
+
self._cached_decay_lines = current_lines
|
|
889
|
+
self._cached_decay_title = self._ax_decay.get_title()
|
|
890
|
+
self._cached_decay_yscale = self._ax_decay.get_yscale()
|
|
891
|
+
decay_lines = self._cached_decay_lines
|
|
892
|
+
decay_title = self._cached_decay_title
|
|
893
|
+
decay_yscale = self._cached_decay_yscale
|
|
894
|
+
for ax in (self._ax_img, self._ax_flim, self._ax_cbar, self._ax_decay, self._ax_resid):
|
|
895
|
+
ax.remove()
|
|
896
|
+
if self._decay_visible:
|
|
897
|
+
gs = GridSpec(3, 3, figure=self._fig,
|
|
898
|
+
height_ratios=[1, 0.6, 0.3],
|
|
899
|
+
width_ratios=[1, 1, 0.05],
|
|
900
|
+
hspace=0.38, wspace=0.15)
|
|
901
|
+
self._ax_img = self._fig.add_subplot(gs[0, 0])
|
|
902
|
+
self._ax_flim = self._fig.add_subplot(gs[0, 1])
|
|
903
|
+
self._ax_cbar = self._fig.add_subplot(gs[0, 2])
|
|
904
|
+
self._ax_decay = self._fig.add_subplot(gs[1, :])
|
|
905
|
+
self._ax_resid = self._fig.add_subplot(gs[2, :], sharex=self._ax_decay)
|
|
906
|
+
else:
|
|
907
|
+
if self._display_mode == 'intensity':
|
|
908
|
+
gs = GridSpec(1, 1, figure=self._fig)
|
|
909
|
+
self._ax_img = self._fig.add_subplot(gs[0, 0])
|
|
910
|
+
self._ax_flim = self._fig.add_axes([0, 0, 0.01, 0.01])
|
|
911
|
+
self._ax_flim.set_visible(False)
|
|
912
|
+
self._ax_cbar = self._fig.add_axes([0, 0, 0.01, 0.01])
|
|
913
|
+
self._ax_cbar.set_visible(False)
|
|
914
|
+
else:
|
|
915
|
+
gs = GridSpec(1, 2, figure=self._fig,
|
|
916
|
+
width_ratios=[1, 0.05],
|
|
917
|
+
wspace=0.08)
|
|
918
|
+
self._ax_flim = self._fig.add_subplot(gs[0, 0])
|
|
919
|
+
self._ax_cbar = self._fig.add_subplot(gs[0, 1])
|
|
920
|
+
self._ax_img = self._fig.add_axes([0, 0, 0.01, 0.01])
|
|
921
|
+
self._ax_img.set_visible(False)
|
|
922
|
+
self._ax_decay = self._fig.add_axes([0, 0, 0.01, 0.01])
|
|
923
|
+
self._ax_decay.set_visible(False)
|
|
924
|
+
self._ax_resid = self._fig.add_axes([0, 0, 0.01, 0.01])
|
|
925
|
+
self._ax_resid.set_visible(False)
|
|
926
|
+
for _ax in (self._ax_img, self._ax_flim):
|
|
927
|
+
_ax.set_facecolor('black')
|
|
928
|
+
if self._ax_img.get_visible() and self._intensity_map is not None:
|
|
929
|
+
import numpy as np
|
|
930
|
+
intensity_clipped = np.clip(self._intensity_map, 0,
|
|
931
|
+
np.percentile(self._intensity_map, 99))
|
|
932
|
+
self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
|
|
933
|
+
self._ax_img.set_title(img_title, fontsize=9, fontweight='bold', color='white')
|
|
934
|
+
self._strip_image_axes(self._ax_img)
|
|
935
|
+
elif self._ax_img.get_visible():
|
|
936
|
+
self._ax_img.set_title(img_title, fontsize=9, fontweight='bold', color='white')
|
|
937
|
+
self._strip_image_axes(self._ax_img)
|
|
938
|
+
if self._ax_flim.get_visible():
|
|
939
|
+
if self._lifetime_map is not None:
|
|
940
|
+
import numpy as np
|
|
941
|
+
scaled = display.apply_color_scale(
|
|
942
|
+
self._lifetime_map,
|
|
943
|
+
vmin=self._flim_color_scale['vmin'],
|
|
944
|
+
vmax=self._flim_color_scale['vmax'],
|
|
945
|
+
gamma=self._flim_color_scale['gamma'],
|
|
946
|
+
)
|
|
947
|
+
cmap = display.get_colormap(self._flim_color_scale['cmap'])
|
|
948
|
+
cmap.set_bad(color='black')
|
|
949
|
+
im = self._ax_flim.imshow(scaled, cmap=cmap, origin='upper',
|
|
950
|
+
vmin=0, vmax=1)
|
|
951
|
+
if self._ax_cbar.get_visible():
|
|
952
|
+
self._ax_cbar.clear()
|
|
953
|
+
self._flim_cbar = None
|
|
954
|
+
valid = self._lifetime_map[~np.isnan(self._lifetime_map)]
|
|
955
|
+
if valid.size > 0:
|
|
956
|
+
cs = self._flim_color_scale
|
|
957
|
+
d_min = cs['vmin'] if cs['vmin'] is not None else float(np.min(valid))
|
|
958
|
+
d_max = cs['vmax'] if cs['vmax'] is not None else float(np.max(valid))
|
|
959
|
+
cbar = self._fig.colorbar(im, cax=self._ax_cbar)
|
|
960
|
+
cbar.set_label('τ (ns)', fontsize=8, color='white')
|
|
961
|
+
self._flim_cbar = cbar
|
|
962
|
+
n_ticks = 5
|
|
963
|
+
tp = np.linspace(0, 1, n_ticks)
|
|
964
|
+
tv = d_min + tp * (d_max - d_min)
|
|
965
|
+
cbar.set_ticks(tp)
|
|
966
|
+
cbar.set_ticklabels([f"{v:.2f}" for v in tv], fontsize=7, color='white')
|
|
967
|
+
cbar.ax.tick_params(colors='white')
|
|
968
|
+
self._ax_flim.set_title(flim_title, fontsize=9, fontweight='bold', color='white')
|
|
969
|
+
self._strip_image_axes(self._ax_flim)
|
|
970
|
+
if self._decay_visible and decay_lines:
|
|
971
|
+
for ld in decay_lines:
|
|
972
|
+
self._ax_decay.plot(
|
|
973
|
+
ld['x'], ld['y'],
|
|
974
|
+
color=ld['color'], linewidth=ld['lw'],
|
|
975
|
+
label=ld['label'], alpha=ld['alpha'],
|
|
976
|
+
marker=ld['marker'], markersize=ld['ms'],
|
|
977
|
+
)
|
|
978
|
+
self._ax_decay.set_yscale(decay_yscale)
|
|
979
|
+
self._ax_decay.set_title(decay_title, fontsize=10, fontweight='bold', color='white')
|
|
980
|
+
self._ax_decay.set_xlabel('Time (ns)', color='white')
|
|
981
|
+
self._ax_decay.set_ylabel('Photon Count', color='white')
|
|
982
|
+
self._ax_decay.set_facecolor('white')
|
|
983
|
+
self._ax_decay.tick_params(labelsize=8, colors='white')
|
|
984
|
+
self._ax_decay.grid(True, alpha=0.3)
|
|
985
|
+
if self._decay_visible and self._cached_resid_data is not None:
|
|
986
|
+
t_r, res_r = self._cached_resid_data
|
|
987
|
+
self._ax_resid.set_facecolor('white')
|
|
988
|
+
self._ax_resid.plot(t_r, res_r, color='steelblue', linewidth=1.0)
|
|
989
|
+
self._ax_resid.axhline(0, color='red', linewidth=1.0,
|
|
990
|
+
linestyle='--', alpha=0.8)
|
|
991
|
+
self._ax_resid.set_ylabel('Resid. (σ)', fontsize=7, color='white')
|
|
992
|
+
self._ax_resid.set_xlabel('Time (ns)', color='white')
|
|
993
|
+
self._ax_resid.tick_params(labelsize=7, colors='white')
|
|
994
|
+
self._ax_resid.grid(True, alpha=0.3)
|
|
995
|
+
self._redraw_region_overlays()
|
|
996
|
+
self._setup_drawing_events()
|
|
997
|
+
self._canvas_mpl.draw_idle()
|
|
998
|
+
def _setup_drawing_events(self):
|
|
999
|
+
for cid in getattr(self, '_draw_cids', []):
|
|
1000
|
+
self._canvas_mpl.mpl_disconnect(cid)
|
|
1001
|
+
self._draw_cids = [
|
|
1002
|
+
self._canvas_mpl.mpl_connect('button_press_event', self._on_draw_press),
|
|
1003
|
+
self._canvas_mpl.mpl_connect('motion_notify_event', self._on_draw_motion),
|
|
1004
|
+
self._canvas_mpl.mpl_connect('button_release_event', self._on_draw_release),
|
|
1005
|
+
]
|
|
1006
|
+
def _active_image_axes(self):
|
|
1007
|
+
return {ax for ax in (self._ax_img, self._ax_flim) if ax.get_visible()}
|
|
1008
|
+
def _on_draw_press(self, event):
|
|
1009
|
+
if event.button != 1:
|
|
1010
|
+
return
|
|
1011
|
+
if not event.inaxes or event.inaxes not in self._active_image_axes():
|
|
1012
|
+
return
|
|
1013
|
+
mode = self._drawing_mode.get()
|
|
1014
|
+
if mode == 'select':
|
|
1015
|
+
return
|
|
1016
|
+
self._is_drawing = True
|
|
1017
|
+
self._draw_coords = [[event.xdata, event.ydata]]
|
|
1018
|
+
self._mouse_press_event = event
|
|
1019
|
+
print(f"[Drawing] Started {mode} at ({event.xdata:.1f}, {event.ydata:.1f})")
|
|
1020
|
+
def _on_draw_motion(self, event):
|
|
1021
|
+
if not self._is_drawing or not event.inaxes or event.inaxes not in self._active_image_axes():
|
|
1022
|
+
return
|
|
1023
|
+
mode = self._drawing_mode.get()
|
|
1024
|
+
if mode in ('rect', 'ellipse') and len(self._draw_coords) > 0:
|
|
1025
|
+
if self._temp_line is not None:
|
|
1026
|
+
try:
|
|
1027
|
+
self._temp_line.remove()
|
|
1028
|
+
except Exception:
|
|
1029
|
+
pass
|
|
1030
|
+
self._temp_line = None
|
|
1031
|
+
x0, y0 = self._draw_coords[0]
|
|
1032
|
+
x1, y1 = event.xdata, event.ydata
|
|
1033
|
+
from matplotlib.patches import Rectangle
|
|
1034
|
+
preview = Rectangle((min(x0, x1), min(y0, y1)),
|
|
1035
|
+
abs(x1 - x0), abs(y1 - y0),
|
|
1036
|
+
edgecolor='cyan', facecolor='none',
|
|
1037
|
+
linewidth=1, linestyle='', alpha=0.5)
|
|
1038
|
+
event.inaxes.add_patch(preview)
|
|
1039
|
+
self._temp_line = preview
|
|
1040
|
+
self._canvas_mpl.draw_idle()
|
|
1041
|
+
elif mode in ('polygon', 'freehand'):
|
|
1042
|
+
self._draw_coords.append([event.xdata, event.ydata])
|
|
1043
|
+
def _on_draw_release(self, event):
|
|
1044
|
+
if not self._is_drawing or not event.inaxes or event.inaxes not in self._active_image_axes():
|
|
1045
|
+
return
|
|
1046
|
+
mode = self._drawing_mode.get()
|
|
1047
|
+
if mode in ('rect', 'ellipse'):
|
|
1048
|
+
if len(self._draw_coords) > 0:
|
|
1049
|
+
self._draw_coords.append([event.xdata, event.ydata])
|
|
1050
|
+
self._finalize_drawing(mode)
|
|
1051
|
+
elif mode == 'polygon':
|
|
1052
|
+
if len(self._draw_coords) >= 3 and event.button == 3:
|
|
1053
|
+
self._finalize_drawing(mode)
|
|
1054
|
+
elif mode == 'freehand':
|
|
1055
|
+
if len(self._draw_coords) >= 3:
|
|
1056
|
+
self._finalize_drawing(mode)
|
|
1057
|
+
if self._temp_line is not None:
|
|
1058
|
+
try:
|
|
1059
|
+
self._temp_line.remove()
|
|
1060
|
+
except Exception:
|
|
1061
|
+
pass
|
|
1062
|
+
self._temp_line = None
|
|
1063
|
+
self._is_drawing = False
|
|
1064
|
+
def _finalize_drawing(self, tool_type: str):
|
|
1065
|
+
if len(self._draw_coords) < 2:
|
|
1066
|
+
print(f"[Drawing] Cancelled {tool_type} (insufficient points)")
|
|
1067
|
+
self._draw_coords = []
|
|
1068
|
+
return
|
|
1069
|
+
try:
|
|
1070
|
+
region_id = self._roi_manager.add_region(
|
|
1071
|
+
f"{tool_type}-{len(self._roi_manager.regions) + 1}",
|
|
1072
|
+
tool_type,
|
|
1073
|
+
self._draw_coords
|
|
1074
|
+
)
|
|
1075
|
+
self._redraw_region_overlays()
|
|
1076
|
+
self._save_regions_update()
|
|
1077
|
+
print(f"[Drawing] Added {tool_type} region {region_id}")
|
|
1078
|
+
if self._roi_analysis_panel:
|
|
1079
|
+
self._roi_analysis_panel._refresh_region_list()
|
|
1080
|
+
except Exception as e:
|
|
1081
|
+
print(f"[Drawing] Error finalizing: {e}")
|
|
1082
|
+
finally:
|
|
1083
|
+
self._draw_coords = []
|
|
1084
|
+
def grid(self, **kw):
|
|
1085
|
+
self.frame.grid(**kw)
|