Skip to content

Commit 050dfeb

Browse files
committed
Improve axis handling; update dependencies in pyproject.toml
1 parent 8508c45 commit 050dfeb

2 files changed

Lines changed: 69 additions & 41 deletions

File tree

datamint/dataset/sliced_dataset.py

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77
import gzip
88
import logging
9-
from typing import Any
9+
from typing import Any, Literal, TYPE_CHECKING
1010
from typing_extensions import override
1111
from collections.abc import Sequence
1212

@@ -16,12 +16,15 @@
1616
import albumentations
1717

1818
from medimgkit.readers import read_array_normalized
19+
from medimgkit import dicom_utils
20+
from medimgkit import nifti_utils
1921

2022
from .base import DatamintBaseDataset
2123
from .annotation_processor import AnnotationProcessor, MergeStrategy
2224

23-
from datamint.entities import Annotation, Resource
2425
from 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

149152
class 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

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ platformdirs = "^4.0.0"
3838
pandas = ">=2.0.0"
3939
matplotlib = "*"
4040
lightning = { extras = ['extra'], version = ">=2.0.0, !=2.5.1, !=2.5.1.post0" }
41-
mlflow-skinny = "==3.8.1"
41+
mlflow-skinny = "==3.8.*"
42+
Flask = { version = "<4" }
43+
Flask-Cors = { version = "<7" }
4244
albumentations = ">=2.0.0"
4345
lazy-loader = ">=0.3.0"
44-
medimgkit = ">=0.11.4"
46+
medimgkit = ">=0.13.0"
4547
typing_extensions = ">=4.0.0"
4648
pydantic = ">=2.6.4"
4749
certifi = ">=2025.0.0"

0 commit comments

Comments
 (0)