2
2
"""
3
3
4
4
import re , string
5
+
5
6
from numpy .compat .py3k import asstr
7
+ import numpy as np
8
+
9
+ from ..externals import OrderedDict
6
10
7
11
8
12
def find_private_section (dcm_data , group_no , creator ):
@@ -53,9 +57,16 @@ def find_private_section(dcm_data, group_no, creator):
53
57
return None
54
58
55
59
60
+ TM_EXP = re .compile (r"^(\d\d)(\d\d)?(\d\d)?(\.\d+)?$" )
61
+ # Allow ACR/NEMA style format which includes colons between hours/minutes and
62
+ # minutes/seconds. See TM / time description in PS3.5 of the DICOM standard at
63
+ # http://dicom.nema.org/Dicom/2011/11_05pu.pdf
64
+ TM_EXP_1COLON = re .compile (r"^(\d\d):(\d\d)()?()?$" )
65
+ TM_EXP_2COLONS = re .compile (r"^(\d\d):(\d\d):(\d\d)?(\.\d+)?$" )
66
+
67
+
56
68
def tm_to_seconds (time_str ):
57
- '''Convert a DICOM time value (value representation of 'TM') to the number
58
- of seconds past midnight.
69
+ '''Convert DICOM time value (VR of 'TM') to seconds past midnight.
59
70
60
71
Parameters
61
72
----------
@@ -66,45 +77,36 @@ def tm_to_seconds(time_str):
66
77
-------
67
78
sec_past_midnight : float
68
79
The number of seconds past midnight
80
+
81
+ Notes
82
+ -----
83
+ From TM / time description in `PS3.5 of the DICOM standard
84
+ <http://dicom.nema.org/Dicom/2011/11_05pu.pdf>`_::
85
+
86
+ A string of characters of the format HHMMSS.FFFFFF; where HH contains
87
+ hours (range "00" - "23"), MM contains minutes (range "00" - "59"), SS
88
+ contains seconds (range "00" - "60"), and FFFFFF contains a fractional
89
+ part of a second as small as 1 millionth of a second (range “000000” -
90
+ “999999”). A 24-hour clock is used. Midnight shall be represented by
91
+ only “0000“ since “2400“ would violate the hour range. The string may
92
+ be padded with trailing spaces. Leading and embedded spaces are not
93
+ allowed.
94
+
95
+ One or more of the components MM, SS, or FFFFFF may be unspecified as
96
+ long as every component to the right of an unspecified component is
97
+ also unspecified, which indicates that the value is not precise to the
98
+ precision of those unspecified components.
69
99
'''
70
100
# Allow trailing white space
71
101
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
102
+ for matcher in (TM_EXP , TM_EXP_1COLON , TM_EXP_2COLONS ):
103
+ match = matcher .match (time_str )
104
+ if match is not None :
105
+ break
88
106
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 )
107
+ raise ValueError ('Invalid tm string "{0}"' .format (time_str ))
108
+ parts = [float (v ) if v else 0 for v in match .groups ()]
109
+ return np .multiply (parts , [3600 , 60 , 1 , 1 ]).sum ()
108
110
109
111
110
112
def seconds_to_tm (seconds ):
@@ -119,18 +121,25 @@ def seconds_to_tm(seconds):
119
121
-------
120
122
tm : str
121
123
String suitable for use as value in DICOM element with VR of 'TM'
124
+
125
+ Notes
126
+ -----
127
+ See docstring for :func:`tm_to_seconds`.
122
128
'''
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
+ hours , seconds = divmod (seconds , 3600 )
130
+ minutes , seconds = divmod (seconds , 60 )
131
+ return '%02d%02d%08.5f' % (hours , minutes , seconds )
132
+
133
+
134
+ CONVERSIONS = OrderedDict ((('Y' , 1 ), ('M' , 12 ), ('W' , (365. / 7 )), ('D' , 365 )))
135
+ CONV_KEYS = list (CONVERSIONS )
136
+ CONV_VALS = np .array (list (CONVERSIONS .values ()))
137
+
138
+ AGE_EXP = re .compile (r'^(\d+)(Y|M|W|D)?$' )
129
139
130
140
131
141
def as_to_years (age_str ):
132
- '''Convert a DICOM age value (value representation of 'AS') to the age in
133
- years.
142
+ '''Convert DICOM age value (VR of 'AS') to the age in years
134
143
135
144
Parameters
136
145
----------
@@ -141,18 +150,23 @@ def as_to_years(age_str):
141
150
-------
142
151
age : float
143
152
The age of the subject in years
153
+
154
+ Notes
155
+ -----
156
+ From AS / age string description in `PS3.5 of the DICOM standard
157
+ <http://dicom.nema.org/Dicom/2011/11_05pu.pdf>`_::
158
+
159
+ A string of characters with one of the following formats -- nnnD, nnnW,
160
+ nnnM, nnnY; where nnn shall contain the number of days for D, weeks for
161
+ W, months for M, or years for Y. Example: “018M” would represent an
162
+ age of 18 months.
144
163
'''
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 )
164
+ match = AGE_EXP .match (age_str .strip ())
165
+ if not match :
166
+ raise ValueError ('Invalid age string "{0}"' .format (age_str ))
167
+ val , code = match .groups ()
168
+ code = 'Y' if code is None else code
169
+ return float (val ) / CONVERSIONS [code ]
156
170
157
171
158
172
def years_to_as (years ):
@@ -167,22 +181,18 @@ def years_to_as(years):
167
181
-------
168
182
as : str
169
183
String suitable for use as value in DICOM element with VR of 'AS'
184
+
185
+ Notes
186
+ -----
187
+ See docstring for :func:`as_to_years`.
170
188
'''
171
189
if years == round (years ):
172
190
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 ])
191
+ # Choose how to represent the age (years, months, weeks, or days).
192
+ # Try all the conversions, ignore ones that have more than three digits,
193
+ # which is the limit for the AS value representation.
194
+ conved = years * CONV_VALS
195
+ conved [conved >= 1000 ] = np .nan # Too many digits for AS field
196
+ year_error = np .abs (conved - np .round (conved )) / CONV_VALS
197
+ best_i = np .nanargmin (year_error )
198
+ return "{0:.0f}{1}" .format (conved [best_i ], CONV_KEYS [best_i ])
0 commit comments