dbdicom 0.2.0__py3-none-any.whl → 0.3.16__py3-none-any.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.
- dbdicom/__init__.py +3 -25
- dbdicom/api.py +496 -0
- dbdicom/const.py +144 -0
- dbdicom/database.py +133 -0
- dbdicom/dataset.py +471 -0
- dbdicom/dbd.py +1290 -0
- dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/emf2sf +57 -57
- dbdicom/register.py +402 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +206 -160
- dbdicom/sop_classes/mr_image.py +338 -0
- dbdicom/sop_classes/parametric_map.py +381 -0
- dbdicom/sop_classes/secondary_capture.py +140 -0
- dbdicom/sop_classes/segmentation.py +311 -0
- dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
- dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
- dbdicom/utils/arrays.py +142 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +43 -466
- dbdicom/utils/pydicom_dataset.py +386 -0
- dbdicom-0.3.16.dist-info/METADATA +26 -0
- dbdicom-0.3.16.dist-info/RECORD +54 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/WHEEL +1 -1
- dbdicom/create.py +0 -450
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -841
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/mr_image.py +0 -267
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/external/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/lib/linux-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libopencv_java.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86-64/libclib_jiio.so +0 -0
- dbdicom/manager.py +0 -2077
- dbdicom/message.py +0 -119
- dbdicom/record.py +0 -1526
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -184
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -816
- dbdicom/types/study.py +0 -58
- dbdicom/utils/variables.py +0 -155
- dbdicom/utils/vreg.py +0 -2626
- dbdicom/wrappers/__init__.py +0 -7
- dbdicom/wrappers/dipy.py +0 -462
- dbdicom/wrappers/elastix.py +0 -855
- dbdicom/wrappers/numpy.py +0 -119
- dbdicom/wrappers/scipy.py +0 -1413
- dbdicom/wrappers/skimage.py +0 -1030
- dbdicom/wrappers/sklearn.py +0 -151
- dbdicom/wrappers/vreg.py +0 -273
- dbdicom-0.2.0.dist-info/METADATA +0 -276
- dbdicom-0.2.0.dist-info/RECORD +0 -81
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info/licenses}/LICENSE +0 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pydicom
|
|
3
|
+
from pydicom.dataset import Dataset, FileDataset
|
|
4
|
+
from pydicom.sequence import Sequence
|
|
5
|
+
import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_binary_segmentation_dicom(rows=128, cols=128):
|
|
9
|
+
# Create file metadata
|
|
10
|
+
meta = Dataset()
|
|
11
|
+
meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.66.4' # Segmentation Storage
|
|
12
|
+
meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid()
|
|
13
|
+
meta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian
|
|
14
|
+
meta.ImplementationClassUID = pydicom.uid.generate_uid()
|
|
15
|
+
|
|
16
|
+
# Create the FileDataset (in memory)
|
|
17
|
+
ds = FileDataset(None, {}, file_meta=meta, preamble=b"\0" * 128)
|
|
18
|
+
ds.is_little_endian = True
|
|
19
|
+
ds.is_implicit_VR = False
|
|
20
|
+
|
|
21
|
+
# Required general attributes
|
|
22
|
+
ds.SOPClassUID = meta.MediaStorageSOPClassUID
|
|
23
|
+
ds.SOPInstanceUID = meta.MediaStorageSOPInstanceUID
|
|
24
|
+
ds.Modality = 'SEG'
|
|
25
|
+
ds.SeriesInstanceUID = pydicom.uid.generate_uid()
|
|
26
|
+
ds.StudyInstanceUID = pydicom.uid.generate_uid()
|
|
27
|
+
ds.FrameOfReferenceUID = pydicom.uid.generate_uid()
|
|
28
|
+
ds.PatientName = 'Seg^Test'
|
|
29
|
+
ds.PatientID = 'SEG001'
|
|
30
|
+
|
|
31
|
+
# Set content date/time
|
|
32
|
+
dt = datetime.datetime.now()
|
|
33
|
+
ds.ContentDate = dt.strftime('%Y%m%d')
|
|
34
|
+
ds.ContentTime = dt.strftime('%H%M%S.%f')[:13]
|
|
35
|
+
|
|
36
|
+
# Segmentation-specific
|
|
37
|
+
ds.SegmentationType = 'BINARY'
|
|
38
|
+
ds.ContentLabel = 'MASK'
|
|
39
|
+
ds.ContentDescription = 'Binary segmentation mask'
|
|
40
|
+
ds.ContentCreatorName = 'AutoGen'
|
|
41
|
+
|
|
42
|
+
ds.Rows = rows
|
|
43
|
+
ds.Columns = cols
|
|
44
|
+
ds.SamplesPerPixel = 1
|
|
45
|
+
ds.BitsAllocated = 1
|
|
46
|
+
ds.BitsStored = 1
|
|
47
|
+
ds.HighBit = 0
|
|
48
|
+
ds.PixelRepresentation = 0
|
|
49
|
+
ds.PhotometricInterpretation = 'MONOCHROME2'
|
|
50
|
+
ds.NumberOfFrames = 1
|
|
51
|
+
ds.BurnedInAnnotation = 'NO'
|
|
52
|
+
ds.ImageType = ['DERIVED', 'PRIMARY']
|
|
53
|
+
|
|
54
|
+
# Create a dummy mask (circle in center)
|
|
55
|
+
Y, X = np.ogrid[:rows, :cols]
|
|
56
|
+
mask = ((X - cols // 2)**2 + (Y - rows // 2)**2) < (min(rows, cols) // 4)**2
|
|
57
|
+
binary_frame = np.packbits(mask.astype(np.uint8).flatten())
|
|
58
|
+
|
|
59
|
+
# Assign Pixel Data
|
|
60
|
+
ds.PixelData = binary_frame.tobytes()
|
|
61
|
+
|
|
62
|
+
# SegmentSequence: define what the mask means
|
|
63
|
+
segment = Dataset()
|
|
64
|
+
segment.SegmentNumber = 1
|
|
65
|
+
segment.SegmentLabel = 'Kidney'
|
|
66
|
+
segment.SegmentAlgorithmType = 'MANUAL'
|
|
67
|
+
segment.SegmentAlgorithmName = 'ManualDraw'
|
|
68
|
+
|
|
69
|
+
segment.SegmentedPropertyCategoryCodeSequence = Sequence([Dataset()])
|
|
70
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeValue = 'T-D0050'
|
|
71
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodingSchemeDesignator = 'SRT'
|
|
72
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeMeaning = 'Tissue'
|
|
73
|
+
|
|
74
|
+
segment.SegmentedPropertyTypeCodeSequence = Sequence([Dataset()])
|
|
75
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeValue = 'T-71000'
|
|
76
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodingSchemeDesignator = 'SRT'
|
|
77
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeMeaning = 'Kidney'
|
|
78
|
+
|
|
79
|
+
ds.SegmentSequence = Sequence([segment])
|
|
80
|
+
|
|
81
|
+
# Functional groups (Plane Position)
|
|
82
|
+
fg = Dataset()
|
|
83
|
+
pp = Dataset()
|
|
84
|
+
pp.ImagePositionPatient = [0.0, 0.0, 0.0]
|
|
85
|
+
fg.PlanePositionSequence = Sequence([pp])
|
|
86
|
+
ds.PerFrameFunctionalGroupsSequence = Sequence([fg])
|
|
87
|
+
|
|
88
|
+
return ds
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_multi_segment_segmentation_dicom(masks_dict, spacing=(1.0, 1.0), origin=(0.0, 0.0, 0.0)):
|
|
94
|
+
"""
|
|
95
|
+
Create a multi-segment binary DICOM Segmentation object.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
- masks_dict: dict of {label: binary 2D NumPy array}
|
|
99
|
+
- spacing: (row_spacing, col_spacing)
|
|
100
|
+
- origin: (x, y, z) ImagePositionPatient
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
- pydicom FileDataset object
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
labels = list(masks_dict.keys())
|
|
107
|
+
first_mask = next(iter(masks_dict.values()))
|
|
108
|
+
rows, cols = first_mask.shape
|
|
109
|
+
|
|
110
|
+
# Create metadata
|
|
111
|
+
meta = Dataset()
|
|
112
|
+
meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.66.4'
|
|
113
|
+
meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid()
|
|
114
|
+
meta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian
|
|
115
|
+
meta.ImplementationClassUID = pydicom.uid.generate_uid()
|
|
116
|
+
|
|
117
|
+
ds = FileDataset(None, {}, file_meta=meta, preamble=b"\0" * 128)
|
|
118
|
+
ds.is_little_endian = True
|
|
119
|
+
ds.is_implicit_VR = False
|
|
120
|
+
|
|
121
|
+
# Required general attributes
|
|
122
|
+
ds.SOPClassUID = meta.MediaStorageSOPClassUID
|
|
123
|
+
ds.SOPInstanceUID = meta.MediaStorageSOPInstanceUID
|
|
124
|
+
ds.Modality = 'SEG'
|
|
125
|
+
ds.SeriesInstanceUID = pydicom.uid.generate_uid()
|
|
126
|
+
ds.StudyInstanceUID = pydicom.uid.generate_uid()
|
|
127
|
+
ds.FrameOfReferenceUID = pydicom.uid.generate_uid()
|
|
128
|
+
ds.PatientName = 'Seg^Multi'
|
|
129
|
+
ds.PatientID = 'MULTISEG001'
|
|
130
|
+
ds.ContentDate = datetime.datetime.now().strftime('%Y%m%d')
|
|
131
|
+
ds.ContentTime = datetime.datetime.now().strftime('%H%M%S.%f')[:13]
|
|
132
|
+
|
|
133
|
+
ds.Rows = rows
|
|
134
|
+
ds.Columns = cols
|
|
135
|
+
ds.SamplesPerPixel = 1
|
|
136
|
+
ds.BitsAllocated = 1
|
|
137
|
+
ds.BitsStored = 1
|
|
138
|
+
ds.HighBit = 0
|
|
139
|
+
ds.PixelRepresentation = 0
|
|
140
|
+
ds.PhotometricInterpretation = 'MONOCHROME2'
|
|
141
|
+
ds.SegmentationType = 'BINARY'
|
|
142
|
+
ds.BurnedInAnnotation = 'NO'
|
|
143
|
+
ds.ImageType = ['DERIVED', 'PRIMARY']
|
|
144
|
+
ds.ContentLabel = 'MULTI_SEG'
|
|
145
|
+
ds.ContentCreatorName = 'AutoGen'
|
|
146
|
+
|
|
147
|
+
# SegmentSequence
|
|
148
|
+
segment_sequence = []
|
|
149
|
+
pixel_data_bytes = b''
|
|
150
|
+
fg_sequence = []
|
|
151
|
+
|
|
152
|
+
for i, label in enumerate(labels):
|
|
153
|
+
segment = Dataset()
|
|
154
|
+
segment.SegmentNumber = i + 1
|
|
155
|
+
segment.SegmentLabel = label
|
|
156
|
+
segment.SegmentAlgorithmType = 'MANUAL'
|
|
157
|
+
segment.SegmentAlgorithmName = 'ManualDraw'
|
|
158
|
+
|
|
159
|
+
# Use generic SRT codes for tissue/organ
|
|
160
|
+
segment.SegmentedPropertyCategoryCodeSequence = Sequence([Dataset()])
|
|
161
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeValue = 'T-D0050'
|
|
162
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodingSchemeDesignator = 'SRT'
|
|
163
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeMeaning = 'Tissue'
|
|
164
|
+
|
|
165
|
+
segment.SegmentedPropertyTypeCodeSequence = Sequence([Dataset()])
|
|
166
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeValue = 'T-00000'
|
|
167
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodingSchemeDesignator = '99LOCAL'
|
|
168
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeMeaning = label
|
|
169
|
+
|
|
170
|
+
segment_sequence.append(segment)
|
|
171
|
+
|
|
172
|
+
# Mask -> 1-bit packed frame
|
|
173
|
+
mask = masks_dict[label]
|
|
174
|
+
assert mask.shape == (rows, cols), f"Shape mismatch for label '{label}'"
|
|
175
|
+
packed = np.packbits(mask.astype(np.uint8).flatten())
|
|
176
|
+
pixel_data_bytes += packed.tobytes()
|
|
177
|
+
|
|
178
|
+
# Per-frame functional group (Plane Position and Segment Identification)
|
|
179
|
+
fg = Dataset()
|
|
180
|
+
|
|
181
|
+
# Plane Position
|
|
182
|
+
pp = Dataset()
|
|
183
|
+
pp.ImagePositionPatient = [float(origin[0]), float(origin[1]), float(origin[2] + i)]
|
|
184
|
+
fg.PlanePositionSequence = Sequence([pp])
|
|
185
|
+
|
|
186
|
+
# Segment Identification
|
|
187
|
+
si = Dataset()
|
|
188
|
+
si.ReferencedSegmentNumber = i + 1
|
|
189
|
+
fg.SegmentIdentificationSequence = Sequence([si])
|
|
190
|
+
|
|
191
|
+
fg_sequence.append(fg)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def create_multiframe_segmentation(masks_dict, pixel_spacing=(1.0, 1.0), slice_thickness=1.0, origin=(0.0, 0.0, 0.0)):
|
|
196
|
+
"""
|
|
197
|
+
Create a DICOM Segmentation object with multiple frames per segment (e.g., 3D masks).
|
|
198
|
+
|
|
199
|
+
Parameters:
|
|
200
|
+
masks_dict: dict {label: 3D numpy array (Z, Y, X)}
|
|
201
|
+
pixel_spacing: tuple of (row_spacing, col_spacing)
|
|
202
|
+
slice_thickness: float
|
|
203
|
+
origin: tuple of (x, y, z)
|
|
204
|
+
"""
|
|
205
|
+
labels = list(masks_dict.keys())
|
|
206
|
+
first_volume = next(iter(masks_dict.values()))
|
|
207
|
+
num_slices, rows, cols = first_volume.shape
|
|
208
|
+
|
|
209
|
+
meta = Dataset()
|
|
210
|
+
meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.66.4'
|
|
211
|
+
meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid()
|
|
212
|
+
meta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian
|
|
213
|
+
meta.ImplementationClassUID = pydicom.uid.generate_uid()
|
|
214
|
+
|
|
215
|
+
ds = FileDataset(None, {}, file_meta=meta, preamble=b"\0" * 128)
|
|
216
|
+
ds.is_little_endian = True
|
|
217
|
+
ds.is_implicit_VR = False
|
|
218
|
+
|
|
219
|
+
ds.SOPClassUID = meta.MediaStorageSOPClassUID
|
|
220
|
+
ds.SOPInstanceUID = meta.MediaStorageSOPInstanceUID
|
|
221
|
+
ds.Modality = 'SEG'
|
|
222
|
+
ds.SeriesInstanceUID = pydicom.uid.generate_uid()
|
|
223
|
+
ds.StudyInstanceUID = pydicom.uid.generate_uid()
|
|
224
|
+
ds.FrameOfReferenceUID = pydicom.uid.generate_uid()
|
|
225
|
+
ds.PatientName = 'Seg^3D'
|
|
226
|
+
ds.PatientID = 'SEG3D001'
|
|
227
|
+
dt = datetime.datetime.now()
|
|
228
|
+
ds.ContentDate = dt.strftime('%Y%m%d')
|
|
229
|
+
ds.ContentTime = dt.strftime('%H%M%S.%f')[:13]
|
|
230
|
+
|
|
231
|
+
ds.Rows = rows
|
|
232
|
+
ds.Columns = cols
|
|
233
|
+
ds.SamplesPerPixel = 1
|
|
234
|
+
ds.BitsAllocated = 1
|
|
235
|
+
ds.BitsStored = 1
|
|
236
|
+
ds.HighBit = 0
|
|
237
|
+
ds.PixelRepresentation = 0
|
|
238
|
+
ds.PhotometricInterpretation = 'MONOCHROME2'
|
|
239
|
+
ds.SegmentationType = 'BINARY'
|
|
240
|
+
ds.BurnedInAnnotation = 'NO'
|
|
241
|
+
ds.ImageType = ['DERIVED', 'PRIMARY']
|
|
242
|
+
ds.ContentLabel = 'MULTIFRAME_SEG'
|
|
243
|
+
ds.ContentCreatorName = 'AutoGen'
|
|
244
|
+
|
|
245
|
+
ds.NumberOfFrames = num_slices * len(labels)
|
|
246
|
+
|
|
247
|
+
pixel_data_bytes = b''
|
|
248
|
+
segment_sequence = []
|
|
249
|
+
per_frame_sequence = []
|
|
250
|
+
|
|
251
|
+
for seg_index, label in enumerate(labels):
|
|
252
|
+
vol = masks_dict[label]
|
|
253
|
+
assert vol.shape == (num_slices, rows, cols)
|
|
254
|
+
|
|
255
|
+
segment = Dataset()
|
|
256
|
+
segment.SegmentNumber = seg_index + 1
|
|
257
|
+
segment.SegmentLabel = label
|
|
258
|
+
segment.SegmentAlgorithmType = 'MANUAL'
|
|
259
|
+
segment.SegmentAlgorithmName = 'ManualDraw'
|
|
260
|
+
|
|
261
|
+
segment.SegmentedPropertyCategoryCodeSequence = Sequence([Dataset()])
|
|
262
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeValue = 'T-D0050'
|
|
263
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodingSchemeDesignator = 'SRT'
|
|
264
|
+
segment.SegmentedPropertyCategoryCodeSequence[0].CodeMeaning = 'Tissue'
|
|
265
|
+
|
|
266
|
+
segment.SegmentedPropertyTypeCodeSequence = Sequence([Dataset()])
|
|
267
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeValue = 'T-00000'
|
|
268
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodingSchemeDesignator = '99LOCAL'
|
|
269
|
+
segment.SegmentedPropertyTypeCodeSequence[0].CodeMeaning = label
|
|
270
|
+
|
|
271
|
+
segment_sequence.append(segment)
|
|
272
|
+
|
|
273
|
+
for z in range(num_slices):
|
|
274
|
+
# Pack each slice (frame)
|
|
275
|
+
frame = np.packbits(vol[z].astype(np.uint8).flatten())
|
|
276
|
+
pixel_data_bytes += frame.tobytes()
|
|
277
|
+
|
|
278
|
+
# Functional Group for this frame
|
|
279
|
+
fg = Dataset()
|
|
280
|
+
|
|
281
|
+
# Position
|
|
282
|
+
pos = list(origin)
|
|
283
|
+
pos[2] += z * slice_thickness
|
|
284
|
+
plane = Dataset()
|
|
285
|
+
plane.ImagePositionPatient = [str(v) for v in pos]
|
|
286
|
+
fg.PlanePositionSequence = Sequence([plane])
|
|
287
|
+
|
|
288
|
+
# Segment reference
|
|
289
|
+
seg_id = Dataset()
|
|
290
|
+
seg_id.ReferencedSegmentNumber = seg_index + 1
|
|
291
|
+
fg.SegmentIdentificationSequence = Sequence([seg_id])
|
|
292
|
+
|
|
293
|
+
per_frame_sequence.append(fg)
|
|
294
|
+
|
|
295
|
+
ds.SegmentSequence = Sequence(segment_sequence)
|
|
296
|
+
ds.PixelData = pixel_data_bytes
|
|
297
|
+
ds.PerFrameFunctionalGroupsSequence = Sequence(per_frame_sequence)
|
|
298
|
+
|
|
299
|
+
# Shared functional groups
|
|
300
|
+
shared = Dataset()
|
|
301
|
+
geom = Dataset()
|
|
302
|
+
geom.PixelSpacing = [str(pixel_spacing[0]), str(pixel_spacing[1])]
|
|
303
|
+
geom.SliceThickness = str(slice_thickness)
|
|
304
|
+
geom.ImageOrientationPatient = ['1', '0', '0', '0', '1', '0']
|
|
305
|
+
shared.PixelMeasuresSequence = Sequence([geom])
|
|
306
|
+
ds.SharedFunctionalGroupsSequence = Sequence([shared])
|
|
307
|
+
|
|
308
|
+
return ds
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
@@ -3,23 +3,9 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
import pydicom
|
|
6
|
-
from pydicom.dataset import
|
|
6
|
+
from pydicom.dataset import FileMetaDataset
|
|
7
7
|
from pydicom.sequence import Sequence
|
|
8
8
|
|
|
9
|
-
from dbdicom.ds.dataset import DbDataset
|
|
10
|
-
|
|
11
|
-
class UltrasoundMultiFrameImage(DbDataset):
|
|
12
|
-
def __init__(self, dataset=None, template=None):
|
|
13
|
-
super().__init__()
|
|
14
|
-
|
|
15
|
-
if (dataset is None) and (template is None):
|
|
16
|
-
template = 'DEFAULT'
|
|
17
|
-
|
|
18
|
-
if dataset is not None:
|
|
19
|
-
self.__dict__ = dataset.__dict__
|
|
20
|
-
|
|
21
|
-
if template == 'DEFAULT':
|
|
22
|
-
default(self)
|
|
23
9
|
|
|
24
10
|
def default(ds):
|
|
25
11
|
|
|
@@ -2,25 +2,10 @@
|
|
|
2
2
|
# Produced by pydicom codify utility script
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pydicom
|
|
5
|
-
from pydicom.dataset import
|
|
5
|
+
from pydicom.dataset import FileMetaDataset
|
|
6
6
|
from pydicom.sequence import Sequence
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class XrayAngiographicImage(DbDataset):
|
|
11
|
-
def __init__(self, dataset=None, template=None):
|
|
12
|
-
super().__init__()
|
|
13
|
-
|
|
14
|
-
if (dataset is None) and (template is None):
|
|
15
|
-
template = 'ANGIO'
|
|
16
|
-
|
|
17
|
-
if dataset is not None:
|
|
18
|
-
self.__dict__ = dataset.__dict__
|
|
19
|
-
|
|
20
|
-
if template == 'ANGIO':
|
|
21
|
-
angio(self)
|
|
22
|
-
|
|
23
|
-
def angio(ds):
|
|
8
|
+
def default(ds):
|
|
24
9
|
|
|
25
10
|
# File meta info data elements
|
|
26
11
|
ds.file_meta = FileMetaDataset()
|
dbdicom/utils/arrays.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from typing import List, Tuple
|
|
4
|
+
|
|
5
|
+
def to_array_list(values):
|
|
6
|
+
arrays = []
|
|
7
|
+
for v in values:
|
|
8
|
+
if np.isscalar(v[0]):
|
|
9
|
+
v_arr = np.array(v)
|
|
10
|
+
else:
|
|
11
|
+
v_arr = np.empty(len(v), dtype=object)
|
|
12
|
+
v_arr[:] = v
|
|
13
|
+
arrays.append(v_arr)
|
|
14
|
+
return arrays
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def meshvals(values) -> Tuple[List[np.ndarray], np.ndarray]:
|
|
18
|
+
"""
|
|
19
|
+
Lexicographically sort flattened N coordinate arrays and reshape back to inferred grid shape,
|
|
20
|
+
preserving original type of each input array.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
*arrays : array-like
|
|
25
|
+
Flattened coordinate arrays of the same length. Can be numbers, strings, or list objects.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
sorted_arrays : list[np.ndarray]
|
|
30
|
+
Coordinate arrays reshaped to inferred N-D grid shape, dtype/type preserved.
|
|
31
|
+
indices : np.ndarray
|
|
32
|
+
Permutation indices applied to the flattened arrays.
|
|
33
|
+
shape : tuple[int, ...]
|
|
34
|
+
Inferred grid shape (number of unique values per axis).
|
|
35
|
+
"""
|
|
36
|
+
# Ensure the list elements are arrays
|
|
37
|
+
arrays = to_array_list(values)
|
|
38
|
+
|
|
39
|
+
# Remember original type/dtype for each array
|
|
40
|
+
orig_types = [a.dtype if isinstance(a[0], np.ndarray) else type(a[0]) for a in arrays]
|
|
41
|
+
|
|
42
|
+
# Convert non arrays to object arrays
|
|
43
|
+
arrs = []
|
|
44
|
+
for a in arrays:
|
|
45
|
+
arrs_a = np.empty(len(a), dtype=object)
|
|
46
|
+
arrs_a[:] = a
|
|
47
|
+
arrs.append(arrs_a)
|
|
48
|
+
|
|
49
|
+
# Stack arrays as columns (M x N)
|
|
50
|
+
coords = np.stack(arrs, axis=1)
|
|
51
|
+
|
|
52
|
+
# Lexicographic sort using structured array
|
|
53
|
+
indices = np.lexsort(coords.T[::-1])
|
|
54
|
+
sorted_coords = coords[indices]
|
|
55
|
+
|
|
56
|
+
# Check that all coordinates are unique
|
|
57
|
+
points = [tuple(col) for col in sorted_coords]
|
|
58
|
+
if not all_elements_unique(points):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Improper coordinates. Coordinate values are not unique."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Infer shape from unique values per axis
|
|
64
|
+
shape = tuple(len(np.unique(sorted_coords[:, i])) for i in range(sorted_coords.shape[1]))
|
|
65
|
+
|
|
66
|
+
# Check perfect grid
|
|
67
|
+
if np.prod(shape) != sorted_coords.shape[0]:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Coordinates do not form a perfect Cartesian grid: inferred shape {shape} "
|
|
70
|
+
f"does not match number of points {sorted_coords.shape[0]}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Split back into individual arrays and cast to original type
|
|
74
|
+
sorted_arrays = []
|
|
75
|
+
for i, orig_type in enumerate(orig_types):
|
|
76
|
+
arr = sorted_coords[:, i]
|
|
77
|
+
arr = arr.astype(orig_type).reshape(shape)
|
|
78
|
+
sorted_arrays.append(arr)
|
|
79
|
+
|
|
80
|
+
return sorted_arrays, indices
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def all_elements_unique(items):
|
|
84
|
+
"""
|
|
85
|
+
The most general uniqueness check, but also the slowest (O(n^2)).
|
|
86
|
+
|
|
87
|
+
It works for ANY type that supports equality checking (==), including
|
|
88
|
+
lists, dicts, and custom objects, without requiring them to be hashable.
|
|
89
|
+
"""
|
|
90
|
+
for i in range(len(items)):
|
|
91
|
+
for j in range(i + 1, len(items)):
|
|
92
|
+
if items[i] == items[j]:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# def NEWmeshvals(coords):
|
|
99
|
+
# stack_coords = [np.array(c, dtype=object) for c in coords]
|
|
100
|
+
# stack_coords = np.stack(stack_coords)
|
|
101
|
+
# mesh_coords, sorted_indices = _meshvals(stack_coords)
|
|
102
|
+
# mesh_coords = [mesh_coords[d,...] for d in range(mesh_coords.shape[0])]
|
|
103
|
+
# return mesh_coords, sorted_indices
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# def _meshvals(coords):
|
|
107
|
+
# # Input array shape: (d, f) with d = nr of dims and f = nr of frames
|
|
108
|
+
# # Output array shape: (d, f1,..., fd)
|
|
109
|
+
# if coords.size == 0:
|
|
110
|
+
# return np.array([])
|
|
111
|
+
# # Sort by column
|
|
112
|
+
# sorted_indices = np.lexsort(coords[::-1])
|
|
113
|
+
# sorted_array = coords[:, sorted_indices]
|
|
114
|
+
# # Find shape
|
|
115
|
+
# shape = _mesh_shape(sorted_array)
|
|
116
|
+
# # Reshape
|
|
117
|
+
# mesh_array = sorted_array.reshape(shape)
|
|
118
|
+
# return mesh_array, sorted_indices
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# def _mesh_shape(sorted_array):
|
|
122
|
+
|
|
123
|
+
# nd = np.unique(sorted_array[0,:]).size
|
|
124
|
+
# shape = (sorted_array.shape[0], nd)
|
|
125
|
+
|
|
126
|
+
# for dim in range(1,shape[0]):
|
|
127
|
+
# shape_dim = (shape[0], np.prod(shape[1:]), -1)
|
|
128
|
+
# sorted_array = sorted_array.reshape(shape_dim)
|
|
129
|
+
# nd = [np.unique(sorted_array[dim,d,:]).size for d in range(shape_dim[1])]
|
|
130
|
+
# shape = shape + (max(nd),)
|
|
131
|
+
|
|
132
|
+
# if np.prod(shape) != sorted_array.size:
|
|
133
|
+
# raise ValueError(
|
|
134
|
+
# 'Improper dimensions for the series. This usually means '
|
|
135
|
+
# 'that there are multiple images at the same location, \n or that '
|
|
136
|
+
# 'there are no images at one or more locations. \n\n'
|
|
137
|
+
# 'Make sure to specify proper dimensions when reading a pixel array or volume. \n'
|
|
138
|
+
# 'If the default dimensions of pixel_array (InstanceNumber) generate this error, '
|
|
139
|
+
# 'the DICOM data may be corrupted.'
|
|
140
|
+
# )
|
|
141
|
+
|
|
142
|
+
# return shape
|
dbdicom/utils/files.py
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
3
|
import zipfile
|
|
4
|
-
import imageio
|
|
5
|
-
import numpy as np
|
|
6
4
|
|
|
7
|
-
from PIL import Image, ImageSequence
|
|
8
|
-
import numpy as np
|
|
9
5
|
|
|
10
6
|
|
|
11
|
-
def _load_gif_frames(image: Image, mode='RGBA'):
|
|
12
|
-
return np.array([
|
|
13
|
-
np.array(frame.convert(mode))
|
|
14
|
-
for frame in ImageSequence.Iterator(image)
|
|
15
|
-
])
|
|
16
|
-
|
|
17
|
-
def gif2numpy(file):
|
|
18
|
-
with Image.open(file) as im:
|
|
19
|
-
frames = _load_gif_frames(im)
|
|
20
|
-
# GIF files are in RGBA format but assume grescale for now
|
|
21
|
-
frames = frames[...,0]
|
|
22
|
-
# Transpose in-plane x-y
|
|
23
|
-
frames = np.transpose(frames, (0,2,1))
|
|
24
|
-
return frames
|
|
25
|
-
#return imageio.imread(file)
|
|
26
|
-
|
|
27
7
|
def all_files(path):
|
|
28
8
|
files = [item.path for item in scan_tree(path) if item.is_file()]
|
|
29
9
|
# Windows has maximum path length of 260 - ignore any files that are longer
|