Skip to content

Commit

Permalink
Support multiple dicom versions
Browse files Browse the repository at this point in the history
  • Loading branch information
smasuda committed Jun 21, 2024
1 parent f41c7fe commit a4b82e8
Show file tree
Hide file tree
Showing 6 changed files with 819 additions and 26 deletions.
674 changes: 674 additions & 0 deletions dicomanonymizer/dicomfields_2024b.py

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions dicomanonymizer/dicomfields_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from dicomanonymizer import dicomfields, dicomfields_2024b


def selector(dicom_version: str = "2013") -> dict:
if dicom_version == "2013":
return {
"D_TAGS": dicomfields.D_TAGS,
"Z_TAGS": dicomfields.Z_TAGS,
"X_TAGS": dicomfields.X_TAGS,
"U_TAGS": dicomfields.U_TAGS,
"Z_D_TAGS": dicomfields.Z_D_TAGS,
"X_Z_TAGS": dicomfields.X_Z_TAGS,
"X_D_TAGS": dicomfields.X_D_TAGS,
"X_Z_D_TAGS": dicomfields.X_Z_D_TAGS,
"X_Z_U_STAR_TAGS": dicomfields.X_Z_U_STAR_TAGS,
"ALL_TAGS": dicomfields.ALL_TAGS,
}
elif dicom_version == "2024b":
return {
"D_TAGS": dicomfields_2024b.D_TAGS,
"Z_TAGS": dicomfields_2024b.Z_TAGS,
"X_TAGS": dicomfields_2024b.X_TAGS,
"U_TAGS": dicomfields_2024b.U_TAGS,
"Z_D_TAGS": dicomfields_2024b.Z_D_TAGS,
"X_Z_TAGS": dicomfields_2024b.X_Z_TAGS,
"X_D_TAGS": dicomfields_2024b.X_D_TAGS,
"X_Z_D_TAGS": dicomfields_2024b.X_Z_D_TAGS,
"X_Z_U_STAR_TAGS": dicomfields_2024b.X_Z_U_STAR_TAGS,
"ALL_TAGS": dicomfields_2024b.ALL_TAGS,
}
else:
raise ValueError(f"Unknown DICOM version: {dicom_version}")
52 changes: 28 additions & 24 deletions dicomanonymizer/simpledicomanonymizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@
import re

from enum import Enum
from typing import List, Union
from typing import Callable, List, Union
from dataclasses import dataclass

from dicomanonymizer.dicomfields import (
D_TAGS,
Z_TAGS,
X_TAGS,
U_TAGS,
Z_D_TAGS,
X_Z_TAGS,
X_D_TAGS,
X_Z_D_TAGS,
X_Z_U_STAR_TAGS,
)
from dicomanonymizer.dicomfields_selector import selector
from dicomanonymizer.format_tag import tag_to_hex_strings


# keeps the mapping from old UID to new UID
dictionary = {}


Expand Down Expand Up @@ -344,28 +334,40 @@ class ActionsMapNameFunctions(Enum):
regexp = Action(regexp, 2)


def initialize_actions() -> dict:
def initialize_actions(dicom_version: str = "2013") -> dict:
"""
Initialize anonymization actions with DICOM standard values
:param dicom_version: DICOM version to use
:return Dict object which map actions to tags
"""
anonymization_actions = {tag: replace for tag in D_TAGS}
anonymization_actions.update({tag: empty for tag in Z_TAGS})
anonymization_actions.update({tag: delete for tag in X_TAGS})
anonymization_actions.update({tag: replace_UID for tag in U_TAGS})
anonymization_actions.update({tag: empty_or_replace for tag in Z_D_TAGS})
anonymization_actions.update({tag: delete_or_empty for tag in X_Z_TAGS})
anonymization_actions.update({tag: delete_or_replace for tag in X_D_TAGS})
tags = selector(dicom_version)

anonymization_actions = {tag: replace for tag in tags["D_TAGS"]}
anonymization_actions.update({tag: empty for tag in tags["Z_TAGS"]})
anonymization_actions.update({tag: delete for tag in tags["X_TAGS"]})
anonymization_actions.update({tag: replace_UID for tag in tags["U_TAGS"]})
anonymization_actions.update({tag: empty_or_replace for tag in tags["Z_D_TAGS"]})
anonymization_actions.update({tag: delete_or_empty for tag in tags["X_Z_TAGS"]})
anonymization_actions.update({tag: delete_or_replace for tag in tags["X_D_TAGS"]})
anonymization_actions.update(
{tag: delete_or_empty_or_replace for tag in X_Z_D_TAGS}
{tag: delete_or_empty_or_replace for tag in tags["X_Z_D_TAGS"]}
)
anonymization_actions.update(
{tag: delete_or_empty_or_replace_UID for tag in X_Z_U_STAR_TAGS}
{tag: delete_or_empty_or_replace_UID for tag in tags["X_Z_U_STAR_TAGS"]}
)
return anonymization_actions


def initialize_actions_2024b() -> dict:
"""
Initialize anonymization actions with DICOM standard values of 2024b
:return Dict object which map actions to tags
"""
return initialize_actions("2024b")


def anonymize_dicom_file(
in_file: str,
out_file: str,
Expand Down Expand Up @@ -448,17 +450,19 @@ def get_private_tags(

def anonymize_dataset(
dataset: pydicom.Dataset,
base_rules_gen: Callable = initialize_actions,
extra_anonymization_rules: dict = None,
delete_private_tags: bool = True,
) -> None:
"""
Anonymize a pydicom Dataset by using anonymization rules which links an action to a tag
:param dataset: Dataset to be anonymize
:param base_rules_gen: Function to generate the base rules
:param extra_anonymization_rules: Rules to be applied on the dataset
:param delete_private_tags: Define if private tags should be delete or not
"""
current_anonymization_actions = initialize_actions()
current_anonymization_actions = base_rules_gen()

if extra_anonymization_rules is not None:
current_anonymization_actions.update(extra_anonymization_rules)
Expand Down
3 changes: 2 additions & 1 deletion scripts/scrap_DICOM_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ def create_DICOM_fields(profiles):

def main(
url="https://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_e.html",
output_path="dicomanonymizer/dicomfields.py",
output_path="dicomanonymizer/dicomfields_2024b.py",
):
# As of 2024.05.14, the current version of DICOM spec is 2024b.
profiles = scrap_profiles(url)
file_content = create_DICOM_fields(profiles=profiles)
with open(output_path, "w") as file:
Expand Down
45 changes: 44 additions & 1 deletion tests/test_anonymization_without_dicom.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import pydicom

from dicomanonymizer import anonymize_dataset
from dicomanonymizer.simpledicomanonymizer import empty
from dicomanonymizer.simpledicomanonymizer import (
empty,
initialize_actions,
initialize_actions_2024b,
)


def test_anonymization_without_dicom_file():
Expand Down Expand Up @@ -74,3 +78,42 @@ def test_anonymization_of_ranged_tags_without_dicom_file():
# Check that the dataset has been anonymized
assert (0x5011, 0x0110) not in anon_ds
assert (0x5012, 0x0112) not in anon_ds


def test_switching_dicom_versions():
"""To confirm the different behavior of annonymization beteen dicom versions of 2013 and 2024b.
In 2013, anonymization method of Patient ID is marked as "Z" (empty value) while it becomes Z/D
in 2024b.
Note, VR of Patient ID remains LO in 2013 and 2024b ("current" is 2024b as of 2024.05.14).
https://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_6.html#table_6-1
https://dicom.nema.org/dicom/current/output/chtml/part06/chapter_6.html#table_6-1
"""
fields = [
{ # Replaced by Anonymized
"id": (0x0010, 0x0020),
"type": "LO",
"value": "Test Patient ID",
},
]

# Create a readable dataset for pydicom
data = pydicom.Dataset()
data_2013 = pydicom.Dataset()
data_2024b = pydicom.Dataset()

for field in fields: # sourcery skip: no-loop-in-tests
data.add_new(field["id"], field["type"], field["value"])
data_2013.add_new(field["id"], field["type"], field["value"])
data_2024b.add_new(field["id"], field["type"], field["value"])

anonymize_dataset(data, base_rules_gen=initialize_actions)
anonymize_dataset(data_2013, base_rules_gen=lambda: initialize_actions("2013"))
anonymize_dataset(data_2024b, base_rules_gen=initialize_actions_2024b)

assert data[(0x0010, 0x0020)].value == "" # default behavior which is DICOM 2013.
assert data_2013[(0x0010, 0x0020)].value == "" # same as the default.
assert (
data_2024b[(0x0010, 0x0020)].value == "ANONYMIZED"
) # 2024b differs from the default
39 changes: 39 additions & 0 deletions tests/test_dicomfields_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from dicomanonymizer.dicomfields_selector import selector

from dicomanonymizer import dicomfields, dicomfields_2024b


def test_selector():
assert selector("2013") == {
"D_TAGS": dicomfields.D_TAGS,
"Z_TAGS": dicomfields.Z_TAGS,
"X_TAGS": dicomfields.X_TAGS,
"U_TAGS": dicomfields.U_TAGS,
"Z_D_TAGS": dicomfields.Z_D_TAGS,
"X_Z_TAGS": dicomfields.X_Z_TAGS,
"X_D_TAGS": dicomfields.X_D_TAGS,
"X_Z_D_TAGS": dicomfields.X_Z_D_TAGS,
"X_Z_U_STAR_TAGS": dicomfields.X_Z_U_STAR_TAGS,
"ALL_TAGS": dicomfields.ALL_TAGS,
}
assert selector("2024b") == {
"D_TAGS": dicomfields_2024b.D_TAGS,
"Z_TAGS": dicomfields_2024b.Z_TAGS,
"X_TAGS": dicomfields_2024b.X_TAGS,
"U_TAGS": dicomfields_2024b.U_TAGS,
"Z_D_TAGS": dicomfields_2024b.Z_D_TAGS,
"X_Z_TAGS": dicomfields_2024b.X_Z_TAGS,
"X_D_TAGS": dicomfields_2024b.X_D_TAGS,
"X_Z_D_TAGS": dicomfields_2024b.X_Z_D_TAGS,
"X_Z_U_STAR_TAGS": dicomfields_2024b.X_Z_U_STAR_TAGS,
"ALL_TAGS": dicomfields_2024b.ALL_TAGS,
}

assert selector() == selector("2013")

try:
selector("2019")
except ValueError as e:
assert str(e) == "Unknown DICOM version: 2019"
else:
assert False

0 comments on commit a4b82e8

Please sign in to comment.