/
datadict.py
655 lines (524 loc) · 18.2 KB
/
datadict.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
# Copyright 2008-2018 pydicom authors. See LICENSE file for details.
# -*- coding: utf-8 -*-
"""Access dicom dictionary information"""
import warnings
from typing import Tuple, Optional, Dict
from pydicom.config import logger
from pydicom.tag import Tag, BaseTag, TagType
# the actual dict of {tag: (VR, VM, name, is_retired, keyword), ...}
from pydicom._dicom_dict import DicomDictionary
# those with tags like "(50xx, 0005)"
from pydicom._dicom_dict import RepeatersDictionary
from pydicom._private_dict import private_dictionaries
# Generate mask dict for checking repeating groups etc.
# Map a true bitwise mask to the DICOM mask with "x"'s in it.
masks: Dict[str, Tuple[int, int]] = {}
for mask_x in RepeatersDictionary:
# mask1 is XOR'd to see that all non-"x" bits
# are identical (XOR result = 0 if bits same)
# then AND those out with 0 bits at the "x"
# ("we don't care") location using mask2
mask1 = int(mask_x.replace("x", "0"), 16)
mask2 = int("".join(["F0"[c == "x"] for c in mask_x]), 16)
masks[mask_x] = (mask1, mask2)
def mask_match(tag: int) -> Optional[str]:
"""Return the repeaters tag mask for `tag`.
Parameters
----------
tag : int
The tag to check.
Returns
-------
str or None
If the tag is in the repeaters dictionary then returns the
corresponding masked tag, otherwise returns ``None``.
"""
for mask_x, (mask1, mask2) in masks.items():
if (tag ^ mask1) & mask2 == 0:
return mask_x
return None
def add_dict_entry(
tag: int,
VR: str,
keyword: str,
description: str,
VM: str = '1',
is_retired: str = ''
) -> None:
"""Update the DICOM dictionary with a new non-private entry.
Parameters
----------
tag : int
The tag number for the new dictionary entry.
VR : str
DICOM value representation.
description : str
The descriptive name used in printing the entry. Often the same as the
keyword, but with spaces between words.
VM : str, optional
DICOM value multiplicity. If not specified, then ``'1'`` is used.
is_retired : str, optional
Usually leave as blank string (default). Set to ``'Retired'`` if is a
retired data element.
Raises
------
ValueError
If the tag is a private tag.
Notes
-----
Does not permanently update the dictionary, but only during run-time.
Will replace an existing entry if the tag already exists in the dictionary.
See Also
--------
pydicom.examples.add_dict_entry
Example file which shows how to use this function
add_dict_entries
Update multiple values at once.
Examples
--------
>>> from pydicom import Dataset
>>> add_dict_entry(0x10021001, "UL", "TestOne", "Test One")
>>> add_dict_entry(0x10021002, "DS", "TestTwo", "Test Two", VM='3')
>>> ds = Dataset()
>>> ds.TestOne = 'test'
>>> ds.TestTwo = ['1', '2', '3']
"""
add_dict_entries({tag: (VR, VM, description, is_retired, keyword)})
def add_dict_entries(
new_entries_dict: Dict[int, Tuple[str, str, str, str, str]]
) -> None:
"""Update the DICOM dictionary with new non-private entries.
Parameters
----------
new_entries_dict : dict
:class:`dict` of form:
``{tag: (VR, VM, description, is_retired, keyword), ...}``
where parameters are as described in :func:`add_dict_entry`.
Raises
------
ValueError
If one of the entries is a private tag.
See Also
--------
add_dict_entry
Add a single entry to the dictionary.
Examples
--------
>>> from pydicom import Dataset
>>> new_dict_items = {
... 0x10021001: ('UL', '1', "Test One", '', 'TestOne'),
... 0x10021002: ('DS', '3', "Test Two", '', 'TestTwo'),
... }
>>> add_dict_entries(new_dict_items)
>>> ds = Dataset()
>>> ds.TestOne = 'test'
>>> ds.TestTwo = ['1', '2', '3']
"""
if any([BaseTag(tag).is_private for tag in new_entries_dict]):
raise ValueError(
'Private tags cannot be added using "add_dict_entries" - '
'use "add_private_dict_entries" instead')
# Update the dictionary itself
DicomDictionary.update(new_entries_dict)
# Update the reverse mapping from name to tag
keyword_dict.update({val[4]: tag for tag, val in new_entries_dict.items()})
def add_private_dict_entry(
private_creator: str, tag: int, VR: str, description: str, VM: str = '1'
) -> None:
"""Update the private DICOM dictionary with a new entry.
.. versionadded:: 1.3
Parameters
----------
private_creator : str
The private creator for the new entry.
tag : int
The tag number for the new dictionary entry. Note that the
2 high bytes of the element part of the tag are ignored.
VR : str
DICOM value representation.
description : str
The descriptive name used in printing the entry.
VM : str, optional
DICOM value multiplicity. If not specified, then ``'1'`` is used.
Raises
------
ValueError
If the tag is a non-private tag.
Notes
-----
Behaves like :func:`add_dict_entry`, only for a private tag entry.
See Also
--------
add_private_dict_entries
Add or update multiple entries at once.
"""
new_dict_val = (VR, VM, description, '')
add_private_dict_entries(private_creator, {tag: new_dict_val})
def add_private_dict_entries(
private_creator: str,
new_entries_dict: Dict[int, Tuple[str, str, str, str]]
) -> None:
"""Update pydicom's private DICOM tag dictionary with new entries.
.. versionadded:: 1.3
Parameters
----------
private_creator: str
The private creator for all entries in `new_entries_dict`.
new_entries_dict : dict
:class:`dict` of form ``{tag: (VR, VM, description, is_retired), ...}``
where parameters are as described in :func:`add_private_dict_entry`.
Raises
------
ValueError
If one of the entries is a non-private tag.
See Also
--------
add_private_dict_entry
Function to add a single entry to the private tag dictionary.
Examples
--------
>>> new_dict_items = {
... 0x00410001: ('UL', '1', "Test One"),
... 0x00410002: ('DS', '3', "Test Two", '3'),
... }
>>> add_private_dict_entries("ACME LTD 1.2", new_dict_items)
>>> add_private_dict_entry("ACME LTD 1.3", 0x00410001, "US", "Test Three")
"""
if not all([BaseTag(tag).is_private for tag in new_entries_dict]):
raise ValueError(
"Non-private tags cannot be added using "
"'add_private_dict_entries()' - use 'add_dict_entries()' instead"
)
new_entries = {
f"{tag >> 16:04x}xx{tag & 0xff:02x}": value
for tag, value in new_entries_dict.items()
}
private_dictionaries.setdefault(private_creator, {}).update(new_entries)
def get_entry(tag: TagType) -> Tuple[str, str, str, str, str]:
"""Return an entry from the DICOM dictionary as a tuple.
If the `tag` is not in the main DICOM dictionary, then the repeating
group dictionary will also be checked.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose entry is to be retrieved, in any of the
forms accepted by :func:`~pydicom.tag.Tag`. Only entries in the
official DICOM dictionary will be checked, not entries in the
private dictionary.
Returns
-------
tuple of str
The (VR, VM, name, is_retired, keyword) from the DICOM dictionary.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
See Also
--------
get_private_entry
Return an entry from the private dictionary.
"""
# Note: tried the lookup with 'if tag in DicomDictionary'
# and with DicomDictionary.get, instead of try/except
# Try/except was fastest using timeit if tag is valid (usual case)
# My test had 5.2 usec vs 8.2 for 'contains' test, vs 5.32 for dict.get
if not isinstance(tag, BaseTag):
tag = Tag(tag)
try:
return DicomDictionary[tag]
except KeyError:
if not tag.is_private:
mask_x = mask_match(tag)
if mask_x:
return RepeatersDictionary[mask_x]
raise KeyError(f"Tag {tag} not found in DICOM dictionary")
def dictionary_is_retired(tag: TagType) -> bool:
"""Return ``True`` if the element corresponding to `tag` is retired.
Only performs the lookup for official DICOM elements.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose retirement status is being checked, in
any of the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
bool
``True`` if the element's retirement status is 'Retired', ``False``
otherwise.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
"""
return 'retired' in get_entry(tag)[3].lower()
def dictionary_VR(tag: TagType) -> str:
"""Return the VR of the element corresponding to `tag`.
Only performs the lookup for official DICOM elements.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose value representation (VR) is being
retrieved, in any of the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
str
The VR of the corresponding element.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
"""
return get_entry(tag)[0]
def dictionary_VM(tag: TagType) -> str:
"""Return the VM of the element corresponding to `tag`.
Only performs the lookup for official DICOM elements.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose value multiplicity (VM) is being
retrieved, in any of the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
str
The VM of the corresponding element.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
"""
return get_entry(tag)[1]
def dictionary_description(tag: TagType) -> str:
"""Return the description of the element corresponding to `tag`.
Only performs the lookup for official DICOM elements.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose description is being retrieved, in any
of the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
str
The description of the corresponding element.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
"""
return get_entry(tag)[2]
def dictionary_keyword(tag: TagType) -> str:
"""Return the keyword of the element corresponding to `tag`.
Only performs the lookup for official DICOM elements.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose keyword is being retrieved, in any of
the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
str
The keyword of the corresponding element.
Raises
------
KeyError
If the tag is not present in the DICOM data dictionary.
"""
return get_entry(tag)[4]
def dictionary_has_tag(tag: TagType) -> bool:
"""Return ``True`` if `tag` is in the official DICOM data dictionary.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag to check, in any of the forms accepted by
:func:`~pydicom.tag.Tag`.
Returns
-------
bool
``True`` if the tag corresponds to an element present in the official
DICOM data dictionary, ``False`` otherwise.
"""
try:
return Tag(tag) in DicomDictionary
except Exception:
return False
def keyword_for_tag(tag: TagType) -> str:
"""Return the keyword of the element corresponding to `tag`.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose keyword is being retrieved, in any of
the forms accepted by :func:`~pydicom.tag.Tag`.
Returns
-------
str
If the element is in the DICOM data dictionary then returns the
corresponding element's keyword, otherwise returns ``''``. For
group length elements will always return ``'GroupLength'``.
"""
try:
return dictionary_keyword(tag)
except KeyError:
return ""
# Provide for the 'reverse' lookup. Given the keyword, what is the tag?
keyword_dict: Dict[str, int] = {
dictionary_keyword(tag): tag for tag in DicomDictionary
}
def tag_for_keyword(keyword: str) -> Optional[int]:
"""Return the tag of the element corresponding to `keyword`.
Only performs the lookup for official DICOM elements.
Parameters
----------
keyword : str
The keyword for the element whose tag is being retrieved.
Returns
-------
int or None
If the element is in the DICOM data dictionary then returns the
corresponding element's tag, otherwise returns ``None``.
"""
return keyword_dict.get(keyword)
def repeater_has_tag(tag: int) -> bool:
"""Return ``True`` if `tag` is in the DICOM repeaters data dictionary.
Parameters
----------
tag : int
The tag to check.
Returns
-------
bool
``True`` if the tag is a non-private element tag present in the
official DICOM repeaters data dictionary, ``False`` otherwise.
"""
return (mask_match(tag) in RepeatersDictionary)
REPEATER_KEYWORDS = [val[4] for val in RepeatersDictionary.values()]
def repeater_has_keyword(keyword: str) -> bool:
"""Return ``True`` if `keyword` is in the DICOM repeaters data dictionary.
Parameters
----------
keyword : str
The keyword to check.
Returns
-------
bool
``True`` if the keyword corresponding to an element present in the
official DICOM repeaters data dictionary, ``False`` otherwise.
"""
return keyword in REPEATER_KEYWORDS
# PRIVATE DICTIONARY handling
# functions in analogy with those of main DICOM dict
def get_private_entry(
tag: TagType, private_creator: str
) -> Tuple[str, str, str, str]:
"""Return an entry from the private dictionary corresponding to `tag`.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose entry is to be retrieved, in any of the
forms accepted by :func:`~pydicom.tag.Tag`. Only entries in the
private dictionary will be checked.
private_creator : str
The name of the private creator.
Returns
-------
tuple of str
The (VR, VM, name, is_retired) from the private dictionary.
Raises
------
KeyError
If the tag or private creator is not present in the private dictionary.
See Also
--------
get_entry
Return an entry from the DICOM data dictionary.
"""
if not isinstance(tag, BaseTag):
tag = Tag(tag)
try:
private_dict = private_dictionaries[private_creator]
except KeyError as exc:
raise KeyError(
f"Private creator '{private_creator}' not in the private "
"dictionary"
) from exc
except TypeError as exc:
msg = (f"{tag.private_creator} '{private_creator}' "
f"is not a valid private creator")
warnings.warn(msg)
raise KeyError(msg) from exc
# private elements are usually agnostic for
# "block" (see PS3.5-2008 7.8.1 p44)
# Some elements in _private_dict are explicit;
# most have "xx" for high-byte of element
# so here put in the "xx" in the block position for key to look up
group_str = f"{tag.group:04x}"
elem_str = f"{tag.elem:04x}"
keys = [
f"{group_str}{elem_str}",
f"{group_str}xx{elem_str[-2:]}",
f"{group_str[:2]}xxxx{elem_str[-2:]}"
]
keys = [k for k in keys if k in private_dict]
if not keys:
raise KeyError(
f"Tag '{tag}' not in private dictionary "
f"for private creator '{private_creator}'"
)
dict_entry = private_dict[keys[0]]
return dict_entry
def private_dictionary_VR(tag: TagType, private_creator: str) -> str:
"""Return the VR of the private element corresponding to `tag`.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose value representation (VR) is being
retrieved, in any of the forms accepted by :func:`~pydicom.tag.Tag`.
private_creator : str
The name of the private creator.
Returns
-------
str
The VR of the corresponding element.
Raises
------
KeyError
If the tag is not present in the private dictionary.
"""
return get_private_entry(tag, private_creator)[0]
def private_dictionary_VM(tag: TagType, private_creator: str) -> str:
"""Return the VM of the private element corresponding to `tag`.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose value multiplicity (VM) is being
retrieved, in any of the forms accepted by :func:`~pydicom.tag.Tag`.
private_creator : str
The name of the private creator.
Returns
-------
str
The VM of the corresponding element.
Raises
------
KeyError
If the tag is not present in the private dictionary.
"""
return get_private_entry(tag, private_creator)[1]
def private_dictionary_description(tag: TagType, private_creator: str) -> str:
"""Return the description of the private element corresponding to `tag`.
Parameters
----------
tag : int or str or Tuple[int, int]
The tag for the element whose description is being retrieved, in any
of the forms accepted by :func:`~pydicom.tag.Tag`.
private_creator : str
The name of the private creator.
Returns
-------
str
The description of the corresponding element.
Raises
------
KeyError
If the tag is not present in the private dictionary,
or if the private creator is not valid.
"""
return get_private_entry(tag, private_creator)[2]