Skip to content

Commit 5be8d58

Browse files
committed
ENH: Add some conversions for DICOM value
Adds conversions between DICOM 'TM' value representation and seconds past midnight, as well as between 'AS' value representation and years of age.
1 parent 65d5fc6 commit 5be8d58

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

nibabel/nicom/tests/test_utils.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
"""
33
import re
44

5-
from ..utils import find_private_section
5+
import pytest
6+
from numpy.testing import (assert_almost_equal,
7+
assert_array_equal)
8+
9+
from ..utils import (find_private_section, seconds_to_tm, tm_to_seconds,
10+
as_to_years, years_to_as)
611

712
from . import dicom_test
813
from ...pydicom_compat import pydicom
@@ -47,3 +52,35 @@ def test_find_private_section_real():
4752
assert find_private_section(ds, 0x11, 'near section') == 0x1300
4853
ds.add_new((0x11, 0x15), 'LO', b'far section')
4954
assert find_private_section(ds, 0x11, 'far section') == 0x1500
55+
56+
57+
def test_tm_to_seconds():
58+
for str_val in ('', '1', '111', '11111', '111111.', '1111111', '1:11',
59+
' 111'):
60+
with pytest.raises(ValueError):
61+
tm_to_seconds(str_val)
62+
assert_almost_equal(tm_to_seconds('01'), 60*60)
63+
assert_almost_equal(tm_to_seconds('0101'), 61*60)
64+
assert_almost_equal(tm_to_seconds('010101'), 61*60 + 1)
65+
assert_almost_equal(tm_to_seconds('010101.001'), 61*60 + 1.001)
66+
assert_almost_equal(tm_to_seconds('01:01:01.001'), 61*60 + 1.001)
67+
68+
69+
def test_tm_rt():
70+
for tm_val in ('010101.00000', '010101.00100', '122432.12345'):
71+
assert tm_val == seconds_to_tm(tm_to_seconds(tm_val))
72+
73+
74+
def test_as_to_years():
75+
assert as_to_years('1') == 1.0
76+
assert as_to_years('1Y') == 1.0
77+
assert as_to_years('53') == 53.0
78+
assert as_to_years('53Y') == 53.0
79+
assert_almost_equal(as_to_years('2M'), 2. / 12.)
80+
assert_almost_equal(as_to_years('2D'), 2. / 365.)
81+
assert_almost_equal(as_to_years('2W'), 2. * (7. / 365.))
82+
83+
84+
def test_as_rt():
85+
for as_val in ('1Y', '53Y', '2M', '2W', '2D'):
86+
assert as_val == years_to_as(as_to_years(as_val))

nibabel/nicom/utils.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" Utilities for working with DICOM datasets
22
"""
33

4+
import re, string
45
from numpy.compat.py3k import asstr
56

67

@@ -50,3 +51,138 @@ def find_private_section(dcm_data, group_no, creator):
5051
if creator == name:
5152
return elno * 0x100
5253
return None
54+
55+
56+
def tm_to_seconds(time_str):
57+
'''Convert a DICOM time value (value representation of 'TM') to the number
58+
of seconds past midnight.
59+
60+
Parameters
61+
----------
62+
time_str : str
63+
The string value from the DICOM element
64+
65+
Returns
66+
-------
67+
sec_past_midnight : float
68+
The number of seconds past midnight
69+
'''
70+
# Allow trailing white space
71+
time_str = time_str.rstrip()
72+
73+
# Allow ACR/NEMA style format which includes colons between hours/minutes
74+
# and minutes/seconds
75+
colons = [x.start() for x in re.finditer(':', time_str)]
76+
if len(colons) > 0:
77+
if colons not in ([2], [2, 5]):
78+
raise ValueError("Invalid use of colons in 'TM' VR")
79+
time_str = time_str.replace(':', '')
80+
81+
# Make sure the string length is valid
82+
str_len = len(time_str)
83+
is_valid = str_len > 0
84+
if str_len <= 6:
85+
# If there are six or less chars, there should be an even number
86+
if str_len % 2 != 0:
87+
is_valid = False
88+
else:
89+
# If there are more than six chars, the seventh position should be
90+
# a decimal followed by at least one digit
91+
if str_len == 7 or time_str[6] != '.':
92+
is_valid = False
93+
if not is_valid:
94+
raise ValueError("Invalid number of digits for 'TM' VR")
95+
96+
# Make sure we don't have leading white space
97+
if time_str[0] in string.whitespace:
98+
raise ValueError("Leading whitespace not allowed in 'TM' VR")
99+
100+
# The minutes and seconds are optional
101+
result = int(time_str[:2]) * 3600
102+
if str_len > 2:
103+
result += int(time_str[2:4]) * 60
104+
if str_len > 4:
105+
result += float(time_str[4:])
106+
107+
return float(result)
108+
109+
110+
def seconds_to_tm(seconds):
111+
'''Convert a float representing seconds past midnight into DICOM TM value
112+
113+
Parameters
114+
----------
115+
seconds : float
116+
Number of seconds past midnights
117+
118+
Returns
119+
-------
120+
tm : str
121+
String suitable for use as value in DICOM element with VR of 'TM'
122+
'''
123+
hours = seconds // 3600
124+
seconds -= hours * 3600
125+
minutes = seconds // 60
126+
seconds -= minutes * 60
127+
res = '%02d%02d%08.5f' % (hours, minutes, seconds)
128+
return res
129+
130+
131+
def as_to_years(age_str):
132+
'''Convert a DICOM age value (value representation of 'AS') to the age in
133+
years.
134+
135+
Parameters
136+
----------
137+
age_str : str
138+
The string value from the DICOM element
139+
140+
Returns
141+
-------
142+
age : float
143+
The age of the subject in years
144+
'''
145+
age_str = age_str.strip()
146+
if age_str[-1] == 'Y':
147+
return float(age_str[:-1])
148+
elif age_str[-1] == 'M':
149+
return float(age_str[:-1]) / 12
150+
elif age_str[-1] == 'W':
151+
return float(age_str[:-1]) / (365. / 7)
152+
elif age_str[-1] == 'D':
153+
return float(age_str[:-1]) / 365
154+
else:
155+
return float(age_str)
156+
157+
158+
def years_to_as(years):
159+
'''Convert float representing age in years to DICOM 'AS' value
160+
161+
Parameters
162+
----------
163+
years : float
164+
The years of age
165+
166+
Returns
167+
-------
168+
as : str
169+
String suitable for use as value in DICOM element with VR of 'AS'
170+
'''
171+
if years == round(years):
172+
return '%dY' % years
173+
174+
# Choose how to represent the age (years, months, weeks, or days)
175+
conversions = (('Y', 1), ('M', 12), ('W', (365. / 7)), ('D', 365))
176+
# Try all the conversions, ignore ones that have more than three digits
177+
# which is the limit for the AS value representation, or where they round
178+
# to zero
179+
results = [(years * x[1], x[0]) for x in conversions]
180+
results = [x for x in results
181+
if round(x[0]) > 0 and len('%d' % x[0]) < 4]
182+
# Choose the first one that is close to the minimum error
183+
errors = [abs(x[0] - round(x[0])) for x in results]
184+
min_error = min(errors)
185+
best_idx = 0
186+
while errors[best_idx] - min_error > 0.001:
187+
best_idx += 1
188+
return '%d%s' % (round(results[best_idx][0]), results[best_idx][1])

0 commit comments

Comments
 (0)