dicube 0.1.4__cp311-cp311-macosx_11_0_arm64.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 +140 -0
- dicube/codecs/__init__.py +152 -0
- dicube/codecs/jph/__init__.py +15 -0
- dicube/codecs/jph/codec.py +161 -0
- dicube/codecs/jph/ojph_complete.cpython-310-darwin.so +0 -0
- dicube/codecs/jph/ojph_complete.cpython-311-darwin.so +0 -0
- dicube/codecs/jph/ojph_complete.cpython-38-darwin.so +0 -0
- dicube/codecs/jph/ojph_complete.cpython-39-darwin.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-310-darwin.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-311-darwin.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-38-darwin.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-39-darwin.so +0 -0
- dicube/core/__init__.py +21 -0
- dicube/core/image.py +349 -0
- dicube/core/io.py +354 -0
- dicube/core/pixel_header.py +117 -0
- dicube/dicom/__init__.py +13 -0
- dicube/dicom/dcb_streaming.py +250 -0
- dicube/dicom/dicom_io.py +153 -0
- dicube/dicom/dicom_meta.py +740 -0
- dicube/dicom/dicom_status.py +259 -0
- dicube/dicom/dicom_tags.py +121 -0
- dicube/dicom/merge_utils.py +283 -0
- dicube/dicom/space_from_meta.py +70 -0
- dicube/exceptions.py +189 -0
- dicube/storage/__init__.py +17 -0
- dicube/storage/dcb_file.py +805 -0
- dicube/storage/pixel_utils.py +141 -0
- dicube/utils/__init__.py +6 -0
- dicube/validation.py +380 -0
- dicube-0.1.4.dist-info/METADATA +271 -0
- dicube-0.1.4.dist-info/RECORD +33 -0
- dicube-0.1.4.dist-info/WHEEL +5 -0
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
|
+
)
|