dbdicom 0.2.6__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of dbdicom might be problematic. Click here for more details.
- dbdicom/__init__.py +1 -28
- dbdicom/api.py +267 -0
- dbdicom/const.py +144 -0
- dbdicom/dataset.py +752 -0
- dbdicom/dbd.py +719 -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/register.py +527 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
- dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
- dbdicom/sop_classes/parametric_map.py +307 -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 +36 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +10 -629
- dbdicom-0.3.0.dist-info/METADATA +28 -0
- dbdicom-0.3.0.dist-info/RECORD +53 -0
- dbdicom/create.py +0 -457
- dbdicom/dro.py +0 -174
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -869
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/extensions/__init__.py +0 -9
- dbdicom/extensions/dipy.py +0 -448
- dbdicom/extensions/elastix.py +0 -503
- dbdicom/extensions/matplotlib.py +0 -107
- dbdicom/extensions/numpy.py +0 -271
- dbdicom/extensions/scipy.py +0 -1512
- dbdicom/extensions/skimage.py +0 -1030
- dbdicom/extensions/sklearn.py +0 -243
- dbdicom/extensions/vreg.py +0 -1390
- dbdicom/manager.py +0 -2132
- dbdicom/message.py +0 -119
- dbdicom/pipelines.py +0 -66
- dbdicom/record.py +0 -1893
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -231
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -2874
- dbdicom/types/study.py +0 -58
- dbdicom-0.2.6.dist-info/METADATA +0 -72
- dbdicom-0.2.6.dist-info/RECORD +0 -66
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/WHEEL +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/top_level.txt +0 -0
dbdicom/dataset.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import struct
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pydicom
|
|
9
|
+
from pydicom.util.codify import code_file
|
|
10
|
+
import pydicom.config
|
|
11
|
+
from pydicom.dataset import Dataset
|
|
12
|
+
import vreg
|
|
13
|
+
|
|
14
|
+
import dbdicom.utils.image as image
|
|
15
|
+
import dbdicom.utils.variables as variables
|
|
16
|
+
from dbdicom.sop_classes import (
|
|
17
|
+
xray_angiographic_image,
|
|
18
|
+
ct_image,
|
|
19
|
+
mr_image,
|
|
20
|
+
enhanced_mr_image,
|
|
21
|
+
ultrasound_multiframe_image,
|
|
22
|
+
parametric_map,
|
|
23
|
+
segmentation,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# This ensures that dates and times are read as TM, DT and DA classes
|
|
28
|
+
pydicom.config.datetime_conversion = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
SOPCLASS = {
|
|
32
|
+
'1.2.840.10008.5.1.4.1.1.4': 'MRImage',
|
|
33
|
+
'1.2.840.10008.5.1.4.1.1.4.1': 'EnhancedMRImage',
|
|
34
|
+
'1.2.840.10008.5.1.4.1.1.2': 'CTImage',
|
|
35
|
+
'1.2.840.10008.5.1.4.1.1.12.2': 'XrayAngiographicImage',
|
|
36
|
+
'1.2.840.10008.5.1.4.1.1.3.1': 'UltrasoundMultiFrameImage',
|
|
37
|
+
'1.2.840.10008.5.1.4.1.1.30': 'ParametricMap',
|
|
38
|
+
'1.2.840.10008.5.1.4.1.1.66.4': 'Segmentation',
|
|
39
|
+
}
|
|
40
|
+
SOPCLASSMODULE = {
|
|
41
|
+
'1.2.840.10008.5.1.4.1.1.4': mr_image,
|
|
42
|
+
'1.2.840.10008.5.1.4.1.1.4.1': enhanced_mr_image,
|
|
43
|
+
'1.2.840.10008.5.1.4.1.1.2': ct_image,
|
|
44
|
+
'1.2.840.10008.5.1.4.1.1.12.2': xray_angiographic_image,
|
|
45
|
+
'1.2.840.10008.5.1.4.1.1.3.1': ultrasound_multiframe_image,
|
|
46
|
+
'1.2.840.10008.5.1.4.1.1.30': parametric_map,
|
|
47
|
+
'1.2.840.10008.5.1.4.1.1.66.4': segmentation,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_dataset(file):
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
ds = pydicom.dcmread(file)
|
|
55
|
+
# ds = pydicom.dcmread(file, force=True) # more robust but hides corrupted data
|
|
56
|
+
except Exception:
|
|
57
|
+
raise FileNotFoundError('File not found')
|
|
58
|
+
|
|
59
|
+
return ds
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def new_dataset(sop_class):
|
|
63
|
+
|
|
64
|
+
if sop_class == 'MRImage':
|
|
65
|
+
return mr_image.default()
|
|
66
|
+
if sop_class == 'EnhancedMRImage':
|
|
67
|
+
return enhanced_mr_image.default()
|
|
68
|
+
if sop_class == 'CTImage':
|
|
69
|
+
return ct_image.default()
|
|
70
|
+
if sop_class == 'XrayAngiographicImage':
|
|
71
|
+
return xray_angiographic_image.default()
|
|
72
|
+
if sop_class == 'UltrasoundMultiFrameImage':
|
|
73
|
+
return ultrasound_multiframe_image.default()
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"DICOM class {sop_class} is not currently supported"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_values(ds, tags):
|
|
82
|
+
"""Return a list of values for a dataset"""
|
|
83
|
+
|
|
84
|
+
# https://pydicom.github.io/pydicom/stable/guides/element_value_types.html
|
|
85
|
+
if np.isscalar(tags):
|
|
86
|
+
return get_values(ds, [tags])[0]
|
|
87
|
+
|
|
88
|
+
row = []
|
|
89
|
+
for tag in tags:
|
|
90
|
+
value = None
|
|
91
|
+
|
|
92
|
+
# If the tag is provided as string
|
|
93
|
+
if isinstance(tag, str):
|
|
94
|
+
if hasattr(ds, tag):
|
|
95
|
+
pydcm_value = ds[tag].value
|
|
96
|
+
try:
|
|
97
|
+
VR = pydicom.datadict.dictionary_VR(tag)
|
|
98
|
+
except:
|
|
99
|
+
VR = None
|
|
100
|
+
value = to_set_type(pydcm_value, VR) # ELIMINATE THIS STEP - return pydicom datatypes
|
|
101
|
+
|
|
102
|
+
# If the tag is a tuple of hexadecimal values
|
|
103
|
+
else:
|
|
104
|
+
if tag in ds:
|
|
105
|
+
try:
|
|
106
|
+
VR = pydicom.datadict.dictionary_VR(tag)
|
|
107
|
+
except:
|
|
108
|
+
VR = None
|
|
109
|
+
value = to_set_type(ds[tag].value, VR)
|
|
110
|
+
|
|
111
|
+
# If a tag is not present in the dataset, check if it can be derived
|
|
112
|
+
if value is None:
|
|
113
|
+
value = derive_data_element(ds, tag)
|
|
114
|
+
|
|
115
|
+
row.append(value)
|
|
116
|
+
return row
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_values(ds, tags, values, VR=None, coords=None):
|
|
120
|
+
|
|
121
|
+
if np.isscalar(tags):
|
|
122
|
+
tags = [tags]
|
|
123
|
+
values = [values]
|
|
124
|
+
VR = [VR]
|
|
125
|
+
elif VR is None:
|
|
126
|
+
VR = [None] * len(tags)
|
|
127
|
+
|
|
128
|
+
if coords is not None:
|
|
129
|
+
tags += list(coords.keys())
|
|
130
|
+
values += list(coords.values())
|
|
131
|
+
|
|
132
|
+
for i, tag in enumerate(tags):
|
|
133
|
+
|
|
134
|
+
if values[i] is None:
|
|
135
|
+
if isinstance(tag, str):
|
|
136
|
+
if hasattr(ds, tag):
|
|
137
|
+
del ds[tag]
|
|
138
|
+
else: # hexadecimal tuple
|
|
139
|
+
if tag in ds:
|
|
140
|
+
del ds[tag]
|
|
141
|
+
|
|
142
|
+
elif isinstance(tag, str):
|
|
143
|
+
if hasattr(ds, tag):
|
|
144
|
+
ds[tag].value = format_value(values[i], tag=tag)
|
|
145
|
+
else:
|
|
146
|
+
_add_new(ds, tag, values[i], VR=VR[i])
|
|
147
|
+
|
|
148
|
+
else: # hexadecimal tuple
|
|
149
|
+
if tag in ds:
|
|
150
|
+
ds[tag].value = format_value(values[i], tag=tag)
|
|
151
|
+
else:
|
|
152
|
+
_add_new(ds, tag, values[i], VR=VR[i])
|
|
153
|
+
|
|
154
|
+
#_set_derived_data_element(ds, tag, values[i])
|
|
155
|
+
|
|
156
|
+
return ds
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def value(ds, tags):
|
|
161
|
+
# Same as get_values but without VR lookup
|
|
162
|
+
|
|
163
|
+
# https://pydicom.github.io/pydicom/stable/guides/element_value_types.html
|
|
164
|
+
if np.isscalar(tags):
|
|
165
|
+
return get_values(ds, [tags])[0]
|
|
166
|
+
|
|
167
|
+
row = []
|
|
168
|
+
for tag in tags:
|
|
169
|
+
value = None
|
|
170
|
+
|
|
171
|
+
# If the tag is provided as string
|
|
172
|
+
if isinstance(tag, str):
|
|
173
|
+
|
|
174
|
+
if hasattr(ds, tag):
|
|
175
|
+
value = to_set_type(ds[tag].value)
|
|
176
|
+
|
|
177
|
+
# If the tag is a tuple of hexadecimal values
|
|
178
|
+
else:
|
|
179
|
+
if tag in ds:
|
|
180
|
+
value = to_set_type(ds[tag].value)
|
|
181
|
+
|
|
182
|
+
# If a tag is not present in the dataset, check if it can be derived
|
|
183
|
+
if value is None:
|
|
184
|
+
value = derive_data_element(ds, tag)
|
|
185
|
+
|
|
186
|
+
row.append(value)
|
|
187
|
+
return row
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def set_value(ds, tags, values):
|
|
191
|
+
# Same as set_values but without VR lookup
|
|
192
|
+
# This excludes new private tags - set those using add_private()
|
|
193
|
+
if np.isscalar(tags):
|
|
194
|
+
tags = [tags]
|
|
195
|
+
values = [values]
|
|
196
|
+
|
|
197
|
+
for i, tag in enumerate(tags):
|
|
198
|
+
|
|
199
|
+
if values[i] is None:
|
|
200
|
+
if isinstance(tag, str):
|
|
201
|
+
if hasattr(ds, tag):
|
|
202
|
+
del ds[tag]
|
|
203
|
+
else: # hexadecimal tuple
|
|
204
|
+
if tag in ds:
|
|
205
|
+
del ds[tag]
|
|
206
|
+
|
|
207
|
+
elif isinstance(tag, str):
|
|
208
|
+
if hasattr(ds, tag):
|
|
209
|
+
ds[tag].value = check_value(values[i], tag)
|
|
210
|
+
else:
|
|
211
|
+
add_new(ds, tag, values[i])
|
|
212
|
+
|
|
213
|
+
else: # hexadecimal tuple
|
|
214
|
+
if tag in ds:
|
|
215
|
+
ds[tag].value = check_value(values[i], tag)
|
|
216
|
+
else:
|
|
217
|
+
add_new(ds, tag, values[i])
|
|
218
|
+
|
|
219
|
+
return ds
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def write(ds, file, status=None):
|
|
224
|
+
# check if directory exists and create it if not
|
|
225
|
+
dir = os.path.dirname(file)
|
|
226
|
+
if not os.path.exists(dir):
|
|
227
|
+
os.makedirs(dir)
|
|
228
|
+
ds.save_as(file, write_like_original=False)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def codify(source_file, save_file, **kwargs):
|
|
232
|
+
str = code_file(source_file, **kwargs)
|
|
233
|
+
file = open(save_file, "w")
|
|
234
|
+
file.write(str)
|
|
235
|
+
file.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def read_data(files, tags, path=None, images_only=False):
|
|
239
|
+
|
|
240
|
+
if np.isscalar(files):
|
|
241
|
+
files = [files]
|
|
242
|
+
if np.isscalar(tags):
|
|
243
|
+
tags = [tags]
|
|
244
|
+
dict = {}
|
|
245
|
+
for i, file in tqdm(enumerate(files), 'reading files..'):
|
|
246
|
+
try:
|
|
247
|
+
ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
|
|
248
|
+
except:
|
|
249
|
+
pass
|
|
250
|
+
else:
|
|
251
|
+
if isinstance(ds, pydicom.dataset.FileDataset):
|
|
252
|
+
if 'TransferSyntaxUID' in ds.file_meta:
|
|
253
|
+
if images_only:
|
|
254
|
+
if not 'Rows' in ds:
|
|
255
|
+
continue
|
|
256
|
+
row = get_values(ds, tags)
|
|
257
|
+
if path is None:
|
|
258
|
+
index = file
|
|
259
|
+
else:
|
|
260
|
+
index = os.path.relpath(file, path)
|
|
261
|
+
dict[index] = row
|
|
262
|
+
return dict
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def read_dataframe(files, tags, path=None, images_only=False):
|
|
267
|
+
if np.isscalar(files):
|
|
268
|
+
files = [files]
|
|
269
|
+
if np.isscalar(tags):
|
|
270
|
+
tags = [tags]
|
|
271
|
+
array = []
|
|
272
|
+
dicom_files = []
|
|
273
|
+
for i, file in tqdm(enumerate(files), desc='Reading DICOM folder'):
|
|
274
|
+
try:
|
|
275
|
+
ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
|
|
276
|
+
except:
|
|
277
|
+
pass
|
|
278
|
+
else:
|
|
279
|
+
if isinstance(ds, pydicom.dataset.FileDataset):
|
|
280
|
+
if 'TransferSyntaxUID' in ds.file_meta:
|
|
281
|
+
if images_only:
|
|
282
|
+
if not 'Rows' in ds:
|
|
283
|
+
continue
|
|
284
|
+
row = get_values(ds, tags)
|
|
285
|
+
array.append(row)
|
|
286
|
+
if path is None:
|
|
287
|
+
index = file
|
|
288
|
+
else:
|
|
289
|
+
index = os.path.relpath(file, path)
|
|
290
|
+
dicom_files.append(index)
|
|
291
|
+
df = pd.DataFrame(array, index = dicom_files, columns = tags)
|
|
292
|
+
return df
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _add_new(ds, tag, value, VR='OW'):
|
|
296
|
+
if not isinstance(tag, pydicom.tag.BaseTag):
|
|
297
|
+
tag = pydicom.tag.Tag(tag)
|
|
298
|
+
if not tag.is_private: # Add a new data element
|
|
299
|
+
value_repr = pydicom.datadict.dictionary_VR(tag)
|
|
300
|
+
if value_repr == 'US or SS':
|
|
301
|
+
if value >= 0:
|
|
302
|
+
value_repr = 'US'
|
|
303
|
+
else:
|
|
304
|
+
value_repr = 'SS'
|
|
305
|
+
elif value_repr == 'OB or OW':
|
|
306
|
+
value_repr = 'OW'
|
|
307
|
+
ds.add_new(tag, value_repr, format_value(value, value_repr))
|
|
308
|
+
else:
|
|
309
|
+
if (tag.group, 0x0010) not in ds:
|
|
310
|
+
ds.private_block(tag.group, 'dbdicom ' + str(tag.group), create=True)
|
|
311
|
+
ds.add_new(tag, VR, format_value(value, VR))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def add_new(ds, tag, value):
|
|
315
|
+
if not isinstance(tag, pydicom.tag.BaseTag):
|
|
316
|
+
tag = pydicom.tag.Tag(tag)
|
|
317
|
+
if tag.is_private:
|
|
318
|
+
raise ValueError("if you want to add a private data element, use "
|
|
319
|
+
"dataset.add_private()")
|
|
320
|
+
# Add a new data element
|
|
321
|
+
value_repr = pydicom.datadict.dictionary_VR(tag)
|
|
322
|
+
if value_repr == 'US or SS':
|
|
323
|
+
if value >= 0:
|
|
324
|
+
value_repr = 'US'
|
|
325
|
+
else:
|
|
326
|
+
value_repr = 'SS'
|
|
327
|
+
elif value_repr == 'OB or OW':
|
|
328
|
+
value_repr = 'OW'
|
|
329
|
+
ds.add_new(tag, value_repr, format_value(value, value_repr))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def add_private(ds, tag, value, VR):
|
|
334
|
+
if not isinstance(tag, pydicom.tag.BaseTag):
|
|
335
|
+
tag = pydicom.tag.Tag(tag)
|
|
336
|
+
if (tag.group, 0x0010) not in ds:
|
|
337
|
+
ds.private_block(tag.group, 'dbdicom ' + str(tag.group), create=True)
|
|
338
|
+
ds.add_new(tag, VR, format_value(value, VR))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def derive_data_element(ds, tag):
|
|
342
|
+
"""Tags that are not required but can be derived from other required tags"""
|
|
343
|
+
|
|
344
|
+
if tag == 'SliceLocation' or tag == (0x0020, 0x1041):
|
|
345
|
+
if 'ImageOrientationPatient' in ds and 'ImagePositionPatient' in ds:
|
|
346
|
+
return image.slice_location(
|
|
347
|
+
ds['ImageOrientationPatient'].value,
|
|
348
|
+
ds['ImagePositionPatient'].value,
|
|
349
|
+
)
|
|
350
|
+
# To be extended ad hoc with other tags that can be derived
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def format_value(value, VR=None, tag=None):
|
|
355
|
+
|
|
356
|
+
# If the change below is made (TM, DA, DT) then this needs to
|
|
357
|
+
# convert those to string before setting
|
|
358
|
+
|
|
359
|
+
# Slow - dictionary lookup for every value write
|
|
360
|
+
|
|
361
|
+
if VR is None:
|
|
362
|
+
VR = pydicom.datadict.dictionary_VR(tag)
|
|
363
|
+
|
|
364
|
+
if VR == 'LO':
|
|
365
|
+
if len(value) > 64:
|
|
366
|
+
return value[-64:]
|
|
367
|
+
#return value[:64]
|
|
368
|
+
if VR == 'TM':
|
|
369
|
+
return variables.seconds_to_str(value)
|
|
370
|
+
|
|
371
|
+
return value
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def check_value(value, tag):
|
|
375
|
+
|
|
376
|
+
# If the change below is made (TM, DA, DT) then this needs to
|
|
377
|
+
# convert those to string before setting
|
|
378
|
+
|
|
379
|
+
LO = [
|
|
380
|
+
'SeriesDescription',
|
|
381
|
+
'StudyDescription',
|
|
382
|
+
]
|
|
383
|
+
TM = [
|
|
384
|
+
'AcquisitionTime',
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
if tag in LO:
|
|
388
|
+
if len(value) > 64:
|
|
389
|
+
return value[-64:]
|
|
390
|
+
if tag in TM:
|
|
391
|
+
return variables.seconds_to_str(value)
|
|
392
|
+
|
|
393
|
+
return value
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def to_set_type(value, VR=None):
|
|
397
|
+
"""
|
|
398
|
+
Convert pydicom datatypes to the python datatypes used to set the parameter.
|
|
399
|
+
"""
|
|
400
|
+
# Not a good idea to modify pydicom set/get values. confusing and requires extra VR lookups
|
|
401
|
+
|
|
402
|
+
if VR == 'TM':
|
|
403
|
+
# pydicom sometimes returns string values for TM data types
|
|
404
|
+
if isinstance(value, str):
|
|
405
|
+
return variables.str_to_seconds(value)
|
|
406
|
+
|
|
407
|
+
if value.__class__.__name__ == 'MultiValue':
|
|
408
|
+
return [to_set_type(v, VR) for v in value]
|
|
409
|
+
if value.__class__.__name__ == 'PersonName':
|
|
410
|
+
return str(value)
|
|
411
|
+
if value.__class__.__name__ == 'Sequence':
|
|
412
|
+
return [ds for ds in value]
|
|
413
|
+
if value.__class__.__name__ == 'TM':
|
|
414
|
+
return variables.time_to_seconds(value) # return datetime.time
|
|
415
|
+
if value.__class__.__name__ == 'UID':
|
|
416
|
+
return str(value)
|
|
417
|
+
if value.__class__.__name__ == 'IS':
|
|
418
|
+
return int(value)
|
|
419
|
+
if value.__class__.__name__ == 'DT':
|
|
420
|
+
return variables.datetime_to_str(value) # return datetime.datetime
|
|
421
|
+
if value.__class__.__name__ == 'DA': # return datetime.date
|
|
422
|
+
return variables.date_to_str(value)
|
|
423
|
+
if value.__class__.__name__ == 'DSfloat':
|
|
424
|
+
return float(value)
|
|
425
|
+
if value.__class__.__name__ == 'DSdecimal':
|
|
426
|
+
return int(value)
|
|
427
|
+
|
|
428
|
+
return value
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def new_uid(n=None):
|
|
432
|
+
|
|
433
|
+
if n is None:
|
|
434
|
+
return pydicom.uid.generate_uid()
|
|
435
|
+
else:
|
|
436
|
+
return [pydicom.uid.generate_uid() for _ in range(n)]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def window(ds):
|
|
442
|
+
"""Centre and width of the pixel data after applying rescale slope and intercept"""
|
|
443
|
+
|
|
444
|
+
if 'WindowCenter' in ds:
|
|
445
|
+
centre = ds.WindowCenter
|
|
446
|
+
if 'WindowWidth' in ds:
|
|
447
|
+
width = ds.WindowWidth
|
|
448
|
+
if centre is None or width is None:
|
|
449
|
+
array = pixel_data(ds)
|
|
450
|
+
#p = np.percentile(array, [25, 50, 75])
|
|
451
|
+
min = np.min(array)
|
|
452
|
+
max = np.max(array)
|
|
453
|
+
if centre is None:
|
|
454
|
+
centre = (max+min)/2
|
|
455
|
+
#centre = p[1]
|
|
456
|
+
if width is None:
|
|
457
|
+
width = 0.9*(max-min)
|
|
458
|
+
#width = p[2] - p[0]
|
|
459
|
+
return centre, width
|
|
460
|
+
|
|
461
|
+
def set_window(ds, center, width):
|
|
462
|
+
ds.WindowCenter = center
|
|
463
|
+
ds.WindowWidth = width
|
|
464
|
+
|
|
465
|
+
# List of all supported (matplotlib) colormaps
|
|
466
|
+
|
|
467
|
+
COLORMAPS = ['cividis', 'magma', 'plasma', 'viridis',
|
|
468
|
+
'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds',
|
|
469
|
+
'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu',
|
|
470
|
+
'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn',
|
|
471
|
+
'binary', 'gist_yarg', 'gist_gray', 'bone', 'pink',
|
|
472
|
+
'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia',
|
|
473
|
+
'hot', 'afmhot', 'gist_heat', 'copper',
|
|
474
|
+
'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu',
|
|
475
|
+
'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
|
|
476
|
+
'twilight', 'twilight_shifted', 'hsv',
|
|
477
|
+
'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
|
|
478
|
+
'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', 'turbo',
|
|
479
|
+
'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar']
|
|
480
|
+
|
|
481
|
+
# Include support for DICOM natiove colormaps (see pydicom guide on working with pixel data)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def lut(ds):
|
|
485
|
+
"""Return RGB as float with values in [0,1]"""
|
|
486
|
+
|
|
487
|
+
if 'PhotometricInterpretation' not in ds:
|
|
488
|
+
return None
|
|
489
|
+
if ds.PhotometricInterpretation != 'PALETTE COLOR':
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
if ds.BitsAllocated == 8:
|
|
493
|
+
dtype = np.ubyte
|
|
494
|
+
elif ds.BitsAllocated == 16:
|
|
495
|
+
dtype = np.uint16
|
|
496
|
+
|
|
497
|
+
R = ds.RedPaletteColorLookupTableData
|
|
498
|
+
G = ds.GreenPaletteColorLookupTableData
|
|
499
|
+
B = ds.BluePaletteColorLookupTableData
|
|
500
|
+
|
|
501
|
+
R = np.frombuffer(R, dtype=dtype)
|
|
502
|
+
G = np.frombuffer(G, dtype=dtype)
|
|
503
|
+
B = np.frombuffer(B, dtype=dtype)
|
|
504
|
+
|
|
505
|
+
R = R.astype(np.float32)
|
|
506
|
+
G = G.astype(np.float32)
|
|
507
|
+
B = B.astype(np.float32)
|
|
508
|
+
|
|
509
|
+
R *= 1.0/(np.power(2, ds.RedPaletteColorLookupTableDescriptor[2]) - 1)
|
|
510
|
+
G *= 1.0/(np.power(2, ds.GreenPaletteColorLookupTableDescriptor[2]) - 1)
|
|
511
|
+
B *= 1.0/(np.power(2, ds.BluePaletteColorLookupTableDescriptor[2]) - 1)
|
|
512
|
+
|
|
513
|
+
return np.transpose([R, G, B])
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def set_lut(ds, RGB):
|
|
517
|
+
"""Set RGB as float with values in range [0,1]"""
|
|
518
|
+
|
|
519
|
+
ds.PhotometricInterpretation = 'PALETTE COLOR'
|
|
520
|
+
|
|
521
|
+
RGB *= (np.power(2, ds.BitsAllocated) - 1)
|
|
522
|
+
|
|
523
|
+
if ds.BitsAllocated == 8:
|
|
524
|
+
RGB = RGB.astype(np.ubyte)
|
|
525
|
+
elif ds.BitsAllocated == 16:
|
|
526
|
+
RGB = RGB.astype(np.uint16)
|
|
527
|
+
|
|
528
|
+
# Define the properties of the LUT
|
|
529
|
+
ds.add_new('0x00281101', 'US', [255, 0, ds.BitsAllocated])
|
|
530
|
+
ds.add_new('0x00281102', 'US', [255, 0, ds.BitsAllocated])
|
|
531
|
+
ds.add_new('0x00281103', 'US', [255, 0, ds.BitsAllocated])
|
|
532
|
+
|
|
533
|
+
# Scale the colorsList to the available range
|
|
534
|
+
ds.RedPaletteColorLookupTableData = bytes(RGB[:,0])
|
|
535
|
+
ds.GreenPaletteColorLookupTableData = bytes(RGB[:,1])
|
|
536
|
+
ds.BluePaletteColorLookupTableData = bytes(RGB[:,2])
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def affine(ds, multislice=False):
|
|
541
|
+
if multislice:
|
|
542
|
+
return image.affine_matrix(
|
|
543
|
+
get_values(ds, 'ImageOrientationPatient'),
|
|
544
|
+
get_values(ds, 'ImagePositionPatient'),
|
|
545
|
+
get_values(ds, 'PixelSpacing'),
|
|
546
|
+
get_values(ds, 'SpacingBetweenSlices'),
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
return image.affine_matrix(
|
|
550
|
+
get_values(ds, 'ImageOrientationPatient'),
|
|
551
|
+
get_values(ds, 'ImagePositionPatient'),
|
|
552
|
+
get_values(ds, 'PixelSpacing'),
|
|
553
|
+
get_values(ds, 'SliceThickness'),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def set_affine(ds, affine, multislice=False):
|
|
558
|
+
if affine is None:
|
|
559
|
+
raise ValueError('The affine cannot be set to an empty value')
|
|
560
|
+
v = image.dismantle_affine_matrix(affine)
|
|
561
|
+
set_values(ds, 'PixelSpacing', v['PixelSpacing'])
|
|
562
|
+
if multislice:
|
|
563
|
+
set_values(ds, 'SpacingBetweenSlices', v['SliceThickness'])
|
|
564
|
+
else:
|
|
565
|
+
set_values(ds, 'SliceThickness', v['SliceThickness'])
|
|
566
|
+
set_values(ds, 'ImageOrientationPatient', v['ImageOrientationPatient'])
|
|
567
|
+
set_values(ds, 'ImagePositionPatient', v['ImagePositionPatient'])
|
|
568
|
+
set_values(ds, 'SliceLocation', np.dot(v['ImagePositionPatient'], v['slice_cosine']))
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def pixel_data(ds):
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
mod = SOPCLASSMODULE[ds.SOPClassUID]
|
|
576
|
+
except KeyError:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f"DICOM class {ds.SOPClassUID} is not currently supported."
|
|
579
|
+
)
|
|
580
|
+
if hasattr(mod, 'pixel_data'):
|
|
581
|
+
return getattr(mod, 'pixel_data')(ds)
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
array = ds.pixel_array
|
|
585
|
+
except:
|
|
586
|
+
return None
|
|
587
|
+
array = array.astype(np.float32)
|
|
588
|
+
slope = float(getattr(ds, 'RescaleSlope', 1))
|
|
589
|
+
intercept = float(getattr(ds, 'RescaleIntercept', 0))
|
|
590
|
+
array *= slope
|
|
591
|
+
array += intercept
|
|
592
|
+
return np.transpose(array)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def set_pixel_data(ds, array, value_range=None):
|
|
596
|
+
if array is None:
|
|
597
|
+
raise ValueError('The pixel array cannot be set to an empty value.')
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
mod = SOPCLASSMODULE[ds.SOPClassUID]
|
|
601
|
+
except KeyError:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"DICOM class {ds.SOPClassUID} is not currently supported."
|
|
604
|
+
)
|
|
605
|
+
if hasattr(mod, 'set_pixel_data'):
|
|
606
|
+
return getattr(mod, 'set_pixel_data')(ds, array)
|
|
607
|
+
|
|
608
|
+
# if array.ndim >= 3: # remove spurious dimensions of 1
|
|
609
|
+
# array = np.squeeze(array)
|
|
610
|
+
|
|
611
|
+
array = image.clip(array.astype(np.float32), value_range=value_range)
|
|
612
|
+
array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
|
|
613
|
+
array = np.transpose(array)
|
|
614
|
+
|
|
615
|
+
ds.PixelRepresentation = 0
|
|
616
|
+
#ds.SmallestImagePixelValue = int(0)
|
|
617
|
+
#ds.LargestImagePixelValue = int(2**ds.BitsAllocated - 1)
|
|
618
|
+
#ds.set_values('SmallestImagePixelValue', int(0))
|
|
619
|
+
#ds.set_values('LargestImagePixelValue', int(2**ds.BitsAllocated - 1))
|
|
620
|
+
ds.RescaleSlope = 1 / slope
|
|
621
|
+
ds.RescaleIntercept = - intercept / slope
|
|
622
|
+
# ds.WindowCenter = (maximum + minimum) / 2
|
|
623
|
+
# ds.WindowWidth = maximum - minimum
|
|
624
|
+
ds.Rows = array.shape[0]
|
|
625
|
+
ds.Columns = array.shape[1]
|
|
626
|
+
ds.PixelData = array.tobytes()
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def volume(ds, multislice=False):
|
|
630
|
+
return vreg.volume(pixel_data(ds), affine(ds, multislice))
|
|
631
|
+
|
|
632
|
+
def set_volume(ds, volume:vreg.Volume3D, multislice=False):
|
|
633
|
+
if volume is None:
|
|
634
|
+
raise ValueError('The volume cannot be set to an empty value.')
|
|
635
|
+
image = np.squeeze(volume.values)
|
|
636
|
+
if image.ndim != 2:
|
|
637
|
+
raise ValueError("Can only write 2D images to a dataset.")
|
|
638
|
+
set_pixel_data(ds, image)
|
|
639
|
+
set_affine(ds, volume.affine, multislice)
|
|
640
|
+
if volume.coords is not None:
|
|
641
|
+
# All other dimensions should have size 1
|
|
642
|
+
coords = volume.coords.reshape((volume.coords.shape[0], -1))
|
|
643
|
+
for i, d in enumerate(volume.dims):
|
|
644
|
+
try:
|
|
645
|
+
set_values(ds, d, coords[i,0])
|
|
646
|
+
except KeyError:
|
|
647
|
+
raise ValueError(
|
|
648
|
+
"Cannot write volume to DICOM. "
|
|
649
|
+
f"Volume dimension {d} is not a recognized DICOM data-element. "
|
|
650
|
+
f"Use Volume3D.set_dims() with proper DICOM keywords "
|
|
651
|
+
"or (group, element) tags to change the dimensions."
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def image_type(ds):
|
|
656
|
+
"""Determine if an image is Magnitude, Phase, Real or Imaginary image or None"""
|
|
657
|
+
|
|
658
|
+
if (0x0043, 0x102f) in ds:
|
|
659
|
+
private_ge = ds[0x0043, 0x102f]
|
|
660
|
+
try:
|
|
661
|
+
value = struct.unpack('h', private_ge.value)[0]
|
|
662
|
+
except:
|
|
663
|
+
value = private_ge.value
|
|
664
|
+
if value == 0:
|
|
665
|
+
return 'MAGNITUDE'
|
|
666
|
+
if value == 1:
|
|
667
|
+
return 'PHASE'
|
|
668
|
+
if value == 2:
|
|
669
|
+
return 'REAL'
|
|
670
|
+
if value == 3:
|
|
671
|
+
return 'IMAGINARY'
|
|
672
|
+
|
|
673
|
+
if 'ImageType' in ds:
|
|
674
|
+
type = set(ds.ImageType)
|
|
675
|
+
if set(['M', 'MAGNITUDE']).intersection(type):
|
|
676
|
+
return 'MAGNITUDE'
|
|
677
|
+
if set(['P', 'PHASE']).intersection(type):
|
|
678
|
+
return 'PHASE'
|
|
679
|
+
if set(['R', 'REAL']).intersection(type):
|
|
680
|
+
return 'REAL'
|
|
681
|
+
if set(['I', 'IMAGINARY']).intersection(type):
|
|
682
|
+
return 'IMAGINARY'
|
|
683
|
+
|
|
684
|
+
if 'ComplexImageComponent' in ds:
|
|
685
|
+
return ds.ComplexImageComponent
|
|
686
|
+
|
|
687
|
+
return 'UNKNOWN'
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def set_image_type(ds, value):
|
|
691
|
+
ds.ImageType = value
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def signal_type(ds):
|
|
695
|
+
"""Determine if an image is Water, Fat, In-Phase, Out-phase image or None"""
|
|
696
|
+
|
|
697
|
+
if hasattr(ds, 'ImageType'):
|
|
698
|
+
type = set(ds.ImageType)
|
|
699
|
+
if set(['W', 'WATER']).intersection(type):
|
|
700
|
+
return 'WATER'
|
|
701
|
+
elif set(['F', 'FAT']).intersection(type):
|
|
702
|
+
return 'FAT'
|
|
703
|
+
elif set(['IP', 'IN_PHASE']).intersection(type):
|
|
704
|
+
return 'IN_PHASE'
|
|
705
|
+
elif set(['OP', 'OUT_PHASE']).intersection(type):
|
|
706
|
+
return 'OP_PHASE'
|
|
707
|
+
return 'UNKNOWN'
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def set_signal_type(ds, value):
|
|
711
|
+
ds.ImageType = value
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
# def _initialize(ds, UID=None, ref=None): # ds is pydicom dataset
|
|
717
|
+
|
|
718
|
+
# # Date and Time of Creation
|
|
719
|
+
# dt = datetime.now()
|
|
720
|
+
# timeStr = dt.strftime('%H%M%S') # long format with micro seconds
|
|
721
|
+
|
|
722
|
+
# ds.ContentDate = dt.strftime('%Y%m%d')
|
|
723
|
+
# ds.ContentTime = timeStr
|
|
724
|
+
# ds.AcquisitionDate = dt.strftime('%Y%m%d')
|
|
725
|
+
# ds.AcquisitionTime = timeStr
|
|
726
|
+
# ds.SeriesDate = dt.strftime('%Y%m%d')
|
|
727
|
+
# ds.SeriesTime = timeStr
|
|
728
|
+
# ds.InstanceCreationDate = dt.strftime('%Y%m%d')
|
|
729
|
+
# ds.InstanceCreationTime = timeStr
|
|
730
|
+
|
|
731
|
+
# if UID is not None:
|
|
732
|
+
|
|
733
|
+
# # overwrite UIDs
|
|
734
|
+
# ds.PatientID = UID[0]
|
|
735
|
+
# ds.StudyInstanceUID = UID[1]
|
|
736
|
+
# ds.SeriesInstanceUID = UID[2]
|
|
737
|
+
# ds.SOPInstanceUID = UID[3]
|
|
738
|
+
|
|
739
|
+
# if ref is not None:
|
|
740
|
+
|
|
741
|
+
# # Series, Instance and Class for Reference
|
|
742
|
+
# refd_instance = Dataset()
|
|
743
|
+
# refd_instance.ReferencedSOPClassUID = ref.SOPClassUID
|
|
744
|
+
# refd_instance.ReferencedSOPInstanceUID = ref.SOPInstanceUID
|
|
745
|
+
|
|
746
|
+
# refd_series = Dataset()
|
|
747
|
+
# refd_series.ReferencedInstanceSequence = Sequence([refd_instance])
|
|
748
|
+
# refd_series.SeriesInstanceUID = ds.SeriesInstanceUID
|
|
749
|
+
|
|
750
|
+
# ds.ReferencedSeriesSequence = Sequence([refd_series])
|
|
751
|
+
|
|
752
|
+
# return ds
|