datamint 1.5.2__py3-none-any.whl → 1.5.5__py3-none-any.whl

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.

Potentially problematic release.


This version of datamint might be problematic. Click here for more details.

@@ -221,7 +221,6 @@ class AnnotationAPIHandler(BaseAPIHandler):
221
221
  filename = os.path.basename(file_path)
222
222
  form = aiohttp.FormData()
223
223
  form.add_field('file', f, filename=filename, content_type='application/x-nifti')
224
- model_id = 'c9daf156-5335-4cb3-b374-5b3a776e0025'
225
224
  if model_id is not None:
226
225
  form.add_field('model_id', model_id) # Add model_id if provided
227
226
  if worklist_id is not None:
@@ -7,15 +7,16 @@ import shutil
7
7
  import json
8
8
  import yaml
9
9
  import pydicom
10
+ from pydicom.dataset import FileDataset
10
11
  import numpy as np
11
12
  from datamint import configs
12
13
  from torch.utils.data import DataLoader
13
14
  import torch
15
+ from torch import Tensor
14
16
  from datamint.apihandler.base_api_handler import DatamintException
15
17
  from datamint.utils.dicom_utils import is_dicom
16
18
  import cv2
17
19
  from datamint.utils.io_utils import read_array_normalized
18
- from deprecated import deprecated
19
20
  from datetime import datetime
20
21
 
21
22
  _LOGGER = logging.getLogger(__name__)
@@ -80,7 +81,7 @@ class DatamintBaseDataset:
80
81
  exclude_frame_label_names: Optional[list[str]] = None
81
82
  ):
82
83
  from datamint.apihandler.api_handler import APIHandler
83
-
84
+
84
85
  if project_name is None:
85
86
  raise ValueError("project_name is required.")
86
87
 
@@ -204,6 +205,9 @@ class DatamintBaseDataset:
204
205
  self.dataset_length = len(self.images_metainfo)
205
206
 
206
207
  self.num_frames_per_resource = self.__compute_num_frames_per_resource()
208
+
209
+ # Precompute cumulative frame counts for faster index lookup
210
+ self._cumulative_frames = np.cumsum([0] + self.num_frames_per_resource)
207
211
 
208
212
  self.subset_indices = list(range(self.dataset_length))
209
213
  # self.labels_set, self.label2code, self.segmentation_labels, self.segmentation_label2code = self.get_labels_set()
@@ -309,7 +313,7 @@ class DatamintBaseDataset:
309
313
  scope (str): The scope of the annotations. It can be 'frame', 'image' or 'all'.
310
314
 
311
315
  Returns:
312
- List[Dict]: The annotations of the image.
316
+ list[dict]: The annotations of the image.
313
317
  """
314
318
  if index >= len(self):
315
319
  raise IndexError(f"Index {index} out of bounds for dataset of length {len(self)}")
@@ -591,7 +595,8 @@ class DatamintBaseDataset:
591
595
  with open(datasetjson, 'w') as file:
592
596
  json.dump(self.metainfo, file)
593
597
 
594
- def _load_image(self, filepath: str, index: int = None) -> tuple[torch.Tensor, pydicom.FileDataset]:
598
+ def _load_image(self, filepath: str,
599
+ index: int | None = None) -> tuple[Tensor, FileDataset | None]:
595
600
  if os.path.isdir(filepath):
596
601
  raise NotImplementedError("Loading a image from a directory is not supported yet.")
597
602
 
@@ -601,14 +606,14 @@ class DatamintBaseDataset:
601
606
  img, ds = read_array_normalized(filepath, return_metainfo=True)
602
607
 
603
608
  if img.dtype == np.uint16:
604
- # Pytorch doesn't support uint16
605
- if self.__logged_uint16_conversion == False:
609
+ if not self.__logged_uint16_conversion:
606
610
  _LOGGER.info("Original image is uint16, converting to uint8")
607
611
  self.__logged_uint16_conversion = True
608
612
 
609
613
  # min-max normalization
610
614
  img = img.astype(np.float32)
611
- img = (img - img.min()) / (img.max() - img.min()) * 255
615
+ mn = img.min()
616
+ img = (img - mn) / (img.max() - mn) * 255
612
617
  img = img.astype(np.uint8)
613
618
 
614
619
  img = torch.from_numpy(img).contiguous()
@@ -618,7 +623,7 @@ class DatamintBaseDataset:
618
623
  return img, ds
619
624
 
620
625
  def _get_image_metainfo(self, index: int, bypass_subset_indices=False) -> dict[str, Any]:
621
- if bypass_subset_indices == False:
626
+ if not bypass_subset_indices:
622
627
  index = self.subset_indices[index]
623
628
  if self.return_frame_by_frame:
624
629
  # Find the correct filepath and index
@@ -635,17 +640,18 @@ class DatamintBaseDataset:
635
640
  return img_metainfo
636
641
 
637
642
  def __find_index(self, index: int) -> tuple[int, int]:
638
- frame_index = index
639
- for i, num_frames in enumerate(self.num_frames_per_resource):
640
- if frame_index < num_frames:
641
- break
642
- frame_index -= num_frames
643
- else:
644
- raise IndexError(f"Index {index} out of bounds for dataset of length {len(self)}")
645
-
646
- return i, frame_index
643
+ """
644
+ Find the resource index and frame index for a given global frame index.
645
+
646
+ """
647
+ # Use binary search to find the resource containing this frame
648
+ resource_index = np.searchsorted(self._cumulative_frames[1:], index, side='right')
649
+ frame_index = index - self._cumulative_frames[resource_index]
650
+
651
+ return resource_index, frame_index
647
652
 
648
- def __getitem_internal(self, index: int, only_load_metainfo=False) -> dict[str, Any]:
653
+ def __getitem_internal(self, index: int,
654
+ only_load_metainfo=False) -> dict[str, Tensor | FileDataset | dict | list]:
649
655
  if self.return_frame_by_frame:
650
656
  resource_index, frame_idx = self.__find_index(index)
651
657
  else:
@@ -711,7 +717,7 @@ class DatamintBaseDataset:
711
717
 
712
718
  return filtered_annotations
713
719
 
714
- def __getitem__(self, index: int) -> dict[str, Any]:
720
+ def __getitem__(self, index: int) -> dict[str, Tensor | FileDataset | dict | list]:
715
721
  """
716
722
  Args:
717
723
  index (int): Index
@@ -725,8 +731,8 @@ class DatamintBaseDataset:
725
731
  return self.__getitem_internal(self.subset_indices[index])
726
732
 
727
733
  def __iter__(self):
728
- for i in range(len(self)):
729
- yield self[i]
734
+ for index in self.subset_indices:
735
+ yield self.__getitem_internal(index)
730
736
 
731
737
  def __len__(self) -> int:
732
738
  return len(self.subset_indices)
@@ -287,7 +287,7 @@ class DatamintDataset(DatamintBaseDataset):
287
287
  if len(all_masks_list) != 0:
288
288
  all_masks_list = torch.concatenate(all_masks_list).numpy().astype(np.uint8)
289
289
  else:
290
- all_masks_list = None#np.empty((0,img.shape[-2], img.shape[-1]), dtype=np.uint8)
290
+ all_masks_list = None # np.empty((0,img.shape[-2], img.shape[-1]), dtype=np.uint8)
291
291
 
292
292
  augmented = self.alb_transform(image=img.numpy().transpose(1, 2, 0),
293
293
  masks=all_masks_list)
@@ -308,6 +308,36 @@ class DatamintDataset(DatamintBaseDataset):
308
308
 
309
309
  return augmented['image'], new_segmentations
310
310
 
311
+ def _seg_labels_to_names(self, seg_labels: dict | list | None) -> dict | list | None:
312
+ """
313
+ Convert segmentation label codes to label names.
314
+
315
+ Args:
316
+ seg_labels: Segmentation labels in various formats:
317
+ - dict[str, list[Tensor]]: author -> list of frame tensors with label codes
318
+ - dict[str, Tensor]: author -> tensor with label codes
319
+ - list[Tensor]: list of frame tensors with label codes
320
+ - Tensor: tensor with label codes
321
+ - None: when no segmentation labels are available
322
+
323
+ Returns:
324
+ Same structure as input but with label codes converted to label names.
325
+ Returns None if input is None.
326
+ """
327
+ if seg_labels is None:
328
+ return None
329
+
330
+ code_to_name = self.segmentation_labels_set
331
+ if isinstance(seg_labels, dict):
332
+ # author -> list of frame tensors
333
+ return {author: [code_to_name[code.item()] for code in labels] for author, labels in seg_labels.items()}
334
+ elif isinstance(seg_labels, list):
335
+ # list of frame tensors
336
+ return [[code_to_name[code.item()] for code in labels] for labels in seg_labels]
337
+
338
+ _LOGGER.warning(f"Unexpected segmentation labels format: {type(seg_labels)}. Returning None")
339
+ return None
340
+
311
341
  def __getitem__(self, index) -> dict[str, Any]:
312
342
  """
313
343
  Get the item at the given index.
@@ -401,6 +431,9 @@ class DatamintDataset(DatamintBaseDataset):
401
431
  seg_labels = seg_labels[0]
402
432
  new_item['segmentations'] = segmentations
403
433
  new_item['seg_labels'] = seg_labels
434
+ # process seg_labels to convert from code to label names
435
+ new_item['seg_labels_names'] = self._seg_labels_to_names(seg_labels)
436
+
404
437
  except Exception:
405
438
  _LOGGER.error(f'Error in loading/processing segmentations of {metainfo}')
406
439
  raise
@@ -638,3 +638,70 @@ def pixel_to_patient(ds: pydicom.Dataset,
638
638
  patient_coords = image_position + pixel_x * pixel_spacing[0] * row_vector + pixel_y * pixel_spacing[1] * col_vector
639
639
 
640
640
  return patient_coords
641
+
642
+
643
+ def determine_anatomical_plane(ds: pydicom.Dataset,
644
+ slice_axis: int,
645
+ alignment_threshold: float = 0.95) -> str:
646
+ """
647
+ Determine the anatomical plane of a DICOM slice (Axial, Sagittal, Coronal, Oblique, or Unknown).
648
+
649
+ Args:
650
+ ds (pydicom.Dataset): The DICOM dataset containing the image metadata.
651
+ slice_axis (int): The axis of the slice to analyze (0, 1, or 2).
652
+ alignment_threshold (float): Threshold for considering alignment with anatomical axes.
653
+
654
+ Returns:
655
+ str: The name of the anatomical plane ('Axial', 'Sagittal', 'Coronal', 'Oblique', or 'Unknown').
656
+
657
+ Raises:
658
+ ValueError: If `slice_index` is not 0, 1, or 2.
659
+ """
660
+
661
+ if slice_axis not in [0, 1, 2]:
662
+ raise ValueError("slice_index must be 0, 1 or 2")
663
+ # Check if Image Orientation Patient exists
664
+ if not hasattr(ds, 'ImageOrientationPatient') or ds.ImageOrientationPatient is None:
665
+ return "Unknown"
666
+ # Get the Image Orientation Patient (IOP) - 6 values defining row and column directions
667
+ iop = np.array(ds.ImageOrientationPatient, dtype=float)
668
+ if len(iop) != 6:
669
+ return "Unknown"
670
+ # Extract row and column direction vectors
671
+ row_dir = iop[:3] # First 3 values: row direction cosines
672
+ col_dir = iop[3:] # Last 3 values: column direction cosines
673
+ # Calculate the normal vector (slice direction) using cross product
674
+ normal = np.cross(row_dir, col_dir)
675
+ normal = normal / np.linalg.norm(normal) # Normalize
676
+ # Define standard anatomical axes
677
+ # LPS coordinate system: L = Left, P = Posterior, S = Superior
678
+ axes = {
679
+ 'sagittal': np.array([1, 0, 0]), # L-R axis (left-right)
680
+ 'coronal': np.array([0, 1, 0]), # A-P axis (anterior-posterior)
681
+ 'axial': np.array([0, 0, 1]) # S-I axis (superior-inferior)
682
+ }
683
+ # For each slice_index, determine which axis we're examining
684
+ if slice_axis == 0:
685
+ # ds.pixel_array[0,:,:] - slicing along first dimension
686
+ # The normal vector corresponds to the direction we're slicing through
687
+ examine_vector = normal
688
+ elif slice_axis == 1:
689
+ # ds.pixel_array[:,0,:] - slicing along second dimension
690
+ # This corresponds to the row direction
691
+ examine_vector = row_dir
692
+ elif slice_axis == 2:
693
+ # ds.pixel_array[:,:,0] - slicing along third dimension
694
+ # This corresponds to the column direction
695
+ examine_vector = col_dir
696
+ # Find which anatomical axis is most aligned with our examine_vector
697
+ max_dot = 0
698
+ best_axis = "Unknown"
699
+ for axis_name, axis_vector in axes.items():
700
+ dot_product = abs(np.dot(examine_vector, axis_vector))
701
+ if dot_product > max_dot:
702
+ max_dot = dot_product
703
+ best_axis = axis_name
704
+ if max_dot >= alignment_threshold:
705
+ return best_axis.capitalize()
706
+ else:
707
+ return "Oblique"
@@ -53,33 +53,42 @@ def read_video(file_path: str, index: int = None) -> np.ndarray:
53
53
  return imgs
54
54
 
55
55
 
56
- def read_nifti(file_path: str) -> np.ndarray:
56
+ def read_nifti(file_path: str, mimetype: str | None = None) -> np.ndarray:
57
57
  """
58
58
  Read a NIfTI file and return the image data in standardized format.
59
59
 
60
60
  Args:
61
61
  file_path: Path to the NIfTI file (.nii or .nii.gz)
62
+ mimetype: Optional MIME type of the file. If provided, it can help in determining how to read the file.
62
63
 
63
64
  Returns:
64
65
  np.ndarray: Image data with shape (#frames, C, H, W)
65
66
  """
67
+ from nibabel.filebasedimages import ImageFileError
66
68
  try:
67
- nii_img = nib.load(file_path)
68
- imgs = nii_img.get_fdata() # shape: (W, H, #frame) or (W, H)
69
-
70
- if imgs.ndim == 2:
71
- imgs = imgs.transpose(1, 0) # (W, H) -> (H, W)
72
- imgs = imgs[np.newaxis, np.newaxis] # -> (1, 1, H, W)
73
- elif imgs.ndim == 3:
74
- imgs = imgs.transpose(2, 1, 0) # (W, H, #frame) -> (#frame, H, W)
75
- imgs = imgs[:, np.newaxis] # -> (#frame, 1, H, W)
69
+ imgs = nib.load(file_path).get_fdata() # shape: (W, H, #frame) or (W, H)
70
+ except ImageFileError as e:
71
+ if mimetype is None:
72
+ raise e
73
+ # has_ext = os.path.splitext(file_path)[1] != ''
74
+ if mimetype == 'application/gzip':
75
+ with gzip.open(file_path, 'rb') as f:
76
+ imgs = nib.Nifti1Image.from_stream(f).get_fdata()
77
+ elif mimetype in ('image/x.nifti', 'application/x-nifti'):
78
+ with open(file_path, 'rb') as f:
79
+ imgs = nib.Nifti1Image.from_stream(f).get_fdata()
76
80
  else:
77
- raise ValueError(f"Unsupported number of dimensions in '{file_path}': {imgs.ndim}")
81
+ raise e
82
+ if imgs.ndim == 2:
83
+ imgs = imgs.transpose(1, 0)
84
+ imgs = imgs[np.newaxis, np.newaxis]
85
+ elif imgs.ndim == 3:
86
+ imgs = imgs.transpose(2, 1, 0)
87
+ imgs = imgs[:, np.newaxis]
88
+ else:
89
+ raise ValueError(f"Unsupported number of dimensions in '{file_path}': {imgs.ndim}")
78
90
 
79
- return imgs
80
- except Exception as e:
81
- _LOGGER.error(f"Failed to read NIfTI file '{file_path}': {e}")
82
- raise e
91
+ return imgs
83
92
 
84
93
 
85
94
  def read_image(file_path: str) -> np.ndarray:
@@ -94,7 +103,7 @@ def read_image(file_path: str) -> np.ndarray:
94
103
 
95
104
 
96
105
  def read_array_normalized(file_path: str,
97
- index: int = None,
106
+ index: int | None = None,
98
107
  return_metainfo: bool = False,
99
108
  use_magic=False) -> np.ndarray | tuple[np.ndarray, Any]:
100
109
  """
@@ -102,6 +111,8 @@ def read_array_normalized(file_path: str,
102
111
 
103
112
  Args:
104
113
  file_path: The path to the file.
114
+ index: If specified, read only the frame at this index (0-based).
115
+ If None, read all frames.
105
116
  Supported file formats are NIfTI (.nii, .nii.gz), PNG (.png), JPEG (.jpg, .jpeg) and npy (.npy).
106
117
 
107
118
  Returns:
@@ -136,8 +147,8 @@ def read_array_normalized(file_path: str,
136
147
  if mime_type.startswith('video/') or file_path.endswith(VIDEO_EXTS):
137
148
  imgs = read_video(file_path, index)
138
149
  else:
139
- if mime_type == 'image/x.nifti' or file_path.endswith(NII_EXTS):
140
- imgs = read_nifti(file_path)
150
+ if mime_type in ('image/x.nifti', 'application/x-nifti') or mime_type == 'application/gzip' or file_path.endswith(NII_EXTS):
151
+ imgs = read_nifti(file_path, mimetype=mime_type)
141
152
  # For NIfTI files, try to load associated JSON metadata
142
153
  if return_metainfo:
143
154
  json_path = file_path.replace('.nii.gz', '.json').replace('.nii', '.json')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: datamint
3
- Version: 1.5.2
3
+ Version: 1.5.5
4
4
  Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
5
5
  Requires-Python: >=3.10
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,5 +1,5 @@
1
1
  datamint/__init__.py,sha256=7rKCCsaa4RBRTIfuHB708rai1xwDHLtkFNFJGKYG5D4,757
2
- datamint/apihandler/annotation_api_handler.py,sha256=n-I-tKRB9g24f9RScbT-tPMF7h-eTbFexaTQ4sYTJoI,47836
2
+ datamint/apihandler/annotation_api_handler.py,sha256=N8WFk-oO84fBKH9t-R1DW5J7hnxQxcz-zxgLuMkNbwA,47766
3
3
  datamint/apihandler/api_handler.py,sha256=cdVSddrFCKlF_BJ81LO1aJ0OP49rssjpNEFzJ6Q7YyY,384
4
4
  datamint/apihandler/base_api_handler.py,sha256=XSxZEQEkbQpuixGDu_P9jbxUQht3Z3JgxaeiFKPkVDM,11690
5
5
  datamint/apihandler/dto/annotation_dto.py,sha256=otCIesoqGBlbSOw4ErqFsXp2HwJsPNUQlkynQh_7pHg,7110
@@ -10,20 +10,20 @@ datamint/client_cmd_tools/datamint_config.py,sha256=md7dnWrbl10lPtXKbmD9yo6onLJs
10
10
  datamint/client_cmd_tools/datamint_upload.py,sha256=VyLL2FgY9ibfbdp4K6HrKt0jgkQH-SVuU71D6e77074,26436
11
11
  datamint/configs.py,sha256=Bdp6NydYwyCJ2dk19_gf_o3M2ZyQOmMHpLi8wEWNHUk,1426
12
12
  datamint/dataset/__init__.py,sha256=4PlUKSvVhdfQvvuq8jQXrkdqnot-iTTizM3aM1vgSwg,47
13
- datamint/dataset/base_dataset.py,sha256=EnnIeF3ZaBL2M8qEV39U0ogKptyvezBNoVOvrS12bZ8,38756
14
- datamint/dataset/dataset.py,sha256=W7W9EcaPdyV8XjOL6jzBqnH2iUCMpA8w-UNUVv1AP9w,25076
13
+ datamint/dataset/base_dataset.py,sha256=MQZ_wNFex4BKBfb4fAcXV6-fQXFV_zBK1ybWrMm6_pg,39092
14
+ datamint/dataset/dataset.py,sha256=AwS92t5kdmpm9NKFfXFmDmZxEbbPfb_FOMn-FWfu3bE,26590
15
15
  datamint/examples/__init__.py,sha256=zcYnd5nLVme9GCTPYH-1JpGo8xXK2WEYvhzcy_2alZc,39
16
16
  datamint/examples/example_projects.py,sha256=7Nb_EaIdzJTQa9zopqc-WhTBQWQJSoQZ_KjRS4PB4FI,2931
17
17
  datamint/experiment/__init__.py,sha256=5qQOMzoG17DEd1YnTF-vS0qiM-DGdbNh42EUo91CRhQ,34
18
18
  datamint/experiment/_patcher.py,sha256=ZgbezoevAYhJsbiJTvWPALGTcUiMT371xddcTllt3H4,23296
19
19
  datamint/experiment/experiment.py,sha256=aHK9dRFdQTi569xgUg1KqlCZLHZpDmSH3g3ndPIZvXw,44546
20
20
  datamint/logging.yaml,sha256=a5dsATpul7QHeUHB2TjABFjWaPXBMbO--dgn8GlRqwk,483
21
- datamint/utils/dicom_utils.py,sha256=n1CrYg1AgnlbgIktDfVXQ1Logh8lwCqYbjqHu5GElUE,26062
22
- datamint/utils/io_utils.py,sha256=ebP1atKkhKEf1mUU1LsVwDq0h_so7kVKkD_7hQYn_kM,6754
21
+ datamint/utils/dicom_utils.py,sha256=sLukP6MB_acx7t868O2HDd_RDEILa97mEe_V9m1EMCY,28991
22
+ datamint/utils/io_utils.py,sha256=lKnUCJEip7W9Xj9wOWsTAA855HnKbjwQON1WjMGqJmM,7374
23
23
  datamint/utils/logging_utils.py,sha256=DvoA35ATYG3JTwfXEXYawDyKRfHeCrH0a9czfkmz8kM,1851
24
24
  datamint/utils/torchmetrics.py,sha256=lwU0nOtsSWfebyp7dvjlAggaqXtj5ohSEUXOg3L0hJE,2837
25
25
  datamint/utils/visualization.py,sha256=yaUVAOHar59VrGUjpAWv5eVvQSfztFG0eP9p5Vt3l-M,4470
26
- datamint-1.5.2.dist-info/METADATA,sha256=WxrWHBdRq5AIOMyZYjVEJ7FrzufZR1yt4d1fMmrZ54U,4065
27
- datamint-1.5.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
- datamint-1.5.2.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
29
- datamint-1.5.2.dist-info/RECORD,,
26
+ datamint-1.5.5.dist-info/METADATA,sha256=o6BFPA7OS3SSPqflC85pJ_2Q7pETUtoZInY97B2Dxm8,4065
27
+ datamint-1.5.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
+ datamint-1.5.5.dist-info/entry_points.txt,sha256=mn5H6jPjO-rY0W0CAZ6Z_KKWhMLvyVaSpoqk77jlTI4,145
29
+ datamint-1.5.5.dist-info/RECORD,,