Skip to content

Commit 2bb807d

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 5a42cb3 commit 2bb807d

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed

nibabel/nicom/tests/test_utils.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
assert_equal, assert_not_equal)
1111

1212

13-
from ..utils import find_private_section
13+
from ..utils import (find_private_section, seconds_to_tm, tm_to_seconds,
14+
as_to_years, years_to_as)
1415

1516
from .test_dicomwrappers import (have_dicom, dicom_test,
1617
IO_DATA_PATH, DATA, DATA_PHILIPS)
@@ -67,3 +68,34 @@ def test_find_private_section_real():
6768
assert_equal(find_private_section(ds, 0x11, 'near section'), 0x1300)
6869
ds.add_new((0x11, 0x15), 'LO', b'far section')
6970
assert_equal(find_private_section(ds, 0x11, 'far section'), 0x1500)
71+
72+
73+
def test_tm_to_seconds():
74+
for str_val in ('', '1', '111', '11111', '111111.', '1111111', '1:11',
75+
' 111'):
76+
assert_raises(ValueError, tm_to_seconds, str_val)
77+
assert_almost_equal(tm_to_seconds('01'), 60*60)
78+
assert_almost_equal(tm_to_seconds('0101'), 61*60)
79+
assert_almost_equal(tm_to_seconds('010101'), 61*60 + 1)
80+
assert_almost_equal(tm_to_seconds('010101.001'), 61*60 + 1.001)
81+
assert_almost_equal(tm_to_seconds('01:01:01.001'), 61*60 + 1.001)
82+
83+
84+
def test_tm_rt():
85+
for tm_val in ('010101.00000', '010101.00100', '122432.12345'):
86+
assert_equal(tm_val, seconds_to_tm(tm_to_seconds(tm_val)))
87+
88+
89+
def test_as_to_years():
90+
assert_equal(as_to_years('1'), 1.0)
91+
assert_equal(as_to_years('1Y'), 1.0)
92+
assert_equal(as_to_years('53'), 53.0)
93+
assert_equal(as_to_years('53Y'), 53.0)
94+
assert_almost_equal(as_to_years('2M'), 2. / 12.)
95+
assert_almost_equal(as_to_years('2D'), 2. / 365.)
96+
assert_almost_equal(as_to_years('2W'), 2. * (7. / 365.))
97+
98+
99+
def test_as_rt():
100+
for as_val in ('1Y', '53Y', '2M', '2W', '2D'):
101+
assert_equal(as_val, years_to_as(as_to_years(as_val)))

nibabel/nicom/utils.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"""
33
from __future__ import division, print_function, absolute_import
44

5+
import re, string
6+
57
from ..py3k import asstr
68

79

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

0 commit comments

Comments
 (0)