Skip to content

Commit c257ef7

Browse files
authored
Merge pull request #83 from smasuda/SUPPORT_MULTIPLE_DICOM_VERSIONS
Support multiple dicom versions
2 parents f41c7fe + 2d6ebab commit c257ef7

File tree

10 files changed

+842
-39
lines changed

10 files changed

+842
-39
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# DicomAnonymizer
22

33
Python package to anonymize DICOM files.
4-
The anonymization answer to the standard . More information about dicom fields for anonymization can be found [here](http://dicom.nema.org/dicom/2013/output/chtml/part15/chapter_E.html#table_E.1-1).
4+
The anonymization answer to the standard . More information about dicom fields for anonymization can be found [here](http://dicom.nema.org/dicom/2023/output/chtml/part15/chapter_E.html#table_E.1-1).
55

66
The default behaviour of this package is to anonymize DICOM fields referenced in [dicomfields](dicomanonymizer/dicomfields.py).
77

dicomanonymizer/dicomfields.py renamed to dicomanonymizer/dicom_anonymization_databases/dicomfields_2023.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tags anonymized in DICOM standard
22
# Documentation for groups meaning can be found in default associated actions.
3-
# http://dicom.nema.org/dicom/2013/output/chtml/part15/chapter_E.html#table_E.1-1
3+
# http://dicom.nema.org/dicom/2023/output/chtml/part15/chapter_E.html#table_E.1-1
44

55
# Replaced tags
66
D_TAGS = [

dicomanonymizer/dicom_anonymization_databases/dicomfields_2024b.py

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import importlib
2+
3+
ANONYMIZATION_CATEGORIES = [
4+
"D_TAGS",
5+
"Z_TAGS",
6+
"X_TAGS",
7+
"U_TAGS",
8+
"Z_D_TAGS",
9+
"X_Z_TAGS",
10+
"X_D_TAGS",
11+
"X_Z_D_TAGS",
12+
"X_Z_U_STAR_TAGS",
13+
"ALL_TAGS",
14+
]
15+
16+
17+
def dicom_anonymization_database_selector(
18+
dicom_version: str = "dicomfields_2023",
19+
) -> dict:
20+
try:
21+
dicom_anonymization_database = importlib.import_module(
22+
f"dicomanonymizer.dicom_anonymization_databases.{dicom_version}"
23+
)
24+
except ModuleNotFoundError:
25+
raise ValueError(f"Unknown DICOM anonymization database: {dicom_version}")
26+
27+
try:
28+
dicom_anonymization_dict = {
29+
anonymization_category: getattr(
30+
dicom_anonymization_database, anonymization_category
31+
)
32+
for anonymization_category in ANONYMIZATION_CATEGORIES
33+
}
34+
except AttributeError:
35+
print(
36+
f"Anonymization database {dicom_version} is missing a category, please check it has them all."
37+
)
38+
raise
39+
return dicom_anonymization_dict

dicomanonymizer/simpledicomanonymizer.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,13 @@
22
import re
33

44
from enum import Enum
5-
from typing import List, Union
5+
from typing import Callable, List, Union
66
from dataclasses import dataclass
77

8-
from dicomanonymizer.dicomfields import (
9-
D_TAGS,
10-
Z_TAGS,
11-
X_TAGS,
12-
U_TAGS,
13-
Z_D_TAGS,
14-
X_Z_TAGS,
15-
X_D_TAGS,
16-
X_Z_D_TAGS,
17-
X_Z_U_STAR_TAGS,
18-
)
8+
from dicomanonymizer.dicomfields_selector import dicom_anonymization_database_selector
199
from dicomanonymizer.format_tag import tag_to_hex_strings
2010

21-
11+
# keeps the mapping from old UID to new UID
2212
dictionary = {}
2313

2414

@@ -344,33 +334,47 @@ class ActionsMapNameFunctions(Enum):
344334
regexp = Action(regexp, 2)
345335

346336

347-
def initialize_actions() -> dict:
337+
def initialize_actions(dicom_version: str = "dicomfields_2023") -> dict:
348338
"""
349339
Initialize anonymization actions with DICOM standard values
350340
341+
:param dicom_version: DICOM version to use
351342
:return Dict object which map actions to tags
352343
"""
353-
anonymization_actions = {tag: replace for tag in D_TAGS}
354-
anonymization_actions.update({tag: empty for tag in Z_TAGS})
355-
anonymization_actions.update({tag: delete for tag in X_TAGS})
356-
anonymization_actions.update({tag: replace_UID for tag in U_TAGS})
357-
anonymization_actions.update({tag: empty_or_replace for tag in Z_D_TAGS})
358-
anonymization_actions.update({tag: delete_or_empty for tag in X_Z_TAGS})
359-
anonymization_actions.update({tag: delete_or_replace for tag in X_D_TAGS})
344+
tags = dicom_anonymization_database_selector(dicom_version)
345+
346+
anonymization_actions = {tag: replace for tag in tags["D_TAGS"]}
347+
anonymization_actions.update({tag: empty for tag in tags["Z_TAGS"]})
348+
anonymization_actions.update({tag: delete for tag in tags["X_TAGS"]})
349+
anonymization_actions.update({tag: replace_UID for tag in tags["U_TAGS"]})
350+
anonymization_actions.update({tag: empty_or_replace for tag in tags["Z_D_TAGS"]})
351+
anonymization_actions.update({tag: delete_or_empty for tag in tags["X_Z_TAGS"]})
352+
anonymization_actions.update({tag: delete_or_replace for tag in tags["X_D_TAGS"]})
360353
anonymization_actions.update(
361-
{tag: delete_or_empty_or_replace for tag in X_Z_D_TAGS}
354+
{tag: delete_or_empty_or_replace for tag in tags["X_Z_D_TAGS"]}
362355
)
363356
anonymization_actions.update(
364-
{tag: delete_or_empty_or_replace_UID for tag in X_Z_U_STAR_TAGS}
357+
{tag: delete_or_empty_or_replace_UID for tag in tags["X_Z_U_STAR_TAGS"]}
365358
)
366359
return anonymization_actions
367360

368361

362+
def initialize_actions_2024b() -> dict:
363+
"""
364+
Initialize anonymization actions with DICOM standard values of 2024b.
365+
If you want to use 2024b version of anonymization, call anonymize_dataset with base_rules_gen=initialize_actions_2024b.
366+
367+
:return Dict object which map actions to tags
368+
"""
369+
return initialize_actions("dicomfields_2024b")
370+
371+
369372
def anonymize_dicom_file(
370373
in_file: str,
371374
out_file: str,
372375
extra_anonymization_rules: dict = None,
373376
delete_private_tags: bool = True,
377+
base_rules_gen: Callable = initialize_actions,
374378
) -> None:
375379
"""
376380
Anonymize a DICOM file by modifying personal tags
@@ -384,7 +388,9 @@ def anonymize_dicom_file(
384388
"""
385389
dataset = pydicom.dcmread(in_file)
386390

387-
anonymize_dataset(dataset, extra_anonymization_rules, delete_private_tags)
391+
anonymize_dataset(
392+
dataset, extra_anonymization_rules, delete_private_tags, base_rules_gen
393+
)
388394

389395
# Store modified image
390396
dataset.save_as(out_file)
@@ -450,15 +456,17 @@ def anonymize_dataset(
450456
dataset: pydicom.Dataset,
451457
extra_anonymization_rules: dict = None,
452458
delete_private_tags: bool = True,
459+
base_rules_gen: Callable = initialize_actions,
453460
) -> None:
454461
"""
455462
Anonymize a pydicom Dataset by using anonymization rules which links an action to a tag
456463
457464
:param dataset: Dataset to be anonymize
465+
:param base_rules_gen: Function to generate the base rules
458466
:param extra_anonymization_rules: Rules to be applied on the dataset
459467
:param delete_private_tags: Define if private tags should be delete or not
460468
"""
461-
current_anonymization_actions = initialize_actions()
469+
current_anonymization_actions = base_rules_gen()
462470

463471
if extra_anonymization_rules is not None:
464472
current_anonymization_actions.update(extra_anonymization_rules)

examples/anonymize_extra_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22

3-
from dicomanonymizer.dicomfields import ALL_TAGS
3+
from dicomanonymizer.dicom_anonymization_databases.dicomfields_2023 import ALL_TAGS
44
from dicomanonymizer import anonymize, keep
55

66

scripts/scrap_DICOM_fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ def create_DICOM_fields(profiles):
115115

116116
def main(
117117
url="https://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_e.html",
118-
output_path="dicomanonymizer/dicomfields.py",
118+
output_path="dicomanonymizer/dicomfields_2024b.py",
119119
):
120+
# As of 2024.05.14, the current version of DICOM spec is 2024b.
120121
profiles = scrap_profiles(url)
121122
file_content = create_DICOM_fields(profiles=profiles)
122123
with open(output_path, "w") as file:

tests/test_anon.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydicom.data import get_testdata_files
99

1010
from dicomanonymizer.simpledicomanonymizer import anonymize_dataset
11-
from dicomanonymizer import dicomfields
11+
from dicomanonymizer.dicom_anonymization_databases import dicomfields_2023
1212

1313
# Ignore warnings from pydicom validation
1414
settings.writing_validation_mode = IGNORE
@@ -51,7 +51,7 @@ def orig_anon_dataset(request):
5151

5252
def test_deleted_tags_are_removed(orig_anon_dataset):
5353
orig_ds, anon_ds = orig_anon_dataset
54-
deleted_tags = dicomfields.X_TAGS
54+
deleted_tags = dicomfields_2023.X_TAGS
5555

5656
for tt in deleted_tags: # sourcery skip: no-loop-in-tests
5757
if (
@@ -66,12 +66,12 @@ def test_deleted_tags_are_removed(orig_anon_dataset):
6666

6767

6868
changed_tags = (
69-
dicomfields.U_TAGS
70-
+ dicomfields.D_TAGS
71-
+ dicomfields.Z_D_TAGS
72-
+ dicomfields.X_D_TAGS
73-
+ dicomfields.X_Z_D_TAGS
74-
+ dicomfields.X_Z_U_STAR_TAGS
69+
dicomfields_2023.U_TAGS
70+
+ dicomfields_2023.D_TAGS
71+
+ dicomfields_2023.Z_D_TAGS
72+
+ dicomfields_2023.X_D_TAGS
73+
+ dicomfields_2023.X_Z_D_TAGS
74+
+ dicomfields_2023.X_Z_U_STAR_TAGS
7575
)
7676

7777
empty_values = (0, "", "00010101", "000000.00", "ANONYMIZED")
@@ -104,7 +104,7 @@ def test_changed_tags_are_replaced(orig_anon_dataset):
104104
), f"({tt[0]:04X},{tt[1]:04x}):{orig_ds[tt].value} not replaced"
105105

106106

107-
empty_tags = dicomfields.Z_TAGS + dicomfields.X_Z_TAGS
107+
empty_tags = dicomfields_2023.Z_TAGS + dicomfields_2023.X_Z_TAGS
108108

109109

110110
def is_elem_empty(elem) -> bool:

tests/test_anonymization_without_dicom.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import pydicom
22

33
from dicomanonymizer import anonymize_dataset
4-
from dicomanonymizer.simpledicomanonymizer import empty
4+
from dicomanonymizer.simpledicomanonymizer import (
5+
empty,
6+
initialize_actions,
7+
initialize_actions_2024b,
8+
)
59

610

711
def test_anonymization_without_dicom_file():
@@ -74,3 +78,36 @@ def test_anonymization_of_ranged_tags_without_dicom_file():
7478
# Check that the dataset has been anonymized
7579
assert (0x5011, 0x0110) not in anon_ds
7680
assert (0x5012, 0x0112) not in anon_ds
81+
82+
83+
def test_switching_dicom_versions():
84+
"""To confirm the different behavior of annonymization beteen dicom versions of 2023 and 2024b."""
85+
fields = [
86+
{ # Replaced by Anonymized
87+
"id": (0x0010, 0x0020),
88+
"type": "LO",
89+
"value": "Test Patient ID",
90+
},
91+
]
92+
93+
# Create a readable dataset for pydicom
94+
data = pydicom.Dataset()
95+
data_2023 = pydicom.Dataset()
96+
data_2024b = pydicom.Dataset()
97+
98+
for field in fields: # sourcery skip: no-loop-in-tests
99+
data.add_new(field["id"], field["type"], field["value"])
100+
data_2023.add_new(field["id"], field["type"], field["value"])
101+
data_2024b.add_new(field["id"], field["type"], field["value"])
102+
103+
anonymize_dataset(data, base_rules_gen=initialize_actions)
104+
anonymize_dataset(
105+
data_2023, base_rules_gen=lambda: initialize_actions("dicomfields_2023")
106+
)
107+
anonymize_dataset(data_2024b, base_rules_gen=initialize_actions_2024b)
108+
109+
assert data[(0x0010, 0x0020)].value == "" # default behavior which is DICOM 2023.
110+
assert data_2023[(0x0010, 0x0020)].value == "" # same as the default.
111+
assert (
112+
data_2024b[(0x0010, 0x0020)].value == "ANONYMIZED"
113+
) # 2024b differs from the default

tests/test_dicomfields_selector.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dicomanonymizer.dicom_anonymization_databases import dicomfields_2024b
2+
from dicomanonymizer.dicomfields_selector import dicom_anonymization_database_selector
3+
4+
from dicomanonymizer.dicom_anonymization_databases import dicomfields_2023
5+
6+
7+
def test_selector():
8+
assert dicom_anonymization_database_selector("dicomfields_2023") == {
9+
"D_TAGS": dicomfields_2023.D_TAGS,
10+
"Z_TAGS": dicomfields_2023.Z_TAGS,
11+
"X_TAGS": dicomfields_2023.X_TAGS,
12+
"U_TAGS": dicomfields_2023.U_TAGS,
13+
"Z_D_TAGS": dicomfields_2023.Z_D_TAGS,
14+
"X_Z_TAGS": dicomfields_2023.X_Z_TAGS,
15+
"X_D_TAGS": dicomfields_2023.X_D_TAGS,
16+
"X_Z_D_TAGS": dicomfields_2023.X_Z_D_TAGS,
17+
"X_Z_U_STAR_TAGS": dicomfields_2023.X_Z_U_STAR_TAGS,
18+
"ALL_TAGS": dicomfields_2023.ALL_TAGS,
19+
}
20+
assert dicom_anonymization_database_selector("dicomfields_2024b") == {
21+
"D_TAGS": dicomfields_2024b.D_TAGS,
22+
"Z_TAGS": dicomfields_2024b.Z_TAGS,
23+
"X_TAGS": dicomfields_2024b.X_TAGS,
24+
"U_TAGS": dicomfields_2024b.U_TAGS,
25+
"Z_D_TAGS": dicomfields_2024b.Z_D_TAGS,
26+
"X_Z_TAGS": dicomfields_2024b.X_Z_TAGS,
27+
"X_D_TAGS": dicomfields_2024b.X_D_TAGS,
28+
"X_Z_D_TAGS": dicomfields_2024b.X_Z_D_TAGS,
29+
"X_Z_U_STAR_TAGS": dicomfields_2024b.X_Z_U_STAR_TAGS,
30+
"ALL_TAGS": dicomfields_2024b.ALL_TAGS,
31+
}
32+
33+
# check default selector
34+
assert (
35+
dicom_anonymization_database_selector()
36+
== dicom_anonymization_database_selector("dicomfields_2023")
37+
)
38+
39+
try:
40+
dicom_anonymization_database_selector("2019")
41+
except ValueError as e:
42+
assert str(e) == "Unknown DICOM anonymization database: 2019"
43+
else:
44+
assert False

0 commit comments

Comments
 (0)