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,406 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
API_VERSION = 1
|
|
4
|
+
|
|
5
|
+
_lock = threading.RLock()
|
|
6
|
+
_tools = {}
|
|
7
|
+
_formats = {}
|
|
8
|
+
_sniffers = []
|
|
9
|
+
_phasor_filters = {}
|
|
10
|
+
_panel_buttons = {}
|
|
11
|
+
_startups = {}
|
|
12
|
+
_version = 0
|
|
13
|
+
_current_source = None
|
|
14
|
+
MODALITIES = ('time', 'frequency', 'intensity')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PluginError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Tool:
|
|
22
|
+
|
|
23
|
+
def __init__(self, id, label, callback, menu, order, source):
|
|
24
|
+
self.id = id
|
|
25
|
+
self.label = label
|
|
26
|
+
self.callback = callback
|
|
27
|
+
self.menu = menu
|
|
28
|
+
self.order = order
|
|
29
|
+
self.source = source
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def menu_path(self):
|
|
33
|
+
return tuple(p.strip() for p in self.menu.split('/') if p.strip())
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f'<Tool {self.id} menu={self.menu!r} source={self.source!r}>'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PanelButton:
|
|
40
|
+
|
|
41
|
+
def __init__(self, id, label, callback, panel, order, source):
|
|
42
|
+
self.id = id
|
|
43
|
+
self.label = label
|
|
44
|
+
self.callback = callback
|
|
45
|
+
self.panel = panel
|
|
46
|
+
self.order = order
|
|
47
|
+
self.source = source
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return f'<PanelButton {self.id} panel={self.panel!r} source={self.source!r}>'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Startup:
|
|
54
|
+
|
|
55
|
+
def __init__(self, id, callback, order, source):
|
|
56
|
+
self.id = id
|
|
57
|
+
self.callback = callback
|
|
58
|
+
self.order = order
|
|
59
|
+
self.source = source
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f'<Startup {self.id} source={self.source!r}>'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Format:
|
|
66
|
+
|
|
67
|
+
def __init__(self, id, label, exts, modality, reader, source):
|
|
68
|
+
self.id = id
|
|
69
|
+
self.label = label
|
|
70
|
+
self.exts = tuple(e.lower() for e in exts)
|
|
71
|
+
self.modality = modality
|
|
72
|
+
self._reader = reader
|
|
73
|
+
self.source = source
|
|
74
|
+
|
|
75
|
+
def reader(self):
|
|
76
|
+
if isinstance(self._reader, str):
|
|
77
|
+
import importlib
|
|
78
|
+
mod_name, _, cls_name = self._reader.partition(':')
|
|
79
|
+
if not cls_name:
|
|
80
|
+
raise PluginError(
|
|
81
|
+
f'format {self.id!r} reader {self._reader!r} is not module:Class')
|
|
82
|
+
return getattr(importlib.import_module(mod_name), cls_name)
|
|
83
|
+
return self._reader
|
|
84
|
+
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return f'<Format {self.id} exts={self.exts} source={self.source!r}>'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Sniffer:
|
|
90
|
+
|
|
91
|
+
def __init__(self, fn, tier, order, source):
|
|
92
|
+
self.fn = fn
|
|
93
|
+
self.tier = tier
|
|
94
|
+
self.order = order
|
|
95
|
+
self.source = source
|
|
96
|
+
|
|
97
|
+
def __call__(self, path):
|
|
98
|
+
return self.fn(path)
|
|
99
|
+
|
|
100
|
+
def __repr__(self):
|
|
101
|
+
return f'<Sniffer tier={self.tier} order={self.order} source={self.source!r}>'
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _source():
|
|
105
|
+
return _current_source or '<direct>'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _bump():
|
|
109
|
+
global _version
|
|
110
|
+
_version += 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def version():
|
|
114
|
+
return _version
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def count():
|
|
118
|
+
with _lock:
|
|
119
|
+
return (len(_tools) + len(_formats) + len(_sniffers)
|
|
120
|
+
+ len(_phasor_filters) + len(_panel_buttons) + len(_startups))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def register_tool(id, label, callback, menu='Tools', order=100, source=None):
|
|
124
|
+
if not id or not isinstance(id, str):
|
|
125
|
+
raise PluginError(f'tool id must be a non-empty string, got {id!r}')
|
|
126
|
+
if not callable(callback):
|
|
127
|
+
raise PluginError(f'tool {id!r} callback is not callable: {callback!r}')
|
|
128
|
+
with _lock:
|
|
129
|
+
existing = _tools.get(id)
|
|
130
|
+
if existing is not None:
|
|
131
|
+
raise PluginError(
|
|
132
|
+
f'tool id {id!r} already registered by {existing.source!r}, '
|
|
133
|
+
f'refused from {source or _source()!r}')
|
|
134
|
+
_tools[id] = Tool(id, label, callback, menu, order, source or _source())
|
|
135
|
+
_bump()
|
|
136
|
+
return _tools[id]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def tool(id, label, menu='Tools', order=100):
|
|
140
|
+
def decorate(fn):
|
|
141
|
+
register_tool(id, label, fn, menu=menu, order=order)
|
|
142
|
+
return fn
|
|
143
|
+
return decorate
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def tools(menu=None):
|
|
147
|
+
with _lock:
|
|
148
|
+
found = list(_tools.values())
|
|
149
|
+
if menu is not None:
|
|
150
|
+
want = tuple(p.strip() for p in menu.split('/') if p.strip())
|
|
151
|
+
found = [t for t in found if t.menu_path[:len(want)] == want]
|
|
152
|
+
found.sort(key=lambda t: (t.order, t.label, t.id))
|
|
153
|
+
return found
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_tool(id):
|
|
157
|
+
with _lock:
|
|
158
|
+
return _tools.get(id)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
PANELS = ('roi',)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def register_panel_button(id, label, callback, panel='roi', order=100, source=None):
|
|
165
|
+
if not id or not isinstance(id, str):
|
|
166
|
+
raise PluginError(f'panel button id must be a non-empty string, got {id!r}')
|
|
167
|
+
if not callable(callback):
|
|
168
|
+
raise PluginError(f'panel button {id!r} callback is not callable: {callback!r}')
|
|
169
|
+
if panel not in PANELS:
|
|
170
|
+
raise PluginError(f'unknown panel {panel!r}, expected one of {PANELS}')
|
|
171
|
+
with _lock:
|
|
172
|
+
existing = _panel_buttons.get(id)
|
|
173
|
+
if existing is not None:
|
|
174
|
+
raise PluginError(
|
|
175
|
+
f'panel button id {id!r} already registered by {existing.source!r}, '
|
|
176
|
+
f'refused from {source or _source()!r}')
|
|
177
|
+
_panel_buttons[id] = PanelButton(
|
|
178
|
+
id, label, callback, panel, order, source or _source())
|
|
179
|
+
_bump()
|
|
180
|
+
return _panel_buttons[id]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def panel_button(id, label, panel='roi', order=100):
|
|
184
|
+
def decorate(fn):
|
|
185
|
+
register_panel_button(id, label, fn, panel=panel, order=order)
|
|
186
|
+
return fn
|
|
187
|
+
return decorate
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def panel_buttons(panel=None):
|
|
191
|
+
with _lock:
|
|
192
|
+
found = list(_panel_buttons.values())
|
|
193
|
+
if panel is not None:
|
|
194
|
+
found = [b for b in found if b.panel == panel]
|
|
195
|
+
found.sort(key=lambda b: (b.order, b.label, b.id))
|
|
196
|
+
return found
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_panel_button(id):
|
|
200
|
+
with _lock:
|
|
201
|
+
return _panel_buttons.get(id)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def register_startup(id, callback, order=100, source=None):
|
|
205
|
+
if not id or not isinstance(id, str):
|
|
206
|
+
raise PluginError(f'startup id must be a non-empty string, got {id!r}')
|
|
207
|
+
if not callable(callback):
|
|
208
|
+
raise PluginError(f'startup {id!r} callback is not callable: {callback!r}')
|
|
209
|
+
with _lock:
|
|
210
|
+
existing = _startups.get(id)
|
|
211
|
+
if existing is not None:
|
|
212
|
+
raise PluginError(
|
|
213
|
+
f'startup id {id!r} already registered by {existing.source!r}, '
|
|
214
|
+
f'refused from {source or _source()!r}')
|
|
215
|
+
_startups[id] = Startup(id, callback, order, source or _source())
|
|
216
|
+
_bump()
|
|
217
|
+
return _startups[id]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def startup(id, order=100):
|
|
221
|
+
def decorate(fn):
|
|
222
|
+
register_startup(id, fn, order=order)
|
|
223
|
+
return fn
|
|
224
|
+
return decorate
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def startups():
|
|
228
|
+
with _lock:
|
|
229
|
+
found = list(_startups.values())
|
|
230
|
+
found.sort(key=lambda s: (s.order, s.id))
|
|
231
|
+
return found
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def run_startups(app):
|
|
235
|
+
failures = []
|
|
236
|
+
for entry in startups():
|
|
237
|
+
try:
|
|
238
|
+
entry.callback(app)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
failures.append((entry, exc))
|
|
241
|
+
return failures
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def register_format(id, label, exts=(), modality='time', reader=None, source=None):
|
|
245
|
+
if not id or not isinstance(id, str):
|
|
246
|
+
raise PluginError(f'format id must be a non-empty string, got {id!r}')
|
|
247
|
+
if modality not in MODALITIES:
|
|
248
|
+
raise PluginError(
|
|
249
|
+
f'format {id!r} modality {modality!r} is not one of {MODALITIES}')
|
|
250
|
+
if reader is None:
|
|
251
|
+
raise PluginError(f'format {id!r} has no reader')
|
|
252
|
+
if isinstance(exts, str):
|
|
253
|
+
exts = (exts,)
|
|
254
|
+
with _lock:
|
|
255
|
+
existing = _formats.get(id)
|
|
256
|
+
if existing is not None:
|
|
257
|
+
raise PluginError(
|
|
258
|
+
f'format id {id!r} already registered by {existing.source!r}, '
|
|
259
|
+
f'refused from {source or _source()!r}')
|
|
260
|
+
_formats[id] = Format(id, label, exts, modality, reader, source or _source())
|
|
261
|
+
_bump()
|
|
262
|
+
return _formats[id]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def file_format(id, label, exts=(), modality='time', reader=None):
|
|
266
|
+
def decorate(obj):
|
|
267
|
+
register_format(id, label, exts=exts, modality=modality,
|
|
268
|
+
reader=reader if reader is not None else obj)
|
|
269
|
+
return obj
|
|
270
|
+
return decorate
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def formats():
|
|
274
|
+
with _lock:
|
|
275
|
+
return sorted(_formats.values(), key=lambda f: f.id)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_format(id):
|
|
279
|
+
with _lock:
|
|
280
|
+
return _formats.get(id)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def register_sniffer(fn, tier='magic', order=100, source=None):
|
|
284
|
+
if not callable(fn):
|
|
285
|
+
raise PluginError(f'sniffer is not callable: {fn!r}')
|
|
286
|
+
if tier not in ('extension', 'magic'):
|
|
287
|
+
raise PluginError(f'sniffer tier {tier!r} is not extension or magic')
|
|
288
|
+
with _lock:
|
|
289
|
+
found = Sniffer(fn, tier, order, source or _source())
|
|
290
|
+
_sniffers.append(found)
|
|
291
|
+
_bump()
|
|
292
|
+
return found
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def format_sniffer(tier='magic', order=100):
|
|
296
|
+
def decorate(fn):
|
|
297
|
+
register_sniffer(fn, tier=tier, order=order)
|
|
298
|
+
return fn
|
|
299
|
+
return decorate
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def sniffers(tier=None):
|
|
303
|
+
with _lock:
|
|
304
|
+
found = list(_sniffers)
|
|
305
|
+
if tier is not None:
|
|
306
|
+
found = [s for s in found if s.tier == tier]
|
|
307
|
+
found.sort(key=lambda s: s.order)
|
|
308
|
+
return found
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class PhasorFilter:
|
|
312
|
+
|
|
313
|
+
def __init__(self, id, label, fn, source):
|
|
314
|
+
self.id = id
|
|
315
|
+
self.label = label
|
|
316
|
+
self.fn = fn
|
|
317
|
+
self.source = source
|
|
318
|
+
|
|
319
|
+
def __call__(self, real, imag, **kwargs):
|
|
320
|
+
import inspect
|
|
321
|
+
params = inspect.signature(self.fn).parameters
|
|
322
|
+
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()):
|
|
323
|
+
allowed = kwargs
|
|
324
|
+
else:
|
|
325
|
+
allowed = {k: v for k, v in kwargs.items() if k in params}
|
|
326
|
+
return self.fn(real, imag, **allowed)
|
|
327
|
+
|
|
328
|
+
def __repr__(self):
|
|
329
|
+
return f'<PhasorFilter {self.id} source={self.source!r}>'
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def register_phasor_filter(id, label, fn, source=None):
|
|
333
|
+
if not id or not isinstance(id, str):
|
|
334
|
+
raise PluginError(f'phasor filter id must be a non-empty string, got {id!r}')
|
|
335
|
+
if not callable(fn):
|
|
336
|
+
raise PluginError(f'phasor filter {id!r} is not callable: {fn!r}')
|
|
337
|
+
id = id.lower()
|
|
338
|
+
with _lock:
|
|
339
|
+
existing = _phasor_filters.get(id)
|
|
340
|
+
if existing is not None:
|
|
341
|
+
raise PluginError(
|
|
342
|
+
f'phasor filter id {id!r} already registered by {existing.source!r}, '
|
|
343
|
+
f'refused from {source or _source()!r}')
|
|
344
|
+
_phasor_filters[id] = PhasorFilter(id, label, fn, source or _source())
|
|
345
|
+
_bump()
|
|
346
|
+
return _phasor_filters[id]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def phasor_filter(id, label):
|
|
350
|
+
def decorate(fn):
|
|
351
|
+
register_phasor_filter(id, label, fn)
|
|
352
|
+
return fn
|
|
353
|
+
return decorate
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def phasor_filters():
|
|
357
|
+
with _lock:
|
|
358
|
+
return sorted(_phasor_filters.values(), key=lambda f: f.id)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_phasor_filter(id):
|
|
362
|
+
with _lock:
|
|
363
|
+
return _phasor_filters.get(str(id).lower())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def sources():
|
|
367
|
+
with _lock:
|
|
368
|
+
every = (list(_tools.values()) + list(_formats.values()) + list(_sniffers)
|
|
369
|
+
+ list(_phasor_filters.values()))
|
|
370
|
+
return sorted({t.source for t in every})
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _set_source(name):
|
|
374
|
+
global _current_source
|
|
375
|
+
prev = _current_source
|
|
376
|
+
_current_source = name
|
|
377
|
+
return prev
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _rollback(source):
|
|
381
|
+
with _lock:
|
|
382
|
+
dropped = [k for k, t in _tools.items() if t.source == source]
|
|
383
|
+
for k in dropped:
|
|
384
|
+
del _tools[k]
|
|
385
|
+
dropped += [k for k, f in _formats.items() if f.source == source]
|
|
386
|
+
for k in [k for k, f in _formats.items() if f.source == source]:
|
|
387
|
+
del _formats[k]
|
|
388
|
+
keep = [s for s in _sniffers if s.source != source]
|
|
389
|
+
dropped += ['sniffer'] * (len(_sniffers) - len(keep))
|
|
390
|
+
_sniffers[:] = keep
|
|
391
|
+
dropped += [k for k, f in _phasor_filters.items() if f.source == source]
|
|
392
|
+
for k in [k for k, f in _phasor_filters.items() if f.source == source]:
|
|
393
|
+
del _phasor_filters[k]
|
|
394
|
+
_bump()
|
|
395
|
+
return dropped
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def clear():
|
|
399
|
+
with _lock:
|
|
400
|
+
_tools.clear()
|
|
401
|
+
_formats.clear()
|
|
402
|
+
_sniffers.clear()
|
|
403
|
+
_phasor_filters.clear()
|
|
404
|
+
_panel_buttons.clear()
|
|
405
|
+
_startups.clear()
|
|
406
|
+
_bump()
|
flimkit/project.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass, asdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_SPTW_CANDIDATES = [
|
|
6
|
+
'{stem}.sptw',
|
|
7
|
+
'PTU.sptw',
|
|
8
|
+
'FLIM.sptw',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
PROJECT_FILENAME = 'project.json'
|
|
12
|
+
DEFAULT_OUTPUT_SUBDIR = 'output'
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ScanRecord:
|
|
16
|
+
stem: str
|
|
17
|
+
scan_type: str
|
|
18
|
+
source_path: str
|
|
19
|
+
ptu_dir: str | None = None
|
|
20
|
+
out_st: str | None = None
|
|
21
|
+
output_prefix: str | None = None
|
|
22
|
+
xlsx_path: str | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def roi_clean(self):
|
|
26
|
+
return self.stem.replace(' ', '_')
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def session_path(self):
|
|
30
|
+
if self.scan_type == 'fov':
|
|
31
|
+
p = Path(self.source_path)
|
|
32
|
+
candidate = p.parent / f'{p.stem}.roi_session.npz'
|
|
33
|
+
elif self.scan_type == 'zstack':
|
|
34
|
+
if not self.out_st:
|
|
35
|
+
return None
|
|
36
|
+
candidate = Path(self.out_st) / self.stem / f'{self.stem}_reference_fit.json'
|
|
37
|
+
else:
|
|
38
|
+
if not self.out_st:
|
|
39
|
+
return None
|
|
40
|
+
candidate = Path(self.out_st) / self.roi_clean / 'roi_session.npz'
|
|
41
|
+
return candidate if candidate.exists() else None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def phasor_session_path(self):
|
|
45
|
+
if self.scan_type != 'fov':
|
|
46
|
+
return None
|
|
47
|
+
p = Path(self.source_path)
|
|
48
|
+
candidate = p.parent / f'{p.stem}_phasor.npz'
|
|
49
|
+
return candidate if candidate.exists() else None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def has_session(self):
|
|
53
|
+
return self.session_path is not None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def has_phasor_session(self):
|
|
57
|
+
return self.phasor_session_path is not None
|
|
58
|
+
|
|
59
|
+
class ProjectFile:
|
|
60
|
+
|
|
61
|
+
def __init__(self, project_dir):
|
|
62
|
+
self.project_dir = Path(project_dir).resolve()
|
|
63
|
+
self.output_base = self.project_dir / DEFAULT_OUTPUT_SUBDIR
|
|
64
|
+
self.scans = {}
|
|
65
|
+
self.config = {}
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def load_or_create(cls, project_dir):
|
|
69
|
+
pf = cls(project_dir)
|
|
70
|
+
json_path = pf.project_dir / PROJECT_FILENAME
|
|
71
|
+
if json_path.exists():
|
|
72
|
+
try:
|
|
73
|
+
with open(json_path, encoding='utf-8') as fh:
|
|
74
|
+
data = json.load(fh)
|
|
75
|
+
ob = data.get('output_base')
|
|
76
|
+
if ob:
|
|
77
|
+
pf.output_base = Path(ob)
|
|
78
|
+
for stem, rec_dict in data.get('scans', {}).items():
|
|
79
|
+
pf.scans[stem] = ScanRecord(**rec_dict)
|
|
80
|
+
pf.config = data.get('config', {})
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
print(f'[Project] Warning: could not read {json_path.name}: {exc}')
|
|
83
|
+
pf._scan_folder()
|
|
84
|
+
return pf
|
|
85
|
+
|
|
86
|
+
def save(self):
|
|
87
|
+
self.project_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
payload = {
|
|
89
|
+
'output_base': str(self.output_base),
|
|
90
|
+
'scans': {stem: asdict(rec) for stem, rec in self.scans.items()},
|
|
91
|
+
}
|
|
92
|
+
if self.config:
|
|
93
|
+
payload['config'] = self.config
|
|
94
|
+
with open(self.project_dir / PROJECT_FILENAME, 'w', encoding='utf-8') as fh:
|
|
95
|
+
json.dump(payload, fh, indent=2, ensure_ascii=False)
|
|
96
|
+
|
|
97
|
+
def _scan_folder(self):
|
|
98
|
+
from flimkit.formats import supported_extensions
|
|
99
|
+
zstack_groups = {}
|
|
100
|
+
zstack_slice_paths = set()
|
|
101
|
+
try:
|
|
102
|
+
from flimkit.utils.batch_fit import group_zstack_files
|
|
103
|
+
for (region, t, s), zslices in group_zstack_files(self.project_dir).items():
|
|
104
|
+
if t == 0 and s == 0 and len(zslices) >= 2:
|
|
105
|
+
zstack_groups[region] = zslices
|
|
106
|
+
zstack_slice_paths.update(str(p) for p in zslices.values())
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
_globs = tuple('*' + e for e in supported_extensions())
|
|
110
|
+
flim_files = sorted(p for ext in _globs
|
|
111
|
+
for p in self.project_dir.glob(ext))
|
|
112
|
+
for ptu in flim_files:
|
|
113
|
+
if ptu.name.startswith('._'):
|
|
114
|
+
continue
|
|
115
|
+
if str(ptu) in zstack_slice_paths:
|
|
116
|
+
continue
|
|
117
|
+
if ptu.stem not in self.scans:
|
|
118
|
+
xlsx_file = self.project_dir / f'{ptu.stem}.xlsx'
|
|
119
|
+
xlsx_path = str(xlsx_file) if xlsx_file.exists() and not xlsx_file.name.startswith('._') else None
|
|
120
|
+
self.scans[ptu.stem] = ScanRecord(
|
|
121
|
+
stem=ptu.stem,
|
|
122
|
+
scan_type='fov',
|
|
123
|
+
source_path=str(ptu),
|
|
124
|
+
ptu_dir=None,
|
|
125
|
+
out_st=None,
|
|
126
|
+
output_prefix=None,
|
|
127
|
+
xlsx_path=xlsx_path,
|
|
128
|
+
)
|
|
129
|
+
for region in zstack_groups:
|
|
130
|
+
if region not in self.scans:
|
|
131
|
+
self.scans[region] = ScanRecord(
|
|
132
|
+
stem=region,
|
|
133
|
+
scan_type='zstack',
|
|
134
|
+
source_path=str(self.project_dir),
|
|
135
|
+
ptu_dir=str(self.project_dir),
|
|
136
|
+
out_st=str(self.output_base),
|
|
137
|
+
output_prefix=None,
|
|
138
|
+
xlsx_path=None,
|
|
139
|
+
)
|
|
140
|
+
for xlif in sorted(self.project_dir.glob('*.xlif')):
|
|
141
|
+
if xlif.name.startswith('._'):
|
|
142
|
+
continue
|
|
143
|
+
if xlif.stem not in self.scans:
|
|
144
|
+
ptu_dir = self._find_sptw(xlif)
|
|
145
|
+
self.scans[xlif.stem] = ScanRecord(
|
|
146
|
+
stem=xlif.stem,
|
|
147
|
+
scan_type='xlif',
|
|
148
|
+
source_path=str(xlif),
|
|
149
|
+
ptu_dir=str(ptu_dir) if ptu_dir else None,
|
|
150
|
+
out_st=str(self.output_base),
|
|
151
|
+
output_prefix=None,
|
|
152
|
+
xlsx_path=None,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _find_sptw(self, xlif):
|
|
156
|
+
base = xlif.parent
|
|
157
|
+
for template in _SPTW_CANDIDATES:
|
|
158
|
+
candidate = base / template.format(stem=xlif.stem)
|
|
159
|
+
if candidate.is_dir():
|
|
160
|
+
return candidate
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def update_after_fit(
|
|
164
|
+
self,
|
|
165
|
+
stem,
|
|
166
|
+
*,
|
|
167
|
+
out_st=None,
|
|
168
|
+
output_prefix=None,
|
|
169
|
+
ptu_dir=None,
|
|
170
|
+
):
|
|
171
|
+
rec = self.scans.get(stem)
|
|
172
|
+
if rec is None:
|
|
173
|
+
return
|
|
174
|
+
if out_st is not None:
|
|
175
|
+
rec.out_st = out_st
|
|
176
|
+
if output_prefix is not None:
|
|
177
|
+
rec.output_prefix = output_prefix
|
|
178
|
+
if ptu_dir is not None:
|
|
179
|
+
rec.ptu_dir = ptu_dir
|
|
180
|
+
|
|
181
|
+
def update_after_phasor(self, stem: str):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def default_out_st(self, stem):
|
|
185
|
+
rec = self.scans.get(stem)
|
|
186
|
+
if rec and rec.out_st:
|
|
187
|
+
return rec.out_st
|
|
188
|
+
return str(self.output_base)
|
|
189
|
+
|
|
190
|
+
def default_output_prefix(self, stem):
|
|
191
|
+
rec = self.scans.get(stem)
|
|
192
|
+
if rec and rec.output_prefix:
|
|
193
|
+
return rec.output_prefix
|
|
194
|
+
return stem
|
|
195
|
+
|
|
196
|
+
def sorted_scans(self):
|
|
197
|
+
yield from sorted(self.scans.items())
|