msr-reader 0.3.0__tar.gz → 0.3.1__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.
- {msr_reader-0.3.0 → msr_reader-0.3.1}/PKG-INFO +9 -1
- {msr_reader-0.3.0 → msr_reader-0.3.1}/README.md +6 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/pyproject.toml +4 -1
- {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/obffile.py +20 -0
- msr_reader-0.3.1/src/msr_reader/xarray_io.py +168 -0
- msr_reader-0.3.1/test.ipynb +797 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/synthetic_obf.py +18 -5
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/test_real_files.py +4 -2
- msr_reader-0.3.1/tests/test_xarray.py +218 -0
- msr_reader-0.3.0/test.ipynb +0 -140
- {msr_reader-0.3.0 → msr_reader-0.3.1}/.gitignore +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/LICENSE +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/__init__.py +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/xml_metadata.py +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/__init__.py +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_4d.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_incomplete_measurement.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_lightbox.obf +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_overwritten_measurement1.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/2889b245416cd9783c627f413caa45c9_overview_1.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/bad44489_field_0.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/bad44489_field_0_sted_0.msr +0 -0
- /msr_reader-0.3.0/tests/data/conf01.msr → /msr_reader-0.3.1/tests/data/conf01_continuous_point_measurement.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/groupa_cell01.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/mesc_yoyo2_img_001.msr +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/test_synthetic_format.py +0 -0
- {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/test_synthetic_grouping.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: msr-reader
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Pure Python reader for MSR/OBF (Imspector) image data
|
|
5
5
|
Project-URL: Homepage, https://github.com/hoerlteam/msr-reader
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/hoerlteam/msr-reader/issues
|
|
@@ -12,6 +12,8 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Requires-Python: >=3.7
|
|
14
14
|
Requires-Dist: numpy
|
|
15
|
+
Provides-Extra: xarray
|
|
16
|
+
Requires-Dist: xarray; extra == 'xarray'
|
|
15
17
|
Description-Content-Type: text/markdown
|
|
16
18
|
|
|
17
19
|
# msr
|
|
@@ -48,6 +50,12 @@ with OBFFile('file_path.msr') as f:
|
|
|
48
50
|
groups = f.stack_groups # list of StackGroup
|
|
49
51
|
img = f.read_stack_group(0) # read all "channels", stacked along a new first axis: e.g. (C, Z, Y, X)
|
|
50
52
|
|
|
53
|
+
# xarray integration (optional dependency: pip install msr-reader[xarray]):
|
|
54
|
+
# stacks as xarray.DataArray with labelled dims and physical coordinates
|
|
55
|
+
# (SI units of the file: meters for spatial axes, seconds for time)
|
|
56
|
+
da = f.read_stack_xr(idx) # dims e.g. ('z', 'y', 'x'), coords in meters
|
|
57
|
+
dag = f.read_stack_group_xr(0) # one DataArray with a leading 'channel' dim, labelled with channel names
|
|
58
|
+
|
|
51
59
|
# stacks without names (sometimes found in old files) can be grouped by geometry alone:
|
|
52
60
|
# with OBFFile('file_path.msr', group_unnamed_stacks=True) as f:
|
|
53
61
|
|
|
@@ -32,6 +32,12 @@ with OBFFile('file_path.msr') as f:
|
|
|
32
32
|
groups = f.stack_groups # list of StackGroup
|
|
33
33
|
img = f.read_stack_group(0) # read all "channels", stacked along a new first axis: e.g. (C, Z, Y, X)
|
|
34
34
|
|
|
35
|
+
# xarray integration (optional dependency: pip install msr-reader[xarray]):
|
|
36
|
+
# stacks as xarray.DataArray with labelled dims and physical coordinates
|
|
37
|
+
# (SI units of the file: meters for spatial axes, seconds for time)
|
|
38
|
+
da = f.read_stack_xr(idx) # dims e.g. ('z', 'y', 'x'), coords in meters
|
|
39
|
+
dag = f.read_stack_group_xr(0) # one DataArray with a leading 'channel' dim, labelled with channel names
|
|
40
|
+
|
|
35
41
|
# stacks without names (sometimes found in old files) can be grouped by geometry alone:
|
|
36
42
|
# with OBFFile('file_path.msr', group_unnamed_stacks=True) as f:
|
|
37
43
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "msr-reader"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="David Hörl", email="hoerldavid@gmail.com" },
|
|
10
10
|
]
|
|
@@ -19,6 +19,9 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = ["numpy"]
|
|
21
21
|
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
xarray = ["xarray"]
|
|
24
|
+
|
|
22
25
|
[project.urls]
|
|
23
26
|
"Homepage" = "https://github.com/hoerlteam/msr-reader"
|
|
24
27
|
"Bug Tracker" = "https://github.com/hoerlteam/msr-reader/issues"
|
|
@@ -556,6 +556,26 @@ class OBFFile:
|
|
|
556
556
|
group = self.stack_groups[group_idx]
|
|
557
557
|
return np.stack([self.read_stack(i) for i in group.indices], axis=axis)
|
|
558
558
|
|
|
559
|
+
def read_stack_xr(self, idx):
|
|
560
|
+
"""
|
|
561
|
+
read stack with index idx into an xarray.DataArray with labelled
|
|
562
|
+
dimensions and physical coordinates (SI units of the file: meters
|
|
563
|
+
for spatial axes, seconds for time). requires the optional xarray
|
|
564
|
+
dependency: pip install msr-reader[xarray]
|
|
565
|
+
"""
|
|
566
|
+
from .xarray_io import stack_to_xarray
|
|
567
|
+
return stack_to_xarray(self, idx)
|
|
568
|
+
|
|
569
|
+
def read_stack_group_xr(self, group_idx, axis=0):
|
|
570
|
+
"""
|
|
571
|
+
read all stacks of a group into one xarray.DataArray, stacked along a
|
|
572
|
+
new 'channel' axis (at position axis, default first) labelled with the
|
|
573
|
+
channel names. requires the optional xarray dependency:
|
|
574
|
+
pip install msr-reader[xarray]
|
|
575
|
+
"""
|
|
576
|
+
from .xarray_io import stack_group_to_xarray
|
|
577
|
+
return stack_group_to_xarray(self, group_idx, axis=axis)
|
|
578
|
+
|
|
559
579
|
def get_imspector_xml_metadata(self, stack_idx):
|
|
560
580
|
"""
|
|
561
581
|
get XML metadata string of stack with index stack_idx
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'''
|
|
2
|
+
xarray integration: read stacks and stack groups as labelled xarray.DataArray.
|
|
3
|
+
|
|
4
|
+
xarray is an optional dependency (pip install msr-reader[xarray]); this
|
|
5
|
+
module is only imported when the read_stack_xr/read_stack_group_xr methods
|
|
6
|
+
of OBFFile are called, so msr_reader itself keeps working without it.
|
|
7
|
+
|
|
8
|
+
Conventions:
|
|
9
|
+
- dims are normalized from the Imspector dimension labels ('ExpControl X'
|
|
10
|
+
-> 'x', 'TimeLapse T'/'ExpControl T' -> 't'); unlabelled axes fall back to
|
|
11
|
+
the positional OBF convention (file axes 0-3 are x/y/z/t, later axes
|
|
12
|
+
'dim_<n>'). the original label is kept in the coord attrs.
|
|
13
|
+
- coordinates are physical positions in the axis SI unit (meters, seconds),
|
|
14
|
+
computed from the axis offset/length; column positions from the footer, if
|
|
15
|
+
present, are used instead (non-uniform axes). the unit string is in the
|
|
16
|
+
coord attrs as 'units' (CF-style), the axis step as 'pixel_size'.
|
|
17
|
+
- size-1 axes are dropped, consistent with read_stack()/shapes.
|
|
18
|
+
- a group is returned as one DataArray with a 'channel' dim labelled with
|
|
19
|
+
the channel names, mirroring read_stack_group().
|
|
20
|
+
'''
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import xarray as xr
|
|
27
|
+
except ImportError:
|
|
28
|
+
xr = None
|
|
29
|
+
|
|
30
|
+
from .obffile import _parse_dtype
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _require_xarray():
|
|
34
|
+
if xr is None:
|
|
35
|
+
raise ImportError(
|
|
36
|
+
'xarray is required for the read_stack_xr/read_stack_group_xr methods; '
|
|
37
|
+
'install it with: pip install msr-reader[xarray]'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# positional fallback names for unlabelled axes: OBF stores the axes with X
|
|
42
|
+
# first (fastest-varying), so file axis 0/1/2/3 conventionally is x/y/z/t
|
|
43
|
+
_POSITION_NAMES = ('x', 'y', 'z', 't')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _dim_name(label, file_axis, used):
|
|
47
|
+
'normalized xarray dim name from an Imspector dimension label'
|
|
48
|
+
name = None
|
|
49
|
+
if label and label != 'None':
|
|
50
|
+
token = re.sub(r'[^a-z0-9_]', '_', label.split()[-1].lower())
|
|
51
|
+
if token and not token[0].isdigit():
|
|
52
|
+
name = token
|
|
53
|
+
if name is None:
|
|
54
|
+
name = _POSITION_NAMES[file_axis] if file_axis < len(_POSITION_NAMES) else f'dim_{file_axis}'
|
|
55
|
+
if name in used:
|
|
56
|
+
base, candidate, n = name, name, 1
|
|
57
|
+
while candidate in used:
|
|
58
|
+
candidate = f'{base}_{n}'
|
|
59
|
+
n += 1
|
|
60
|
+
name = candidate
|
|
61
|
+
used.add(name)
|
|
62
|
+
return name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _stack_dims_coords(obf, idx):
|
|
66
|
+
'''
|
|
67
|
+
(dims, coords, has_samples_axis) for stack idx, in numpy axis order.
|
|
68
|
+
coords maps dim name -> (dims, values, attrs) tuples. size-1 axes are
|
|
69
|
+
dropped, consistent with read_stack()/shapes.
|
|
70
|
+
'''
|
|
71
|
+
header = obf.stack_headers[idx]
|
|
72
|
+
footer = obf.stack_footers[idx]
|
|
73
|
+
rank = len(header.size)
|
|
74
|
+
# kept axes (size > 1), in numpy order (file order is reversed)
|
|
75
|
+
kept = [i for i in range(rank) if header.size[i] > 1][::-1]
|
|
76
|
+
|
|
77
|
+
dims = []
|
|
78
|
+
coords = {}
|
|
79
|
+
used = set()
|
|
80
|
+
for fi in kept:
|
|
81
|
+
label = footer.dimension_labels[fi]
|
|
82
|
+
name = _dim_name(label, fi, used)
|
|
83
|
+
dims.append(name)
|
|
84
|
+
n = header.size[fi]
|
|
85
|
+
|
|
86
|
+
attrs = {}
|
|
87
|
+
if label:
|
|
88
|
+
attrs['imspector_label'] = label
|
|
89
|
+
if footer.si_dimensions is not None:
|
|
90
|
+
attrs['units'] = str(footer.si_dimensions[fi])
|
|
91
|
+
|
|
92
|
+
if footer.col_positions is not None and footer.col_positions[fi] is not None:
|
|
93
|
+
# explicit per-sample positions (non-uniform axis), in axis units
|
|
94
|
+
values = np.asarray(footer.col_positions[fi], dtype=np.float64)
|
|
95
|
+
else:
|
|
96
|
+
step = header.length[fi] / n
|
|
97
|
+
# physical center of sample k along the axis, per the OBF spec:
|
|
98
|
+
# off[i] + (0.5 + k) * len[i] / res[i]
|
|
99
|
+
values = header.offset[fi] + (0.5 + np.arange(n, dtype=np.float64)) * step
|
|
100
|
+
attrs['pixel_size'] = step
|
|
101
|
+
coords[name] = ((name,), values, attrs)
|
|
102
|
+
|
|
103
|
+
if footer.col_labels is not None and footer.col_labels[fi] is not None:
|
|
104
|
+
coords[f'{name}_labels'] = ((name,), list(footer.col_labels[fi]), {})
|
|
105
|
+
|
|
106
|
+
# RGB-style stacks carry an extra trailing samples axis
|
|
107
|
+
try:
|
|
108
|
+
samples_per_pixel = _parse_dtype(header.dtype)[1]
|
|
109
|
+
except ValueError:
|
|
110
|
+
samples_per_pixel = 1
|
|
111
|
+
|
|
112
|
+
return dims, coords, samples_per_pixel > 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _stack_attrs(obf, idx):
|
|
116
|
+
header = obf.stack_headers[idx]
|
|
117
|
+
# same placeholder as for group channel names of unnamed stacks
|
|
118
|
+
attrs = {'stack_name': header.name if header.name else f'stack {idx}'}
|
|
119
|
+
series, channel = obf._parse_stack_name(idx)
|
|
120
|
+
if channel is not None:
|
|
121
|
+
attrs['series'] = series
|
|
122
|
+
attrs['channel_name'] = channel
|
|
123
|
+
return attrs
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def stack_to_xarray(obf, idx):
|
|
127
|
+
"""
|
|
128
|
+
read stack idx into an xarray.DataArray with labelled dims and physical
|
|
129
|
+
coordinates (SI units of the file: meters for spatial axes, seconds for
|
|
130
|
+
time). requires the optional xarray dependency.
|
|
131
|
+
"""
|
|
132
|
+
_require_xarray()
|
|
133
|
+
data = obf.read_stack(idx)
|
|
134
|
+
dims, coords, has_samples_axis = _stack_dims_coords(obf, idx)
|
|
135
|
+
if has_samples_axis:
|
|
136
|
+
dims = dims + ['samples']
|
|
137
|
+
coords['samples'] = (('samples',), np.arange(data.shape[-1]), {})
|
|
138
|
+
attrs = _stack_attrs(obf, idx)
|
|
139
|
+
name = attrs.get('channel_name', attrs['stack_name'])
|
|
140
|
+
return xr.DataArray(data, dims=dims, coords=coords, attrs=attrs, name=name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def stack_group_to_xarray(obf, group_idx, axis=0):
|
|
144
|
+
"""
|
|
145
|
+
read all stacks of a stack group into one xarray.DataArray, stacked along
|
|
146
|
+
a new 'channel' axis (at position axis, default first) labelled with the
|
|
147
|
+
channel names. requires the optional xarray dependency.
|
|
148
|
+
"""
|
|
149
|
+
_require_xarray()
|
|
150
|
+
group = obf.stack_groups[group_idx]
|
|
151
|
+
data = obf.read_stack_group(group_idx, axis=axis)
|
|
152
|
+
dims, coords, has_samples_axis = _stack_dims_coords(obf, group.indices[0])
|
|
153
|
+
if has_samples_axis:
|
|
154
|
+
dims = dims + ['samples']
|
|
155
|
+
coords['samples'] = (('samples',), np.arange(data.shape[-1]), {})
|
|
156
|
+
# normalize axis the same way np.stack does for the stacked result
|
|
157
|
+
axis = axis % (len(dims) + 1)
|
|
158
|
+
channel_dim = 'channel'
|
|
159
|
+
n = 1
|
|
160
|
+
while channel_dim in dims:
|
|
161
|
+
channel_dim = f'channel_{n}'
|
|
162
|
+
n += 1
|
|
163
|
+
dims.insert(axis, channel_dim)
|
|
164
|
+
coords[channel_dim] = ((channel_dim,), np.array(group.channel_names), {})
|
|
165
|
+
attrs = {'series': group.series,
|
|
166
|
+
'stack_names': [obf.stack_names[i] for i in group.indices]}
|
|
167
|
+
name = str(group.series) if group.series is not None else f'group_{group_idx}'
|
|
168
|
+
return xr.DataArray(data, dims=dims, coords=coords, attrs=attrs, name=name)
|