dicube 0.1.4__cp39-cp39-win_amd64.whl → 0.2.2__cp39-cp39-win_amd64.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.
dicube/__init__.py CHANGED
@@ -39,27 +39,47 @@ except PackageNotFoundError:
39
39
  # editable install / source tree
40
40
  __version__ = "0.1.0+unknown"
41
41
 
42
+ # Default to the number of CPU cores, but cap at 8 threads to avoid excessive resource usage
43
+ # Fall back to 4 if cpu_count() returns None (which can happen in some environments)
44
+ _num_threads = 4
45
+
46
+ def get_num_threads() -> int:
47
+ """Get the global number of threads for parallel processing.
48
+
49
+ Returns:
50
+ int: Current number of threads setting.
51
+ """
52
+ global _num_threads
53
+ return _num_threads
54
+
55
+ def set_num_threads(num_threads: int) -> None:
56
+ """Set the global number of threads for parallel processing.
57
+
58
+ Args:
59
+ num_threads (int): Number of threads for parallel processing tasks.
60
+ """
61
+ global _num_threads
62
+ if num_threads < 1:
63
+ raise ValueError("Number of threads must be at least 1")
64
+ _num_threads = num_threads
65
+
42
66
  # Top-level convenience methods
43
- def load(file_path: str, num_threads: int = 4, **kwargs) -> DicomCubeImage:
67
+ def load(file_path: str) -> DicomCubeImage:
44
68
  """Load a DicomCubeImage from a file.
45
69
 
46
70
  Args:
47
71
  file_path (str): Path to the input file.
48
- num_threads (int): Number of parallel decoding threads. Defaults to 4.
49
- **kwargs: Additional parameters passed to the underlying reader.
50
72
 
51
73
  Returns:
52
74
  DicomCubeImage: The loaded image object.
53
75
  """
54
- return DicomCubeImageIO.load(file_path, num_threads, **kwargs)
76
+ return DicomCubeImageIO.load(file_path)
55
77
 
56
78
 
57
79
  def save(
58
80
  image: DicomCubeImage,
59
81
  file_path: str,
60
82
  file_type: str = "s",
61
- num_threads: int = 4,
62
- **kwargs
63
83
  ) -> None:
64
84
  """Save a DicomCubeImage to a file.
65
85
 
@@ -68,16 +88,13 @@ def save(
68
88
  file_path (str): Output file path.
69
89
  file_type (str): File type, "s" (speed priority), "a" (compression priority),
70
90
  or "l" (lossy compression). Defaults to "s".
71
- num_threads (int): Number of parallel encoding threads. Defaults to 4.
72
- **kwargs: Additional parameters passed to the underlying writer.
73
91
  """
74
- return DicomCubeImageIO.save(image, file_path, file_type, num_threads, **kwargs)
92
+ return DicomCubeImageIO.save(image, file_path, file_type)
75
93
 
76
94
 
77
95
  def load_from_dicom_folder(
78
96
  folder_path: str,
79
97
  sort_method: SortMethod = SortMethod.INSTANCE_NUMBER_ASC,
80
- **kwargs
81
98
  ) -> DicomCubeImage:
82
99
  """Load a DicomCubeImage from a DICOM folder.
83
100
 
@@ -90,10 +107,10 @@ def load_from_dicom_folder(
90
107
  Returns:
91
108
  DicomCubeImage: The loaded image object.
92
109
  """
93
- return DicomCubeImageIO.load_from_dicom_folder(folder_path, sort_method, **kwargs)
110
+ return DicomCubeImageIO.load_from_dicom_folder(folder_path, sort_method)
94
111
 
95
112
 
96
- def load_from_nifti(file_path: str, **kwargs) -> DicomCubeImage:
113
+ def load_from_nifti(file_path: str) -> DicomCubeImage:
97
114
  """Load a DicomCubeImage from a NIfTI file.
98
115
 
99
116
  Args:
@@ -103,24 +120,38 @@ def load_from_nifti(file_path: str, **kwargs) -> DicomCubeImage:
103
120
  Returns:
104
121
  DicomCubeImage: The loaded image object.
105
122
  """
106
- return DicomCubeImageIO.load_from_nifti(file_path, **kwargs)
123
+ return DicomCubeImageIO.load_from_nifti(file_path)
107
124
 
108
125
 
109
126
  def save_to_dicom_folder(
110
127
  image: DicomCubeImage,
111
128
  folder_path: str,
112
- **kwargs
113
129
  ) -> None:
114
130
  """Save a DicomCubeImage as a DICOM folder.
115
131
 
116
132
  Args:
117
133
  image (DicomCubeImage): The image object to save.
118
134
  folder_path (str): Output directory path.
119
- **kwargs: Additional parameters.
120
135
  """
121
136
  return DicomCubeImageIO.save_to_dicom_folder(image, folder_path)
122
137
 
123
138
 
139
+ def save_to_nifti(
140
+ image: DicomCubeImage,
141
+ file_path: str,
142
+ ) -> None:
143
+ """Save a DicomCubeImage as a NIfTI file.
144
+
145
+ Args:
146
+ image (DicomCubeImage): The image object to save.
147
+ file_path (str): Output file path.
148
+
149
+ Raises:
150
+ ImportError: When nibabel is not installed.
151
+ """
152
+ return DicomCubeImageIO.save_to_nifti(image, file_path)
153
+
154
+
124
155
  __all__ = [
125
156
  "DicomCubeImage",
126
157
  "DicomMeta",
@@ -135,6 +166,9 @@ __all__ = [
135
166
  "load_from_dicom_folder",
136
167
  "load_from_nifti",
137
168
  "save_to_dicom_folder",
169
+ "save_to_nifti",
170
+ "set_num_threads",
171
+ "get_num_threads",
138
172
  # IO class (for direct use if needed)
139
173
  "DicomCubeImageIO",
140
174
  ]
dicube/core/image.py CHANGED
@@ -35,7 +35,7 @@ class DicomCubeImage:
35
35
  pixel_header (PixelDataHeader): Pixel data header containing metadata about the image pixels.
36
36
  dicom_meta (DicomMeta, optional): DICOM metadata associated with the image.
37
37
  space (Space, optional): Spatial information describing the image dimensions and orientation.
38
- dicom_status (str, optional): DICOM status string. Defaults to DicomStatus.CONSISTENT.value.
38
+ dicom_status (DicomStatus): DICOM status enumeration. Defaults to DicomStatus.CONSISTENT.
39
39
  """
40
40
 
41
41
  def __init__(
@@ -44,7 +44,7 @@ class DicomCubeImage:
44
44
  pixel_header: PixelDataHeader,
45
45
  dicom_meta: Optional[DicomMeta] = None,
46
46
  space: Optional[Space] = None,
47
- dicom_status: str = DicomStatus.CONSISTENT.value,
47
+ dicom_status: DicomStatus = DicomStatus.CONSISTENT,
48
48
  ):
49
49
  """Initialize a DicomCubeImage instance.
50
50
 
@@ -226,13 +226,13 @@ class DicomCubeImage:
226
226
  meta.set_shared_item(CommonTags.PixelRepresentation, 0)
227
227
 
228
228
  # Rescale Information from pixel_header
229
- if self.pixel_header.RESCALE_SLOPE is not None:
229
+ if self.pixel_header.RescaleSlope is not None:
230
230
  meta.set_shared_item(
231
- CommonTags.RescaleSlope, float(self.pixel_header.RESCALE_SLOPE)
231
+ CommonTags.RescaleSlope, float(self.pixel_header.RescaleSlope)
232
232
  )
233
- if self.pixel_header.RESCALE_INTERCEPT is not None:
233
+ if self.pixel_header.RescaleIntercept is not None:
234
234
  meta.set_shared_item(
235
- CommonTags.RescaleIntercept, float(self.pixel_header.RESCALE_INTERCEPT)
235
+ CommonTags.RescaleIntercept, float(self.pixel_header.RescaleIntercept)
236
236
  )
237
237
 
238
238
  def init_meta(
dicube/core/io.py CHANGED
@@ -16,7 +16,7 @@ from ..dicom import (
16
16
  )
17
17
  from ..dicom.dicom_io import save_to_dicom_folder
18
18
  from ..storage.dcb_file import DcbSFile, DcbFile, DcbAFile, DcbLFile
19
- from ..storage.pixel_utils import derive_pixel_header_from_array
19
+ from ..storage.pixel_utils import derive_pixel_header_from_array, determine_optimal_nifti_dtype
20
20
  from .pixel_header import PixelDataHeader
21
21
 
22
22
  from ..validation import (
@@ -52,7 +52,6 @@ class DicomCubeImageIO:
52
52
  image: "DicomCubeImage",
53
53
  file_path: str,
54
54
  file_type: str = "s",
55
- num_threads: int = 4,
56
55
  ) -> None:
57
56
  """Save DicomCubeImage to a file.
58
57
 
@@ -61,7 +60,6 @@ class DicomCubeImageIO:
61
60
  file_path (str): Output file path.
62
61
  file_type (str): File type, "s" (speed priority), "a" (compression priority),
63
62
  or "l" (lossy compression). Defaults to "s".
64
- num_threads (int): Number of parallel encoding threads. Defaults to 4.
65
63
 
66
64
  Raises:
67
65
  InvalidCubeFileError: If the file_type is not supported.
@@ -69,7 +67,6 @@ class DicomCubeImageIO:
69
67
  # Validate required parameters
70
68
  validate_not_none(image, "image", "save operation", DataConsistencyError)
71
69
  validate_string_not_empty(file_path, "file_path", "save operation", InvalidCubeFileError)
72
- validate_numeric_range(num_threads, "num_threads", min_value=1, context="save operation")
73
70
 
74
71
  # Validate file_type parameter
75
72
  if file_type not in ("s", "a", "l"):
@@ -82,6 +79,7 @@ class DicomCubeImageIO:
82
79
 
83
80
  try:
84
81
  # Choose appropriate writer based on file type
82
+ # The writer will automatically ensure correct file extension
85
83
  if file_type == "s":
86
84
  writer = DcbSFile(file_path, mode="w")
87
85
  elif file_type == "a":
@@ -89,14 +87,16 @@ class DicomCubeImageIO:
89
87
  elif file_type == "l":
90
88
  writer = DcbLFile(file_path, mode="w")
91
89
 
90
+ # Update file_path to the corrected path from writer
91
+ file_path = writer.filename
92
+
92
93
  # Write to file
93
94
  writer.write(
94
95
  images=image.raw_image,
95
96
  pixel_header=image.pixel_header,
96
97
  dicom_meta=image.dicom_meta,
97
98
  space=image.space,
98
- num_threads=num_threads,
99
- dicom_status=image.dicom_status
99
+ dicom_status=image.dicom_status,
100
100
  )
101
101
  except Exception as e:
102
102
  if isinstance(e, (InvalidCubeFileError, CodecError)):
@@ -108,13 +108,11 @@ class DicomCubeImageIO:
108
108
  ) from e
109
109
 
110
110
  @staticmethod
111
- def load(file_path: str, num_threads: int = 4, **kwargs) -> 'DicomCubeImage':
111
+ def load(file_path: str) -> 'DicomCubeImage':
112
112
  """Load DicomCubeImage from a file.
113
113
 
114
114
  Args:
115
115
  file_path (str): Input file path.
116
- num_threads (int): Number of parallel decoding threads. Defaults to 4.
117
- **kwargs: Additional parameters passed to the underlying reader.
118
116
 
119
117
  Returns:
120
118
  DicomCubeImage: The loaded object from the file.
@@ -152,7 +150,7 @@ class DicomCubeImageIO:
152
150
  pixel_header = reader.read_pixel_header()
153
151
  dicom_status = reader.read_dicom_status()
154
152
 
155
- images = reader.read_images(num_threads=num_threads)
153
+ images = reader.read_images()
156
154
  if isinstance(images, list):
157
155
  # Convert list to ndarray if needed
158
156
  images = np.stack(images)
@@ -180,7 +178,6 @@ class DicomCubeImageIO:
180
178
  def load_from_dicom_folder(
181
179
  folder_path: str,
182
180
  sort_method: SortMethod = SortMethod.INSTANCE_NUMBER_ASC,
183
- **kwargs
184
181
  ) -> 'DicomCubeImage':
185
182
  """Load DicomCubeImage from a DICOM folder.
186
183
 
@@ -188,7 +185,6 @@ class DicomCubeImageIO:
188
185
  folder_path (str): Path to the DICOM folder.
189
186
  sort_method (SortMethod): Method to sort DICOM files.
190
187
  Defaults to SortMethod.INSTANCE_NUMBER_ASC.
191
- **kwargs: Additional parameters.
192
188
 
193
189
  Returns:
194
190
  DicomCubeImage: The object created from the DICOM folder.
@@ -243,15 +239,21 @@ class DicomCubeImageIO:
243
239
  intercept = meta.get_shared_value(CommonTags.RescaleIntercept)
244
240
  wind_center = meta.get_shared_value(CommonTags.WindowCenter)
245
241
  wind_width = meta.get_shared_value(CommonTags.WindowWidth)
242
+ try:
243
+ wind_center = float(wind_center)
244
+ wind_width = float(wind_width)
245
+ except:
246
+ wind_center = None
247
+ wind_width = None
246
248
 
247
249
  # Create pixel_header
248
250
  pixel_header = PixelDataHeader(
249
- RESCALE_SLOPE=float(slope) if slope is not None else 1.0,
250
- RESCALE_INTERCEPT=float(intercept) if intercept is not None else 0.0,
251
- ORIGINAL_PIXEL_DTYPE=str(images[0].dtype),
252
- PIXEL_DTYPE=str(images[0].dtype),
253
- WINDOW_CENTER=float(wind_center) if wind_center is not None else None,
254
- WINDOW_WIDTH=float(wind_width) if wind_width is not None else None,
251
+ RescaleSlope=float(slope) if slope is not None else 1.0,
252
+ RescaleIntercept=float(intercept) if intercept is not None else 0.0,
253
+ OriginalPixelDtype=str(images[0].dtype),
254
+ PixelDtype=str(images[0].dtype),
255
+ WindowCenter=wind_center,
256
+ WindowWidth=wind_width,
255
257
  )
256
258
 
257
259
  # Validate PixelDataHeader initialization success
@@ -282,12 +284,11 @@ class DicomCubeImageIO:
282
284
  ) from e
283
285
 
284
286
  @staticmethod
285
- def load_from_nifti(file_path: str, **kwargs) -> 'DicomCubeImage':
287
+ def load_from_nifti(file_path: str) -> 'DicomCubeImage':
286
288
  """Load DicomCubeImage from a NIfTI file.
287
289
 
288
290
  Args:
289
291
  file_path (str): Path to the NIfTI file.
290
- **kwargs: Additional parameters.
291
292
 
292
293
  Returns:
293
294
  DicomCubeImage: The object created from the NIfTI file.
@@ -351,4 +352,57 @@ class DicomCubeImageIO:
351
352
  dicom_meta=image.dicom_meta,
352
353
  pixel_header=image.pixel_header,
353
354
  output_dir=folder_path,
354
- )
355
+ )
356
+
357
+ @staticmethod
358
+ def save_to_nifti(
359
+ image: 'DicomCubeImage',
360
+ file_path: str,
361
+ ) -> None:
362
+ """Save DicomCubeImage as a NIfTI file.
363
+
364
+ Args:
365
+ image (DicomCubeImage): The DicomCubeImage object to save.
366
+ file_path (str): Output file path.
367
+
368
+ Raises:
369
+ ImportError: When nibabel is not installed.
370
+ InvalidCubeFileError: When saving fails.
371
+ """
372
+ # Validate required parameters
373
+ validate_not_none(image, "image", "save_to_nifti operation", DataConsistencyError)
374
+ validate_string_not_empty(file_path, "file_path", "save_to_nifti operation", InvalidCubeFileError)
375
+
376
+ try:
377
+ import nibabel as nib
378
+ except ImportError:
379
+ raise ImportError("nibabel is required to write NIfTI files")
380
+
381
+ try:
382
+ if image.space is None:
383
+ raise InvalidCubeFileError(
384
+ "Cannot save to NIfTI without space information",
385
+ context="save_to_nifti operation",
386
+ suggestion="Ensure the DicomCubeImage has valid space information"
387
+ )
388
+
389
+ # Get affine matrix from space
390
+ affine = image.space.to_nifti_affine()
391
+
392
+ # 根据像素数据和metadata确定最佳的数据类型
393
+ optimal_data, dtype_name = determine_optimal_nifti_dtype(image.raw_image, image.pixel_header)
394
+
395
+ # Create NIfTI image with optimized data type
396
+ nii = nib.Nifti1Image(optimal_data, affine)
397
+
398
+ # Save to file
399
+ nib.save(nii, file_path)
400
+ except Exception as e:
401
+ if isinstance(e, (ImportError, InvalidCubeFileError)):
402
+ raise
403
+ raise InvalidCubeFileError(
404
+ f"Failed to save NIfTI file: {str(e)}",
405
+ context="save_to_nifti operation",
406
+ details={"file_path": file_path},
407
+ suggestion="Check file permissions and ensure space information is valid"
408
+ ) from e
@@ -12,42 +12,42 @@ class PixelDataHeader:
12
12
  - Original pixel data type
13
13
  - Window settings (center/width)
14
14
  - Value range (min/max)
15
- - Additional metadata in EXTRAS
15
+ - Additional metadata in extras
16
16
 
17
17
  Attributes:
18
- RESCALE_SLOPE (float): Slope for linear transformation.
19
- RESCALE_INTERCEPT (float): Intercept for linear transformation.
20
- PIXEL_DTYPE (str): Pixel data type string (after convert to dcb file).
21
- ORIGINAL_PIXEL_DTYPE (str): Original pixel data type string (before convert to dcb file).
22
- WINDOW_CENTER (float, optional): Window center value for display.
23
- WINDOW_WIDTH (float, optional): Window width value for display.
24
- MAX_VAL (float, optional): Maximum pixel value.
25
- MIN_VAL (float, optional): Minimum pixel value.
26
- EXTRAS (Dict[str, any]): Dictionary for additional metadata.
18
+ RescaleSlope (float): Slope for linear transformation.
19
+ RescaleIntercept (float): Intercept for linear transformation.
20
+ PixelDtype (str): Pixel data type string (after convert to dcb file).
21
+ OriginalPixelDtype (str): Original pixel data type string (before convert to dcb file).
22
+ WindowCenter (float, optional): Window center value for display.
23
+ WindowWidth (float, optional): Window width value for display.
24
+ MaxVal (float, optional): Maximum pixel value.
25
+ MinVal (float, optional): Minimum pixel value.
26
+ Extras (Dict[str, any]): Dictionary for additional metadata.
27
27
  """
28
28
 
29
- RESCALE_SLOPE: float = 1.0
30
- RESCALE_INTERCEPT: float = 0.0
31
- ORIGINAL_PIXEL_DTYPE: str = "uint16"
32
- PIXEL_DTYPE: str = "uint16"
33
- WINDOW_CENTER: Optional[float] = None
34
- WINDOW_WIDTH: Optional[float] = None
35
- MAX_VAL: Optional[float] = None
36
- MIN_VAL: Optional[float] = None
37
- EXTRAS: Dict[str, any] = field(default_factory=dict)
29
+ RescaleSlope: float = 1.0
30
+ RescaleIntercept: float = 0.0
31
+ OriginalPixelDtype: str = "uint16"
32
+ PixelDtype: str = "uint16"
33
+ WindowCenter: Optional[float] = None
34
+ WindowWidth: Optional[float] = None
35
+ MaxVal: Optional[float] = None
36
+ MinVal: Optional[float] = None
37
+ Extras: Dict[str, any] = field(default_factory=dict)
38
38
 
39
39
  def to_dict(self) -> dict:
40
40
  """Convert the header to a dictionary for serialization.
41
41
 
42
- Merges EXTRAS field into the main dictionary and removes
43
- the redundant EXTRAS key.
42
+ Merges extras field into the main dictionary and removes
43
+ the redundant extras key.
44
44
 
45
45
  Returns:
46
46
  dict: Dictionary representation of the header.
47
47
  """
48
48
  data = asdict(self)
49
- data.update(self.EXTRAS) # Merge EXTRAS into dictionary
50
- data.pop("EXTRAS", None) # Remove redundant EXTRAS field
49
+ data.update(self.Extras) # Merge Extras into dictionary
50
+ data.pop("Extras", None) # Remove redundant Extras field
51
51
  return data
52
52
 
53
53
  @classmethod
@@ -60,39 +60,42 @@ class PixelDataHeader:
60
60
  Returns:
61
61
  PixelDataHeader: A new instance with values from the dictionary.
62
62
  """
63
- rescale_slope = d.get("RESCALE_SLOPE", 1.0)
64
- rescale_intercept = d.get("RESCALE_INTERCEPT", 0.0)
65
- original_pixel_dtype = d.get("ORIGINAL_PIXEL_DTYPE", "uint16")
66
- window_center = d.get("WINDOW_CENTER") # Defaults to None
67
- window_width = d.get("WINDOW_WIDTH") # Defaults to None
68
- max_val = d.get("MAX_VAL") # Defaults to None
69
- min_val = d.get("MIN_VAL") # Defaults to None
70
-
71
- # All other keys go into EXTRAS
63
+ rescale_slope = d.get("RescaleSlope", 1.0)
64
+ rescale_intercept = d.get("RescaleIntercept", 0.0)
65
+ original_pixel_dtype = d.get("OriginalPixelDtype", "uint16")
66
+ pixel_dtype = d.get("PixelDtype", "uint16")
67
+ window_center = d.get("WindowCenter") # Defaults to None
68
+ window_width = d.get("WindowWidth") # Defaults to None
69
+ max_val = d.get("MaxVal") # Defaults to None
70
+ min_val = d.get("MinVal") # Defaults to None
71
+
72
+ # All other keys go into Extras
72
73
  extras = {
73
74
  k: v
74
75
  for k, v in d.items()
75
76
  if k
76
77
  not in {
77
- "RESCALE_SLOPE",
78
- "RESCALE_INTERCEPT",
79
- "ORIGINAL_PIXEL_DTYPE",
80
- "WINDOW_CENTER",
81
- "WINDOW_WIDTH",
82
- "MAX_VAL",
83
- "MIN_VAL",
78
+ "RescaleSlope",
79
+ "RescaleIntercept",
80
+ "OriginalPixelDtype",
81
+ "PixelDtype",
82
+ "WindowCenter",
83
+ "WindowWidth",
84
+ "MaxVal",
85
+ "MinVal",
84
86
  }
85
87
  }
86
88
 
87
89
  return cls(
88
- RESCALE_SLOPE=rescale_slope,
89
- RESCALE_INTERCEPT=rescale_intercept,
90
- ORIGINAL_PIXEL_DTYPE=original_pixel_dtype,
91
- WINDOW_CENTER=window_center,
92
- WINDOW_WIDTH=window_width,
93
- MAX_VAL=max_val,
94
- MIN_VAL=min_val,
95
- EXTRAS=extras,
90
+ RescaleSlope=rescale_slope,
91
+ RescaleIntercept=rescale_intercept,
92
+ OriginalPixelDtype=original_pixel_dtype,
93
+ PixelDtype=pixel_dtype,
94
+ WindowCenter=window_center,
95
+ WindowWidth=window_width,
96
+ MaxVal=max_val,
97
+ MinVal=min_val,
98
+ Extras=extras,
96
99
  )
97
100
 
98
101
  def to_json(self) -> str: