photonsfile 2026.7.8__tar.gz
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.
- photonsfile-2026.7.8/LICENSE +21 -0
- photonsfile-2026.7.8/PKG-INFO +81 -0
- photonsfile-2026.7.8/README.md +61 -0
- photonsfile-2026.7.8/photonsfile/__init__.py +2 -0
- photonsfile-2026.7.8/photonsfile/_d7.py +323 -0
- photonsfile-2026.7.8/photonsfile/photonsfile.py +174 -0
- photonsfile-2026.7.8/photonsfile.egg-info/PKG-INFO +81 -0
- photonsfile-2026.7.8/photonsfile.egg-info/SOURCES.txt +12 -0
- photonsfile-2026.7.8/photonsfile.egg-info/dependency_links.txt +1 -0
- photonsfile-2026.7.8/photonsfile.egg-info/requires.txt +8 -0
- photonsfile-2026.7.8/photonsfile.egg-info/top_level.txt +1 -0
- photonsfile-2026.7.8/pyproject.toml +25 -0
- photonsfile-2026.7.8/setup.cfg +4 -0
- photonsfile-2026.7.8/tests/test_photonsfile.py +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Hunt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: photonsfile
|
|
3
|
+
Version: 2026.7.8
|
|
4
|
+
Summary: Read Photonscore LINCam .photons (D7) files
|
|
5
|
+
Author: Alex Hunt
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alex1075/photonsfile
|
|
8
|
+
Project-URL: D7 format, https://github.com/photonscore/d7
|
|
9
|
+
Keywords: Photonscore,LINCam,D7,photons,TCSPC,FLIM
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: numba; extra == "all"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: numba; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# photonsfile
|
|
22
|
+
|
|
23
|
+
Pure-Python reader for Photonscore LINCam `.photons` files (the **D7** container).
|
|
24
|
+
numpy only, with optional numba acceleration for the varint decode.
|
|
25
|
+
|
|
26
|
+
Photonscore LINCam is a position-sensitive single-photon detector: every photon
|
|
27
|
+
carries an `(x, y)` detector position, a TCSPC micro time (`dt`), and a macro time
|
|
28
|
+
in milliseconds (`ms`). This library decodes those per-photon streams and can bin
|
|
29
|
+
them into an intensity image, a TCSPC decay, or a `(Y, X, H)` FLIM cube.
|
|
30
|
+
|
|
31
|
+
## The D7 format
|
|
32
|
+
|
|
33
|
+
A `.photons` file is a paged container: 16 KB pages, each carrying a 2-byte page
|
|
34
|
+
marker, holding a protobuf-style header, a per-dataset index, and an epilogue. Each
|
|
35
|
+
dataset (`/photons/x`, `/photons/y`, `/photons/dt`, `/photons/ms`, and on dual-TDC
|
|
36
|
+
detectors `/start/time` and `/stop/time`) is stored as a seed value followed by
|
|
37
|
+
zigzag-varint delta blocks. `dt` to time is calibrated from the file's
|
|
38
|
+
`/photons/TacChannel` attribute (picoseconds per raw unit).
|
|
39
|
+
|
|
40
|
+
The format is documented at [github.com/photonscore/d7](https://github.com/photonscore/d7)
|
|
41
|
+
(Apache-2.0). This reader was written from that specification and **validated
|
|
42
|
+
bit-exact against the Photonscore SDK**. No vendor source is redistributed.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install photonsfile # numpy only
|
|
48
|
+
pip install photonsfile[all] # + numba, for a faster varint decode
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from photonsfile import PhotonsFile, imread
|
|
55
|
+
|
|
56
|
+
with PhotonsFile('sample.photons') as f:
|
|
57
|
+
ph = f.photons() # {'x', 'y', 'dt', 'ms'} arrays, one per photon
|
|
58
|
+
img = f.image(pixels=512) # (Y, X) intensity image
|
|
59
|
+
decay = f.decay(bins=256) # summed TCSPC histogram
|
|
60
|
+
cube = f.flim_image(512, 256) # (Y, X, H) FLIM cube
|
|
61
|
+
dt_res = f.tcspc_resolution(256) # seconds per bin, from TacChannel
|
|
62
|
+
print(f.attributes) # D7 file attributes
|
|
63
|
+
|
|
64
|
+
img = imread('sample.photons') # shortcut for the intensity image
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Low-level access to the decoder is also exported: `read_header`,
|
|
68
|
+
`read_attributes`, `read_photons`, `dataset_names`, `has_dual_tdc`.
|
|
69
|
+
|
|
70
|
+
numba is optional; without it the varint decode uses a vectorised numpy fallback.
|
|
71
|
+
`photonsfile.have_numba()` reports which path is active.
|
|
72
|
+
|
|
73
|
+
## Provenance
|
|
74
|
+
|
|
75
|
+
Reverse-engineered from the public D7 specification and checked bit-exact against
|
|
76
|
+
the Photonscore SDK on a 284-million-photon sample. Thanks to Photonscore for
|
|
77
|
+
open-sourcing the D7 format.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT - see `LICENSE`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# photonsfile
|
|
2
|
+
|
|
3
|
+
Pure-Python reader for Photonscore LINCam `.photons` files (the **D7** container).
|
|
4
|
+
numpy only, with optional numba acceleration for the varint decode.
|
|
5
|
+
|
|
6
|
+
Photonscore LINCam is a position-sensitive single-photon detector: every photon
|
|
7
|
+
carries an `(x, y)` detector position, a TCSPC micro time (`dt`), and a macro time
|
|
8
|
+
in milliseconds (`ms`). This library decodes those per-photon streams and can bin
|
|
9
|
+
them into an intensity image, a TCSPC decay, or a `(Y, X, H)` FLIM cube.
|
|
10
|
+
|
|
11
|
+
## The D7 format
|
|
12
|
+
|
|
13
|
+
A `.photons` file is a paged container: 16 KB pages, each carrying a 2-byte page
|
|
14
|
+
marker, holding a protobuf-style header, a per-dataset index, and an epilogue. Each
|
|
15
|
+
dataset (`/photons/x`, `/photons/y`, `/photons/dt`, `/photons/ms`, and on dual-TDC
|
|
16
|
+
detectors `/start/time` and `/stop/time`) is stored as a seed value followed by
|
|
17
|
+
zigzag-varint delta blocks. `dt` to time is calibrated from the file's
|
|
18
|
+
`/photons/TacChannel` attribute (picoseconds per raw unit).
|
|
19
|
+
|
|
20
|
+
The format is documented at [github.com/photonscore/d7](https://github.com/photonscore/d7)
|
|
21
|
+
(Apache-2.0). This reader was written from that specification and **validated
|
|
22
|
+
bit-exact against the Photonscore SDK**. No vendor source is redistributed.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install photonsfile # numpy only
|
|
28
|
+
pip install photonsfile[all] # + numba, for a faster varint decode
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from photonsfile import PhotonsFile, imread
|
|
35
|
+
|
|
36
|
+
with PhotonsFile('sample.photons') as f:
|
|
37
|
+
ph = f.photons() # {'x', 'y', 'dt', 'ms'} arrays, one per photon
|
|
38
|
+
img = f.image(pixels=512) # (Y, X) intensity image
|
|
39
|
+
decay = f.decay(bins=256) # summed TCSPC histogram
|
|
40
|
+
cube = f.flim_image(512, 256) # (Y, X, H) FLIM cube
|
|
41
|
+
dt_res = f.tcspc_resolution(256) # seconds per bin, from TacChannel
|
|
42
|
+
print(f.attributes) # D7 file attributes
|
|
43
|
+
|
|
44
|
+
img = imread('sample.photons') # shortcut for the intensity image
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Low-level access to the decoder is also exported: `read_header`,
|
|
48
|
+
`read_attributes`, `read_photons`, `dataset_names`, `has_dual_tdc`.
|
|
49
|
+
|
|
50
|
+
numba is optional; without it the varint decode uses a vectorised numpy fallback.
|
|
51
|
+
`photonsfile.have_numba()` reports which path is active.
|
|
52
|
+
|
|
53
|
+
## Provenance
|
|
54
|
+
|
|
55
|
+
Reverse-engineered from the public D7 specification and checked bit-exact against
|
|
56
|
+
the Photonscore SDK on a 284-million-photon sample. Thanks to Photonscore for
|
|
57
|
+
open-sourcing the D7 format.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT - see `LICENSE`.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from numba import njit as _njit
|
|
5
|
+
_HAVE_NUMBA = True
|
|
6
|
+
except Exception:
|
|
7
|
+
_HAVE_NUMBA = False
|
|
8
|
+
|
|
9
|
+
MAGIC = b'D7 Photons Data'
|
|
10
|
+
_DEFAULT_PAGE = 16384
|
|
11
|
+
|
|
12
|
+
_DTYPES = {
|
|
13
|
+
0: np.int8, 1: np.int16, 2: np.int32, 3: np.int64,
|
|
14
|
+
4: np.uint8, 5: np.uint16, 6: np.uint32, 7: np.uint64,
|
|
15
|
+
8: np.single, 9: np.double,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def _read_varint(buf, i):
|
|
19
|
+
shift = 0
|
|
20
|
+
val = 0
|
|
21
|
+
while True:
|
|
22
|
+
c = buf[i]
|
|
23
|
+
i += 1
|
|
24
|
+
val |= (c & 0x7f) << shift
|
|
25
|
+
if not (c & 0x80):
|
|
26
|
+
return val, i
|
|
27
|
+
shift += 7
|
|
28
|
+
|
|
29
|
+
def _zigzag(v):
|
|
30
|
+
return (v >> 1) ^ -(v & 1)
|
|
31
|
+
|
|
32
|
+
def _strip_markers(raw, phys_start, page):
|
|
33
|
+
b = np.frombuffer(raw, dtype=np.uint8)
|
|
34
|
+
n = b.shape[0]
|
|
35
|
+
first = (-phys_start) % page
|
|
36
|
+
if first >= n:
|
|
37
|
+
return b
|
|
38
|
+
m0 = np.arange(first, n, page)
|
|
39
|
+
keep = np.ones(n, dtype=bool)
|
|
40
|
+
keep[m0] = False
|
|
41
|
+
m1 = m0[m0 + 1 < n] + 1
|
|
42
|
+
keep[m1] = False
|
|
43
|
+
return b[keep]
|
|
44
|
+
|
|
45
|
+
def _clean_pos(phys, region_start, page):
|
|
46
|
+
markers = (phys // page) - (region_start // page)
|
|
47
|
+
return (phys - region_start) - 2 * markers
|
|
48
|
+
|
|
49
|
+
def _parse_descriptor(payload):
|
|
50
|
+
name = None
|
|
51
|
+
code = None
|
|
52
|
+
k = 0
|
|
53
|
+
while k < len(payload):
|
|
54
|
+
tag = payload[k]
|
|
55
|
+
k += 1
|
|
56
|
+
field = tag >> 3
|
|
57
|
+
wt = tag & 7
|
|
58
|
+
if wt == 2:
|
|
59
|
+
ln, k = _read_varint(payload, k)
|
|
60
|
+
chunk = payload[k:k + ln]
|
|
61
|
+
k += ln
|
|
62
|
+
if field == 1:
|
|
63
|
+
name = chunk.decode('ascii', 'replace')
|
|
64
|
+
elif wt == 0:
|
|
65
|
+
val, k = _read_varint(payload, k)
|
|
66
|
+
if field == 2:
|
|
67
|
+
code = val
|
|
68
|
+
else:
|
|
69
|
+
break
|
|
70
|
+
return name, _DTYPES.get(code, None)
|
|
71
|
+
|
|
72
|
+
def read_header(path):
|
|
73
|
+
with open(path, 'rb') as fh:
|
|
74
|
+
head = fh.read(_DEFAULT_PAGE)
|
|
75
|
+
if MAGIC not in head[:64]:
|
|
76
|
+
raise ValueError(f'Not a D7 .photons file (magic {MAGIC!r} not found): {path}')
|
|
77
|
+
i = 2
|
|
78
|
+
tag, i = _read_varint(head, i)
|
|
79
|
+
hlen, i = _read_varint(head, i)
|
|
80
|
+
end = i + hlen
|
|
81
|
+
header = {'magic': MAGIC.decode(), 'version': None, 'index_step': None,
|
|
82
|
+
'page_size': _DEFAULT_PAGE, 'datasets': []}
|
|
83
|
+
while i < end:
|
|
84
|
+
tag, i = _read_varint(head, i)
|
|
85
|
+
field = tag >> 3
|
|
86
|
+
wt = tag & 7
|
|
87
|
+
if wt == 2:
|
|
88
|
+
ln, i = _read_varint(head, i)
|
|
89
|
+
payload = head[i:i + ln]
|
|
90
|
+
i += ln
|
|
91
|
+
if field == 7:
|
|
92
|
+
name, dtype = _parse_descriptor(payload)
|
|
93
|
+
header['datasets'].append({'name': name, 'dtype': dtype})
|
|
94
|
+
elif wt == 0:
|
|
95
|
+
val, i = _read_varint(head, i)
|
|
96
|
+
if field == 2:
|
|
97
|
+
header['version'] = val
|
|
98
|
+
elif field == 6:
|
|
99
|
+
header['index_step'] = val
|
|
100
|
+
elif field == 8 and val > 0:
|
|
101
|
+
header['page_size'] = val
|
|
102
|
+
else:
|
|
103
|
+
break
|
|
104
|
+
header['header_end'] = end
|
|
105
|
+
return header
|
|
106
|
+
|
|
107
|
+
def dataset_names(path):
|
|
108
|
+
return [d['name'] for d in read_header(path)['datasets']]
|
|
109
|
+
|
|
110
|
+
def _read_epilogue(fh, filesize, page):
|
|
111
|
+
n = min(filesize, 8192)
|
|
112
|
+
fh.seek(filesize - n)
|
|
113
|
+
tail = _strip_markers(fh.read(n), filesize - n, page).tobytes()
|
|
114
|
+
e = tail.rfind(b'End of D7 Photons Data File')
|
|
115
|
+
if e < 0:
|
|
116
|
+
raise ValueError('D7 epilogue signature not found')
|
|
117
|
+
j = e - 3
|
|
118
|
+
while j - 1 >= 0 and (tail[j - 1] & 0x80):
|
|
119
|
+
j -= 1
|
|
120
|
+
giof, _ = _read_varint(tail, j)
|
|
121
|
+
return giof
|
|
122
|
+
|
|
123
|
+
def _parse_index(fh, index_off, filesize, page, n_datasets):
|
|
124
|
+
fh.seek(index_off)
|
|
125
|
+
idx = _strip_markers(fh.read(filesize - index_off), index_off, page).tobytes()
|
|
126
|
+
_tag, p = _read_varint(idx, 0)
|
|
127
|
+
ln, p = _read_varint(idx, p)
|
|
128
|
+
body = idx[p:p + ln]
|
|
129
|
+
offsets = {}
|
|
130
|
+
i = 0
|
|
131
|
+
n = len(body)
|
|
132
|
+
while i < n - 2:
|
|
133
|
+
if body[i] == 0x0a:
|
|
134
|
+
l = body[i + 1]
|
|
135
|
+
pay = body[i + 2:i + 2 + l]
|
|
136
|
+
if len(pay) >= 3 and pay[0] == 0x08 and pay[1] < n_datasets and pay[2] == 0x10:
|
|
137
|
+
did = pay[1]
|
|
138
|
+
off, _ = _read_varint(pay, 3)
|
|
139
|
+
offsets.setdefault(did, []).append(off)
|
|
140
|
+
i += 2 + l
|
|
141
|
+
continue
|
|
142
|
+
i += 1
|
|
143
|
+
for did in offsets:
|
|
144
|
+
offsets[did].sort()
|
|
145
|
+
return offsets
|
|
146
|
+
|
|
147
|
+
def read_attributes(path):
|
|
148
|
+
import os
|
|
149
|
+
header = read_header(path)
|
|
150
|
+
page = header['page_size']
|
|
151
|
+
filesize = os.path.getsize(path)
|
|
152
|
+
with open(path, 'rb') as fh:
|
|
153
|
+
index_off = _read_epilogue(fh, filesize, page)
|
|
154
|
+
fh.seek(index_off)
|
|
155
|
+
idx = _strip_markers(fh.read(filesize - index_off), index_off, page).tobytes()
|
|
156
|
+
_tag, p = _read_varint(idx, 0)
|
|
157
|
+
ln, p = _read_varint(idx, p)
|
|
158
|
+
body = idx[p:p + ln]
|
|
159
|
+
attrs = {}
|
|
160
|
+
i = 0
|
|
161
|
+
n = len(body)
|
|
162
|
+
while i < n:
|
|
163
|
+
tag, i = _read_varint(body, i)
|
|
164
|
+
f = tag >> 3
|
|
165
|
+
wt = tag & 7
|
|
166
|
+
if wt == 2:
|
|
167
|
+
l, i = _read_varint(body, i)
|
|
168
|
+
ent = body[i:i + l]
|
|
169
|
+
i += l
|
|
170
|
+
if f == 2:
|
|
171
|
+
key = val = None
|
|
172
|
+
k = 0
|
|
173
|
+
while k < len(ent):
|
|
174
|
+
etag, k = _read_varint(ent, k)
|
|
175
|
+
if etag & 7 != 2:
|
|
176
|
+
break
|
|
177
|
+
el, k = _read_varint(ent, k)
|
|
178
|
+
s = ent[k:k + el].decode('utf8', 'replace')
|
|
179
|
+
k += el
|
|
180
|
+
if etag >> 3 == 1:
|
|
181
|
+
key = s
|
|
182
|
+
elif etag >> 3 == 2:
|
|
183
|
+
val = s
|
|
184
|
+
if key:
|
|
185
|
+
attrs[key] = val
|
|
186
|
+
elif wt == 0:
|
|
187
|
+
_, i = _read_varint(body, i)
|
|
188
|
+
else:
|
|
189
|
+
break
|
|
190
|
+
return attrs
|
|
191
|
+
|
|
192
|
+
if _HAVE_NUMBA:
|
|
193
|
+
@_njit(cache=True)
|
|
194
|
+
def _decode_varints_zigzag_nb(b):
|
|
195
|
+
n = b.shape[0]
|
|
196
|
+
out = np.empty(n, dtype=np.int64)
|
|
197
|
+
m = 0
|
|
198
|
+
val = np.uint64(0)
|
|
199
|
+
shift = np.uint64(0)
|
|
200
|
+
for i in range(n):
|
|
201
|
+
c = b[i]
|
|
202
|
+
val |= np.uint64(c & 0x7f) << shift
|
|
203
|
+
if c & 0x80:
|
|
204
|
+
shift += np.uint64(7)
|
|
205
|
+
else:
|
|
206
|
+
out[m] = np.int64(val >> np.uint64(1)) ^ -np.int64(val & np.uint64(1))
|
|
207
|
+
m += 1
|
|
208
|
+
val = np.uint64(0)
|
|
209
|
+
shift = np.uint64(0)
|
|
210
|
+
return out[:m]
|
|
211
|
+
|
|
212
|
+
def _decode_varints_zigzag_np(b):
|
|
213
|
+
cont = (b & 0x80) != 0
|
|
214
|
+
term = ~cont
|
|
215
|
+
payload = (b & 0x7f).astype(np.uint64)
|
|
216
|
+
n = b.shape[0]
|
|
217
|
+
newgrp = np.empty(n, dtype=bool)
|
|
218
|
+
newgrp[0] = True
|
|
219
|
+
newgrp[1:] = term[:-1]
|
|
220
|
+
starts = np.flatnonzero(newgrp)
|
|
221
|
+
within = (np.arange(n) - np.repeat(starts, np.diff(np.append(starts, n)))).astype(np.uint64)
|
|
222
|
+
contrib = payload << (np.uint64(7) * within)
|
|
223
|
+
summed = np.add.reduceat(contrib, starts)
|
|
224
|
+
return (summed >> np.uint64(1)).astype(np.int64) ^ -(summed & np.uint64(1)).astype(np.int64)
|
|
225
|
+
|
|
226
|
+
def _decode_varints_zigzag(b):
|
|
227
|
+
if b.shape[0] == 0:
|
|
228
|
+
return np.zeros(0, dtype=np.int64)
|
|
229
|
+
if _HAVE_NUMBA:
|
|
230
|
+
return _decode_varints_zigzag_nb(b)
|
|
231
|
+
return _decode_varints_zigzag_np(b)
|
|
232
|
+
|
|
233
|
+
def _decode_data_block(buf, cp, n, n_datasets):
|
|
234
|
+
q = cp - 1
|
|
235
|
+
while True:
|
|
236
|
+
q = buf.find(b'\x12', q + 1, cp + 16)
|
|
237
|
+
if q < 0:
|
|
238
|
+
return None
|
|
239
|
+
msg_len, k = _read_varint(buf, q + 1)
|
|
240
|
+
if (k + 2 < n and buf[k] == 0x08 and buf[k + 1] < n_datasets
|
|
241
|
+
and buf[k + 2] in (0x18, 0x22, 0x2a)):
|
|
242
|
+
break
|
|
243
|
+
msg = buf[k:k + msg_len]
|
|
244
|
+
seed = 0
|
|
245
|
+
src = None
|
|
246
|
+
m = 0
|
|
247
|
+
while m < len(msg):
|
|
248
|
+
tag, m = _read_varint(msg, m)
|
|
249
|
+
f = tag >> 3
|
|
250
|
+
wt = tag & 7
|
|
251
|
+
if wt == 0:
|
|
252
|
+
v, m = _read_varint(msg, m)
|
|
253
|
+
if f == 3:
|
|
254
|
+
seed = _zigzag(v)
|
|
255
|
+
elif wt == 2:
|
|
256
|
+
l, m = _read_varint(msg, m)
|
|
257
|
+
if f in (4, 5):
|
|
258
|
+
src = msg[m:m + l]
|
|
259
|
+
m += l
|
|
260
|
+
else:
|
|
261
|
+
break
|
|
262
|
+
if src is None:
|
|
263
|
+
return None
|
|
264
|
+
deltas = _decode_varints_zigzag(np.frombuffer(src, dtype=np.uint8))
|
|
265
|
+
vals = np.empty(deltas.shape[0] + 1, dtype=np.int64)
|
|
266
|
+
vals[0] = seed
|
|
267
|
+
np.cumsum(deltas, out=vals[1:])
|
|
268
|
+
vals[1:] += seed
|
|
269
|
+
return vals
|
|
270
|
+
|
|
271
|
+
def _decode_region(fh, offsets, region_start, region_end, page, n_datasets):
|
|
272
|
+
fh.seek(region_start)
|
|
273
|
+
clean = _strip_markers(fh.read(region_end - region_start), region_start, page)
|
|
274
|
+
buf = clean.tobytes()
|
|
275
|
+
n = len(buf)
|
|
276
|
+
parts = []
|
|
277
|
+
for phys in offsets:
|
|
278
|
+
vals = _decode_data_block(buf, _clean_pos(phys, region_start, page), n, n_datasets)
|
|
279
|
+
if vals is not None:
|
|
280
|
+
parts.append(vals)
|
|
281
|
+
if not parts:
|
|
282
|
+
return np.zeros(0, dtype=np.int64)
|
|
283
|
+
return np.concatenate(parts)
|
|
284
|
+
|
|
285
|
+
def has_dual_tdc(path):
|
|
286
|
+
names = set(dataset_names(path))
|
|
287
|
+
return '/start/time' in names and '/stop/time' in names
|
|
288
|
+
|
|
289
|
+
def read_photons(path, wanted=('x', 'y', 'dt', 'ms')):
|
|
290
|
+
header = read_header(path)
|
|
291
|
+
page = header['page_size']
|
|
292
|
+
datasets = header['datasets']
|
|
293
|
+
n_datasets = len(datasets)
|
|
294
|
+
id_by_name = {d['name']: i for i, d in enumerate(datasets)}
|
|
295
|
+
dtype_by_id = {i: d['dtype'] for i, d in enumerate(datasets)}
|
|
296
|
+
import os
|
|
297
|
+
filesize = os.path.getsize(path)
|
|
298
|
+
with open(path, 'rb') as fh:
|
|
299
|
+
index_off = _read_epilogue(fh, filesize, page)
|
|
300
|
+
offsets = _parse_index(fh, index_off, filesize, page, n_datasets)
|
|
301
|
+
def decode(full):
|
|
302
|
+
did = id_by_name.get(full)
|
|
303
|
+
if did is None or did not in offsets or not offsets[did]:
|
|
304
|
+
return None
|
|
305
|
+
v = offsets[did]
|
|
306
|
+
start, end = min(v), min(index_off, max(v) + 65536)
|
|
307
|
+
vals = _decode_region(fh, v, start, end, page, n_datasets)
|
|
308
|
+
return vals.astype(dtype_by_id.get(did) or np.int64, copy=False)
|
|
309
|
+
# newer LINCam files carry two TDCs: dt = photon /stop/time - laser /start/time
|
|
310
|
+
dual = '/start/time' in id_by_name and '/stop/time' in id_by_name
|
|
311
|
+
out = {}
|
|
312
|
+
for name in wanted:
|
|
313
|
+
if name == 'dt' and dual:
|
|
314
|
+
start_t = decode('/start/time')
|
|
315
|
+
stop_t = decode('/stop/time')
|
|
316
|
+
if start_t is not None and stop_t is not None:
|
|
317
|
+
m = min(start_t.shape[0], stop_t.shape[0])
|
|
318
|
+
out['dt'] = (stop_t[:m].astype(np.int64) - start_t[:m].astype(np.int64))
|
|
319
|
+
continue
|
|
320
|
+
vals = decode('/photons/' + name)
|
|
321
|
+
if vals is not None:
|
|
322
|
+
out[name] = vals
|
|
323
|
+
return out
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Read Photonscore LINCam .photons files.
|
|
2
|
+
|
|
3
|
+
Photonscore LINCam detectors write time- and space-resolved single-photon data
|
|
4
|
+
to a `.photons` file, the D7 container. This is a pure-Python reader for it:
|
|
5
|
+
numpy only, with optional numba acceleration for the varint decode.
|
|
6
|
+
|
|
7
|
+
The D7 container is a paged file (16 KB pages, each carrying a 2-byte page
|
|
8
|
+
marker), holding a protobuf-style header, a per-dataset index, and an epilogue.
|
|
9
|
+
Each photon dataset (`/photons/x`, `/photons/y`, `/photons/dt`, `/photons/ms`,
|
|
10
|
+
and on dual-TDC detectors `/start/time` and `/stop/time`) is stored as a seed
|
|
11
|
+
value followed by zigzag varint delta blocks. `x`/`y` are detector positions,
|
|
12
|
+
`dt` is the TCSPC micro time, and `ms` is the macro time in milliseconds.
|
|
13
|
+
|
|
14
|
+
The D7 format is documented at https://github.com/photonscore/d7 (Apache-2.0).
|
|
15
|
+
This reader was written from that specification and validated bit-exact against
|
|
16
|
+
the Photonscore SDK. No vendor source is redistributed.
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> from photonsfile import PhotonsFile
|
|
21
|
+
>>> with PhotonsFile('sample.photons') as f: # doctest: +SKIP
|
|
22
|
+
... ph = f.photons() # dict of x, y, dt, ms arrays
|
|
23
|
+
... image = f.image(pixels=512) # (Y, X) intensity image
|
|
24
|
+
... decay = f.decay(bins=256) # TCSPC histogram
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from ._d7 import (
|
|
31
|
+
MAGIC,
|
|
32
|
+
read_header,
|
|
33
|
+
read_attributes,
|
|
34
|
+
read_photons,
|
|
35
|
+
dataset_names,
|
|
36
|
+
has_dual_tdc,
|
|
37
|
+
_HAVE_NUMBA,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = '2026.7.8'
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
'PhotonsFile',
|
|
44
|
+
'imread',
|
|
45
|
+
'read_header',
|
|
46
|
+
'read_attributes',
|
|
47
|
+
'read_photons',
|
|
48
|
+
'dataset_names',
|
|
49
|
+
'has_dual_tdc',
|
|
50
|
+
'have_numba',
|
|
51
|
+
'MAGIC',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
def have_numba():
|
|
55
|
+
"""Return True if the varint decode is numba-accelerated."""
|
|
56
|
+
return _HAVE_NUMBA
|
|
57
|
+
|
|
58
|
+
class PhotonsFile:
|
|
59
|
+
"""A Photonscore LINCam `.photons` (D7) file.
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
filename: Name of the `.photons` file to open.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, filename):
|
|
67
|
+
self.filename = str(filename)
|
|
68
|
+
self.header = read_header(self.filename)
|
|
69
|
+
self.attributes = read_attributes(self.filename)
|
|
70
|
+
self.version = self.header['version']
|
|
71
|
+
self.datasets = [d['name'] for d in self.header['datasets']]
|
|
72
|
+
self.dual_tdc = has_dual_tdc(self.filename)
|
|
73
|
+
self.position_bits = int(self.attributes.get('/photons/PositionBits', 12))
|
|
74
|
+
self.tac_bits = int(self.attributes.get('/photons/TacBits', 12))
|
|
75
|
+
self.position_range = 1 << self.position_bits
|
|
76
|
+
self.tac_range = 1 << self.tac_bits
|
|
77
|
+
self.tac_channel = self.attributes.get('/photons/TacChannel')
|
|
78
|
+
self._photons = None
|
|
79
|
+
|
|
80
|
+
def photons(self, wanted=('x', 'y', 'dt', 'ms')):
|
|
81
|
+
"""Return a dict of per-photon arrays for the requested datasets.
|
|
82
|
+
|
|
83
|
+
Keys are the short names (`'x'`, `'y'`, `'dt'`, `'ms'`). On dual-TDC
|
|
84
|
+
detectors `dt` is `stop - start` in picoseconds and `tac_range` is
|
|
85
|
+
updated from the data.
|
|
86
|
+
"""
|
|
87
|
+
if self._photons is None:
|
|
88
|
+
self._photons = read_photons(self.filename, wanted)
|
|
89
|
+
if self.dual_tdc and 'dt' in self._photons:
|
|
90
|
+
dt = np.asarray(self._photons['dt'])
|
|
91
|
+
dt = dt[dt >= 0]
|
|
92
|
+
if dt.size:
|
|
93
|
+
self.tac_range = int(dt.max()) + 1
|
|
94
|
+
return self._photons
|
|
95
|
+
|
|
96
|
+
def tcspc_resolution(self, bins):
|
|
97
|
+
"""Return the TCSPC bin width in seconds for a given number of bins.
|
|
98
|
+
|
|
99
|
+
Uses the `/photons/TacChannel` attribute (picoseconds per raw dt unit),
|
|
100
|
+
or 1 ps per unit for dual-TDC detectors. Returns 0.0 if unknown.
|
|
101
|
+
"""
|
|
102
|
+
if self.dual_tdc:
|
|
103
|
+
period_s = self.tac_range * 1e-12
|
|
104
|
+
elif self.tac_channel:
|
|
105
|
+
period_s = self.tac_range * float(self.tac_channel) * 1e-12
|
|
106
|
+
else:
|
|
107
|
+
return 0.0
|
|
108
|
+
return period_s / bins if bins else 0.0
|
|
109
|
+
|
|
110
|
+
def _binned(self, pixels, binning):
|
|
111
|
+
s = self.photons()
|
|
112
|
+
x = np.asarray(s['x']).astype(np.int64)
|
|
113
|
+
y = np.asarray(s['y']).astype(np.int64)
|
|
114
|
+
dt = np.asarray(s['dt']).astype(np.int64)
|
|
115
|
+
n = min(x.shape[0], y.shape[0], dt.shape[0])
|
|
116
|
+
x, y, dt = x[:n], y[:n], dt[:n]
|
|
117
|
+
valid = ((x >= 0) & (x < self.position_range)
|
|
118
|
+
& (y >= 0) & (y < self.position_range)
|
|
119
|
+
& (dt >= 0) & (dt < self.tac_range))
|
|
120
|
+
return x[valid], y[valid], dt[valid]
|
|
121
|
+
|
|
122
|
+
def image(self, pixels=512, binning=1):
|
|
123
|
+
"""Return the `(Y, X)` intensity image, photon positions binned to a grid."""
|
|
124
|
+
x, y, _ = self._binned(pixels, binning)
|
|
125
|
+
p = int(pixels)
|
|
126
|
+
xi = (x * p) // self.position_range
|
|
127
|
+
yi = (y * p) // self.position_range
|
|
128
|
+
if binning > 1:
|
|
129
|
+
xi //= binning
|
|
130
|
+
yi //= binning
|
|
131
|
+
p = (p + binning - 1) // binning
|
|
132
|
+
if p == 0:
|
|
133
|
+
return np.zeros((0, 0), dtype=np.uint32)
|
|
134
|
+
flat = yi * p + xi
|
|
135
|
+
return np.bincount(flat, minlength=p * p).reshape(p, p).astype(np.uint32)
|
|
136
|
+
|
|
137
|
+
def decay(self, bins=256):
|
|
138
|
+
"""Return the summed TCSPC histogram of length `bins`."""
|
|
139
|
+
s = self.photons()
|
|
140
|
+
dt = np.asarray(s['dt']).astype(np.int64)
|
|
141
|
+
dt = dt[(dt >= 0) & (dt < self.tac_range)]
|
|
142
|
+
di = (dt * bins) // self.tac_range
|
|
143
|
+
return np.bincount(di, minlength=bins)[:bins].astype(np.uint32)
|
|
144
|
+
|
|
145
|
+
def flim_image(self, pixels=512, bins=256, binning=1):
|
|
146
|
+
"""Return the `(Y, X, H)` FLIM cube: intensity image with a TCSPC axis."""
|
|
147
|
+
x, y, dt = self._binned(pixels, binning)
|
|
148
|
+
p = int(pixels)
|
|
149
|
+
b = int(bins)
|
|
150
|
+
xi = (x * p) // self.position_range
|
|
151
|
+
yi = (y * p) // self.position_range
|
|
152
|
+
di = (dt * b) // self.tac_range
|
|
153
|
+
if binning > 1:
|
|
154
|
+
xi //= binning
|
|
155
|
+
yi //= binning
|
|
156
|
+
p = (p + binning - 1) // binning
|
|
157
|
+
if p == 0 or b == 0:
|
|
158
|
+
return np.zeros((p, p, b), dtype=np.uint32)
|
|
159
|
+
flat = (yi * p + xi) * b + di
|
|
160
|
+
return np.bincount(flat, minlength=p * p * b).reshape(p, p, b).astype(np.uint32)
|
|
161
|
+
|
|
162
|
+
def close(self):
|
|
163
|
+
self._photons = None
|
|
164
|
+
|
|
165
|
+
def __enter__(self):
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def __exit__(self, exc_type, exc, tb):
|
|
169
|
+
self.close()
|
|
170
|
+
|
|
171
|
+
def imread(filename, pixels=512, binning=1):
|
|
172
|
+
"""Read a `.photons` file and return the `(Y, X)` intensity image."""
|
|
173
|
+
with PhotonsFile(filename) as f:
|
|
174
|
+
return f.image(pixels=pixels, binning=binning)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: photonsfile
|
|
3
|
+
Version: 2026.7.8
|
|
4
|
+
Summary: Read Photonscore LINCam .photons (D7) files
|
|
5
|
+
Author: Alex Hunt
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alex1075/photonsfile
|
|
8
|
+
Project-URL: D7 format, https://github.com/photonscore/d7
|
|
9
|
+
Keywords: Photonscore,LINCam,D7,photons,TCSPC,FLIM
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: numba; extra == "all"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: numba; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# photonsfile
|
|
22
|
+
|
|
23
|
+
Pure-Python reader for Photonscore LINCam `.photons` files (the **D7** container).
|
|
24
|
+
numpy only, with optional numba acceleration for the varint decode.
|
|
25
|
+
|
|
26
|
+
Photonscore LINCam is a position-sensitive single-photon detector: every photon
|
|
27
|
+
carries an `(x, y)` detector position, a TCSPC micro time (`dt`), and a macro time
|
|
28
|
+
in milliseconds (`ms`). This library decodes those per-photon streams and can bin
|
|
29
|
+
them into an intensity image, a TCSPC decay, or a `(Y, X, H)` FLIM cube.
|
|
30
|
+
|
|
31
|
+
## The D7 format
|
|
32
|
+
|
|
33
|
+
A `.photons` file is a paged container: 16 KB pages, each carrying a 2-byte page
|
|
34
|
+
marker, holding a protobuf-style header, a per-dataset index, and an epilogue. Each
|
|
35
|
+
dataset (`/photons/x`, `/photons/y`, `/photons/dt`, `/photons/ms`, and on dual-TDC
|
|
36
|
+
detectors `/start/time` and `/stop/time`) is stored as a seed value followed by
|
|
37
|
+
zigzag-varint delta blocks. `dt` to time is calibrated from the file's
|
|
38
|
+
`/photons/TacChannel` attribute (picoseconds per raw unit).
|
|
39
|
+
|
|
40
|
+
The format is documented at [github.com/photonscore/d7](https://github.com/photonscore/d7)
|
|
41
|
+
(Apache-2.0). This reader was written from that specification and **validated
|
|
42
|
+
bit-exact against the Photonscore SDK**. No vendor source is redistributed.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install photonsfile # numpy only
|
|
48
|
+
pip install photonsfile[all] # + numba, for a faster varint decode
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from photonsfile import PhotonsFile, imread
|
|
55
|
+
|
|
56
|
+
with PhotonsFile('sample.photons') as f:
|
|
57
|
+
ph = f.photons() # {'x', 'y', 'dt', 'ms'} arrays, one per photon
|
|
58
|
+
img = f.image(pixels=512) # (Y, X) intensity image
|
|
59
|
+
decay = f.decay(bins=256) # summed TCSPC histogram
|
|
60
|
+
cube = f.flim_image(512, 256) # (Y, X, H) FLIM cube
|
|
61
|
+
dt_res = f.tcspc_resolution(256) # seconds per bin, from TacChannel
|
|
62
|
+
print(f.attributes) # D7 file attributes
|
|
63
|
+
|
|
64
|
+
img = imread('sample.photons') # shortcut for the intensity image
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Low-level access to the decoder is also exported: `read_header`,
|
|
68
|
+
`read_attributes`, `read_photons`, `dataset_names`, `has_dual_tdc`.
|
|
69
|
+
|
|
70
|
+
numba is optional; without it the varint decode uses a vectorised numpy fallback.
|
|
71
|
+
`photonsfile.have_numba()` reports which path is active.
|
|
72
|
+
|
|
73
|
+
## Provenance
|
|
74
|
+
|
|
75
|
+
Reverse-engineered from the public D7 specification and checked bit-exact against
|
|
76
|
+
the Photonscore SDK on a 284-million-photon sample. Thanks to Photonscore for
|
|
77
|
+
open-sourcing the D7 format.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT - see `LICENSE`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
photonsfile/__init__.py
|
|
5
|
+
photonsfile/_d7.py
|
|
6
|
+
photonsfile/photonsfile.py
|
|
7
|
+
photonsfile.egg-info/PKG-INFO
|
|
8
|
+
photonsfile.egg-info/SOURCES.txt
|
|
9
|
+
photonsfile.egg-info/dependency_links.txt
|
|
10
|
+
photonsfile.egg-info/requires.txt
|
|
11
|
+
photonsfile.egg-info/top_level.txt
|
|
12
|
+
tests/test_photonsfile.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
photonsfile
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['setuptools>=61']
|
|
3
|
+
build-backend = 'setuptools.build_meta'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = 'photonsfile'
|
|
7
|
+
version = '2026.7.8'
|
|
8
|
+
description = 'Read Photonscore LINCam .photons (D7) files'
|
|
9
|
+
readme = 'README.md'
|
|
10
|
+
requires-python = '>=3.10'
|
|
11
|
+
license = { text = 'MIT' }
|
|
12
|
+
authors = [{ name = 'Alex Hunt' }]
|
|
13
|
+
keywords = ['Photonscore', 'LINCam', 'D7', 'photons', 'TCSPC', 'FLIM']
|
|
14
|
+
dependencies = ['numpy']
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
all = ['numba']
|
|
18
|
+
test = ['pytest', 'numba']
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = 'https://github.com/alex1075/photonsfile'
|
|
22
|
+
'D7 format' = 'https://github.com/photonscore/d7'
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ['photonsfile']
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import photonsfile
|
|
6
|
+
from photonsfile import PhotonsFile, imread, read_header, have_numba
|
|
7
|
+
|
|
8
|
+
def _find_sample():
|
|
9
|
+
pats = [
|
|
10
|
+
os.path.expanduser('~/Downloads/**/*.photons'),
|
|
11
|
+
os.path.expanduser('~/Downloads/*.photons'),
|
|
12
|
+
os.path.join(os.path.dirname(__file__), 'data', '*.photons'),
|
|
13
|
+
]
|
|
14
|
+
for p in pats:
|
|
15
|
+
hits = sorted(glob.glob(p, recursive=True))
|
|
16
|
+
if hits:
|
|
17
|
+
return hits[0]
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
_SAMPLE = _find_sample()
|
|
21
|
+
|
|
22
|
+
def test_api_surface():
|
|
23
|
+
for name in ('PhotonsFile', 'imread', 'read_header', 'read_attributes',
|
|
24
|
+
'read_photons', 'dataset_names', 'has_dual_tdc', 'MAGIC'):
|
|
25
|
+
assert hasattr(photonsfile, name)
|
|
26
|
+
assert isinstance(have_numba(), bool)
|
|
27
|
+
assert photonsfile.MAGIC == b'D7 Photons Data'
|
|
28
|
+
|
|
29
|
+
@pytest.mark.skipif(_SAMPLE is None, reason='no .photons sample present')
|
|
30
|
+
def test_read_real_sample():
|
|
31
|
+
f = PhotonsFile(_SAMPLE)
|
|
32
|
+
assert f.header['magic'] == 'D7 Photons Data'
|
|
33
|
+
assert f.position_range >= 2
|
|
34
|
+
assert isinstance(f.attributes, dict)
|
|
35
|
+
|
|
36
|
+
ph = f.photons()
|
|
37
|
+
assert 'x' in ph and 'y' in ph and 'dt' in ph
|
|
38
|
+
n = min(len(ph['x']), len(ph['y']), len(ph['dt']))
|
|
39
|
+
assert n > 0
|
|
40
|
+
|
|
41
|
+
img = f.image(pixels=256)
|
|
42
|
+
assert img.shape == (256, 256)
|
|
43
|
+
assert int(img.sum()) > 0
|
|
44
|
+
|
|
45
|
+
decay = f.decay(bins=256)
|
|
46
|
+
assert decay.shape == (256,)
|
|
47
|
+
assert int(decay.sum()) > 0
|
|
48
|
+
|
|
49
|
+
cube = f.flim_image(pixels=128, bins=256)
|
|
50
|
+
assert cube.shape == (128, 128, 256)
|
|
51
|
+
assert int(cube.sum()) > 0
|
|
52
|
+
assert int(cube.sum(axis=2).sum()) == int(f.image(pixels=128).sum())
|
|
53
|
+
|
|
54
|
+
res = f.tcspc_resolution(256)
|
|
55
|
+
assert res >= 0.0
|
|
56
|
+
f.close()
|
|
57
|
+
|
|
58
|
+
@pytest.mark.skipif(_SAMPLE is None, reason='no .photons sample present')
|
|
59
|
+
def test_imread_matches_method():
|
|
60
|
+
img = imread(_SAMPLE, pixels=128)
|
|
61
|
+
assert img.shape == (128, 128)
|
|
62
|
+
assert np.array_equal(img, PhotonsFile(_SAMPLE).image(pixels=128))
|