Skip to content
This repository was archived by the owner on Dec 27, 2022. It is now read-only.

Commit 12bfadb

Browse files
authored
Merge pull request #1 from richford/id-outliers
Add nodes to identify and drop scans with too many outlier slices
2 parents d751eb8 + f6a1b11 commit 12bfadb

File tree

2 files changed

+144
-3
lines changed

2 files changed

+144
-3
lines changed

dmriprep/run.py

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,74 @@ def run_dmriprep_pe(dwi_file, dwi_file_AP, dwi_file_PA, bvec_file, bval_file,
266266
eddy.inputs.repol = True
267267
eddy.inputs.niter = 1 # TODO: make this a parameter to the function with default 5
268268

269+
def id_outliers_fn(outlier_report, threshold, dwi_file):
270+
"""Get list of scans that exceed threshold for number of outliers
271+
272+
Parameters
273+
----------
274+
outlier_report: string
275+
Path to the fsl_eddy outlier report
276+
277+
threshold: int or float
278+
If threshold is an int, it is treated as number of allowed outlier
279+
slices. If threshold is a float between 0 and 1 (exclusive), it is
280+
treated the fraction of allowed outlier slices before we drop the
281+
whole volume. Float param in not yet implemented
282+
283+
dwi_file: string
284+
Path to nii dwi file to determine total number of slices
285+
286+
Returns
287+
-------
288+
drop_scans: numpy.ndarray
289+
List of scan indices to drop
290+
"""
291+
import nibabel as nib
292+
import numpy as np
293+
import os.path as op
294+
import parse
295+
296+
with open(op.abspath(outlier_report), 'r') as fp:
297+
lines = fp.readlines()
298+
299+
p = parse.compile(
300+
"Slice {slice:d} in scan {scan:d} is an outlier with "
301+
"mean {mean_sd:f} standard deviations off, and mean "
302+
"squared {mean_sq_sd:f} standard deviations off."
303+
)
304+
305+
outliers = [p.parse(l).named for l in lines]
306+
scans = {d['scan'] for d in outliers}
307+
308+
def num_outliers(scan, outliers):
309+
return len([d for d in outliers if d['scan'] == scan])
310+
311+
if 0 < threshold < 1:
312+
threshold *= nib.load(dwi_file).shape[2]
313+
314+
drop_scans = np.array([
315+
s for s in scans
316+
if num_outliers(s, outliers) > threshold
317+
])
318+
319+
outpath = op.abspath("dropped_scans.txt")
320+
np.savetxt(outpath, drop_scans, fmt="%d")
321+
322+
return drop_scans, outpath
323+
324+
id_outliers_node = pe.Node(niu.Function(
325+
input_names=["outlier_report", "threshold", "dwi_file"],
326+
output_names=["drop_scans", "outpath"],
327+
function=id_outliers_fn),
328+
name="id_outliers_node"
329+
)
330+
331+
# TODO: make this a parameter to the function
332+
id_outliers_node.inputs.threshold = 0.02
333+
id_outliers_node.inputs.dwi_file = dwi_file
334+
wf.connect(prep, "fsl_eddy.out_outlier_report",
335+
id_outliers_node, "outlier_report")
336+
269337
merge = pe.Node(fsl.Merge(dimension='t'), name="mergeAPPA")
270338
merge.inputs.in_files = [dwi_file_AP, dwi_file_PA]
271339
wf.connect(merge, 'merged_file', prep, 'inputnode.alt_file')
@@ -281,10 +349,76 @@ def run_dmriprep_pe(dwi_file, dwi_file_AP, dwi_file_PA, bvec_file, bval_file,
281349
bbreg.inputs.subject_id = 'freesurfer' # bids_sub_name
282350
wf.connect(fslroi, "roi_file", bbreg, "source_file")
283351

352+
def drop_outliers_fn(in_file, in_bval, in_bvec, drop_scans):
353+
"""Drop outlier volumes from dwi file
354+
355+
Parameters
356+
----------
357+
in_file: string
358+
Path to nii dwi file to drop outlier volumes from
359+
360+
in_bval: string
361+
Path to bval file to drop outlier volumes from
362+
363+
in_bvec: string
364+
Path to bvec file to drop outlier volumes from
365+
366+
drop_scans: numpy.ndarray
367+
List of scan indices to drop
368+
369+
Returns
370+
-------
371+
out_file: string
372+
Path to "thinned" output dwi file
373+
374+
out_bval: string
375+
Path to "thinned" output bval file
376+
377+
out_bvec: string
378+
Path to "thinned" output bvec file
379+
"""
380+
import nibabel as nib
381+
import numpy as np
382+
import os.path as op
383+
384+
img = nib.load(op.abspath(in_file))
385+
img_data = img.get_fdata()
386+
img_data_thinned = np.delete(img_data, drop_scans, axis=3)
387+
img_thinned = nib.Nifti1Image(img_data_thinned.astype(np.float64), img.affine)
388+
389+
root, ext1 = op.splitext(in_file)
390+
root, ext0 = op.splitext(root)
391+
out_file = ''.join([root + "_thinned", ext0, ext1])
392+
nib.save(img_thinned, op.abspath(out_file))
393+
394+
bval = np.loadtxt(in_bval)
395+
bval_thinned = np.delete(bval, drop_scans, axis=0)
396+
out_bval = '_thinned'.join(op.splitext(in_bval))
397+
np.savetxt(out_bval, bval_thinned)
398+
399+
bvec = np.loadtxt(in_bvec)
400+
bvec_thinned = np.delete(bvec, drop_scans, axis=1)
401+
out_bvec = '_thinned'.join(op.splitext(in_bvec))
402+
np.savetxt(out_bvec, bvec_thinned)
403+
404+
return out_file, out_bval, out_bvec
405+
406+
drop_outliers_node = pe.Node(niu.Function(
407+
input_names=["in_file", "in_bval", "in_bvec", "drop_scans"],
408+
output_names=["out_file", "out_bval", "out_bvec"],
409+
function=drop_outliers_fn),
410+
name="drop_outliers_node"
411+
)
412+
413+
wf.connect(id_outliers_node, "drop_scans", drop_outliers_node, "drop_scans")
414+
wf.connect(prep, "outputnode.out_file", drop_outliers_node, "in_file")
415+
drop_outliers_node.inputs.in_bval = bval_file
416+
wf.connect(prep, "outputnode.out_bvec", drop_outliers_node, "in_bvec")
417+
284418
get_tensor = pe.Node(DTI(), name="dipy_tensor")
285-
wf.connect(prep, "outputnode.out_file", get_tensor, "in_file")
286-
get_tensor.inputs.in_bval = bval_file
287-
wf.connect(prep, "outputnode.out_bvec", get_tensor, "in_bvec")
419+
wf.connect(drop_outliers_node, "out_file", get_tensor, "in_file")
420+
wf.connect(drop_outliers_node, "out_bval", get_tensor, "in_bval")
421+
wf.connect(drop_outliers_node, "out_bvec", get_tensor, "in_bvec")
288422

289423
scale_tensor = pe.Node(name='scale_tensor', interface=fsl.BinaryMaths())
290424
scale_tensor.inputs.operation = 'mul'
@@ -371,6 +505,10 @@ def get_orig(subjects_dir, sub):
371505
("derivatives/dmriprep", "derivatives/{}/dmriprep".format(bids_sub_name))
372506
]
373507

508+
wf.connect(drop_outliers_node, "out_file", datasink, "dmriprep.dwi.@thinned")
509+
wf.connect(drop_outliers_node, "out_bval", datasink, "dmriprep.dwi.@bval_thinned")
510+
wf.connect(drop_outliers_node, "out_bvec", datasink, "dmriprep.dwi.@bvec_thinned")
511+
374512
wf.connect(prep, "outputnode.out_file", datasink, "dmriprep.dwi.@corrected")
375513
wf.connect(prep, "outputnode.out_bvec", datasink, "dmriprep.dwi.@rotated")
376514
wf.connect(prep, "fsl_eddy.out_parameter",
@@ -385,6 +523,8 @@ def get_orig(subjects_dir, sub):
385523
wf.connect(prep, "fsl_eddy.out_shell_alignment_parameters",
386524
datasink, "dmriprep.qc.@eddyparamsshellalign")
387525

526+
wf.connect(id_outliers_node, "outpath", datasink, "dmriprep.qc.@droppedscans")
527+
388528
wf.connect(get_tensor, "out_file", datasink, "dmriprep.dti.@tensor")
389529
wf.connect(get_tensor, "fa_file", datasink, "dmriprep.dti.@fa")
390530
wf.connect(get_tensor, "md_file", datasink, "dmriprep.dti.@md")

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'Click>=6.0',
1616
'nipype',
1717
'dipy',
18+
'parse',
1819
]
1920

2021
setup_requirements = ['pytest-runner', ]

0 commit comments

Comments
 (0)