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.
Files changed (27) hide show
  1. {msr_reader-0.3.0 → msr_reader-0.3.1}/PKG-INFO +9 -1
  2. {msr_reader-0.3.0 → msr_reader-0.3.1}/README.md +6 -0
  3. {msr_reader-0.3.0 → msr_reader-0.3.1}/pyproject.toml +4 -1
  4. {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/obffile.py +20 -0
  5. msr_reader-0.3.1/src/msr_reader/xarray_io.py +168 -0
  6. msr_reader-0.3.1/test.ipynb +797 -0
  7. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/synthetic_obf.py +18 -5
  8. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/test_real_files.py +4 -2
  9. msr_reader-0.3.1/tests/test_xarray.py +218 -0
  10. msr_reader-0.3.0/test.ipynb +0 -140
  11. {msr_reader-0.3.0 → msr_reader-0.3.1}/.gitignore +0 -0
  12. {msr_reader-0.3.0 → msr_reader-0.3.1}/LICENSE +0 -0
  13. {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/__init__.py +0 -0
  14. {msr_reader-0.3.0 → msr_reader-0.3.1}/src/msr_reader/xml_metadata.py +0 -0
  15. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/__init__.py +0 -0
  16. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_4d.msr +0 -0
  17. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_incomplete_measurement.msr +0 -0
  18. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_lightbox.obf +0 -0
  19. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/20261019tests_overwritten_measurement1.msr +0 -0
  20. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/2889b245416cd9783c627f413caa45c9_overview_1.msr +0 -0
  21. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/bad44489_field_0.msr +0 -0
  22. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/bad44489_field_0_sted_0.msr +0 -0
  23. /msr_reader-0.3.0/tests/data/conf01.msr → /msr_reader-0.3.1/tests/data/conf01_continuous_point_measurement.msr +0 -0
  24. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/groupa_cell01.msr +0 -0
  25. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/data/mesc_yoyo2_img_001.msr +0 -0
  26. {msr_reader-0.3.0 → msr_reader-0.3.1}/tests/test_synthetic_format.py +0 -0
  27. {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.0
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.0"
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)