66"""
77import gzip
88import logging
9- from typing import Any
9+ from typing import Any , Literal , TYPE_CHECKING
1010from typing_extensions import override
1111from collections .abc import Sequence
1212
1616import albumentations
1717
1818from medimgkit .readers import read_array_normalized
19+ from medimgkit import dicom_utils
20+ from medimgkit import nifti_utils
1921
2022from .base import DatamintBaseDataset
2123from .annotation_processor import AnnotationProcessor , MergeStrategy
2224
23- from datamint .entities import Annotation , Resource
2425from datamint .entities .cache_manager import CacheManager
26+ if TYPE_CHECKING :
27+ from datamint .entities import Annotation , Resource
2528
2629_LOGGER = logging .getLogger (__name__ )
2730
@@ -52,7 +55,7 @@ class SlicedVolumeResource:
5255
5356 def __init__ (
5457 self ,
55- parent : Resource ,
58+ parent : ' Resource' ,
5659 slice_index : int ,
5760 slice_axis : int ,
5861 sliced_vols_cache : CacheManager ,
@@ -123,7 +126,7 @@ def fetch_slice_data(self) -> np.ndarray:
123126 return sliced
124127
125128 @property
126- def parent_resource (self ) -> Resource :
129+ def parent_resource (self ) -> ' Resource' :
127130 """The original volume Resource being proxied."""
128131 return self ._parent
129132
@@ -136,14 +139,14 @@ def __repr__(self) -> str:
136139 )
137140
138141
139- # Axis mapping for anatomical orientations
140- SLICE_AXIS_MAP = {
141- 'axial' : 0 , # slicing along depth (superior-inferior)
142- 'coronal' : 1 , # slicing along height (anterior-posterior)
143- 'sagittal' : 2 , # slicing along width (left-right)
144- }
142+ # # Axis mapping for anatomical orientations
143+ # SLICE_AXIS_MAP = {
144+ # 'axial': 0, # slicing along depth (superior-inferior)
145+ # 'coronal': 1, # slicing along height (anterior-posterior)
146+ # 'sagittal': 2, # slicing along width (left-right)
147+ # }
145148
146- _AXIS_INT_TO_NAME = {v : k for k , v in SLICE_AXIS_MAP .items ()}
149+ # _AXIS_INT_TO_NAME = {v: k for k, v in SLICE_AXIS_MAP.items()}
147150
148151
149152class SlicedVolumeDataset (DatamintBaseDataset ):
@@ -166,26 +169,25 @@ class SlicedVolumeDataset(DatamintBaseDataset):
166169 def __init__ (
167170 self ,
168171 parent_dataset : 'DatamintBaseDataset' ,
169- slice_axis : str | int = 'axial' ,
172+ slice_axis : Literal [ 'axial' , 'coronal' , 'sagittal' ] | int = 'axial' ,
170173 ):
171174 # We intentionally do NOT call super().__init__() because that
172175 # requires project/API interaction. Instead, copy needed state
173176 # from the parent dataset.
174177
175178 # --- Resolve axis ---
176179 if isinstance (slice_axis , str ):
177- if slice_axis not in SLICE_AXIS_MAP :
180+ valid_slice_axis = ['axial' , 'coronal' , 'sagittal' ]
181+ if slice_axis not in valid_slice_axis :
178182 raise ValueError (
179183 f"Unknown axis '{ slice_axis } '. "
180- f"Must be one of { list ( SLICE_AXIS_MAP . keys ()) } or an int 0-2."
184+ f"Must be one of { valid_slice_axis } or an int 0-2."
181185 )
182- self ._slice_axis_int = SLICE_AXIS_MAP [slice_axis ]
183186 self ._slice_axis = slice_axis
184187 else :
185188 if not (0 <= slice_axis <= 2 ):
186189 raise ValueError (f"axis must be 0, 1, or 2, got { slice_axis } " )
187190 self ._slice_axis_int = slice_axis
188- self ._slice_axis = _AXIS_INT_TO_NAME .get (slice_axis , str (slice_axis ))
189191
190192 self .project = parent_dataset .project
191193
@@ -235,17 +237,30 @@ def __init__(
235237 self .resources = expanded_resources # type: ignore[assignment]
236238 self .resource_annotations = expanded_annotations
237239
238- _LOGGER .info (
239- f"Created SlicedVolumeDataset with { len (self .resources )} slices "
240- f"from { len (parent_dataset .resources )} volumes (axis={ self ._slice_axis } )"
241- )
240+ @staticmethod
241+ def _get_slice_axis_int (r : 'Resource' ,
242+ slice_axis : Literal ['axial' , 'coronal' , 'sagittal' ]) -> int :
243+ if r .is_dicom ():
244+ dicom_data = r .fetch_file_data (auto_convert = True , use_cache = True )
245+ ret = dicom_utils .get_plane_axis (dicom_data , plane = slice_axis )
246+ if ret is None :
247+ raise ValueError (f"Could not determine slice axis for DICOM resource { r .id } with plane '{ slice_axis } '" )
248+ return ret
249+ elif r .is_nifti ():
250+ nifti_data = r .fetch_file_data (auto_convert = True , use_cache = True )
251+ ret = nifti_utils .get_plane_axis (nifti_data , plane = slice_axis )
252+ if ret is None :
253+ raise ValueError (f"Could not determine slice axis for NIfTI resource { r .id } with plane '{ slice_axis } '" )
254+ return ret
255+ else :
256+ raise ValueError (f"Unsupported resource type for slice axis inference: { r .filename } | { r .mimetype } " )
242257
243258 def _expand_resources (
244259 self ,
245- resources : Sequence [Resource ],
246- resource_annotations : Sequence [Sequence [Annotation ]],
260+ resources : Sequence [' Resource' ],
261+ resource_annotations : Sequence [Sequence [' Annotation' ]],
247262 volume_cache : CacheManager ,
248- ) -> tuple [list [SlicedVolumeResource ], list [Sequence [Annotation ]]]:
263+ ) -> tuple [list [SlicedVolumeResource ], list [Sequence [' Annotation' ]]]:
249264 """Expand volume resources into per-slice proxy resources.
250265
251266 Args:
@@ -256,27 +271,36 @@ def _expand_resources(
256271 Returns:
257272 Tuple of (sliced_resources, sliced_annotations).
258273 """
259- axis_int = self ._slice_axis_int
260274 sliced_resources : list [SlicedVolumeResource ] = []
261- sliced_annotations : list [Sequence [Annotation ]] = []
262-
263- for i , resource in enumerate (resources ):
264- # Determine number of slices along the requested axis
265- if axis_int == 0 :
266- # Axial: use metadata (no full load needed)
267- num_slices = resource .get_depth ()
275+ sliced_annotations : list [Sequence ['Annotation' ]] = []
276+
277+ for i , r in enumerate (resources ):
278+ if not hasattr (self , '_slice_axis_int' ):
279+ res_data = r .fetch_file_data (auto_convert = True , use_cache = True )
280+ if r .is_dicom ():
281+ axis_int = dicom_utils .get_plane_axis (res_data , plane = self ._slice_axis )
282+
283+ if axis_int is None :
284+ raise ValueError (
285+ f"Could not determine slice axis for DICOM resource { r .id } with plane '{ self ._slice_axis } '" )
286+ axis_size = dicom_utils .get_dim_size (res_data , axis_int )
287+ elif r .is_nifti ():
288+ axis_int = nifti_utils .get_plane_axis (res_data , plane = self ._slice_axis )
289+ if axis_int is None :
290+ raise ValueError (
291+ f"Could not determine slice axis for NIfTI resource { r .id } with plane '{ self ._slice_axis } '" )
292+ axis_size = nifti_utils .get_dim_size (res_data , axis_int )
293+ else :
294+ raise ValueError (f"Unsupported resource type for slice axis inference: { r .filename } | { r .mimetype } " )
268295 else :
269- # Coronal/Sagittal: parse volume once to infer spatial dims
270- raw = resource .fetch_file_data (auto_convert = False , use_cache = True )
271- vol , _meta = read_array_normalized (raw , return_metainfo = True )
272- vol = vol .transpose (1 , 0 , 2 , 3 )
273- num_slices = vol .shape [axis_int + 1 ]
296+ # TODO
297+ raise NotImplementedError
274298
275299 anns = resource_annotations [i ]
276300
277- for s in range (num_slices ):
301+ for s in range (axis_size ):
278302 sliced_resources .append (
279- SlicedVolumeResource (resource , s , axis_int , volume_cache )
303+ SlicedVolumeResource (r , s , axis_int , volume_cache )
280304 )
281305 sliced_annotations .append (anns )
282306
@@ -328,6 +352,7 @@ def __getitem__(self, index: int) -> dict[str, Any]:
328352 _LOGGER .debug (f"Loaded slice { resource .slice_index } from { resource .filename } with shape { img .shape } " )
329353
330354 # Process segmentations
355+ # FIXME: This currently re-loads the slice data for each segmentation annotation, which is inefficient. We should ideally load the slice once and reuse it for all segmentations. This may require refactoring how annotations are processed to avoid redundant data loading.
331356 if self .return_segmentations :
332357 seg_anns = AnnotationProcessor .filter_annotations (
333358 annotations , type = 'segmentation' , scope = 'all'
@@ -341,7 +366,8 @@ def __getitem__(self, index: int) -> dict[str, Any]:
341366 for author , seg_array in segmentations .items ():
342367 # seg_array shape: (#instances, D, H, W)
343368 # Select the slice: (#instances, H, W)
344- sliced_segs [author ] = np .take (seg_array , slice_idx , axis = self ._slice_axis_int + 1 )
369+ axis_index = resource .slice_axis + 1 # account for instance dimension
370+ sliced_segs [author ] = np .take (seg_array , slice_idx , axis = axis_index )
345371 # Add depth=1 dim back for consistency with pipeline: (#instances, 1, H, W)
346372 sliced_segs [author ] = np .expand_dims (sliced_segs [author ], axis = 1 )
347373
0 commit comments