Skip to content

Commit 6b74fd2

Browse files
authored
Merge pull request #2825 from richford/fsl-eddy-quad-interface
ENH: Add FSL `eddy_quad` interface
2 parents f2a21fd + 41d82ad commit 6b74fd2

File tree

3 files changed

+196
-1
lines changed

3 files changed

+196
-1
lines changed

.zenodo.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,11 @@
589589
"affiliation": "MIT, HMS",
590590
"name": "Ghosh, Satrajit",
591591
"orcid": "0000-0002-5312-6729"
592+
},
593+
{
594+
"affiliation": "University of Washington",
595+
"name": "Richie-Halford, Adam",
596+
"orcid": "0000-0001-9276-9084"
592597
}
593598
],
594599
"keywords": [

nipype/interfaces/fsl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
WarpPointsFromStd, RobustFOV, CopyGeom, MotionOutliers)
2222

2323
from .epi import (PrepareFieldmap, TOPUP, ApplyTOPUP, Eddy, EPIDeWarp, SigLoss,
24-
EddyCorrect, EpiReg)
24+
EddyCorrect, EpiReg, EddyQuad)
2525
from .dti import (BEDPOSTX, XFibres, DTIFit, ProbTrackX, ProbTrackX2, VecReg,
2626
ProjThresh, FindTheBiggest, DistanceMap, TractSkeleton,
2727
MakeDyadicVectors, BEDPOSTX5, XFibres5)

nipype/interfaces/fsl/epi.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,3 +1232,193 @@ def _run_interface(self, runtime):
12321232
if runtime.stderr:
12331233
self.raise_exception(runtime)
12341234
return runtime
1235+
1236+
1237+
class EddyQuadInputSpec(FSLCommandInputSpec):
1238+
base_name = traits.Str(
1239+
'eddy_corrected',
1240+
usedefault=True,
1241+
argstr='%s',
1242+
desc=("Basename (including path) for EDDY output files, i.e., "
1243+
"corrected images and QC files"),
1244+
position=0,
1245+
)
1246+
idx_file = File(
1247+
exists=True,
1248+
mandatory=True,
1249+
argstr="--eddyIdx=%s",
1250+
desc=("File containing indices for all volumes into acquisition "
1251+
"parameters")
1252+
)
1253+
param_file = File(
1254+
exists=True,
1255+
mandatory=True,
1256+
argstr="--eddyParams=%s",
1257+
desc="File containing acquisition parameters"
1258+
)
1259+
mask_file = File(
1260+
exists=True,
1261+
mandatory=True,
1262+
argstr="--mask=%s",
1263+
desc="Binary mask file"
1264+
)
1265+
bval_file = File(
1266+
exists=True,
1267+
mandatory=True,
1268+
argstr="--bvals=%s",
1269+
desc="b-values file"
1270+
)
1271+
bvec_file = File(
1272+
exists=True,
1273+
argstr="--bvecs=%s",
1274+
desc=("b-vectors file - only used when <base_name>.eddy_residuals "
1275+
"file is present")
1276+
)
1277+
output_dir = traits.Str(
1278+
name_template='%s.qc',
1279+
name_source=['base_name'],
1280+
argstr='--output-dir=%s',
1281+
desc="Output directory - default = '<base_name>.qc'",
1282+
)
1283+
field = File(
1284+
exists=True,
1285+
argstr='--field=%s',
1286+
desc="TOPUP estimated field (in Hz)",
1287+
)
1288+
slice_spec = File(
1289+
exists=True,
1290+
argstr='--slspec=%s',
1291+
desc="Text file specifying slice/group acquisition",
1292+
)
1293+
verbose = traits.Bool(
1294+
argstr='--verbose',
1295+
desc="Display debug messages",
1296+
)
1297+
1298+
1299+
class EddyQuadOutputSpec(TraitedSpec):
1300+
qc_json = File(
1301+
exists=True,
1302+
desc=("Single subject database containing quality metrics and data "
1303+
"info.")
1304+
)
1305+
qc_pdf = File(
1306+
exists=True,
1307+
desc="Single subject QC report."
1308+
)
1309+
avg_b_png = traits.List(
1310+
File(exists=True),
1311+
desc=("Image showing mid-sagittal, -coronal and -axial slices of "
1312+
"each averaged b-shell volume.")
1313+
)
1314+
avg_b0_pe_png = traits.List(
1315+
File(exists=True),
1316+
desc=("Image showing mid-sagittal, -coronal and -axial slices of "
1317+
"each averaged pe-direction b0 volume. Generated when using "
1318+
"the -f option.")
1319+
)
1320+
cnr_png = traits.List(
1321+
File(exists=True),
1322+
desc=("Image showing mid-sagittal, -coronal and -axial slices of "
1323+
"each b-shell CNR volume. Generated when CNR maps are "
1324+
"available.")
1325+
)
1326+
vdm_png = File(
1327+
exists=True,
1328+
desc=("Image showing mid-sagittal, -coronal and -axial slices of "
1329+
"the voxel displacement map. Generated when using the -f "
1330+
"option.")
1331+
)
1332+
residuals = File(
1333+
exists=True,
1334+
desc=("Text file containing the volume-wise mask-averaged squared "
1335+
"residuals. Generated when residual maps are available.")
1336+
)
1337+
clean_volumes = File(
1338+
exists=True,
1339+
desc=("Text file containing a list of clean volumes, based on "
1340+
"the eddy squared residuals. To generate a version of the "
1341+
"pre-processed dataset without outlier volumes, use: "
1342+
"`fslselectvols -i <eddy_corrected_data> -o "
1343+
"eddy_corrected_data_clean --vols=vols_no_outliers.txt`")
1344+
)
1345+
1346+
1347+
class EddyQuad(FSLCommand):
1348+
"""
1349+
Interface for FSL eddy_quad, a tool for generating single subject reports
1350+
and storing the quality assessment indices for each subject.
1351+
`User guide <https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddyqc/UsersGuide>`_
1352+
1353+
Examples
1354+
--------
1355+
1356+
>>> from nipype.interfaces.fsl import EddyQuad
1357+
>>> quad = EddyQuad()
1358+
>>> quad.inputs.base_name = 'eddy_corrected'
1359+
>>> quad.inputs.idx_file = 'epi_index.txt'
1360+
>>> quad.inputs.param_file = 'epi_acqp.txt'
1361+
>>> quad.inputs.mask_file = 'epi_mask.nii'
1362+
>>> quad.inputs.bval_file = 'bvals.scheme'
1363+
>>> quad.inputs.bvec_file = 'bvecs.scheme'
1364+
>>> quad.inputs.output_dir = 'eddy_corrected.qc'
1365+
>>> quad.inputs.field = 'fieldmap_phase_fslprepared.nii'
1366+
>>> quad.inputs.verbose = True
1367+
>>> quad.cmdline
1368+
'eddy_quad eddy_corrected --bvals=bvals.scheme --bvecs=bvecs.scheme \
1369+
--field=fieldmap_phase_fslprepared.nii --eddyIdx=epi_index.txt \
1370+
--mask=epi_mask.nii --output-dir=eddy_corrected.qc --eddyParams=epi_acqp.txt \
1371+
--verbose'
1372+
>>> res = quad.run() # doctest: +SKIP
1373+
1374+
"""
1375+
_cmd = 'eddy_quad'
1376+
input_spec = EddyQuadInputSpec
1377+
output_spec = EddyQuadOutputSpec
1378+
1379+
def _list_outputs(self):
1380+
from glob import glob
1381+
outputs = self.output_spec().get()
1382+
1383+
# If the output directory isn't defined, the interface seems to use
1384+
# the default but not set its value in `self.inputs.output_dir`
1385+
if not isdefined(self.inputs.output_dir):
1386+
out_dir = os.path.abspath(os.path.basename(self.inputs.base_name) + '.qc.nii.gz')
1387+
else:
1388+
out_dir = os.path.abspath(self.inputs.output_dir)
1389+
1390+
outputs['qc_json'] = os.path.join(out_dir, 'qc.json')
1391+
outputs['qc_pdf'] = os.path.join(out_dir, 'qc.pdf')
1392+
1393+
# Grab all b* files here. This will also grab the b0_pe* files
1394+
# as well, but only if the field input was provided. So we'll remove
1395+
# them later in the next conditional.
1396+
outputs['avg_b_png'] = sorted(glob(
1397+
os.path.join(out_dir, 'avg_b*.png')
1398+
))
1399+
1400+
if isdefined(self.inputs.field):
1401+
outputs['avg_b0_pe_png'] = sorted(glob(
1402+
os.path.join(out_dir, 'avg_b0_pe*.png')
1403+
))
1404+
1405+
# The previous glob for `avg_b_png` also grabbed the
1406+
# `avg_b0_pe_png` files so we have to remove them
1407+
# from `avg_b_png`.
1408+
for fname in outputs['avg_b0_pe_png']:
1409+
outputs['avg_b_png'].remove(fname)
1410+
1411+
outputs['vdm_png'] = os.path.join(out_dir, 'vdm.png')
1412+
1413+
outputs['cnr_png'] = sorted(glob(os.path.join(out_dir, 'cnr*.png')))
1414+
1415+
residuals = os.path.join(out_dir, 'eddy_msr.txt')
1416+
if os.path.isfile(residuals):
1417+
outputs['residuals'] = residuals
1418+
1419+
clean_volumes = os.path.join(out_dir, 'vols_no_outliers.txt')
1420+
if os.path.isfile(clean_volumes):
1421+
outputs['clean_volumes'] = clean_volumes
1422+
1423+
return outputs
1424+

0 commit comments

Comments
 (0)