diff --git a/deid/config/standards.py b/deid/config/standards.py index 5931c363..5e908a9e 100644 --- a/deid/config/standards.py +++ b/deid/config/standards.py @@ -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"] diff --git a/deid/config/utils.py b/deid/config/utils.py index cdc92304..e89eb226 100644 --- a/deid/config/utils.py +++ b/deid/config/utils.py @@ -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}) diff --git a/deid/data/deid.dicom b/deid/data/deid.dicom index 1eeaf081..55162844 100644 --- a/deid/data/deid.dicom +++ b/deid/data/deid.dicom @@ -745,6 +745,7 @@ LABEL Burned In Annotation # (CTP) %header +REMAP contains:((? 1 + return [generate_uid(entropy_srcs=[x]) for x in field.value] + else: + return generate_uid(entropy_srcs=[field.value]) diff --git a/deid/dicom/parser.py b/deid/dicom/parser.py index 0e55bbbb..b87aa015 100644 --- a/deid/dicom/parser.py +++ b/deid/dicom/parser.py @@ -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 @@ -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) @@ -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 diff --git a/deid/tests/test_config.py b/deid/tests/test_config.py index 71c25a60..766220c9 100644 --- a/deid/tests/test_config.py +++ b/deid/tests/test_config.py @@ -88,6 +88,7 @@ def test_standards(self): "KEEP", "REPLACE", "REMOVE", + "REMAP", "JITTER", "LABEL", ] diff --git a/deid/tests/test_replace_identifiers.py b/deid/tests/test_replace_identifiers.py index 80f87cc3..19fd8f0d 100644 --- a/deid/tests/test_replace_identifiers.py +++ b/deid/tests/test_replace_identifiers.py @@ -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