Skip to content

Commit 8ff0af5

Browse files
authored
Merge pull request #1839 from chrisfilo/enh/nstdstate
Non steady state detector
2 parents 1188b50 + f95fe84 commit 8ff0af5

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Upcoming Release
22
=====================
33

4+
* ENH: Added non-steady state detector for EPI data (https://github.com/nipy/nipype/pull/1839)
45
* ENH: Enable new BBRegister init options for FSv6+ (https://github.com/nipy/nipype/pull/1811)
56
* REF: Splits nipype.interfaces.utility into base, csv, and wrappers (https://github.com/nipy/nipype/pull/1828)
67
* FIX: Makespec now runs with nipype in current directory (https://github.com/nipy/nipype/pull/1813)

nipype/algorithms/confounds.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,79 @@ def _list_outputs(self):
579579
outputs['detrended_file'] = op.abspath(self.inputs.detrended_file)
580580
return outputs
581581

582+
583+
class NonSteadyStateDetectorInputSpec(BaseInterfaceInputSpec):
584+
in_file = File(exists=True, mandatory=True, desc='4D NIFTI EPI file')
585+
586+
587+
class NonSteadyStateDetectorOutputSpec(TraitedSpec):
588+
n_volumes_to_discard = traits.Int(desc='Number of non-steady state volumes'
589+
'detected in the beginning of the scan.')
590+
591+
592+
class NonSteadyStateDetector(BaseInterface):
593+
"""
594+
Returns the number of non-steady state volumes detected at the beginning
595+
of the scan.
596+
"""
597+
598+
input_spec = NonSteadyStateDetectorInputSpec
599+
output_spec = NonSteadyStateDetectorOutputSpec
600+
601+
def _run_interface(self, runtime):
602+
in_nii = nb.load(self.inputs.in_plots)
603+
global_signal = in_nii.get_data()[:,:,:,:50].mean(axis=0).mean(axis=0).mean(axis=0)
604+
605+
self._results = {
606+
'out_file': _is_outlier(global_signal)
607+
}
608+
609+
return runtime
610+
611+
def _list_outputs(self):
612+
return self._results
613+
614+
def _is_outlier(points, thresh=3.5):
615+
"""
616+
Returns a boolean array with True if points are outliers and False
617+
otherwise.
618+
619+
Parameters:
620+
-----------
621+
points : An numobservations by numdimensions array of observations
622+
thresh : The modified z-score to use as a threshold. Observations with
623+
a modified z-score (based on the median absolute deviation) greater
624+
than this value will be classified as outliers.
625+
626+
Returns:
627+
--------
628+
mask : A numobservations-length boolean array.
629+
630+
References:
631+
----------
632+
Boris Iglewicz and David Hoaglin (1993), "Volume 16: How to Detect and
633+
Handle Outliers", The ASQC Basic References in Quality Control:
634+
Statistical Techniques, Edward F. Mykytka, Ph.D., Editor.
635+
"""
636+
if len(points.shape) == 1:
637+
points = points[:, None]
638+
median = np.median(points, axis=0)
639+
diff = np.sum((points - median) ** 2, axis=-1)
640+
diff = np.sqrt(diff)
641+
med_abs_deviation = np.median(diff)
642+
643+
modified_z_score = 0.6745 * diff / med_abs_deviation
644+
645+
timepoints_to_discard = 0
646+
for i in range(len(modified_z_score)):
647+
if modified_z_score[i] <= thresh:
648+
break
649+
else:
650+
timepoints_to_discard += 1
651+
652+
return timepoints_to_discard
653+
654+
582655
def regress_poly(degree, data, remove_mean=True, axis=-1):
583656
''' returns data with degree polynomial regressed out.
584657
Be default it is calculated along the last axis (usu. time).

nipype/algorithms/tests/test_confounds.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import pytest
88
from nipype.testing import example_data
9-
from nipype.algorithms.confounds import FramewiseDisplacement, ComputeDVARS
9+
from nipype.algorithms.confounds import FramewiseDisplacement, ComputeDVARS, \
10+
_is_outlier
1011
import numpy as np
1112

1213

@@ -63,4 +64,11 @@ def test_dvars(tmpdir):
6364

6465
assert (np.abs(dv1[:, 1] - ground_truth[:, 1]).sum() / len(dv1)) > 0.05
6566

66-
assert (np.abs(dv1[:, 2] - ground_truth[:, 2]).sum() / len(dv1)) < 0.05
67+
assert (np.abs(dv1[:, 2] - ground_truth[:, 2]).sum() / len(dv1)) < 0.05
68+
69+
def test_outliers(tmpdir):
70+
in_data = np.random.randn(100)
71+
in_data[0] += 10
72+
73+
assert _is_outlier(in_data) == 1
74+

0 commit comments

Comments
 (0)