Skip to content

Commit be6618b

Browse files
committed
feat: Add multi-value timestamp support and improve jitter function
- Add support for multi-value DICOM date/time fields (DA, DT) in jitter_timestamp - Handle pydicom MultiValue objects from real DICOM files - Support list/tuple inputs for programmatic use - Return backslash-separated strings per DICOM standard (e.g., "20131220\20131225") - Add early exit optimization for empty field values - Skip jittering in parser when field.element.value is empty (None, "", b"") - Return None from jitter_timestamp for empty values - Prevents unnecessary processing and warnings for empty fields - Add comprehensive test coverage - Test multi-value date jittering (Case 6) - Test unsupported VR types (Case 7 with OB) This enables proper de-identification of DICOM files that contain multiple acquisition dates/times in a single field.
1 parent 52597db commit be6618b

File tree

5 files changed

+59
-16
lines changed

5 files changed

+59
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
1414
Referenced versions in headers are tagged on Github, in parentheses are for pypi.
1515

1616
## [vxx](https://github.com/pydicom/deid/tree/master) (master)
17+
- Add multi-value timestamp support and improve jitter function [#296](https://github.com/pydicom/deid/pull/296) (0.4.11)
1718
- Optimize KEEP action performance by caching field contenders & fix SyntaxWarning for invalid escape sequences [#293](https://github.com/pydicom/deid/pull/295) (0.4.10)
1819
- Fix field removal and blanking to clean up child UID references [#293](https://github.com/pydicom/deid/pull/293) (0.4.9)
1920
- Fix UID lookup for nested sequence fields in DICOM datasets [#292](https://github.com/pydicom/deid/pull/292) (0.4.8)

deid/dicom/actions/jitter.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
__copyright__ = "Copyright 2016-2025, Vanessa Sochat"
33
__license__ = "MIT"
44

5+
from pydicom.multival import MultiValue
6+
57
from deid.logger import bot
68
from deid.utils import get_timestamp, parse_keyvalue_pairs
79

@@ -40,44 +42,60 @@ def jitter_timestamp(field, value):
4042
value = int(value)
4143

4244
original = field.element.value
43-
new_value = original
4445

45-
if original is not None:
46+
# Early exit for empty values
47+
if not original:
48+
return None
49+
50+
# Normalize to list for uniform processing of multi-value fields
51+
is_multi_values = isinstance(original, (list, tuple, MultiValue))
52+
values = list(original) if is_multi_values else [original]
53+
dcmvr = field.element.VR
54+
55+
jittered = []
56+
for val in values:
4657
# Create default for new value
47-
new_value = None
48-
dcmvr = field.element.VR
58+
single_value = None
4959

5060
# DICOM Value Representation can be either DA (Date) DT (Timestamp),
5161
# or something else, which is not supported.
5262
if dcmvr == "DA":
5363
# NEMA-compliant format for DICOM date is YYYYMMDD
54-
new_value = get_timestamp(original, jitter_days=value, format="%Y%m%d")
64+
single_value = get_timestamp(val, jitter_days=value, format="%Y%m%d")
5565

5666
elif dcmvr == "DT":
5767
# NEMA-compliant format for DICOM timestamp is
5868
# YYYYMMDDHHMMSS.FFFFFF&ZZXX
5969
try:
60-
new_value = get_timestamp(
61-
original, jitter_days=value, format="%Y%m%d%H%M%S.%f%z"
70+
single_value = get_timestamp(
71+
val, jitter_days=value, format="%Y%m%d%H%M%S.%f%z"
6272
)
6373
except Exception:
64-
new_value = get_timestamp(
65-
original, jitter_days=value, format="%Y%m%d%H%M%S.%f"
74+
single_value = get_timestamp(
75+
val, jitter_days=value, format="%Y%m%d%H%M%S.%f"
6676
)
6777

6878
else:
6979
# If the field type is not supplied, attempt to parse different formats
7080
for fmtstr in ["%Y%m%d", "%Y%m%d%H%M%S.%f%z", "%Y%m%d%H%M%S.%f"]:
7181
try:
72-
new_value = get_timestamp(
73-
original, jitter_days=value, format=fmtstr
74-
)
82+
single_value = get_timestamp(val, jitter_days=value, format=fmtstr)
7583
break
7684
except Exception:
7785
pass
7886

7987
# If nothing works, do nothing and issue a warning.
80-
if not new_value:
81-
bot.warning("JITTER not supported for %s with VR=%s" % (field, dcmvr))
88+
if not single_value:
89+
bot.warning(
90+
f"JITTER not supported for tag={field.element.tag}, name={field.name}, VR={dcmvr}"
91+
)
92+
93+
# If jittering failed (single_value is None), keep the original value
94+
jittered.append(single_value if single_value else val)
8295

83-
return new_value
96+
# Return in same format as input
97+
if is_multi_values:
98+
# For multi-value DICOM fields, return as backslash-separated string
99+
return "\\".join(str(v) for v in jittered)
100+
else:
101+
return jittered[0]

deid/dicom/parser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,10 @@ def _run_action(self, field, action, value=None):
609609

610610
# Code the value with something in the response
611611
elif action == "JITTER":
612+
# Skip jittering if field value is empty
613+
if not field.element.value:
614+
return
615+
612616
value = parse_value(
613617
item=self.lookup,
614618
dicom=self.dicom,

deid/tests/test_dicom_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ def test_jitter_timestamp(self):
115115
expected = None
116116
self.assertEqual(actual, expected)
117117

118+
print("Case 6: Testing with multi-value dates")
119+
name = "DateOfLastCalibration"
120+
tag = get_tag(name)
121+
dicom.DateOfLastCalibration = ["20131210", "20131215"]
122+
dicom.data_element(name).VR = "DA"
123+
field = DicomField(dicom.data_element(name), name, str(tag["tag"]))
124+
actual = jitter_timestamp(field, 10)
125+
expected = "20131220\\20131225"
126+
self.assertEqual(actual, expected)
127+
128+
print("Case 7: Testing with non supported VR")
129+
name = "FrameOriginTimestamp"
130+
tag = get_tag(name)
131+
dicom.FrameOriginTimestamp = b""
132+
dicom.data_element(name).VR = "OB"
133+
field = DicomField(dicom.data_element(name), name, str(tag["tag"]))
134+
actual = jitter_timestamp(field, 10)
135+
expected = None
136+
self.assertEqual(actual, expected)
137+
118138

119139
if __name__ == "__main__":
120140
unittest.main()

deid/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__copyright__ = "Copyright 2016-2025, Vanessa Sochat"
33
__license__ = "MIT"
44

5-
__version__ = "0.4.10"
5+
__version__ = "0.4.11"
66
AUTHOR = "Vanessa Sochat"
77
AUTHOR_EMAIL = "vsoch@users.noreply.github.com"
88
NAME = "deid"

0 commit comments

Comments
 (0)