Skip to content

Commit 68f2888

Browse files
committed
adding pydicom_uuid support and docs
Signed-off-by: vsoch <[email protected]>
1 parent 65e24c4 commit 68f2888

File tree

7 files changed

+152
-5
lines changed

7 files changed

+152
-5
lines changed

deid/dicom/actions/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .jitter import jitter_timestamp, jitter_timestamp_func
2-
from .uids import basic_uuid, dicom_uuid, suffix_uuid
2+
from .uids import basic_uuid, dicom_uuid, suffix_uuid, pydicom_uuid
33

44
# Function lookup
55
# Functions here must take an item, field, and value
@@ -9,4 +9,5 @@
99
"dicom_uuid": dicom_uuid,
1010
"suffix_uuid": suffix_uuid,
1111
"basic_uuid": basic_uuid,
12+
"pydicom_uuid": pydicom_uuid,
1213
}

deid/dicom/actions/uids.py

+29
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"""
2424

2525
from deid.utils import parse_keyvalue_pairs
26+
from pydicom.uid import generate_uid as pydicom_generate_uid
27+
from deid.logger import bot
2628
import uuid
2729

2830

@@ -31,6 +33,33 @@ def basic_uuid(item, value, field, **kwargs):
3133
return str(uuid.uuid4())
3234

3335

36+
def pydicom_uuid(item, value, field, **kwargs):
37+
"""
38+
Use pydicom to generate the UID. Optional kwargs include:
39+
40+
prefix (str): provide a custom prefix
41+
stable_remapping (bool): if true, use the orignal value for entropy.
42+
This ensures stability across different runs that use the same UID.
43+
44+
The prefix must match '^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*\\.$'
45+
"""
46+
opts = parse_keyvalue_pairs(kwargs.get("extras"))
47+
48+
# We always provide a prefix so the stable remapping is done
49+
prefix = opts.get("prefix", "2.25.")
50+
stable_remapping = opts.get("stable_remapping", True)
51+
entropy_srcs = []
52+
53+
# They would need to unset the default prefix
54+
if stable_remapping is True and not prefix:
55+
bot.warning("A prefix must be provided to use stable remapping.")
56+
57+
if stable_remapping is True:
58+
original = str(field.element.value)
59+
entropy_srcs.append(original)
60+
return pydicom_generate_uid(prefix=prefix, entropy_srcs=entropy_srcs)
61+
62+
3463
def suffix_uuid(item, value, field, **kwargs):
3564
"""Return the same field, with a uuid suffix.
3665

deid/tests/common.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,20 @@ def get_dicom(dataset):
5252
return read_file(next(dicom_files))
5353

5454

55+
def get_same_file(dataset):
56+
"""
57+
get a consistent dicom file
58+
"""
59+
from deid.dicom import get_files
60+
61+
dicom_files = list(get_files(dataset))
62+
return dicom_files[0]
63+
64+
5565
def get_file(dataset):
56-
"""helper to get a dicom file"""
66+
"""
67+
get a dicom file
68+
"""
5769
from deid.dicom import get_files
5870

5971
dicom_files = get_files(dataset)

deid/tests/test_dicom_funcs.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from deid.utils import get_installdir
3434
from deid.data import get_dataset
3535
from deid.dicom.parser import DicomParser
36-
from deid.tests.common import get_file, create_recipe
36+
from deid.tests.common import get_file, get_same_file, create_recipe
3737

3838
uuid_regex = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
3939

@@ -111,6 +111,75 @@ def test_basic_uuid(self):
111111
print(parser.dicom["ReferringPhysicianName"].value)
112112
assert re.search(uuid_regex, str(parser.dicom["ReferringPhysicianName"].value))
113113

114+
def test_pydicom_uuid(self):
115+
"""
116+
%header
117+
REPLACE ReferringPhysicianName deid_func:pydicom_uuid
118+
"""
119+
print("Test deid_func:pydicom_uuid")
120+
121+
dicom_file = get_file(self.dataset)
122+
actions = [
123+
{
124+
"action": "REPLACE",
125+
"field": "ReferringPhysicianName",
126+
"value": "deid_func:pydicom_uuid",
127+
}
128+
]
129+
recipe = create_recipe(actions)
130+
131+
# Create a parser, define function for it
132+
parser = DicomParser(dicom_file, recipe=recipe)
133+
parser.parse()
134+
135+
# Randomness is anything, but should be all numbers
136+
print(parser.dicom["ReferringPhysicianName"].value)
137+
name = str(parser.dicom["ReferringPhysicianName"].value)
138+
assert re.search("([0-9]|.)+", name)
139+
140+
# This is the pydicom default, and we default to stable remapping
141+
assert (
142+
name == "2.25.39101090714049289438893821151950032074223798085258118413707"
143+
)
144+
145+
# Add a custom prefix
146+
# must match '^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*\\.$'
147+
actions = [
148+
{
149+
"action": "REPLACE",
150+
"field": "ReferringPhysicianName",
151+
"value": "deid_func:pydicom_uuid prefix=1.55.",
152+
}
153+
]
154+
recipe = create_recipe(actions)
155+
parser = DicomParser(dicom_file, recipe=recipe)
156+
parser.parse()
157+
158+
# Randomness is anything, but should be all numbers
159+
print(parser.dicom["ReferringPhysicianName"].value)
160+
name = str(parser.dicom["ReferringPhysicianName"].value)
161+
assert name.startswith("1.55.")
162+
163+
# This should always be consistent if we use the original as entropy
164+
dicom_file = get_same_file(self.dataset)
165+
actions = [
166+
{
167+
"action": "REPLACE",
168+
"field": "ReferringPhysicianName",
169+
"value": "deid_func:pydicom_uuid stable_remapping=false",
170+
}
171+
]
172+
recipe = create_recipe(actions)
173+
parser = DicomParser(dicom_file, recipe=recipe)
174+
parser.parse()
175+
176+
# Randomness is anything, but should be all numbers
177+
print(parser.dicom["ReferringPhysicianName"].value)
178+
name = str(parser.dicom["ReferringPhysicianName"].value)
179+
assert (
180+
name != "2.25.39101090714049289438893821151950032074223798085258118413707"
181+
)
182+
114183
def test_suffix_uuid(self):
115184
"""
116185
%header

deid/utils/actions.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,16 @@ def parse_keyvalue_pairs(pairs):
100100
if "=" not in pair:
101101
continue
102102
key, value = pair.split("=", 1)
103-
values[key.strip()] = value.strip()
103+
value = value.strip()
104+
105+
# Ensure we convert booleans and none/null
106+
if value == "true":
107+
value = True
108+
if value == "false":
109+
value = False
110+
if value in ["none", "null"]:
111+
value = None
112+
values[key.strip()] = value
104113
return values
105114

106115

docs/_docs/contributing/code.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ See the repository `CONTRIBUTING.md` for these same details.
1616
## Contributing a Custom Function
1717

1818
Deid ships (as of version 0.2.3) with deid-provided functions that can be used in
19-
header parsing. To contribute a recipe you should do the following:
19+
header parsing. To contribute a custom function you should do the following:
2020

2121

2222
1. Add a function to deid/dicom/actions, ideally in the appropriate file (e.g., uid functions in uuid.py, etc)

docs/_docs/user-docs/recipe-funcs.md

+27
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,33 @@ The only change is that we replaced `func` with `deid_func`. Deid will see this
4848
is provided in its library, and grab it for use.
4949

5050

51+
## A Pydicom UUID
52+
53+
Pydicom provides [a function to generate a UUID](https://pydicom.github.io/pydicom/dev/reference/generated/pydicom.uid.generate_uid.html)
54+
and for most this is likely a good approach to take. The most basic usage (for one run) is to generate a random valid
55+
unique identifier:
56+
57+
```
58+
%header
59+
60+
REPLACE ReferringPhysicianName deid_func:pydicom_uuid
61+
```
62+
63+
The default uses `stable_remapping=true`, which means we use the original UUID as entropy
64+
to be able to consistently return the same value between runs. You can disable it, however
65+
we do not recommended it (but maybe could be appropriate for your use case).
66+
67+
You can also optionally define a custom prefix. Note that it needs to match the
68+
regular expression `^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*\\.$` which (in spoken terms)
69+
is a number followed by a period, another number, and ending also in a period (e.g, `1.55.`).
70+
71+
72+
```
73+
%header
74+
75+
REPLACE ReferringPhysicianName deid_func:pydicom_uuid prefix=1.55.
76+
```
77+
5178
## A Dicom UUID
5279

5380
A more "formal" uuid function was added that requires an organization root. Your

0 commit comments

Comments
 (0)