11import os
22from collections .abc import Mapping , Sequence
3- from typing import Union
3+ from typing import Optional , Tuple , Union
44
55import 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
9898class 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