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.
Files changed (104) hide show
  1. flimkit/FLIM/__init__.py +0 -0
  2. flimkit/FLIM/assemble.py +254 -0
  3. flimkit/FLIM/batch.py +681 -0
  4. flimkit/FLIM/bg_tools.py +51 -0
  5. flimkit/FLIM/fit_tools.py +244 -0
  6. flimkit/FLIM/fitters.py +1471 -0
  7. flimkit/FLIM/irf_tools.py +617 -0
  8. flimkit/FLIM/models.py +391 -0
  9. flimkit/GPU/__init__.py +85 -0
  10. flimkit/GPU/_base.py +391 -0
  11. flimkit/GPU/cuda.py +10 -0
  12. flimkit/GPU/mlx_backend.py +381 -0
  13. flimkit/GPU/mps.py +10 -0
  14. flimkit/GPU/rocm.py +10 -0
  15. flimkit/GPU/torch_backend.py +385 -0
  16. flimkit/UI/app_state.py +10 -0
  17. flimkit/UI/controller.py +139 -0
  18. flimkit/UI/expert_settings.py +248 -0
  19. flimkit/UI/fit_help.py +206 -0
  20. flimkit/UI/fov_preview.py +1085 -0
  21. flimkit/UI/gui.py +3919 -0
  22. flimkit/UI/icon.icns +0 -0
  23. flimkit/UI/icon.ico +0 -0
  24. flimkit/UI/icon.png +0 -0
  25. flimkit/UI/irf_widget.py +103 -0
  26. flimkit/UI/mode_controller.py +118 -0
  27. flimkit/UI/modes/__init__.py +0 -0
  28. flimkit/UI/modes/base.py +3 -0
  29. flimkit/UI/modes/batch_mode.py +312 -0
  30. flimkit/UI/modes/fov_mode.py +164 -0
  31. flimkit/UI/modes/irf_mode.py +80 -0
  32. flimkit/UI/modes/phasor_mode.py +131 -0
  33. flimkit/UI/modes/stitch_mode.py +254 -0
  34. flimkit/UI/phasor_panel.py +1087 -0
  35. flimkit/UI/progress_window.py +113 -0
  36. flimkit/UI/project_panel.py +262 -0
  37. flimkit/UI/results_panel.py +332 -0
  38. flimkit/UI/roi_tools.py +794 -0
  39. flimkit/UI/utils.py +217 -0
  40. flimkit/__init__.py +0 -0
  41. flimkit/_version.py +41 -0
  42. flimkit/cli.py +120 -0
  43. flimkit/configs.py +148 -0
  44. flimkit/dialogs.py +46 -0
  45. flimkit/formats/BH/__init__.py +0 -0
  46. flimkit/formats/BH/reader.py +296 -0
  47. flimkit/formats/BH/writer.py +86 -0
  48. flimkit/formats/ISS/__init__.py +0 -0
  49. flimkit/formats/ISS/fdflim.py +86 -0
  50. flimkit/formats/ISS/image.py +114 -0
  51. flimkit/formats/ISS/reader.py +223 -0
  52. flimkit/formats/PS/__init__.py +0 -0
  53. flimkit/formats/PS/reader.py +202 -0
  54. flimkit/formats/PTU/__init__.py +0 -0
  55. flimkit/formats/PTU/decode.py +27 -0
  56. flimkit/formats/PTU/phu.py +85 -0
  57. flimkit/formats/PTU/reader.py +235 -0
  58. flimkit/formats/PTU/series.py +258 -0
  59. flimkit/formats/PTU/stitch.py +1182 -0
  60. flimkit/formats/PTU/tools.py +94 -0
  61. flimkit/formats/__init__.py +2 -0
  62. flimkit/formats/flim_file.py +232 -0
  63. flimkit/formats/phasor.py +132 -0
  64. flimkit/formats/signal.py +170 -0
  65. flimkit/image/tools.py +124 -0
  66. flimkit/interactive.py +1857 -0
  67. flimkit/mpl_backend.py +22 -0
  68. flimkit/phasor/__init__.py +40 -0
  69. flimkit/phasor/filters.py +127 -0
  70. flimkit/phasor/fret.py +654 -0
  71. flimkit/phasor/interactive.py +556 -0
  72. flimkit/phasor/peaks.py +186 -0
  73. flimkit/phasor/signal.py +90 -0
  74. flimkit/phasor_launcher.py +314 -0
  75. flimkit/plugins/__init__.py +137 -0
  76. flimkit/plugins/bindings.py +116 -0
  77. flimkit/plugins/builtin/__init__.py +3 -0
  78. flimkit/plugins/builtin/core_tools.py +28 -0
  79. flimkit/plugins/loader.py +371 -0
  80. flimkit/plugins/registry.py +406 -0
  81. flimkit/project.py +197 -0
  82. flimkit/synth.py +145 -0
  83. flimkit/utils/__init__.py +0 -0
  84. flimkit/utils/batch_fit.py +301 -0
  85. flimkit/utils/config_manager.py +119 -0
  86. flimkit/utils/config_snapshot.py +30 -0
  87. flimkit/utils/crash_handler.py +183 -0
  88. flimkit/utils/display.py +197 -0
  89. flimkit/utils/enhanced_outputs.py +345 -0
  90. flimkit/utils/fancy.py +103 -0
  91. flimkit/utils/lifetime_image.py +243 -0
  92. flimkit/utils/misc.py +111 -0
  93. flimkit/utils/plotting.py +190 -0
  94. flimkit/utils/roi.py +370 -0
  95. flimkit/utils/session.py +51 -0
  96. flimkit/utils/update_check.py +198 -0
  97. flimkit/utils/xlsx_tools.py +97 -0
  98. flimkit/utils/xml_utils.py +219 -0
  99. flimkit-0.12.0.dist-info/METADATA +356 -0
  100. flimkit-0.12.0.dist-info/RECORD +104 -0
  101. flimkit-0.12.0.dist-info/WHEEL +5 -0
  102. flimkit-0.12.0.dist-info/entry_points.txt +2 -0
  103. flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
  104. flimkit-0.12.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,223 @@
1
+ import struct
2
+ import numpy as np
3
+ from pathlib import Path
4
+
5
+ _FRAME_CH = 5
6
+ _LINE_CH = 6
7
+ _PIXEL_CH = 7
8
+ _CLOCK_CH = 8
9
+
10
+ _TAG_EXTS = {
11
+ 'time': '.TAGTIME',
12
+ 'channel': '.TAGCHANNEL',
13
+ 'decay': '.TAGDECAY',
14
+ }
15
+
16
+ def _find_with_ext(stem, ext):
17
+ direct = Path(str(stem) + ext)
18
+ if direct.exists():
19
+ return direct
20
+ parent = stem.parent if str(stem.parent) else Path('.')
21
+ target = (stem.name + ext).lower()
22
+ if parent.exists():
23
+ for f in parent.iterdir():
24
+ if f.name.lower() == target:
25
+ return f
26
+ return None
27
+
28
+ def _resolve_triplet(path):
29
+ p = Path(path)
30
+ stem = p.with_suffix('') if p.suffix.upper() in (e.upper() for e in _TAG_EXTS.values()) else p
31
+ out = {}
32
+ for key, ext in _TAG_EXTS.items():
33
+ cand = _find_with_ext(stem, ext)
34
+ if cand is None:
35
+ raise FileNotFoundError(f"Missing ISS TD-FLIM file '{stem.name}{ext}' next to {p}")
36
+ out[key] = cand
37
+ return out
38
+
39
+ def _read_tag_file(path, byteorder, value_bytes):
40
+ dtype = np.dtype(f'{byteorder}i{value_bytes}')
41
+ with open(path, 'rb') as fh:
42
+ period = struct.unpack(f'{byteorder}i', fh.read(4))[0]
43
+ data = np.frombuffer(fh.read(), dtype=dtype)
44
+ return period, data
45
+
46
+ class ISSFile:
47
+ def __init__(self, path, verbose=True, byteorder='<', n_bins=None):
48
+ self.path = str(path)
49
+ self.verbose = verbose
50
+ self.byteorder = byteorder
51
+ self.paths = _resolve_triplet(path)
52
+ self._total_photons = None
53
+ self._load()
54
+ self._parse_meta(n_bins=n_bins)
55
+ def _load(self):
56
+ p_t, self.arrival_ps = _read_tag_file(self.paths['time'], self.byteorder, 8)
57
+ p_c, self.channel = _read_tag_file(self.paths['channel'], self.byteorder, 4)
58
+ p_d, self.decay = _read_tag_file(self.paths['decay'], self.byteorder, 4)
59
+ periods = [p for p in (p_t, p_c, p_d) if p > 0]
60
+ self.period_ps = periods[0] if periods else 0
61
+ if len(set(periods)) > 1 and self.verbose:
62
+ print(f' WARNING: excitation period differs across files: {p_t}, {p_c}, {p_d} ps')
63
+ n = min(self.arrival_ps.shape[0], self.channel.shape[0], self.decay.shape[0])
64
+ if not (self.arrival_ps.shape[0] == self.channel.shape[0] == self.decay.shape[0]):
65
+ if self.verbose:
66
+ print(f' WARNING: event counts differ '
67
+ f'(time={self.arrival_ps.shape[0]}, channel={self.channel.shape[0]}, '
68
+ f'decay={self.decay.shape[0]}); truncating to {n}')
69
+ self.arrival_ps = self.arrival_ps[:n]
70
+ self.channel = self.channel[:n]
71
+ self.decay = self.decay[:n]
72
+ def _parse_meta(self, n_bins=None):
73
+ ach = np.abs(self.channel)
74
+ is_det = (ach >= 1) & (ach <= 4)
75
+ if n_bins is not None:
76
+ self.n_bins = int(n_bins)
77
+ else:
78
+ dec_ph = self.decay[is_det]
79
+ self.n_bins = int(dec_ph.max()) + 1 if dec_ph.size else 0
80
+ period_s = self.period_ps * 1e-12
81
+ self.frequency = (1.0 / period_s) if period_s > 0 else 0.0
82
+ self.period_ns = period_s * 1e9
83
+ self.tcspc_res = (period_s / self.n_bins) if self.n_bins > 0 else 0.0
84
+ self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
85
+ det = ach[is_det]
86
+ self.photon_channel = int(np.bincount(det, minlength=5)[1:5].argmax() + 1) if det.size else None
87
+ self.detector_channels = [int(c) for c in np.unique(det)] if det.size else []
88
+ self.sync_rate = self.frequency
89
+ self.n_records = int(self.channel.shape[0])
90
+ self.rec_type = None
91
+ is_frame = ach == _FRAME_CH
92
+ is_line = ach == _LINE_CH
93
+ is_pixel = ach == _PIXEL_CH
94
+ self.n_y = int(self._counts_since(is_frame, is_line).max()) if is_line.any() else 0
95
+ self.n_x = int(self._counts_since(is_line, is_pixel).max()) if is_pixel.any() else 0
96
+ self.tags = {
97
+ 'ISS_ExcitationPeriod_ps': int(self.period_ps),
98
+ 'ISS_NumEvents': int(self.channel.shape[0]),
99
+ 'ISS_NumPhotons': int(is_det.sum()),
100
+ 'ISS_DecayResolution': int(self.n_bins),
101
+ 'ISS_DetectorChannels': self.detector_channels,
102
+ 'ISS_ImageShape': (self.n_y, self.n_x),
103
+ 'ISS_SourceFiles': {k: str(v) for k, v in self.paths.items()},
104
+ }
105
+ if self.verbose:
106
+ print(f" ISS TD-FLIM : {self.paths['time'].stem}")
107
+ print(f' Period : {self.period_ns:.3f} ns ({self.frequency/1e6:.3f} MHz)')
108
+ print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
109
+ print(f' Events : {self.channel.shape[0]:,} (photons {int(is_det.sum()):,})')
110
+ print(f' Channels : {self.detector_channels}')
111
+ print(' ')
112
+ def _counts_since(self, reset_mask, count_mask):
113
+ # counter that resets to 0 at every reset_mask event
114
+ n = reset_mask.shape[0]
115
+ cum = np.cumsum(count_mask.astype(np.int64))
116
+ reset_pos = np.where(reset_mask, np.arange(n), -1)
117
+ last = np.maximum.accumulate(reset_pos)
118
+ base = np.zeros(n, dtype=np.int64)
119
+ seen = last >= 0
120
+ base[seen] = cum[last[seen]]
121
+ return cum - base
122
+ def summed_decay(self, channel=None):
123
+ ach = np.abs(self.channel)
124
+ is_det = (ach >= 1) & (ach <= 4)
125
+ if channel is None:
126
+ channel = self.photon_channel
127
+ else:
128
+ channel = int(channel)
129
+ if channel is None:
130
+ return np.zeros(self.n_bins, dtype=float)
131
+ ph = is_det & (ach == channel)
132
+ dec = self.decay[ph]
133
+ dec = dec[(dec >= 0) & (dec < self.n_bins)].astype(np.int64)
134
+ decay = np.bincount(dec, minlength=self.n_bins).astype(float)
135
+ self._total_photons = int(decay.sum())
136
+ return decay[:self.n_bins]
137
+ n_sync = None
138
+
139
+ @property
140
+ def photons_per_pulse(self):
141
+ return None
142
+ def pixel_stack(self, channel=None, binning=1, n_x=None, n_y=None):
143
+ ach = np.abs(self.channel)
144
+ is_frame = ach == _FRAME_CH
145
+ is_line = ach == _LINE_CH
146
+ is_pixel = ach == _PIXEL_CH
147
+ is_det = (ach >= 1) & (ach <= 4)
148
+ if not is_line.any():
149
+ raise RuntimeError('No line markers (ch6) found; cannot build an image cube.')
150
+ # y/x are 0-based: line/pixel markers seen since the parent reset, minus one
151
+ y = self._counts_since(is_frame, is_line) - 1
152
+ x = self._counts_since(is_line, is_pixel) - 1
153
+ if channel is not None:
154
+ ph = is_det & (ach == int(channel))
155
+ else:
156
+ ph = is_det
157
+ dec = self.decay
158
+ valid = ph & (y >= 0) & (x >= 0) & (dec >= 0) & (dec < self.n_bins)
159
+ yv = y[valid].astype(np.int64)
160
+ xv = x[valid].astype(np.int64)
161
+ dv = dec[valid].astype(np.int64)
162
+ ny = int(n_y) if n_y is not None else (self.n_y or (int(yv.max()) + 1 if yv.size else 0))
163
+ nx = int(n_x) if n_x is not None else (self.n_x or (int(xv.max()) + 1 if xv.size else 0))
164
+ keep = (yv < ny) & (xv < nx)
165
+ yv, xv, dv = yv[keep], xv[keep], dv[keep]
166
+ if binning > 1:
167
+ yv = yv // binning
168
+ xv = xv // binning
169
+ ny = (ny + binning - 1) // binning
170
+ nx = (nx + binning - 1) // binning
171
+ if ny == 0 or nx == 0 or self.n_bins == 0:
172
+ return np.zeros((ny, nx, self.n_bins), dtype=np.uint32)
173
+ flat = (yv * nx + xv) * self.n_bins + dv
174
+ cube = np.bincount(flat, minlength=ny * nx * self.n_bins)
175
+ cube = cube.reshape(ny, nx, self.n_bins).astype(np.uint32)
176
+ self._total_photons = int(cube.sum())
177
+ if self.verbose:
178
+ print(f' Built cube {cube.shape} ({self._total_photons:,} photons)')
179
+ return cube
180
+ def raw_pixel_stack(self, channel=None, binning=1):
181
+ return self.pixel_stack(channel=channel, binning=binning)
182
+
183
+ def read_iss(path, binning=1, channel=None, verbose=False):
184
+ iss = ISSFile(path, verbose=verbose)
185
+ data = iss.pixel_stack(channel=channel, binning=binning)
186
+ metadata = {
187
+ 'frequency': iss.frequency,
188
+ 'tcspc_resolution': iss.tcspc_res,
189
+ 'shape': data.shape,
190
+ 'dims': ('Y', 'X', 'H'),
191
+ 'tags': iss.tags,
192
+ 'x_pixel_size': 0,
193
+ 'y_pixel_size': 0,
194
+ 'n_bins': iss.n_bins,
195
+ 'time_ns': iss.time_ns,
196
+ 'photon_channel': iss.photon_channel,
197
+ }
198
+ return data, metadata
199
+
200
+ def get_intensity_image(path, binning=1, channel=None):
201
+ data, metadata = read_iss(path, binning=binning, channel=channel, verbose=False)
202
+ img = data.sum(axis=2)
203
+ return img, metadata
204
+
205
+ def get_flim_data(path, binning=1, channel=None):
206
+ return read_iss(path, binning=binning, channel=channel, verbose=False)
207
+
208
+ def normalise_flim(flim):
209
+ if flim is None:
210
+ return None
211
+ if flim.ndim == 5:
212
+ return flim[0, :, :, 0, :]
213
+ if flim.ndim == 4:
214
+ if flim.shape[0] == 1:
215
+ return flim[0]
216
+ else:
217
+ return flim[:, :, 0, :]
218
+ if flim.ndim == 3:
219
+ return flim
220
+ return None
221
+
222
+ def create_time_axis(n_bins, tcspc_resolution):
223
+ return np.arange(n_bins) * tcspc_resolution * 1e9
File without changes
@@ -0,0 +1,202 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+ from photonsfile import read_header, read_attributes, has_dual_tdc, read_photons
4
+
5
+ class PSFile:
6
+ def __init__(self, path, verbose=True, channel=None, pixels=512,
7
+ n_bins=256, period_ns=None):
8
+ self.path = str(path)
9
+ self.verbose = verbose
10
+ self.header = read_header(self.path)
11
+ self.attrs = read_attributes(self.path)
12
+ self._names = [d['name'] for d in self.header['datasets']]
13
+ self.dual_tdc = has_dual_tdc(self.path)
14
+ self.pos_range = 1 << int(self.attrs.get('/photons/PositionBits', 12))
15
+ self.tac_range = 1 << int(self.attrs.get('/photons/TacBits', 12))
16
+ self.pixels = int(pixels)
17
+ self.n_bins = int(n_bins)
18
+ self.n_x = self.pixels
19
+ self.n_y = self.pixels
20
+ self._period_ns = period_ns
21
+ self._streams = None
22
+ self._total_photons = None
23
+ self.n_records = 0
24
+ self.n_channels = 1
25
+ self.photon_channel = 1
26
+ self.detector_channels = [1]
27
+ self._parse_meta(period_ns)
28
+
29
+ def _parse_meta(self, period_ns, announce=True):
30
+ # TacChannel = picoseconds per raw dt unit; dual-TDC dt is already in ps
31
+ tac_channel = self.attrs.get('/photons/TacChannel')
32
+ if period_ns and float(period_ns) > 0:
33
+ period_s = float(period_ns) * 1e-9
34
+ self.calib_source = 'user'
35
+ elif self.dual_tdc:
36
+ period_s = self.tac_range * 1e-12
37
+ self.calib_source = 'dual_tdc'
38
+ elif tac_channel:
39
+ period_s = self.tac_range * float(tac_channel) * 1e-12
40
+ self.calib_source = 'TacChannel'
41
+ else:
42
+ period_s = 0.0
43
+ self.calib_source = 'unknown'
44
+ if period_s > 0:
45
+ self.frequency = 1.0 / period_s
46
+ self.period_ns = period_s * 1e9
47
+ self.tcspc_res = period_s / self.n_bins
48
+ else:
49
+ self.frequency = 0.0
50
+ self.period_ns = 0.0
51
+ self.tcspc_res = 0.0
52
+ self.sync_rate = self.frequency
53
+ self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
54
+ self.dt_unit = 'ps' if self.dual_tdc else 'tac_bin'
55
+ self.tags = {
56
+ 'PS_Magic': self.header['magic'],
57
+ 'PS_Version': self.header['version'],
58
+ 'PS_Datasets': self._names,
59
+ 'PS_PositionRange': self.pos_range,
60
+ 'PS_TacRange': self.tac_range,
61
+ 'PS_TacBias': self.attrs.get('/photons/TacBias'),
62
+ 'PS_TacChannel': self.attrs.get('/photons/TacChannel'),
63
+ 'PS_TimerFrequency': self.attrs.get('/photons/TimerFrequency'),
64
+ 'PS_DetectorGuid': self.attrs.get('/photons/DetectorGuid'),
65
+ 'PS_Created': self.attrs.get('Created'),
66
+ 'PS_Pixels': self.pixels,
67
+ 'PS_DecayResolution': self.n_bins,
68
+ 'PS_CalibrationSource': self.calib_source,
69
+ 'PS_DetectorChannels': self.detector_channels,
70
+ 'PS_SourceFile': self.path,
71
+ }
72
+ if self.verbose and announce:
73
+ print(f' Photonscore : {Path(self.path).name}')
74
+ print(f' Container : {self.header["magic"]} v{self.header["version"]}')
75
+ print(f' Image : {self.pixels} x {self.pixels} px '
76
+ f'(from {self.pos_range} raw positions)')
77
+ print(f' TCSPC : {self.n_bins} bins over {self.tac_range} dt units '
78
+ f'({self.tcspc_res*1e12:.2f} ps/bin, {self.calib_source})')
79
+ if self.dual_tdc:
80
+ print(' Timing : dual-TDC, dt = stop - start in ps '
81
+ '(range set from data)')
82
+ if self.calib_source == 'unknown':
83
+ print(' NOTE : dt->time calibration unknown; '
84
+ 'pass period_ns=<laser period ns> for a real time axis')
85
+ print(' ')
86
+
87
+ n_sync = None
88
+
89
+ @property
90
+ def photons_per_pulse(self):
91
+ return None
92
+
93
+ def _ensure_streams(self):
94
+ if self._streams is None:
95
+ self._streams = read_photons(self.path)
96
+ if self.dual_tdc and 'dt' in self._streams:
97
+ dt = np.asarray(self._streams['dt'])
98
+ dt = dt[dt >= 0]
99
+ if dt.size:
100
+ self.tac_range = int(dt.max()) + 1
101
+ self._parse_meta(self._period_ns, announce=False)
102
+ return self._streams
103
+
104
+ def _bin_positions(self, binning):
105
+ s = self._ensure_streams()
106
+ x = np.asarray(s['x']).astype(np.int64)
107
+ y = np.asarray(s['y']).astype(np.int64)
108
+ dt = np.asarray(s['dt']).astype(np.int64)
109
+ n = min(x.shape[0], y.shape[0], dt.shape[0])
110
+ x, y, dt = x[:n], y[:n], dt[:n]
111
+ valid = ((x >= 0) & (x < self.pos_range) & (y >= 0) & (y < self.pos_range)
112
+ & (dt >= 0) & (dt < self.tac_range))
113
+ x, y, dt = x[valid], y[valid], dt[valid]
114
+ p = self.pixels
115
+ xi = (x * p) // self.pos_range
116
+ yi = (y * p) // self.pos_range
117
+ di = (dt * self.n_bins) // self.tac_range
118
+ if binning > 1:
119
+ xi = xi // binning
120
+ yi = yi // binning
121
+ p = (p + binning - 1) // binning
122
+ return xi, yi, di, p
123
+
124
+ def pixel_stack(self, channel=None, binning=1):
125
+ xi, yi, di, p = self._bin_positions(binning)
126
+ b = self.n_bins
127
+ if p == 0 or b == 0:
128
+ return np.zeros((p, p, b), dtype=np.uint32)
129
+ flat = (yi * p + xi) * b + di
130
+ cube = np.bincount(flat, minlength=p * p * b).reshape(p, p, b).astype(np.uint32)
131
+ self.n_y, self.n_x = cube.shape[0], cube.shape[1]
132
+ self._total_photons = int(cube.sum())
133
+ self.n_records = self._total_photons
134
+ if self.verbose:
135
+ print(f' Built cube {cube.shape} ({self._total_photons:,} photons)')
136
+ return cube
137
+
138
+ def raw_pixel_stack(self, channel=None, binning=1):
139
+ return self.pixel_stack(channel=channel, binning=binning)
140
+
141
+ def summed_decay(self, channel=None):
142
+ s = self._ensure_streams()
143
+ dt = np.asarray(s['dt']).astype(np.int64)
144
+ dt = dt[(dt >= 0) & (dt < self.tac_range)]
145
+ di = (dt * self.n_bins) // self.tac_range
146
+ decay = np.bincount(di, minlength=self.n_bins).astype(float)
147
+ self._total_photons = int(decay.sum())
148
+ return decay[:self.n_bins]
149
+
150
+ def intensity_image(self, channel=None, binning=1):
151
+ xi, yi, _, p = self._bin_positions(binning)
152
+ if p == 0:
153
+ return np.zeros((0, 0), dtype=np.uint64)
154
+ flat = yi * p + xi
155
+ img = np.bincount(flat, minlength=p * p).reshape(p, p).astype(np.uint64)
156
+ return img
157
+
158
+ def _metadata(ps, data):
159
+ return {
160
+ 'frequency': ps.frequency,
161
+ 'tcspc_resolution': ps.tcspc_res,
162
+ 'shape': data.shape,
163
+ 'dims': ('Y', 'X', 'H'),
164
+ 'tags': ps.tags,
165
+ 'x_pixel_size': 0,
166
+ 'y_pixel_size': 0,
167
+ 'n_bins': ps.n_bins,
168
+ 'time_ns': ps.time_ns,
169
+ 'photon_channel': ps.photon_channel,
170
+ }
171
+
172
+ def read_ps(path, binning=1, channel=None, verbose=False, pixels=512,
173
+ n_bins=256, period_ns=None):
174
+ ps = PSFile(path, verbose=verbose, pixels=pixels, n_bins=n_bins, period_ns=period_ns)
175
+ data = ps.pixel_stack(channel=channel, binning=binning)
176
+ metadata = _metadata(ps, data)
177
+ return data, metadata
178
+
179
+ def get_flim_data(path, binning=1, channel=None, pixels=512, n_bins=256, period_ns=None):
180
+ return read_ps(path, binning=binning, channel=channel, verbose=False,
181
+ pixels=pixels, n_bins=n_bins, period_ns=period_ns)
182
+
183
+ def get_intensity_image(path, binning=1, channel=None, pixels=512, n_bins=256):
184
+ ps = PSFile(path, verbose=False, pixels=pixels, n_bins=n_bins)
185
+ img = ps.intensity_image(channel=channel, binning=binning)
186
+ metadata = _metadata(ps, np.empty(0))
187
+ metadata['shape'] = (ps.n_y, ps.n_x, ps.n_bins)
188
+ return img, metadata
189
+
190
+ def normalise_flim(flim):
191
+ if flim is None:
192
+ return None
193
+ if flim.ndim == 5:
194
+ return flim[0, :, :, 0, :]
195
+ if flim.ndim == 4:
196
+ return flim[0] if flim.shape[0] == 1 else flim[:, :, 0, :]
197
+ if flim.ndim == 3:
198
+ return flim
199
+ return None
200
+
201
+ def create_time_axis(n_bins, tcspc_resolution):
202
+ return np.arange(n_bins) * tcspc_resolution * 1e9
File without changes
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+ from .reader import PTUFile, normalise_flim
3
+
4
+ def create_time_axis(n_bins, tcspc_resolution):
5
+ return np.arange(n_bins) * tcspc_resolution * 1e9
6
+
7
+ def get_flim_histogram_from_ptufile(
8
+ ptu_path,
9
+ rotate_cw=True,
10
+ binning=1,
11
+ channel=None
12
+ ):
13
+ ptu = PTUFile(str(ptu_path), verbose=False)
14
+ stack = ptu.raw_pixel_stack(channel=channel, binning=binning)
15
+ if stack.sum() == 0:
16
+ raise ValueError('PTU decoded to zero photons')
17
+ if rotate_cw:
18
+ stack = np.rot90(stack, k=-1, axes=(0, 1))
19
+ metadata = {
20
+ 'tcspc_resolution': ptu.tcspc_res,
21
+ 'n_time_bins': ptu.n_bins,
22
+ 'tile_shape': (ptu.n_y // binning, ptu.n_x // binning),
23
+ 'frequency': ptu.sync_rate,
24
+ 'binning': binning,
25
+ 'channel': channel,
26
+ }
27
+ return stack, metadata
@@ -0,0 +1,85 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ def _load_ptufile():
5
+ try:
6
+ import ptufile
7
+ except ImportError:
8
+ raise ImportError('ptufile is required to read PicoQuant .phu files '
9
+ '(pip install ptufile)')
10
+ return ptufile
11
+
12
+ class PHUFile:
13
+ modality = 'time'
14
+
15
+ def __init__(self, path, verbose=True, channel=None, **kwargs):
16
+ self.path = str(path)
17
+ self.verbose = verbose
18
+ self.channel = channel
19
+ ptufile = _load_ptufile()
20
+ with ptufile.PhuFile(self.path) as f:
21
+ self._hist = [np.asarray(h, dtype=np.uint32) for h in f.histograms()]
22
+ self.tcspc_res = float(f.tcspc_resolution)
23
+ self._resolutions = [float(r) for r in f.histogram_resolutions]
24
+ self.tags = dict(f.tags)
25
+ self.n_channels = len(self._hist)
26
+ self.n_bins = int(max((h.size for h in self._hist), default=0))
27
+ self.n_x = 0
28
+ self.n_y = 0
29
+ self.is_image = False
30
+ self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
31
+ self.sync_rate = 0.0
32
+ self.period_ns = 0.0
33
+ self.photon_channel = None
34
+ self._total_photons = None
35
+ if self.verbose:
36
+ print(f' PicoQuant PHU : {Path(self.path).name}')
37
+ print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
38
+ print(f' Point : no image (histogram mode), {self.n_channels} histogram(s)')
39
+ print(' ')
40
+
41
+ def _ch_index(self, channel):
42
+ if channel is None:
43
+ totals = [int(h.sum()) for h in self._hist]
44
+ idx = int(np.argmax(totals)) if totals else 0
45
+ self.photon_channel = idx + 1
46
+ return idx
47
+ c = int(channel)
48
+ idx = c - 1 if c >= 1 else c
49
+ return idx if 0 <= idx < self.n_channels else 0
50
+
51
+ def summed_decay(self, channel=None):
52
+ if not self._hist:
53
+ return np.zeros(self.n_bins, dtype=float)
54
+ idx = self._ch_index(self.channel if channel is None else channel)
55
+ decay = np.zeros(self.n_bins, dtype=float)
56
+ h = self._hist[idx]
57
+ decay[:h.size] = h
58
+ self._total_photons = int(decay.sum())
59
+ return decay
60
+
61
+ def pixel_stack(self, *args, **kwargs):
62
+ raise RuntimeError('PicoQuant .phu is histogram mode and has no scan image; '
63
+ 'use summed_decay')
64
+
65
+ def raw_pixel_stack(self, *args, **kwargs):
66
+ return self.pixel_stack()
67
+
68
+ @property
69
+ def photons_per_pulse(self):
70
+ return None
71
+
72
+ def read_phu(path, channel=None, verbose=False):
73
+ phu = PHUFile(path, verbose=verbose)
74
+ decay = phu.summed_decay(channel=channel)
75
+ metadata = {
76
+ 'frequency': phu.sync_rate,
77
+ 'tcspc_resolution': phu.tcspc_res,
78
+ 'shape': decay.shape,
79
+ 'dims': ('H',),
80
+ 'tags': phu.tags,
81
+ 'n_bins': phu.n_bins,
82
+ 'time_ns': phu.time_ns,
83
+ 'photon_channel': phu.photon_channel,
84
+ }
85
+ return decay, metadata