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,183 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
_LOG_DIR = os.path.join(os.path.expanduser('~'), '.flimkit', 'logs')
|
|
10
|
+
_session_log_path: str | None = None
|
|
11
|
+
_logger: logging.Logger | None = None
|
|
12
|
+
_original_excepthook = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_system_info() -> str:
|
|
16
|
+
try:
|
|
17
|
+
from flimkit._version import __version__
|
|
18
|
+
except Exception:
|
|
19
|
+
__version__ = 'unknown'
|
|
20
|
+
|
|
21
|
+
lines = [
|
|
22
|
+
f"FLIMKit version : {__version__}",
|
|
23
|
+
f"Python : {sys.version}",
|
|
24
|
+
f"Platform : {platform.platform()}",
|
|
25
|
+
f"OS : {platform.system()} {platform.release()}",
|
|
26
|
+
f"Machine : {platform.machine()}",
|
|
27
|
+
f"Processor : {platform.processor() or 'N/A'}",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# NumPy / Matplotlib versions if available
|
|
31
|
+
for pkg_name in ('numpy', 'matplotlib', 'scipy'):
|
|
32
|
+
try:
|
|
33
|
+
mod = __import__(pkg_name)
|
|
34
|
+
lines.append(f"{pkg_name:16s}: {mod.__version__}")
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return '\n'.join(lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _crash_excepthook(exc_type, exc_value, exc_tb):
|
|
42
|
+
if _logger is not None:
|
|
43
|
+
tb_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
44
|
+
_logger.critical(
|
|
45
|
+
'UNCAUGHT EXCEPTION (main thread)\n%s\n%s',
|
|
46
|
+
tb_text,
|
|
47
|
+
_get_system_info(),
|
|
48
|
+
)
|
|
49
|
+
# Force flush so log is written even if the process is killed
|
|
50
|
+
for handler in _logger.handlers:
|
|
51
|
+
handler.flush()
|
|
52
|
+
|
|
53
|
+
# Call the original hook so the traceback still prints to stderr
|
|
54
|
+
if _original_excepthook is not None:
|
|
55
|
+
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _thread_excepthook(args):
|
|
59
|
+
if _logger is not None:
|
|
60
|
+
tb_text = ''.join(
|
|
61
|
+
traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)
|
|
62
|
+
)
|
|
63
|
+
thread_name = args.thread.name if args.thread else 'unknown'
|
|
64
|
+
_logger.critical(
|
|
65
|
+
'UNCAUGHT EXCEPTION (thread: %s)\n%s',
|
|
66
|
+
thread_name,
|
|
67
|
+
tb_text,
|
|
68
|
+
)
|
|
69
|
+
for handler in _logger.handlers:
|
|
70
|
+
handler.flush()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def install_tk_error_handler(root):
|
|
74
|
+
def _tk_error_handler(exc_type, exc_value, exc_tb):
|
|
75
|
+
if _logger is not None:
|
|
76
|
+
tb_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
77
|
+
_logger.error('UNCAUGHT EXCEPTION (Tk callback)\n%s', tb_text)
|
|
78
|
+
for handler in _logger.handlers:
|
|
79
|
+
handler.flush()
|
|
80
|
+
# Still print to stderr so the user sees it
|
|
81
|
+
sys.__stderr__.write(''.join(traceback.format_exception(exc_type, exc_value, exc_tb)))
|
|
82
|
+
|
|
83
|
+
root.report_callback_exception = _tk_error_handler
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def init_crash_handler() -> str:
|
|
87
|
+
global _session_log_path, _logger, _original_excepthook
|
|
88
|
+
|
|
89
|
+
os.makedirs(_LOG_DIR, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# Create a timestamped log file for this session
|
|
92
|
+
timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
|
93
|
+
_session_log_path = os.path.join(_LOG_DIR, f"session_{timestamp}.log")
|
|
94
|
+
|
|
95
|
+
# Configure the logger
|
|
96
|
+
_logger = logging.getLogger('flimkit.crash')
|
|
97
|
+
_logger.setLevel(logging.DEBUG)
|
|
98
|
+
_logger.handlers.clear()
|
|
99
|
+
|
|
100
|
+
fh = logging.FileHandler(_session_log_path, encoding='utf-8')
|
|
101
|
+
fh.setLevel(logging.DEBUG)
|
|
102
|
+
fh.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
|
|
103
|
+
_logger.addHandler(fh)
|
|
104
|
+
|
|
105
|
+
# Write session header
|
|
106
|
+
_logger.info('SESSION START')
|
|
107
|
+
_logger.info('System info:\n%s', _get_system_info())
|
|
108
|
+
|
|
109
|
+
# Install the global exception hook
|
|
110
|
+
_original_excepthook = sys.excepthook
|
|
111
|
+
sys.excepthook = _crash_excepthook
|
|
112
|
+
|
|
113
|
+
# Install the threading exception hook (catches errors in background threads)
|
|
114
|
+
import threading
|
|
115
|
+
threading.excepthook = _thread_excepthook
|
|
116
|
+
|
|
117
|
+
# Log a clean shutdown if we exit normally
|
|
118
|
+
atexit.register(lambda: _logger.info('SESSION END (clean exit)') if _logger else None)
|
|
119
|
+
|
|
120
|
+
return _session_log_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def log_event(message: str, level: str = 'info'):
|
|
124
|
+
if _logger is None:
|
|
125
|
+
return
|
|
126
|
+
getattr(_logger, level, _logger.info)(message)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def log_exception(context: str = ''):
|
|
130
|
+
if _logger is None:
|
|
131
|
+
return
|
|
132
|
+
_logger.error(
|
|
133
|
+
'CAUGHT EXCEPTION%s\n%s',
|
|
134
|
+
f" ({context})" if context else '',
|
|
135
|
+
traceback.format_exc(),
|
|
136
|
+
)
|
|
137
|
+
for handler in _logger.handlers:
|
|
138
|
+
handler.flush()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_log_dir() -> str:
|
|
142
|
+
return _LOG_DIR
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_session_log_path() -> str | None:
|
|
146
|
+
return _session_log_path
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_export_report(include_all_sessions: bool = False) -> str:
|
|
150
|
+
import glob
|
|
151
|
+
|
|
152
|
+
sections = []
|
|
153
|
+
sections.append('=' * 60)
|
|
154
|
+
sections.append('FLIMKit Error Report')
|
|
155
|
+
sections.append(f"Generated: {datetime.datetime.now().isoformat()}")
|
|
156
|
+
sections.append('=' * 60)
|
|
157
|
+
sections.append('')
|
|
158
|
+
sections.append('SYSTEM INFO')
|
|
159
|
+
sections.append('-' * 40)
|
|
160
|
+
sections.append(_get_system_info())
|
|
161
|
+
sections.append('')
|
|
162
|
+
|
|
163
|
+
log_files = sorted(
|
|
164
|
+
glob.glob(os.path.join(_LOG_DIR, '*.log')),
|
|
165
|
+
key=os.path.getmtime,
|
|
166
|
+
reverse=True,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if not include_all_sessions and _session_log_path:
|
|
170
|
+
log_files = [f for f in log_files if f == _session_log_path]
|
|
171
|
+
|
|
172
|
+
for log_file in log_files:
|
|
173
|
+
sections.append('=' * 60)
|
|
174
|
+
sections.append(f"LOG: {os.path.basename(log_file)}")
|
|
175
|
+
sections.append('=' * 60)
|
|
176
|
+
try:
|
|
177
|
+
with open(log_file, 'r', encoding='utf-8') as f:
|
|
178
|
+
sections.append(f.read())
|
|
179
|
+
except Exception as e:
|
|
180
|
+
sections.append(f"(could not read: {e})")
|
|
181
|
+
sections.append('')
|
|
182
|
+
|
|
183
|
+
return '\n'.join(sections)
|
flimkit/utils/display.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from matplotlib.colors import Normalize, PowerNorm
|
|
6
|
+
|
|
7
|
+
def load_zstack_display_slices(group_dir, ptu_dir=None, region=None):
|
|
8
|
+
from flimkit.utils.batch_fit import group_zstack_files
|
|
9
|
+
group_dir = Path(group_dir)
|
|
10
|
+
ref = {}
|
|
11
|
+
ref_files = list(group_dir.glob('*_reference_fit.json'))
|
|
12
|
+
if ref_files:
|
|
13
|
+
try:
|
|
14
|
+
ref = json.loads(ref_files[0].read_text())
|
|
15
|
+
except Exception:
|
|
16
|
+
ref = {}
|
|
17
|
+
taus_ns = ref.get('taus_ns', [])
|
|
18
|
+
nexp = ref.get('nexp', len(taus_ns))
|
|
19
|
+
ref_decay = ref_time = ref_model = ref_irf = None
|
|
20
|
+
ref_chi2 = None
|
|
21
|
+
ref_calibrated = ref.get('calibrated_chi2_pearson')
|
|
22
|
+
ref_calibrated_tail = ref.get('calibrated_chi2_tail_pearson')
|
|
23
|
+
npz_path = group_dir / 'reference_decay.npz'
|
|
24
|
+
if npz_path.exists():
|
|
25
|
+
try:
|
|
26
|
+
with np.load(str(npz_path)) as zf:
|
|
27
|
+
ref_decay = zf['decay']
|
|
28
|
+
ref_time = zf['time_ns']
|
|
29
|
+
ref_model = zf['model'] if zf['model'].size > 0 else None
|
|
30
|
+
ref_irf = zf['irf_prompt'] if zf['irf_prompt'].size > 0 else None
|
|
31
|
+
ref_chi2 = float(zf['reduced_chi2_tail'][0]) if zf['reduced_chi2_tail'].size > 0 else None
|
|
32
|
+
if ref_calibrated is None and 'calibrated_chi2_pearson' in zf:
|
|
33
|
+
ref_calibrated = float(zf['calibrated_chi2_pearson'][0])
|
|
34
|
+
if ref_calibrated_tail is None and 'calibrated_chi2_tail_pearson' in zf:
|
|
35
|
+
ref_calibrated_tail = float(zf['calibrated_chi2_tail_pearson'][0])
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
z_to_ptu = {}
|
|
39
|
+
if ptu_dir is not None and Path(ptu_dir).is_dir():
|
|
40
|
+
groups = group_zstack_files(ptu_dir)
|
|
41
|
+
for (r, _t, _s), zsl in groups.items():
|
|
42
|
+
if region is None or r == region:
|
|
43
|
+
z_to_ptu = {z: str(p) for z, p in zsl.items()}
|
|
44
|
+
break
|
|
45
|
+
slices = []
|
|
46
|
+
for slice_dir in sorted(group_dir.glob('z[0-9]*')):
|
|
47
|
+
if not slice_dir.is_dir():
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
z = int(slice_dir.name[1:])
|
|
51
|
+
except ValueError:
|
|
52
|
+
continue
|
|
53
|
+
def _load(name):
|
|
54
|
+
p = slice_dir / f'{name}.npy'
|
|
55
|
+
return np.load(str(p)) if p.exists() else None
|
|
56
|
+
pixel_maps = {}
|
|
57
|
+
for k in ('tau_mean_int', 'tau_mean_amp', 'alpha_1', 'alpha_2', 'alpha_3',
|
|
58
|
+
'chi2_r', 'calibrated_chi2_r'):
|
|
59
|
+
m = _load(k)
|
|
60
|
+
if m is not None:
|
|
61
|
+
pixel_maps[k] = m
|
|
62
|
+
global_summary = {'taus_ns': list(taus_ns), 'n_exp': nexp}
|
|
63
|
+
if ref_model is not None:
|
|
64
|
+
global_summary['model'] = ref_model
|
|
65
|
+
if ref_chi2 is not None and ref_chi2 == ref_chi2:
|
|
66
|
+
global_summary['reduced_chi2_tail'] = ref_chi2
|
|
67
|
+
if ref_calibrated is not None and ref_calibrated == ref_calibrated:
|
|
68
|
+
global_summary['calibrated_chi2_pearson'] = ref_calibrated
|
|
69
|
+
if ref_calibrated_tail is not None and ref_calibrated_tail == ref_calibrated_tail:
|
|
70
|
+
global_summary['calibrated_chi2_tail_pearson'] = ref_calibrated_tail
|
|
71
|
+
fit_result = {
|
|
72
|
+
'pixel_maps': pixel_maps,
|
|
73
|
+
'intensity': _load('intensity'),
|
|
74
|
+
'global_summary': global_summary,
|
|
75
|
+
}
|
|
76
|
+
if ref_decay is not None and ref_time is not None:
|
|
77
|
+
fit_result['decay'] = ref_decay
|
|
78
|
+
fit_result['time_ns'] = ref_time
|
|
79
|
+
if ref_irf is not None:
|
|
80
|
+
fit_result['irf_prompt'] = ref_irf
|
|
81
|
+
slices.append({'z': z, 'ptu_path': z_to_ptu.get(z), 'fit_result': fit_result})
|
|
82
|
+
slices.sort(key=lambda d: d['z'])
|
|
83
|
+
return slices
|
|
84
|
+
|
|
85
|
+
COLORMAPS = {
|
|
86
|
+
'hsv': 'hsv',
|
|
87
|
+
'viridis': 'viridis',
|
|
88
|
+
'cool': 'cool',
|
|
89
|
+
'hot': 'hot',
|
|
90
|
+
'twilight': 'twilight',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def compute_weighted_lifetime(
|
|
94
|
+
pixel_maps,
|
|
95
|
+
intensity,
|
|
96
|
+
n_exp=2,
|
|
97
|
+
weighting='amplitude',
|
|
98
|
+
) -> np.ndarray:
|
|
99
|
+
primary = 'tau_mean_int' if weighting == 'intensity' else 'tau_mean_amp'
|
|
100
|
+
fallback = 'tau_mean_amp' if weighting == 'intensity' else 'tau_mean_int'
|
|
101
|
+
for key in (primary, fallback):
|
|
102
|
+
if key in pixel_maps:
|
|
103
|
+
arr = np.asarray(pixel_maps[key], dtype=np.float32).copy()
|
|
104
|
+
arr[arr == 0] = np.nan
|
|
105
|
+
return arr
|
|
106
|
+
shape = intensity.shape
|
|
107
|
+
num = np.zeros(shape, dtype=np.float64)
|
|
108
|
+
den = np.zeros(shape, dtype=np.float64)
|
|
109
|
+
for i in range(1, n_exp + 1):
|
|
110
|
+
tau_key = f'tau{i}'
|
|
111
|
+
amp_key = f'a{i}'
|
|
112
|
+
if tau_key in pixel_maps and amp_key in pixel_maps:
|
|
113
|
+
tau = np.asarray(pixel_maps[tau_key], dtype=np.float64)
|
|
114
|
+
amp = np.asarray(pixel_maps[amp_key], dtype=np.float64)
|
|
115
|
+
valid = np.isfinite(tau) & np.isfinite(amp)
|
|
116
|
+
if weighting == 'intensity':
|
|
117
|
+
num[valid] += amp[valid] * tau[valid] ** 2
|
|
118
|
+
den[valid] += amp[valid] * tau[valid]
|
|
119
|
+
else:
|
|
120
|
+
num[valid] += amp[valid] * tau[valid]
|
|
121
|
+
den[valid] += amp[valid]
|
|
122
|
+
result = np.full(shape, np.nan, dtype=np.float32)
|
|
123
|
+
mask = den > 0
|
|
124
|
+
result[mask] = (num[mask] / den[mask]).astype(np.float32)
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
def apply_color_scale(
|
|
128
|
+
image,
|
|
129
|
+
vmin=None,
|
|
130
|
+
vmax=None,
|
|
131
|
+
gamma=1.0,
|
|
132
|
+
percentile_auto=(2, 98),
|
|
133
|
+
):
|
|
134
|
+
valid_mask = ~np.isnan(image)
|
|
135
|
+
valid_pixels = image[valid_mask]
|
|
136
|
+
if vmin is None:
|
|
137
|
+
vmin = np.percentile(valid_pixels, percentile_auto[0]) if valid_pixels.size > 0 else 0
|
|
138
|
+
if vmax is None:
|
|
139
|
+
vmax = np.percentile(valid_pixels, percentile_auto[1]) if valid_pixels.size > 0 else 1
|
|
140
|
+
clipped = np.clip(image, vmin, vmax)
|
|
141
|
+
if vmax > vmin:
|
|
142
|
+
normalized = (clipped - vmin) / (vmax - vmin)
|
|
143
|
+
else:
|
|
144
|
+
normalized = np.zeros_like(clipped)
|
|
145
|
+
if gamma != 1.0:
|
|
146
|
+
normalized = np.power(normalized, 1.0 / gamma)
|
|
147
|
+
normalized[~valid_mask] = np.nan
|
|
148
|
+
return normalized
|
|
149
|
+
|
|
150
|
+
def get_colormap(name='viridis'):
|
|
151
|
+
cmap_name = COLORMAPS.get(name, name)
|
|
152
|
+
return plt.cm.get_cmap(cmap_name)
|
|
153
|
+
|
|
154
|
+
def compute_region_stats(
|
|
155
|
+
lifetime_map,
|
|
156
|
+
intensity_map,
|
|
157
|
+
region_mask,
|
|
158
|
+
full_stats=False,
|
|
159
|
+
):
|
|
160
|
+
region_lifetime = lifetime_map[region_mask]
|
|
161
|
+
region_intensity = intensity_map[region_mask]
|
|
162
|
+
valid_mask = ~np.isnan(region_lifetime)
|
|
163
|
+
valid_lifetime = region_lifetime[valid_mask]
|
|
164
|
+
valid_intensity = region_intensity[valid_mask]
|
|
165
|
+
stats = {
|
|
166
|
+
'median_tau': float(np.nanmedian(valid_lifetime)) if valid_lifetime.size > 0 else np.nan,
|
|
167
|
+
'mean_amplitude': float(np.mean(valid_intensity)) if valid_intensity.size > 0 else np.nan,
|
|
168
|
+
'photon_count': int(np.sum(valid_intensity)),
|
|
169
|
+
'n_pixels': int(np.sum(valid_mask)),
|
|
170
|
+
}
|
|
171
|
+
if full_stats and valid_lifetime.size > 0:
|
|
172
|
+
stats.update({
|
|
173
|
+
'min_tau': float(np.nanmin(valid_lifetime)),
|
|
174
|
+
'max_tau': float(np.nanmax(valid_lifetime)),
|
|
175
|
+
'std_tau': float(np.nanstd(valid_lifetime)),
|
|
176
|
+
'mean_tau': float(np.nanmean(valid_lifetime)),
|
|
177
|
+
'percentiles': {
|
|
178
|
+
'p25': float(np.nanpercentile(valid_lifetime, 25)),
|
|
179
|
+
'p50': float(np.nanpercentile(valid_lifetime, 50)),
|
|
180
|
+
'p75': float(np.nanpercentile(valid_lifetime, 75)),
|
|
181
|
+
'p90': float(np.nanpercentile(valid_lifetime, 90)),
|
|
182
|
+
'p95': float(np.nanpercentile(valid_lifetime, 95)),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
return stats
|
|
186
|
+
|
|
187
|
+
def mask_to_rgba(
|
|
188
|
+
mask,
|
|
189
|
+
color=(1.0, 1.0, 1.0),
|
|
190
|
+
alpha=0.3,
|
|
191
|
+
):
|
|
192
|
+
rgba = np.zeros((*mask.shape, 4), dtype=np.float32)
|
|
193
|
+
rgba[mask, 0] = color[0]
|
|
194
|
+
rgba[mask, 1] = color[1]
|
|
195
|
+
rgba[mask, 2] = color[2]
|
|
196
|
+
rgba[mask, 3] = alpha
|
|
197
|
+
return rgba
|