Coverage for nilearn/interfaces/fmriprep/load_confounds_scrub.py: 17%
36 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1"""Helper functions for load_scrub and sample_mask functions."""
3import warnings
5import numpy as np
6import pandas as pd
8from nilearn._utils.logger import find_stack_level
11def optimize_scrub(motion_outliers_index, n_scans, scrub):
12 """Remove continuous segments with fewer than a minimal segment length.
14 Parameters
15 ----------
16 motion_outliers_index : numpy.ndarray
17 Index array of shape (n_motion_outliers) indicating the volumes
18 that are motion outliers.
20 n_scans : :obj:`int`
21 Number of volumes in the functional image.
23 scrub : :obj:`int`, default=5
24 Minimal segment length.
26 Returns
27 -------
28 motion_outliers_index : numpy.ndarray
29 Index of outlier volumes.
30 """
31 # Start by checking if the beginning continuous segment is fewer than
32 # a minimal segment length (default to 5)
33 if motion_outliers_index[0] < scrub:
34 motion_outliers_index = np.asarray(
35 list(range(motion_outliers_index[0])) + list(motion_outliers_index)
36 )
37 # Do the same for the ending segment of scans
38 if n_scans - (motion_outliers_index[-1] + 1) < scrub:
39 motion_outliers_index = np.asarray(
40 list(motion_outliers_index)
41 + list(range(motion_outliers_index[-1], n_scans))
42 )
43 # Now do everything in between
44 fd_outlier_ind_diffs = np.diff(motion_outliers_index)
45 short_segments_inds = np.where(
46 np.logical_and(
47 fd_outlier_ind_diffs > 1, fd_outlier_ind_diffs < (scrub + 1)
48 )
49 )[0]
50 for ind in short_segments_inds:
51 motion_outliers_index = np.asarray(
52 list(motion_outliers_index)
53 + list(
54 range(
55 motion_outliers_index[ind] + 1,
56 motion_outliers_index[ind + 1],
57 )
58 )
59 )
60 motion_outliers_index = np.sort(np.unique(motion_outliers_index))
61 return motion_outliers_index
64def extract_outlier_regressors(confounds):
65 """Separate outlier one-hot regressors from other confounds \
66 variables and generate a sample mask, indicates the volumes kept.
68 Parameters
69 ----------
70 confounds : pandas.DataFrame
71 DataFrame of confounds.
73 Returns
74 -------
75 sample_mask : numpy.ndarray
76 Index array of shape (n_samples) indicating the volumes
77 that are not outliers.
79 confounds : pandas.DataFrame
80 DataFrame of confounds without the one-hot encoders for
81 outlier regressors.
83 outliers : pandas.DataFrame
84 DataFrame of outlier regressors.
85 """
86 outlier_cols, confounds_cols = _get_outlier_cols(confounds.columns)
87 if outlier_cols:
88 outliers = confounds.loc[:, outlier_cols]
89 outliers = outliers.T.drop_duplicates().T
90 else:
91 outliers = pd.DataFrame()
92 confounds = confounds.loc[:, confounds_cols]
93 sample_mask = _outlier_to_sample_mask(outliers)
95 if sample_mask is not None and sample_mask.size == 0:
96 warnings.warn(
97 category=RuntimeWarning,
98 message="All volumes were marked as motion outliers. "
99 "This would lead to all volumes in the time "
100 "series to be scrubbed.",
101 stacklevel=find_stack_level(),
102 )
103 return sample_mask, confounds, outliers
106def _get_outlier_cols(confounds_columns):
107 """Get outlier regressor column names.
109 Parameters
110 ----------
111 confounds_columns : list
112 List of confounds column names.
114 Returns
115 -------
116 outlier_cols : list
117 List of outlier regressor column names.
119 confounds_cols : list
120 List of confounds column names without outlier regressors.
121 """
122 outlier_cols = {
123 col
124 for col in confounds_columns
125 if "motion_outlier" in col or "non_steady_state" in col
126 }
127 confounds_cols = set(confounds_columns) - outlier_cols
128 return sorted(outlier_cols), sorted(confounds_cols)
131def _outlier_to_sample_mask(outliers):
132 """Generate sample mask from outlier regressors.
134 Parameters
135 ----------
136 outliers : pandas.DataFrame
137 DataFrame of outlier regressors. The shape should be
138 (number of volumes, number of outlier regressors).
140 Returns
141 -------
142 sample_mask : numpy.ndarray
143 Index array of shape indicating the volumes that are not
144 outliers. (number of volumes - number of outlier regressors, ).
145 """
146 outliers_one_hot = outliers.copy()
147 if outliers_one_hot.size == 0: # Do not supply sample mask
148 return None # consistency with nilearn sample_mask
149 outliers_one_hot = outliers_one_hot.sum(axis=1).to_numpy()
150 return np.where(outliers_one_hot == 0)[0]