Skip to content

Commit 3fe634d

Browse files
Update MSR file wrapper specification (#139)
* Change msr sample collection for per * Update msr sample collection: added optional args, merged with other class. * Fix type annotations and docstring issues in MSRSampleCollection - Use Optional/Tuple from typing instead of built-in generics - Add type annotations to dtype and shape constructor params - Fix StackSelection | None annotation on read_sample - Fix Note section to accurately describe shape mismatch risk - Fix Sequence[int]/Sequence[str] in prose (was tuple[int]/tuple[str]) - Clarify that file read is skipped only when both dtype and shape are given - Revert E203 slice spacing change in MSRDataset.__getitem__ --------- Co-authored-by: Oleh.Bakumenko <oleh.bakumenko@itmp.fraunhofer.de>
1 parent d492209 commit 3fe634d

1 file changed

Lines changed: 64 additions & 14 deletions

File tree

elf/io/msr_wrapper.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
from collections.abc import Mapping, Sequence
3-
from typing import Union
3+
from typing import Optional, Tuple, Union
44

55
import numpy as np
66

@@ -41,7 +41,7 @@ def _read_msr_stack(msr_path: PathLike, stack_index: int = 0) -> np.ndarray:
4141
return image
4242

4343

44-
def _normalize_stack_selection(stack_selection: StackSelection) -> tuple[Union[int, str], ...]:
44+
def _normalize_stack_selection(stack_selection: StackSelection) -> Tuple[Union[int, str], ...]:
4545
if isinstance(stack_selection, (str, int)):
4646
return (stack_selection,)
4747
stack_selection = tuple(stack_selection)
@@ -50,7 +50,7 @@ def _normalize_stack_selection(stack_selection: StackSelection) -> tuple[Union[i
5050
return stack_selection
5151

5252

53-
def _normalize_stack_indices(stack_index: StackIndexSelection = 0) -> tuple[int, ...]:
53+
def _normalize_stack_indices(stack_index: StackIndexSelection = 0) -> Tuple[int, ...]:
5454
if isinstance(stack_index, int):
5555
return (stack_index,)
5656
stack_index = tuple(stack_index)
@@ -61,7 +61,7 @@ def _normalize_stack_indices(stack_index: StackIndexSelection = 0) -> tuple[int,
6161
return stack_index
6262

6363

64-
def _normalize_stack_names(stack_names: StackNameSelection) -> tuple[str, ...]:
64+
def _normalize_stack_names(stack_names: StackNameSelection) -> Tuple[str, ...]:
6565
if isinstance(stack_names, str):
6666
return (stack_names,)
6767
stack_names = tuple(stack_names)
@@ -72,7 +72,7 @@ def _normalize_stack_names(stack_names: StackNameSelection) -> tuple[str, ...]:
7272
return stack_names
7373

7474

75-
def _resolve_stack_indices(msr, stack_selection: StackSelection) -> tuple[int, ...]:
75+
def _resolve_stack_indices(msr, stack_selection: StackSelection) -> Tuple[int, ...]:
7676
resolved = []
7777
for stack in _normalize_stack_selection(stack_selection):
7878
if isinstance(stack, int):
@@ -82,27 +82,57 @@ def _resolve_stack_indices(msr, stack_selection: StackSelection) -> tuple[int, .
8282
return tuple(resolved)
8383

8484

85-
def _get_msr_stack_shape(msr_path: PathLike, stack_selection: StackSelection = 0) -> tuple[int, int]:
85+
def _get_msr_stack_shape(msr_path: PathLike, stack_selection: StackSelection = 0) -> Tuple[int, int]:
8686
_require_msr_reader()
8787
with OBFFile(os.fspath(msr_path)) as msr:
8888
stack_index = _resolve_stack_indices(msr, stack_selection)[0]
8989
return tuple(_read_msr_stack(msr_path, stack_index=stack_index).shape)
9090

9191

92-
def _resolve_stack_indices_for_path(msr_path: PathLike, stack_selection: StackSelection) -> tuple[int, ...]:
92+
def _resolve_stack_indices_for_path(msr_path: PathLike, stack_selection: StackSelection) -> Tuple[int, ...]:
9393
_require_msr_reader()
9494
with OBFFile(os.fspath(msr_path)) as msr:
9595
return _resolve_stack_indices(msr, stack_selection)
9696

9797

9898
class MSRSampleCollection:
99-
"""Collection-like helper for loading one sample per MSR file."""
99+
"""Collection-like helper for loading one sample per MSR file.
100+
101+
Each MSR file in image_paths is treated as one sample. Stacks to load are specified globally
102+
via stack_index (integer | Sequence[int]) or stack_names (str | Sequence[str]);
103+
these are mutually exclusive. The resolved selection is stored as self.stack_selection and used
104+
by default in read_sample.
105+
106+
dtype and shape are inferred by loading the first file unless both are provided explicitly.
107+
108+
For datasets where stack order or naming is inconsistent across files, read_sample accepts a
109+
per-call stack_selection that overrides self.stack_selection for that single read. This covers
110+
the case where e.g. channel 0 in one file is a different marker than channel 0 in another, or
111+
where stack names contain typos or extra suffixes that differ per file.
112+
113+
Args:
114+
image_paths: Paths to the .msr files.
115+
stack_index: Integer or sequence of integers selecting stacks by position. Default 0.
116+
stack_names: String or sequence of strings selecting stacks by name. Mutually exclusive
117+
with stack_index.
118+
dtype: numpy dtype of the loaded arrays. Inferred from the first file if None.
119+
shape: Shape of one sample as a tuple. Inferred from the first file if None.
120+
For a single stack this is (H, W); for multiple stacks it is (C, H, W).
121+
122+
Note:
123+
When read_sample is called with a stack_selection that selects a different number of
124+
channels than the constructor, the returned array shape will not match self.shape.
125+
Similarly, if dtype or shape are provided explicitly but do not match the actual file
126+
contents, self.dtype and self.shape will silently diverge from what read_sample returns.
127+
"""
100128

101129
def __init__(
102130
self,
103131
image_paths: Sequence[PathLike],
104132
stack_index: StackIndexSelection = 0,
105133
stack_names: StackNameSelection | None = None,
134+
dtype: Optional[np.dtype] = None,
135+
shape: Optional[Tuple[int, ...]] = None,
106136
):
107137
self.image_paths = [os.fspath(path) for path in image_paths]
108138
if stack_names is not None and stack_index != 0:
@@ -111,10 +141,17 @@ def __init__(
111141
self.stack_selection = _normalize_stack_names(stack_names)
112142
else:
113143
self.stack_selection = _normalize_stack_indices(stack_index)
114-
self.stack_indices = _resolve_stack_indices_for_path(self.image_paths[0], self.stack_selection)
115-
sample = _read_msr_stack(self.image_paths[0], self.stack_indices[0])
116-
self._dtype = sample.dtype
117-
self._shape = sample.shape if len(self.stack_indices) == 1 else (len(self.stack_indices),) + tuple(sample.shape)
144+
145+
if dtype is None or shape is None:
146+
stack_indices = _resolve_stack_indices_for_path(self.image_paths[0], self.stack_selection)
147+
sample = _read_msr_stack(self.image_paths[0], stack_indices[0])
148+
if dtype is None:
149+
dtype = sample.dtype
150+
if shape is None:
151+
shape = sample.shape if len(stack_indices) == 1 else (len(stack_indices),) + tuple(sample.shape)
152+
153+
self._dtype = dtype
154+
self._shape = shape
118155

119156
@property
120157
def dtype(self):
@@ -140,8 +177,21 @@ def size(self):
140177
def attrs(self):
141178
return {}
142179

143-
def read_sample(self, index: int) -> np.ndarray:
144-
stack_indices = _resolve_stack_indices_for_path(self.image_paths[index], self.stack_selection)
180+
def read_sample(self, index: int, stack_selection: Optional[StackSelection] = None) -> np.ndarray:
181+
"""Load one sample from image_paths[index].
182+
183+
Args:
184+
index: Position in image_paths to read.
185+
stack_selection: Optional override for which stacks to load from this specific file.
186+
Accepts the same forms as the constructor (int, str, or sequences thereof).
187+
When provided, self.stack_selection is ignored for this call. Useful when stack
188+
order or names differ per file.
189+
190+
Returns:
191+
numpy array of shape (H, W) for a single stack, or (C, H, W) for multiple stacks.
192+
"""
193+
selection = self.stack_selection if stack_selection is None else stack_selection
194+
stack_indices = _resolve_stack_indices_for_path(self.image_paths[index], selection)
145195
if len(stack_indices) == 1:
146196
return _read_msr_stack(self.image_paths[index], stack_indices[0])
147197
data = [_read_msr_stack(self.image_paths[index], stack_index) for stack_index in stack_indices]

0 commit comments

Comments
 (0)