Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP+ENH: Add REMAP action for automatically remapping (instance) UIDs #203

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deid/config/standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
sections = ["header", "labels", "filter", "values", "fields"]

# Supported Header Actions
actions = ("ADD", "BLANK", "JITTER", "KEEP", "REPLACE", "REMOVE", "LABEL")
actions = ("ADD", "BLANK", "JITTER", "KEEP", "REPLACE", "REMOVE", "REMAP", "LABEL")

# Supported Group actions (SPLIT only supported for values)
groups = ["values", "fields"]
Expand Down
2 changes: 1 addition & 1 deletion deid/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def parse_config_action(section, line, config, section_name=None):
config[section].append({"action": action, "field": field, "value": value})

# Actions that don't require a value
elif action in ["BLANK", "KEEP"]:
elif action in ["BLANK", "KEEP", "REMAP"]:
bot.debug("%s: adding %s" % (section, line))
config[section].append({"action": action, "field": field})

Expand Down
5 changes: 2 additions & 3 deletions deid/data/deid.dicom
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ LABEL Burned In Annotation # (CTP)

%header

REMAP contains:((?<!SOPClass)UID)
REMOVE endswith:Time
REMOVE endswith:Date
REMOVE endswith:time
Expand All @@ -765,8 +766,7 @@ REMOVE PatientsName
REMOVE ReasonForStudy
REMOVE contains:Trial
REMOVE startswith:PatientTelephoneNumber
REMOVE endswith:ID
REMOVE endswith:IDs
REMOVE contains:([^U]IDs?)
REMOVE ReferringPhysicianName
REMOVE ConsultingPhysicianName
REMOVE EvaluatorName
Expand Down Expand Up @@ -799,7 +799,6 @@ REMOVE SourceApplicatorName
REMOVE ClinicalTrialSponsorName
REMOVE ContentCreatorName
REMOVE ClinicalTrialProtocolEthicsCommitteeName
REMOVE contains:UID
REMOVE RegionOfResidence
REMOVE CurrentPatientLocation
REMOVE PatientComments
Expand Down
19 changes: 19 additions & 0 deletions deid/dicom/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

"""

from pydicom.uid import generate_uid

from deid.logger import bot
from deid.utils import get_timestamp

Expand Down Expand Up @@ -86,3 +88,20 @@ def jitter_timestamp(field, value):
bot.warning("JITTER not supported for %s with VR=%s" % (field, dcmvr))

return new_value


# UIDs

def remap_uid(field):
"""Remap existing UID in stable and secure manner

Same input UID creates the same output UID, which keeps study / series
associations correct (or even FrameOfReferenceUID) provided the full
study / series in anonymized. At same time input UID can't be recovered
from output so any potential PHI (i.e. dates / times) is eliminated.
"""
if isinstance(field.value, list):
# Handle VM > 1
return [generate_uid(entropy_srcs=[x]) for x in field.value]
else:
return generate_uid(entropy_srcs=[field.value])
11 changes: 10 additions & 1 deletion deid/dicom/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from deid.config import DeidRecipe
from deid.config.standards import actions as valid_actions
from deid.dicom.utils import save_dicom
from deid.dicom.actions import jitter_timestamp
from deid.dicom.actions import jitter_timestamp, remap_uid
from deid.dicom.tags import remove_sequences, get_private, get_tag, add_tag
from deid.dicom.groups import extract_values_list, extract_fields_list
from deid.dicom.fields import get_fields, expand_field_expression, DicomField
Expand Down Expand Up @@ -445,6 +445,7 @@ def _run_action(self, field, action, value=None):
Both result in a call to this function. If an action fails or is not
done, None is returned, and the calling function should handle this.
"""

# Blank the value
if action == "BLANK":
self.blank_field(field)
Expand All @@ -470,6 +471,14 @@ def _run_action(self, field, action, value=None):
else:
bot.warning("JITTER %s unsuccessful" % field)

# Remap UIDs
elif action == "REMAP":
if field.element.VR == 'UI':
new_val = remap_uid(field.element)
else:
bot.warning("REMAP called on invalid (non-UID) element %s" % field)
self.replace_field(field, new_val)

# elif "KEEP" --> Do nothing. Keep the original

# Remove the field entirely
Expand Down
1 change: 1 addition & 0 deletions deid/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_standards(self):
"KEEP",
"REPLACE",
"REMOVE",
"REMAP",
"JITTER",
"LABEL",
]
Expand Down
31 changes: 31 additions & 0 deletions deid/tests/test_replace_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,37 @@ def test_jitter_timestamp(self):
"20230102011721.621000", result[0]["AcquisitionDateTime"].value
)

def test_remap_uid(self):

print("Test uid remapping")
dicom_file = get_file(self.dataset)
dicom = read_file(dicom_file)
orig_uid = dicom.StudyInstanceUID

actions = [{"action": "REMAP", "field": "StudyInstanceUID"}]
recipe = create_recipe(actions)

result1 = replace_identifiers(
dicom_files=dicom_file,
deid=recipe,
save=False,
remove_private=False,
strip_sequences=False,
)
self.assertEqual(1, len(result1))
self.assertNotEqual(
orig_uid, result1[0]["StudyInstanceUID"].value
)
result2 = replace_identifiers(
dicom_files=dicom_file,
deid=recipe,
save=False,
remove_private=False,
strip_sequences=False,
)
self.assertEqual(1, len(result2))
self.assertEqual(result1[0]["StudyInstanceUID"].value, result2[0]["StudyInstanceUID"].value)

def test_expanders(self):
"""RECIPE RULES
REMOVE contains:Collimation
Expand Down