dicube 0.2.2__cp310-cp310-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.
- dicube/__init__.py +174 -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-aarch64-linux-gnu.so +0 -0
- dicube/codecs/jph/ojph_complete.cpython-38-aarch64-linux-gnu.so +0 -0
- dicube/codecs/jph/ojph_complete.cpython-39-aarch64-linux-gnu.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-310-aarch64-linux-gnu.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-38-aarch64-linux-gnu.so +0 -0
- dicube/codecs/jph/ojph_decode_complete.cpython-39-aarch64-linux-gnu.so +0 -0
- dicube/core/__init__.py +21 -0
- dicube/core/image.py +349 -0
- dicube/core/io.py +408 -0
- dicube/core/pixel_header.py +120 -0
- dicube/dicom/__init__.py +13 -0
- dicube/dicom/dcb_streaming.py +248 -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 +824 -0
- dicube/storage/pixel_utils.py +259 -0
- dicube/utils/__init__.py +6 -0
- dicube/validation.py +380 -0
- dicube-0.2.2.dist-info/METADATA +272 -0
- dicube-0.2.2.dist-info/RECORD +31 -0
- dicube-0.2.2.dist-info/WHEEL +6 -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 (DicomStatus): DICOM status enumeration. Defaults to DicomStatus.CONSISTENT.
|
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: DicomStatus = DicomStatus.CONSISTENT,
|
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.RescaleSlope is not None:
|
230
|
+
meta.set_shared_item(
|
231
|
+
CommonTags.RescaleSlope, float(self.pixel_header.RescaleSlope)
|
232
|
+
)
|
233
|
+
if self.pixel_header.RescaleIntercept is not None:
|
234
|
+
meta.set_shared_item(
|
235
|
+
CommonTags.RescaleIntercept, float(self.pixel_header.RescaleIntercept)
|
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)
|