dicube 0.1.4__cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.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/core/image.py ADDED
@@ -0,0 +1,349 @@
1
+ # core/image.py
2
+ import warnings
3
+ from typing import Optional
4
+
5
+ import numpy as np
6
+
7
+ from ..dicom import (
8
+ CommonTags,
9
+ DicomMeta,
10
+ DicomStatus,
11
+ )
12
+ from .pixel_header import PixelDataHeader
13
+ from ..storage.pixel_utils import get_float_data
14
+ from spacetransformer import Space
15
+ from ..validation import (
16
+ validate_not_none,
17
+ validate_parameter_type,
18
+ validate_array_shape,
19
+ validate_string_not_empty
20
+ )
21
+ from ..exceptions import (
22
+ DataConsistencyError,
23
+ MetaDataError
24
+ )
25
+
26
+
27
+ class DicomCubeImage:
28
+ """A class representing a DICOM image with associated metadata and space information.
29
+
30
+ This class handles DICOM image data along with its pixel header, metadata, and space information.
31
+ It provides methods for file I/O and data manipulation.
32
+
33
+ Attributes:
34
+ raw_image (np.ndarray): The raw image data array.
35
+ pixel_header (PixelDataHeader): Pixel data header containing metadata about the image pixels.
36
+ dicom_meta (DicomMeta, optional): DICOM metadata associated with the image.
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.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ raw_image: np.ndarray,
44
+ pixel_header: PixelDataHeader,
45
+ dicom_meta: Optional[DicomMeta] = None,
46
+ space: Optional[Space] = None,
47
+ dicom_status: str = DicomStatus.CONSISTENT.value,
48
+ ):
49
+ """Initialize a DicomCubeImage instance.
50
+
51
+ Args:
52
+ raw_image (np.ndarray): Raw image data array.
53
+ pixel_header (PixelDataHeader): Pixel data header information.
54
+ dicom_meta (DicomMeta, optional): DICOM metadata. Defaults to None.
55
+ space (Space, optional): Spatial information. Defaults to None.
56
+ """
57
+ # Validate required parameters using validation utilities
58
+ validate_not_none(raw_image, "raw_image", "DicomCubeImage constructor", DataConsistencyError)
59
+ validate_not_none(pixel_header, "pixel_header", "DicomCubeImage constructor", DataConsistencyError)
60
+ validate_array_shape(raw_image, min_dims=2, name="raw_image", context="DicomCubeImage constructor")
61
+ validate_parameter_type(pixel_header, PixelDataHeader, "pixel_header", "DicomCubeImage constructor", DataConsistencyError)
62
+
63
+ # Validate optional parameters if provided
64
+ if dicom_meta is not None:
65
+ validate_parameter_type(dicom_meta, DicomMeta, "dicom_meta", "DicomCubeImage constructor", MetaDataError)
66
+ if space is not None:
67
+ validate_parameter_type(space, Space, "space", "DicomCubeImage constructor", DataConsistencyError)
68
+
69
+ self.raw_image = raw_image
70
+ self.pixel_header = pixel_header
71
+ self.dicom_meta = dicom_meta
72
+ self.space = space
73
+ self.dicom_status = dicom_status
74
+ self._validate_shape()
75
+
76
+
77
+ def _generate_uids(self):
78
+ """Generate necessary UIDs for DICOM metadata.
79
+
80
+ Returns:
81
+ dict: Dictionary containing generated UIDs.
82
+ """
83
+ from pydicom.uid import generate_uid
84
+
85
+ return {
86
+ 'study_uid': generate_uid(),
87
+ 'series_uid': generate_uid(),
88
+ 'sop_uid': generate_uid(),
89
+ 'frame_uid': generate_uid()
90
+ }
91
+
92
+ def _set_patient_info(self, meta: DicomMeta, patient_name: str, patient_id: str):
93
+ """Set patient information in DICOM metadata.
94
+
95
+ Args:
96
+ meta (DicomMeta): The metadata object to update.
97
+ patient_name (str): Patient name.
98
+ patient_id (str): Patient ID.
99
+ """
100
+ meta.set_shared_item(CommonTags.PatientName, {'Alphabetic': patient_name})
101
+ meta.set_shared_item(CommonTags.PatientID, patient_id)
102
+ meta.set_shared_item(CommonTags.PatientBirthDate, "19700101")
103
+ meta.set_shared_item(CommonTags.PatientSex, "O")
104
+
105
+ def _set_study_info(self, meta: DicomMeta, uids: dict, modality: str):
106
+ """Set study information in DICOM metadata.
107
+
108
+ Args:
109
+ meta (DicomMeta): The metadata object to update.
110
+ uids (dict): Dictionary containing generated UIDs.
111
+ modality (str): Image modality.
112
+ """
113
+ import datetime
114
+
115
+ now = datetime.datetime.now()
116
+ date_str = now.strftime("%Y%m%d")
117
+ time_str = now.strftime("%H%M%S")
118
+
119
+ meta.set_shared_item(CommonTags.StudyInstanceUID, uids['study_uid'])
120
+ meta.set_shared_item(CommonTags.StudyDate, date_str)
121
+ meta.set_shared_item(CommonTags.StudyTime, time_str)
122
+ meta.set_shared_item(CommonTags.StudyID, "1")
123
+ meta.set_shared_item(CommonTags.StudyDescription, f"Default {modality} Study")
124
+
125
+ def _set_series_info(self, meta: DicomMeta, uids: dict, modality: str):
126
+ """Set series information in DICOM metadata.
127
+
128
+ Args:
129
+ meta (DicomMeta): The metadata object to update.
130
+ uids (dict): Dictionary containing generated UIDs.
131
+ modality (str): Image modality.
132
+ """
133
+ meta.set_shared_item(CommonTags.SeriesInstanceUID, uids['series_uid'])
134
+ meta.set_shared_item(CommonTags.SeriesNumber, "1")
135
+ meta.set_shared_item(
136
+ CommonTags.SeriesDescription, f"Default {modality} Series"
137
+ )
138
+
139
+ def _set_image_info(self, meta: DicomMeta, uids: dict, num_slices: int):
140
+ """Set image-specific information in DICOM metadata.
141
+
142
+ Args:
143
+ meta (DicomMeta): The metadata object to update.
144
+ uids (dict): Dictionary containing generated UIDs.
145
+ num_slices (int): Number of slices in the image.
146
+ """
147
+ from pydicom.uid import generate_uid
148
+
149
+ if num_slices > 1:
150
+ sop_uids = [generate_uid() for _ in range(num_slices)]
151
+ instance_numbers = [str(i + 1) for i in range(num_slices)]
152
+ meta.set_nonshared_item(CommonTags.SOPInstanceUID, sop_uids)
153
+ meta.set_nonshared_item(CommonTags.InstanceNumber, instance_numbers)
154
+ else:
155
+ meta.set_shared_item(CommonTags.SOPInstanceUID, uids['sop_uid'])
156
+ meta.set_shared_item(CommonTags.InstanceNumber, "1")
157
+
158
+ meta.set_shared_item(CommonTags.FrameOfReferenceUID, uids['frame_uid'])
159
+
160
+ def _set_space_info(self, meta: DicomMeta, num_slices: int):
161
+ """Set spatial information in DICOM metadata.
162
+
163
+ Args:
164
+ meta (DicomMeta): The metadata object to update.
165
+ num_slices (int): Number of slices in the image.
166
+ """
167
+ if self.space is not None:
168
+ # Set orientation information
169
+ orientation = self.space.to_dicom_orientation()
170
+ meta.set_shared_item(
171
+ CommonTags.ImageOrientationPatient, list(orientation)
172
+ )
173
+ meta.set_shared_item(CommonTags.PixelSpacing, list(self.space.spacing[:2]))
174
+ meta.set_shared_item(
175
+ CommonTags.SliceThickness, float(self.space.spacing[2])
176
+ )
177
+
178
+ # Set position information
179
+ if num_slices > 1:
180
+ positions = []
181
+ for i in range(num_slices):
182
+ # Calculate position for each slice using space's z_orientation
183
+ pos = np.array(self.space.origin) + i * self.space.spacing[
184
+ 2
185
+ ] * np.array(self.space.z_orientation)
186
+ positions.append(pos.tolist())
187
+ meta.set_nonshared_item(CommonTags.ImagePositionPatient, positions)
188
+ else:
189
+ meta.set_shared_item(
190
+ CommonTags.ImagePositionPatient, list(self.space.origin)
191
+ )
192
+ else:
193
+ # If no space information, set default values
194
+ meta.set_shared_item(
195
+ CommonTags.ImageOrientationPatient, [1, 0, 0, 0, 1, 0]
196
+ )
197
+ meta.set_shared_item(CommonTags.PixelSpacing, [1.0, 1.0])
198
+ meta.set_shared_item(CommonTags.SliceThickness, 1.0)
199
+ if num_slices > 1:
200
+ positions = [[0, 0, i] for i in range(num_slices)]
201
+ meta.set_nonshared_item(CommonTags.ImagePositionPatient, positions)
202
+ else:
203
+ meta.set_shared_item(CommonTags.ImagePositionPatient, [0, 0, 0])
204
+
205
+ def _set_pixel_info(self, meta: DicomMeta):
206
+ """Set pixel data information in DICOM metadata.
207
+
208
+ Args:
209
+ meta (DicomMeta): The metadata object to update.
210
+ """
211
+ # Image dimensions
212
+ shape = self.raw_image.shape
213
+ if len(shape) == 3:
214
+ meta.set_shared_item(CommonTags.Rows, shape[1])
215
+ meta.set_shared_item(CommonTags.Columns, shape[2])
216
+ else:
217
+ meta.set_shared_item(CommonTags.Rows, shape[0])
218
+ meta.set_shared_item(CommonTags.Columns, shape[1])
219
+
220
+ # Pixel characteristics
221
+ meta.set_shared_item(CommonTags.SamplesPerPixel, 1)
222
+ meta.set_shared_item(CommonTags.PhotometricInterpretation, "MONOCHROME2")
223
+ meta.set_shared_item(CommonTags.BitsAllocated, 16)
224
+ meta.set_shared_item(CommonTags.BitsStored, 16)
225
+ meta.set_shared_item(CommonTags.HighBit, 15)
226
+ meta.set_shared_item(CommonTags.PixelRepresentation, 0)
227
+
228
+ # Rescale Information from pixel_header
229
+ if self.pixel_header.RESCALE_SLOPE is not None:
230
+ meta.set_shared_item(
231
+ CommonTags.RescaleSlope, float(self.pixel_header.RESCALE_SLOPE)
232
+ )
233
+ if self.pixel_header.RESCALE_INTERCEPT is not None:
234
+ meta.set_shared_item(
235
+ CommonTags.RescaleIntercept, float(self.pixel_header.RESCALE_INTERCEPT)
236
+ )
237
+
238
+ def init_meta(
239
+ self,
240
+ modality: str = "OT",
241
+ patient_name: str = "ANONYMOUS^",
242
+ patient_id: str = "0000000",
243
+ ) -> DicomMeta:
244
+ """Initialize a basic DicomMeta when none is provided.
245
+
246
+ Sets required DICOM fields with default values.
247
+
248
+ Args:
249
+ modality (str): Image modality, such as CT/MR/PT. Defaults to "OT".
250
+ patient_name (str): Patient name. Defaults to "ANONYMOUS^".
251
+ patient_id (str): Patient ID. Defaults to "0000000".
252
+
253
+ Returns:
254
+ DicomMeta: A new DicomMeta instance with basic required fields.
255
+ """
256
+ # Validate input parameters
257
+ validate_string_not_empty(modality, "modality", "init_meta operation", MetaDataError)
258
+ validate_string_not_empty(patient_name, "patient_name", "init_meta operation", MetaDataError)
259
+ validate_string_not_empty(patient_id, "patient_id", "init_meta operation", MetaDataError)
260
+
261
+ try:
262
+ # Create empty DicomMeta
263
+ num_slices = self.raw_image.shape[0] if len(self.raw_image.shape) == 3 else 1
264
+ meta = DicomMeta({}, [f"slice_{i:04d}.dcm" for i in range(num_slices)])
265
+
266
+ # Generate necessary UIDs
267
+ uids = self._generate_uids()
268
+
269
+ # Set metadata sections
270
+ self._set_patient_info(meta, patient_name, patient_id)
271
+ self._set_study_info(meta, uids, modality)
272
+ self._set_series_info(meta, uids, modality)
273
+ self._set_image_info(meta, uids, num_slices)
274
+ self._set_space_info(meta, num_slices)
275
+ self._set_pixel_info(meta)
276
+
277
+ # Set modality
278
+ meta.set_shared_item(CommonTags.Modality, modality)
279
+
280
+ # Validate initialization success
281
+ if meta is None:
282
+ raise MetaDataError(
283
+ "DicomMeta initialization returned None",
284
+ context="init_meta operation",
285
+ suggestion="Check DicomMeta constructor parameters and dependencies"
286
+ )
287
+
288
+ self.dicom_meta = meta
289
+ return meta
290
+
291
+ except Exception as e:
292
+ if isinstance(e, MetaDataError):
293
+ raise
294
+ raise MetaDataError(
295
+ f"Failed to initialize DicomMeta: {str(e)}",
296
+ context="init_meta operation",
297
+ suggestion="Verify image data and metadata parameters are valid"
298
+ ) from e
299
+
300
+ @property
301
+ def shape(self):
302
+ """Get the shape of the raw image.
303
+
304
+ Returns:
305
+ tuple: The shape of the raw image array.
306
+ """
307
+ return self.raw_image.shape
308
+
309
+ @property
310
+ def dtype(self):
311
+ """Get the data type of the raw image.
312
+
313
+ Returns:
314
+ numpy.dtype: The data type of the raw image array.
315
+ """
316
+ return self.raw_image.dtype
317
+
318
+ def _validate_shape(self):
319
+ """Validate that the image shape matches the space shape if both are present.
320
+
321
+ Both raw_image and space are now in (z,y,x) format internally.
322
+
323
+ Raises:
324
+ DataConsistencyError: If space shape doesn't match image dimensions.
325
+ """
326
+ if self.space and self.raw_image.ndim >= 3:
327
+ expected_shape = tuple(self.space.shape)
328
+ if self.raw_image.shape[-len(expected_shape) :] != expected_shape:
329
+ raise DataConsistencyError(
330
+ f"Space shape mismatch with image dimensions",
331
+ context="DicomCubeImage shape validation",
332
+ details={
333
+ "space_shape": expected_shape,
334
+ "image_shape": self.raw_image.shape,
335
+ "image_dims": self.raw_image.ndim
336
+ },
337
+ suggestion="Ensure space dimensions match the image array dimensions"
338
+ )
339
+
340
+ def get_fdata(self, dtype="float32") -> np.ndarray:
341
+ """Get image data as floating point array with slope/intercept applied.
342
+
343
+ Args:
344
+ dtype (str): Output data type, must be one of: float16, float32, float64. Defaults to "float32".
345
+
346
+ Returns:
347
+ np.ndarray: Floating point image data with rescale factors applied.
348
+ """
349
+ return get_float_data(self.raw_image, self.pixel_header, dtype)
dicube/core/io.py ADDED
@@ -0,0 +1,354 @@
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
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
+ num_threads: int = 4,
56
+ ) -> None:
57
+ """Save DicomCubeImage to a file.
58
+
59
+ Args:
60
+ image (DicomCubeImage): The DicomCubeImage object to save.
61
+ file_path (str): Output file path.
62
+ file_type (str): File type, "s" (speed priority), "a" (compression priority),
63
+ or "l" (lossy compression). Defaults to "s".
64
+ num_threads (int): Number of parallel encoding threads. Defaults to 4.
65
+
66
+ Raises:
67
+ InvalidCubeFileError: If the file_type is not supported.
68
+ """
69
+ # Validate required parameters
70
+ validate_not_none(image, "image", "save operation", DataConsistencyError)
71
+ 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
+
74
+ # Validate file_type parameter
75
+ if file_type not in ("s", "a", "l"):
76
+ raise InvalidCubeFileError(
77
+ f"Unsupported file type: {file_type}",
78
+ context="save operation",
79
+ details={"file_type": file_type, "supported_types": ["s", "a", "l"]},
80
+ suggestion="Use 's' for speed priority, 'a' for compression priority, or 'l' for lossy compression"
81
+ )
82
+
83
+ try:
84
+ # Choose appropriate writer based on file type
85
+ if file_type == "s":
86
+ writer = DcbSFile(file_path, mode="w")
87
+ elif file_type == "a":
88
+ writer = DcbAFile(file_path, mode="w")
89
+ elif file_type == "l":
90
+ writer = DcbLFile(file_path, mode="w")
91
+
92
+ # Write to file
93
+ writer.write(
94
+ images=image.raw_image,
95
+ pixel_header=image.pixel_header,
96
+ dicom_meta=image.dicom_meta,
97
+ space=image.space,
98
+ num_threads=num_threads,
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, num_threads: int = 4, **kwargs) -> 'DicomCubeImage':
112
+ """Load DicomCubeImage from a file.
113
+
114
+ Args:
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
+
119
+ Returns:
120
+ DicomCubeImage: The loaded object from the file.
121
+
122
+ Raises:
123
+ ValueError: When the file format is not supported.
124
+ """
125
+ # Validate required parameters
126
+ validate_not_none(file_path, "file_path", "load operation", InvalidCubeFileError)
127
+ validate_file_exists(file_path, "load operation", InvalidCubeFileError)
128
+
129
+ try:
130
+ # Read file header to determine format
131
+ header_size = struct.calcsize(DcbFile.HEADER_STRUCT)
132
+ with open(file_path, "rb") as f:
133
+ header_data = f.read(header_size)
134
+ magic = struct.unpack(DcbFile.HEADER_STRUCT, header_data)[0]
135
+
136
+ # Choose appropriate reader based on magic number
137
+ if magic == DcbAFile.MAGIC:
138
+ reader = DcbAFile(file_path, mode="r")
139
+ elif magic == DcbSFile.MAGIC:
140
+ reader = DcbSFile(file_path, mode="r")
141
+ else:
142
+ raise InvalidCubeFileError(
143
+ f"Unsupported file format",
144
+ context="load operation",
145
+ details={"file_path": file_path, "magic_number": magic},
146
+ suggestion="Ensure the file is a valid DicomCube file"
147
+ )
148
+
149
+ # Read file contents
150
+ dicom_meta = reader.read_meta()
151
+ space = reader.read_space()
152
+ pixel_header = reader.read_pixel_header()
153
+ dicom_status = reader.read_dicom_status()
154
+
155
+ images = reader.read_images(num_threads=num_threads)
156
+ if isinstance(images, list):
157
+ # Convert list to ndarray if needed
158
+ images = np.stack(images)
159
+
160
+ # Use lazy import to avoid circular dependency
161
+ from .image import DicomCubeImage
162
+
163
+ return DicomCubeImage(
164
+ raw_image=images,
165
+ pixel_header=pixel_header,
166
+ dicom_meta=dicom_meta,
167
+ space=space,
168
+ dicom_status=dicom_status,
169
+ )
170
+ except Exception as e:
171
+ if isinstance(e, (InvalidCubeFileError, CodecError)):
172
+ raise
173
+ raise InvalidCubeFileError(
174
+ f"Failed to load file: {str(e)}",
175
+ context="load operation",
176
+ details={"file_path": file_path}
177
+ ) from e
178
+
179
+ @staticmethod
180
+ def load_from_dicom_folder(
181
+ folder_path: str,
182
+ sort_method: SortMethod = SortMethod.INSTANCE_NUMBER_ASC,
183
+ **kwargs
184
+ ) -> 'DicomCubeImage':
185
+ """Load DicomCubeImage from a DICOM folder.
186
+
187
+ Args:
188
+ folder_path (str): Path to the DICOM folder.
189
+ sort_method (SortMethod): Method to sort DICOM files.
190
+ Defaults to SortMethod.INSTANCE_NUMBER_ASC.
191
+ **kwargs: Additional parameters.
192
+
193
+ Returns:
194
+ DicomCubeImage: The object created from the DICOM folder.
195
+
196
+ Raises:
197
+ ValueError: When the DICOM status is not supported.
198
+ """
199
+ # Validate required parameters
200
+ validate_not_none(folder_path, "folder_path", "load_from_dicom_folder operation", InvalidCubeFileError)
201
+ validate_folder_exists(folder_path, "load_from_dicom_folder operation", InvalidCubeFileError)
202
+
203
+ try:
204
+ # Read DICOM folder
205
+ meta, datasets = read_dicom_dir(folder_path, sort_method=sort_method)
206
+ images = [d.pixel_array for d in datasets]
207
+ status = get_dicom_status(meta)
208
+
209
+ if status in (
210
+ DicomStatus.NON_UNIFORM_RESCALE_FACTOR,
211
+ DicomStatus.MISSING_DTYPE,
212
+ DicomStatus.NON_UNIFORM_DTYPE,
213
+ DicomStatus.MISSING_SHAPE,
214
+ DicomStatus.INCONSISTENT,
215
+ ):
216
+ raise MetaDataError(
217
+ f"Unsupported DICOM status: {status}",
218
+ context="load_from_dicom_folder operation",
219
+ details={"dicom_status": status, "folder_path": folder_path},
220
+ suggestion="Ensure DICOM files have consistent metadata and proper format"
221
+ )
222
+
223
+ if status in (
224
+ DicomStatus.MISSING_SPACING,
225
+ DicomStatus.NON_UNIFORM_SPACING,
226
+ DicomStatus.MISSING_ORIENTATION,
227
+ DicomStatus.NON_UNIFORM_ORIENTATION,
228
+ DicomStatus.MISSING_LOCATION,
229
+ DicomStatus.REVERSED_LOCATION,
230
+ DicomStatus.DWELLING_LOCATION,
231
+ DicomStatus.GAP_LOCATION,
232
+ ):
233
+ warnings.warn(f"DICOM status: {status}, cannot calculate space information")
234
+ space = None
235
+ else:
236
+ if get_space_from_DicomMeta is not None:
237
+ space = get_space_from_DicomMeta(meta, axis_order="zyx")
238
+ else:
239
+ space = None
240
+
241
+ # Get rescale parameters
242
+ slope = meta.get_shared_value(CommonTags.RescaleSlope)
243
+ intercept = meta.get_shared_value(CommonTags.RescaleIntercept)
244
+ wind_center = meta.get_shared_value(CommonTags.WindowCenter)
245
+ wind_width = meta.get_shared_value(CommonTags.WindowWidth)
246
+
247
+ # Create pixel_header
248
+ 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,
255
+ )
256
+
257
+ # Validate PixelDataHeader initialization success
258
+ if pixel_header is None:
259
+ raise MetaDataError(
260
+ "PixelDataHeader initialization failed",
261
+ context="load_from_dicom_folder operation",
262
+ suggestion="Check DICOM metadata for required pixel data information"
263
+ )
264
+
265
+ # Use lazy import to avoid circular dependency
266
+ from .image import DicomCubeImage
267
+
268
+ return DicomCubeImage(
269
+ raw_image=np.array(images),
270
+ pixel_header=pixel_header,
271
+ dicom_meta=meta,
272
+ space=space,
273
+ dicom_status=status
274
+ )
275
+ except Exception as e:
276
+ if isinstance(e, (InvalidCubeFileError, MetaDataError)):
277
+ raise
278
+ raise MetaDataError(
279
+ f"Failed to load DICOM folder: {str(e)}",
280
+ context="load_from_dicom_folder operation",
281
+ details={"folder_path": folder_path}
282
+ ) from e
283
+
284
+ @staticmethod
285
+ def load_from_nifti(file_path: str, **kwargs) -> 'DicomCubeImage':
286
+ """Load DicomCubeImage from a NIfTI file.
287
+
288
+ Args:
289
+ file_path (str): Path to the NIfTI file.
290
+ **kwargs: Additional parameters.
291
+
292
+ Returns:
293
+ DicomCubeImage: The object created from the NIfTI file.
294
+
295
+ Raises:
296
+ ImportError: When nibabel is not installed.
297
+ """
298
+ # Validate required parameters
299
+ validate_not_none(file_path, "file_path", "load_from_nifti operation", InvalidCubeFileError)
300
+ validate_file_exists(file_path, "load_from_nifti operation", InvalidCubeFileError)
301
+
302
+ try:
303
+ import nibabel as nib
304
+ except ImportError:
305
+ raise ImportError("nibabel is required to read NIfTI files")
306
+
307
+ try:
308
+ nii = nib.load(file_path)
309
+ space = get_space_from_nifti(nii)
310
+
311
+ # Fix numpy array warning
312
+ raw_image, header = derive_pixel_header_from_array(
313
+ np.asarray(nii.dataobj, dtype=nii.dataobj.dtype)
314
+ )
315
+
316
+ # Use lazy import to avoid circular dependency
317
+ from .image import DicomCubeImage
318
+
319
+ return DicomCubeImage(raw_image, header, space=space)
320
+ except Exception as e:
321
+ if isinstance(e, ImportError):
322
+ raise
323
+ raise InvalidCubeFileError(
324
+ f"Failed to load NIfTI file: {str(e)}",
325
+ context="load_from_nifti operation",
326
+ details={"file_path": file_path},
327
+ suggestion="Ensure the file is a valid NIfTI format and nibabel is installed"
328
+ ) from e
329
+
330
+ @staticmethod
331
+ def save_to_dicom_folder(
332
+ image: 'DicomCubeImage',
333
+ folder_path: str,
334
+ ) -> None:
335
+ """Save DicomCubeImage as a DICOM folder.
336
+
337
+ Args:
338
+ image (DicomCubeImage): The DicomCubeImage object to save.
339
+ folder_path (str): Output directory path.
340
+ """
341
+ # Validate required parameters
342
+ validate_not_none(image, "image", "save_to_dicom_folder operation", DataConsistencyError)
343
+ validate_string_not_empty(folder_path, "folder_path", "save_to_dicom_folder operation", InvalidCubeFileError)
344
+
345
+ if image.dicom_meta is None:
346
+ warnings.warn("dicom_meta is None, initializing with default values")
347
+ image.init_meta()
348
+
349
+ save_to_dicom_folder(
350
+ raw_images=image.raw_image,
351
+ dicom_meta=image.dicom_meta,
352
+ pixel_header=image.pixel_header,
353
+ output_dir=folder_path,
354
+ )