dicube 0.2.2__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
Files changed (35) hide show
  1. dicube/__init__.py +174 -0
  2. dicube/codecs/__init__.py +152 -0
  3. dicube/codecs/jph/__init__.py +15 -0
  4. dicube/codecs/jph/codec.py +161 -0
  5. dicube/codecs/jph/ojph_complete.cpython-310-aarch64-linux-gnu.so +0 -0
  6. dicube/codecs/jph/ojph_complete.cpython-311-aarch64-linux-gnu.so +0 -0
  7. dicube/codecs/jph/ojph_complete.cpython-312-aarch64-linux-gnu.so +0 -0
  8. dicube/codecs/jph/ojph_complete.cpython-38-aarch64-linux-gnu.so +0 -0
  9. dicube/codecs/jph/ojph_complete.cpython-39-aarch64-linux-gnu.so +0 -0
  10. dicube/codecs/jph/ojph_decode_complete.cpython-310-aarch64-linux-gnu.so +0 -0
  11. dicube/codecs/jph/ojph_decode_complete.cpython-311-aarch64-linux-gnu.so +0 -0
  12. dicube/codecs/jph/ojph_decode_complete.cpython-312-aarch64-linux-gnu.so +0 -0
  13. dicube/codecs/jph/ojph_decode_complete.cpython-38-aarch64-linux-gnu.so +0 -0
  14. dicube/codecs/jph/ojph_decode_complete.cpython-39-aarch64-linux-gnu.so +0 -0
  15. dicube/core/__init__.py +21 -0
  16. dicube/core/image.py +349 -0
  17. dicube/core/io.py +408 -0
  18. dicube/core/pixel_header.py +120 -0
  19. dicube/dicom/__init__.py +13 -0
  20. dicube/dicom/dcb_streaming.py +248 -0
  21. dicube/dicom/dicom_io.py +153 -0
  22. dicube/dicom/dicom_meta.py +740 -0
  23. dicube/dicom/dicom_status.py +259 -0
  24. dicube/dicom/dicom_tags.py +121 -0
  25. dicube/dicom/merge_utils.py +283 -0
  26. dicube/dicom/space_from_meta.py +70 -0
  27. dicube/exceptions.py +189 -0
  28. dicube/storage/__init__.py +17 -0
  29. dicube/storage/dcb_file.py +824 -0
  30. dicube/storage/pixel_utils.py +259 -0
  31. dicube/utils/__init__.py +6 -0
  32. dicube/validation.py +380 -0
  33. dicube-0.2.2.dist-info/METADATA +272 -0
  34. dicube-0.2.2.dist-info/RECORD +35 -0
  35. dicube-0.2.2.dist-info/WHEEL +6 -0
dicube/core/io.py ADDED
@@ -0,0 +1,408 @@
1
+ import struct
2
+ import warnings
3
+ from typing import Optional, Union
4
+
5
+ import numpy as np
6
+ from spacetransformer import Space, get_space_from_nifti
7
+
8
+ from ..dicom import (
9
+ CommonTags,
10
+ DicomMeta,
11
+ DicomStatus,
12
+ SortMethod,
13
+ get_dicom_status,
14
+ read_dicom_dir,
15
+ get_space_from_DicomMeta,
16
+ )
17
+ from ..dicom.dicom_io import save_to_dicom_folder
18
+ from ..storage.dcb_file import DcbSFile, DcbFile, DcbAFile, DcbLFile
19
+ from ..storage.pixel_utils import derive_pixel_header_from_array, determine_optimal_nifti_dtype
20
+ from .pixel_header import PixelDataHeader
21
+
22
+ from ..validation import (
23
+ validate_not_none,
24
+ validate_file_exists,
25
+ validate_folder_exists,
26
+ validate_parameter_type,
27
+ validate_string_not_empty,
28
+ validate_numeric_range
29
+ )
30
+ from ..exceptions import (
31
+ InvalidCubeFileError,
32
+ CodecError,
33
+ MetaDataError,
34
+ DataConsistencyError
35
+ )
36
+ import os
37
+
38
+
39
+ class DicomCubeImageIO:
40
+ """Static I/O utility class responsible for DicomCubeImage file operations.
41
+
42
+ Responsibilities:
43
+ - Provides unified file I/O interface
44
+ - Automatically detects file formats
45
+ - Handles conversion between various file formats
46
+ """
47
+
48
+
49
+
50
+ @staticmethod
51
+ def save(
52
+ image: "DicomCubeImage",
53
+ file_path: str,
54
+ file_type: str = "s",
55
+ ) -> None:
56
+ """Save DicomCubeImage to a file.
57
+
58
+ Args:
59
+ image (DicomCubeImage): The DicomCubeImage object to save.
60
+ file_path (str): Output file path.
61
+ file_type (str): File type, "s" (speed priority), "a" (compression priority),
62
+ or "l" (lossy compression). Defaults to "s".
63
+
64
+ Raises:
65
+ InvalidCubeFileError: If the file_type is not supported.
66
+ """
67
+ # Validate required parameters
68
+ validate_not_none(image, "image", "save operation", DataConsistencyError)
69
+ validate_string_not_empty(file_path, "file_path", "save operation", InvalidCubeFileError)
70
+
71
+ # Validate file_type parameter
72
+ if file_type not in ("s", "a", "l"):
73
+ raise InvalidCubeFileError(
74
+ f"Unsupported file type: {file_type}",
75
+ context="save operation",
76
+ details={"file_type": file_type, "supported_types": ["s", "a", "l"]},
77
+ suggestion="Use 's' for speed priority, 'a' for compression priority, or 'l' for lossy compression"
78
+ )
79
+
80
+ try:
81
+ # Choose appropriate writer based on file type
82
+ # The writer will automatically ensure correct file extension
83
+ if file_type == "s":
84
+ writer = DcbSFile(file_path, mode="w")
85
+ elif file_type == "a":
86
+ writer = DcbAFile(file_path, mode="w")
87
+ elif file_type == "l":
88
+ writer = DcbLFile(file_path, mode="w")
89
+
90
+ # Update file_path to the corrected path from writer
91
+ file_path = writer.filename
92
+
93
+ # Write to file
94
+ writer.write(
95
+ images=image.raw_image,
96
+ pixel_header=image.pixel_header,
97
+ dicom_meta=image.dicom_meta,
98
+ space=image.space,
99
+ dicom_status=image.dicom_status,
100
+ )
101
+ except Exception as e:
102
+ if isinstance(e, (InvalidCubeFileError, CodecError)):
103
+ raise
104
+ raise InvalidCubeFileError(
105
+ f"Failed to save file: {str(e)}",
106
+ context="save operation",
107
+ details={"file_path": file_path, "file_type": file_type}
108
+ ) from e
109
+
110
+ @staticmethod
111
+ def load(file_path: str) -> 'DicomCubeImage':
112
+ """Load DicomCubeImage from a file.
113
+
114
+ Args:
115
+ file_path (str): Input file path.
116
+
117
+ Returns:
118
+ DicomCubeImage: The loaded object from the file.
119
+
120
+ Raises:
121
+ ValueError: When the file format is not supported.
122
+ """
123
+ # Validate required parameters
124
+ validate_not_none(file_path, "file_path", "load operation", InvalidCubeFileError)
125
+ validate_file_exists(file_path, "load operation", InvalidCubeFileError)
126
+
127
+ try:
128
+ # Read file header to determine format
129
+ header_size = struct.calcsize(DcbFile.HEADER_STRUCT)
130
+ with open(file_path, "rb") as f:
131
+ header_data = f.read(header_size)
132
+ magic = struct.unpack(DcbFile.HEADER_STRUCT, header_data)[0]
133
+
134
+ # Choose appropriate reader based on magic number
135
+ if magic == DcbAFile.MAGIC:
136
+ reader = DcbAFile(file_path, mode="r")
137
+ elif magic == DcbSFile.MAGIC:
138
+ reader = DcbSFile(file_path, mode="r")
139
+ else:
140
+ raise InvalidCubeFileError(
141
+ f"Unsupported file format",
142
+ context="load operation",
143
+ details={"file_path": file_path, "magic_number": magic},
144
+ suggestion="Ensure the file is a valid DicomCube file"
145
+ )
146
+
147
+ # Read file contents
148
+ dicom_meta = reader.read_meta()
149
+ space = reader.read_space()
150
+ pixel_header = reader.read_pixel_header()
151
+ dicom_status = reader.read_dicom_status()
152
+
153
+ images = reader.read_images()
154
+ if isinstance(images, list):
155
+ # Convert list to ndarray if needed
156
+ images = np.stack(images)
157
+
158
+ # Use lazy import to avoid circular dependency
159
+ from .image import DicomCubeImage
160
+
161
+ return DicomCubeImage(
162
+ raw_image=images,
163
+ pixel_header=pixel_header,
164
+ dicom_meta=dicom_meta,
165
+ space=space,
166
+ dicom_status=dicom_status,
167
+ )
168
+ except Exception as e:
169
+ if isinstance(e, (InvalidCubeFileError, CodecError)):
170
+ raise
171
+ raise InvalidCubeFileError(
172
+ f"Failed to load file: {str(e)}",
173
+ context="load operation",
174
+ details={"file_path": file_path}
175
+ ) from e
176
+
177
+ @staticmethod
178
+ def load_from_dicom_folder(
179
+ folder_path: str,
180
+ sort_method: SortMethod = SortMethod.INSTANCE_NUMBER_ASC,
181
+ ) -> 'DicomCubeImage':
182
+ """Load DicomCubeImage from a DICOM folder.
183
+
184
+ Args:
185
+ folder_path (str): Path to the DICOM folder.
186
+ sort_method (SortMethod): Method to sort DICOM files.
187
+ Defaults to SortMethod.INSTANCE_NUMBER_ASC.
188
+
189
+ Returns:
190
+ DicomCubeImage: The object created from the DICOM folder.
191
+
192
+ Raises:
193
+ ValueError: When the DICOM status is not supported.
194
+ """
195
+ # Validate required parameters
196
+ validate_not_none(folder_path, "folder_path", "load_from_dicom_folder operation", InvalidCubeFileError)
197
+ validate_folder_exists(folder_path, "load_from_dicom_folder operation", InvalidCubeFileError)
198
+
199
+ try:
200
+ # Read DICOM folder
201
+ meta, datasets = read_dicom_dir(folder_path, sort_method=sort_method)
202
+ images = [d.pixel_array for d in datasets]
203
+ status = get_dicom_status(meta)
204
+
205
+ if status in (
206
+ DicomStatus.NON_UNIFORM_RESCALE_FACTOR,
207
+ DicomStatus.MISSING_DTYPE,
208
+ DicomStatus.NON_UNIFORM_DTYPE,
209
+ DicomStatus.MISSING_SHAPE,
210
+ DicomStatus.INCONSISTENT,
211
+ ):
212
+ raise MetaDataError(
213
+ f"Unsupported DICOM status: {status}",
214
+ context="load_from_dicom_folder operation",
215
+ details={"dicom_status": status, "folder_path": folder_path},
216
+ suggestion="Ensure DICOM files have consistent metadata and proper format"
217
+ )
218
+
219
+ if status in (
220
+ DicomStatus.MISSING_SPACING,
221
+ DicomStatus.NON_UNIFORM_SPACING,
222
+ DicomStatus.MISSING_ORIENTATION,
223
+ DicomStatus.NON_UNIFORM_ORIENTATION,
224
+ DicomStatus.MISSING_LOCATION,
225
+ DicomStatus.REVERSED_LOCATION,
226
+ DicomStatus.DWELLING_LOCATION,
227
+ DicomStatus.GAP_LOCATION,
228
+ ):
229
+ warnings.warn(f"DICOM status: {status}, cannot calculate space information")
230
+ space = None
231
+ else:
232
+ if get_space_from_DicomMeta is not None:
233
+ space = get_space_from_DicomMeta(meta, axis_order="zyx")
234
+ else:
235
+ space = None
236
+
237
+ # Get rescale parameters
238
+ slope = meta.get_shared_value(CommonTags.RescaleSlope)
239
+ intercept = meta.get_shared_value(CommonTags.RescaleIntercept)
240
+ wind_center = meta.get_shared_value(CommonTags.WindowCenter)
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
248
+
249
+ # Create pixel_header
250
+ pixel_header = PixelDataHeader(
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,
257
+ )
258
+
259
+ # Validate PixelDataHeader initialization success
260
+ if pixel_header is None:
261
+ raise MetaDataError(
262
+ "PixelDataHeader initialization failed",
263
+ context="load_from_dicom_folder operation",
264
+ suggestion="Check DICOM metadata for required pixel data information"
265
+ )
266
+
267
+ # Use lazy import to avoid circular dependency
268
+ from .image import DicomCubeImage
269
+
270
+ return DicomCubeImage(
271
+ raw_image=np.array(images),
272
+ pixel_header=pixel_header,
273
+ dicom_meta=meta,
274
+ space=space,
275
+ dicom_status=status
276
+ )
277
+ except Exception as e:
278
+ if isinstance(e, (InvalidCubeFileError, MetaDataError)):
279
+ raise
280
+ raise MetaDataError(
281
+ f"Failed to load DICOM folder: {str(e)}",
282
+ context="load_from_dicom_folder operation",
283
+ details={"folder_path": folder_path}
284
+ ) from e
285
+
286
+ @staticmethod
287
+ def load_from_nifti(file_path: str) -> 'DicomCubeImage':
288
+ """Load DicomCubeImage from a NIfTI file.
289
+
290
+ Args:
291
+ file_path (str): Path to the NIfTI file.
292
+
293
+ Returns:
294
+ DicomCubeImage: The object created from the NIfTI file.
295
+
296
+ Raises:
297
+ ImportError: When nibabel is not installed.
298
+ """
299
+ # Validate required parameters
300
+ validate_not_none(file_path, "file_path", "load_from_nifti operation", InvalidCubeFileError)
301
+ validate_file_exists(file_path, "load_from_nifti operation", InvalidCubeFileError)
302
+
303
+ try:
304
+ import nibabel as nib
305
+ except ImportError:
306
+ raise ImportError("nibabel is required to read NIfTI files")
307
+
308
+ try:
309
+ nii = nib.load(file_path)
310
+ space = get_space_from_nifti(nii)
311
+
312
+ # Fix numpy array warning
313
+ raw_image, header = derive_pixel_header_from_array(
314
+ np.asarray(nii.dataobj, dtype=nii.dataobj.dtype)
315
+ )
316
+
317
+ # Use lazy import to avoid circular dependency
318
+ from .image import DicomCubeImage
319
+
320
+ return DicomCubeImage(raw_image, header, space=space)
321
+ except Exception as e:
322
+ if isinstance(e, ImportError):
323
+ raise
324
+ raise InvalidCubeFileError(
325
+ f"Failed to load NIfTI file: {str(e)}",
326
+ context="load_from_nifti operation",
327
+ details={"file_path": file_path},
328
+ suggestion="Ensure the file is a valid NIfTI format and nibabel is installed"
329
+ ) from e
330
+
331
+ @staticmethod
332
+ def save_to_dicom_folder(
333
+ image: 'DicomCubeImage',
334
+ folder_path: str,
335
+ ) -> None:
336
+ """Save DicomCubeImage as a DICOM folder.
337
+
338
+ Args:
339
+ image (DicomCubeImage): The DicomCubeImage object to save.
340
+ folder_path (str): Output directory path.
341
+ """
342
+ # Validate required parameters
343
+ validate_not_none(image, "image", "save_to_dicom_folder operation", DataConsistencyError)
344
+ validate_string_not_empty(folder_path, "folder_path", "save_to_dicom_folder operation", InvalidCubeFileError)
345
+
346
+ if image.dicom_meta is None:
347
+ warnings.warn("dicom_meta is None, initializing with default values")
348
+ image.init_meta()
349
+
350
+ save_to_dicom_folder(
351
+ raw_images=image.raw_image,
352
+ dicom_meta=image.dicom_meta,
353
+ pixel_header=image.pixel_header,
354
+ output_dir=folder_path,
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
@@ -0,0 +1,120 @@
1
+ import json
2
+ from dataclasses import asdict, dataclass, field
3
+ from typing import Dict, Optional
4
+
5
+
6
+ @dataclass
7
+ class PixelDataHeader:
8
+ """Header class for storing pixel data information in medical images.
9
+
10
+ Stores metadata including:
11
+ - Rescale factors (slope/intercept)
12
+ - Original pixel data type
13
+ - Window settings (center/width)
14
+ - Value range (min/max)
15
+ - Additional metadata in extras
16
+
17
+ Attributes:
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
+ """
28
+
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
+
39
+ def to_dict(self) -> dict:
40
+ """Convert the header to a dictionary for serialization.
41
+
42
+ Merges extras field into the main dictionary and removes
43
+ the redundant extras key.
44
+
45
+ Returns:
46
+ dict: Dictionary representation of the header.
47
+ """
48
+ data = asdict(self)
49
+ data.update(self.Extras) # Merge Extras into dictionary
50
+ data.pop("Extras", None) # Remove redundant Extras field
51
+ return data
52
+
53
+ @classmethod
54
+ def from_dict(cls, d: dict):
55
+ """Create a PixelDataHeader from a dictionary.
56
+
57
+ Args:
58
+ d (dict): Dictionary containing header data.
59
+
60
+ Returns:
61
+ PixelDataHeader: A new instance with values from the dictionary.
62
+ """
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
73
+ extras = {
74
+ k: v
75
+ for k, v in d.items()
76
+ if k
77
+ not in {
78
+ "RescaleSlope",
79
+ "RescaleIntercept",
80
+ "OriginalPixelDtype",
81
+ "PixelDtype",
82
+ "WindowCenter",
83
+ "WindowWidth",
84
+ "MaxVal",
85
+ "MinVal",
86
+ }
87
+ }
88
+
89
+ return cls(
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,
99
+ )
100
+
101
+ def to_json(self) -> str:
102
+ """Serialize the header to a JSON string.
103
+
104
+ Returns:
105
+ str: JSON string representation of the header.
106
+ """
107
+ return json.dumps(self.to_dict())
108
+
109
+ @classmethod
110
+ def from_json(cls, json_str: str):
111
+ """Create a PixelDataHeader from a JSON string.
112
+
113
+ Args:
114
+ json_str (str): JSON string containing header data.
115
+
116
+ Returns:
117
+ PixelDataHeader: A new instance created from the JSON data.
118
+ """
119
+ obj_dict = json.loads(json_str)
120
+ return cls.from_dict(obj_dict)
@@ -0,0 +1,13 @@
1
+ from .dicom_meta import DicomMeta, SortMethod, read_dicom_dir
2
+ from .dicom_status import DicomStatus, get_dicom_status
3
+ from .dicom_tags import CommonTags
4
+ from .space_from_meta import get_space_from_DicomMeta
5
+ __all__ = [
6
+ "DicomMeta",
7
+ "read_dicom_dir",
8
+ "DicomStatus",
9
+ "get_dicom_status",
10
+ "CommonTags",
11
+ "SortMethod",
12
+ "get_space_from_DicomMeta",
13
+ ]