dbdicom 0.2.5__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.

Files changed (52) hide show
  1. dbdicom/__init__.py +1 -28
  2. dbdicom/api.py +267 -0
  3. dbdicom/const.py +144 -0
  4. dbdicom/dataset.py +752 -0
  5. dbdicom/dbd.py +719 -0
  6. dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
  7. dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
  8. dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
  9. dbdicom/register.py +527 -0
  10. dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
  11. dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
  12. dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
  13. dbdicom/sop_classes/parametric_map.py +307 -0
  14. dbdicom/sop_classes/secondary_capture.py +140 -0
  15. dbdicom/sop_classes/segmentation.py +311 -0
  16. dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
  17. dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
  18. dbdicom/utils/arrays.py +36 -0
  19. dbdicom/utils/files.py +0 -20
  20. dbdicom/utils/image.py +10 -629
  21. dbdicom-0.3.0.dist-info/METADATA +28 -0
  22. dbdicom-0.3.0.dist-info/RECORD +53 -0
  23. {dbdicom-0.2.5.dist-info → dbdicom-0.3.0.dist-info}/WHEEL +1 -1
  24. dbdicom/create.py +0 -457
  25. dbdicom/dro.py +0 -174
  26. dbdicom/ds/__init__.py +0 -10
  27. dbdicom/ds/create.py +0 -63
  28. dbdicom/ds/dataset.py +0 -869
  29. dbdicom/ds/dictionaries.py +0 -620
  30. dbdicom/ds/types/parametric_map.py +0 -226
  31. dbdicom/extensions/__init__.py +0 -9
  32. dbdicom/extensions/dipy.py +0 -448
  33. dbdicom/extensions/elastix.py +0 -503
  34. dbdicom/extensions/matplotlib.py +0 -107
  35. dbdicom/extensions/numpy.py +0 -271
  36. dbdicom/extensions/scipy.py +0 -1512
  37. dbdicom/extensions/skimage.py +0 -1030
  38. dbdicom/extensions/sklearn.py +0 -243
  39. dbdicom/extensions/vreg.py +0 -1390
  40. dbdicom/manager.py +0 -2132
  41. dbdicom/message.py +0 -119
  42. dbdicom/pipelines.py +0 -66
  43. dbdicom/record.py +0 -1893
  44. dbdicom/types/database.py +0 -107
  45. dbdicom/types/instance.py +0 -231
  46. dbdicom/types/patient.py +0 -40
  47. dbdicom/types/series.py +0 -2874
  48. dbdicom/types/study.py +0 -58
  49. dbdicom-0.2.5.dist-info/METADATA +0 -71
  50. dbdicom-0.2.5.dist-info/RECORD +0 -66
  51. {dbdicom-0.2.5.dist-info → dbdicom-0.3.0.dist-info/licenses}/LICENSE +0 -0
  52. {dbdicom-0.2.5.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