Skip to content

Commit 89fdba9

Browse files
authored
Merge pull request #477 from effigies/enh/estimated-trt
feat(get_trt): Allow estimated and fallback TotalReadoutTime
2 parents 0d86ff4 + 6fb8fd5 commit 89fdba9

File tree

2 files changed

+96
-35
lines changed

2 files changed

+96
-35
lines changed

sdcflows/interfaces/epi.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
class _GetReadoutTimeInputSpec(BaseInterfaceInputSpec):
3838
in_file = File(exists=True, desc="EPI image corresponding to the metadata")
3939
metadata = traits.Dict(mandatory=True, desc="metadata corresponding to the inputs")
40+
use_estimate = traits.Bool(
41+
False, usedefault=True, desc='Use "Estimated*" fields to calculate TotalReadoutTime'
42+
)
43+
fallback = traits.Float(desc="A fallback value, in seconds.")
4044

4145

4246
class _GetReadoutTimeOutputSpec(TraitedSpec):
@@ -57,6 +61,8 @@ def _run_interface(self, runtime):
5761
self._results["readout_time"] = get_trt(
5862
self.inputs.metadata,
5963
self.inputs.in_file if isdefined(self.inputs.in_file) else None,
64+
use_estimate=self.inputs.use_estimate,
65+
fallback=self.inputs.fallback or None,
6066
)
6167
self._results["pe_direction"] = self.inputs.metadata["PhaseEncodingDirection"]
6268
self._results["pe_dir_fsl"] = (

sdcflows/utils/epimanip.py

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@
2626
"""
2727

2828

29-
def get_trt(in_meta, in_file=None):
29+
def get_trt(
30+
in_meta,
31+
in_file=None,
32+
*,
33+
use_estimate=False,
34+
fallback=None,
35+
):
3036
r"""
3137
Obtain the *total readout time* :math:`T_\text{ro}` from available metadata.
3238
@@ -43,6 +49,26 @@ def get_trt(in_meta, in_file=None):
4349
>>> nb.Nifti1Image(np.zeros((90, 90, 60)), None, None).to_filename(
4450
... tmpdir.join('epi.nii.gz').strpath)
4551
52+
Parameters
53+
----------
54+
in_meta: :class:`dict`
55+
BIDS metadata dictionary.
56+
in_file: :class:`str`, optional
57+
Path to the EPI file. Used to determine the number of voxels along the
58+
phase-encoding direction.
59+
use_estimate: :class:`bool`, optional
60+
Whether to use "Estimated*" fields to calculate the total readout time.
61+
These are generated by dcm2niix when authoritative metadata is not available
62+
but heuristic methods permit an estimation.
63+
fallback: :class:`float`, optional
64+
A fallback value, in seconds, to use when the total readout time cannot be
65+
calculated. This should only be used in situations where the field is to be
66+
determined from displacement fields, as in SyN-SDC.
67+
A recommended "plausible" value would be 0.03125, to minimize the impact of
68+
floating-point errors in the calculations.
69+
70+
Examples
71+
--------
4672
4773
>>> meta = {'TotalReadoutTime': 0.05251}
4874
>>> get_trt(meta)
@@ -159,6 +185,23 @@ def get_trt(in_meta, in_file=None):
159185
Traceback (most recent call last):
160186
ValueError:
161187
188+
dcm2niix may provide "EstimatedTotalReadoutTime" or "EstimatedEffectiveEchoSpacing"
189+
fields when converting Philips data. In order to use these fields, pass
190+
``use_estimate=True``:
191+
192+
>>> get_trt({'EstimatedTotalReadoutTime': 0.05251}, use_estimate=True)
193+
0.05251
194+
>>> meta = {'EstimatedEffectiveEchoSpacing': 0.00059,
195+
... 'PhaseEncodingDirection': 'j-'}
196+
>>> f"{get_trt(meta, in_file='epi.nii.gz', use_estimate=True):g}"
197+
'0.05251'
198+
199+
Finally, if a fallback value is provided, it will be used when the total readout
200+
time cannot be calculated by any method:
201+
202+
>>> get_trt({}, fallback=0.03125)
203+
0.03125
204+
162205
.. testcleanup::
163206
164207
>>> os.chdir(cwd)
@@ -194,42 +237,54 @@ def get_trt(in_meta, in_file=None):
194237
raise ValueError(f"'{trt}'")
195238

196239
return trt
240+
elif use_estimate and "EstimatedTotalReadoutTime" in in_meta:
241+
trt = in_meta.get("EstimatedTotalReadoutTime")
242+
if not trt:
243+
raise ValueError(f"'{trt}'")
244+
245+
return trt
246+
247+
if "PhaseEncodingDirection" in in_meta:
248+
# npe = N voxels PE direction
249+
pe_index = "ijk".index(in_meta["PhaseEncodingDirection"][0])
250+
npe = nb.load(in_file).shape[pe_index]
197251

198-
# npe = N voxels PE direction
199-
pe_index = "ijk".index(in_meta["PhaseEncodingDirection"][0])
200-
npe = nb.load(in_file).shape[pe_index]
201-
202-
# Use case 2: EES is defined
203-
ees = in_meta.get("EffectiveEchoSpacing")
204-
if ees:
205-
# Effective echo spacing means that acceleration factors have been accounted for.
206-
return ees * (npe - 1)
207-
208-
try:
209-
echospacing = in_meta["EchoSpacing"]
210-
acc_factor = in_meta["ParallelReductionFactorInPlane"]
211-
except KeyError:
212-
pass
213-
else:
214-
# etl = effective train length
215-
etl = npe // acc_factor
216-
return echospacing * (etl - 1)
217-
218-
# Use case 3 (Philips scans)
219-
try:
220-
wfs = in_meta["WaterFatShift"]
221-
epifactor = in_meta["EPIFactor"]
222-
except KeyError:
223-
pass
224-
else:
225-
wfs_hz = (
226-
(in_meta.get("ImagingFrequency", 0) * 3.39941)
227-
or (in_meta.get("MagneticFieldStrength", 0) * 144.7383333)
228-
or None
229-
)
230-
if wfs_hz:
231-
ees = wfs / (wfs_hz * (epifactor + 1))
252+
# Use case 2: EES is defined
253+
ees = in_meta.get("EffectiveEchoSpacing")
254+
if ees:
255+
# Effective echo spacing means that acceleration factors have been accounted for.
232256
return ees * (npe - 1)
257+
elif use_estimate and "EstimatedEffectiveEchoSpacing" in in_meta:
258+
return in_meta.get("EstimatedEffectiveEchoSpacing") * (npe - 1)
259+
260+
try:
261+
echospacing = in_meta["EchoSpacing"]
262+
acc_factor = in_meta["ParallelReductionFactorInPlane"]
263+
except KeyError:
264+
pass
265+
else:
266+
# etl = effective train length
267+
etl = npe // acc_factor
268+
return echospacing * (etl - 1)
269+
270+
# Use case 3 (Philips scans)
271+
try:
272+
wfs = in_meta["WaterFatShift"]
273+
epifactor = in_meta["EPIFactor"]
274+
except KeyError:
275+
pass
276+
else:
277+
wfs_hz = (
278+
(in_meta.get("ImagingFrequency", 0) * 3.39941)
279+
or (in_meta.get("MagneticFieldStrength", 0) * 144.7383333)
280+
or None
281+
)
282+
if wfs_hz:
283+
ees = wfs / (wfs_hz * (epifactor + 1))
284+
return ees * (npe - 1)
285+
286+
if fallback:
287+
return fallback
233288

234289
raise ValueError("Unknown total-readout time specification")
235290

0 commit comments

Comments
 (0)