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

1"""Helper functions for load_scrub and sample_mask functions.""" 

2 

3import warnings 

4 

5import numpy as np 

6import pandas as pd 

7 

8from nilearn._utils.logger import find_stack_level 

9 

10 

11def optimize_scrub(motion_outliers_index, n_scans, scrub): 

12 """Remove continuous segments with fewer than a minimal segment length. 

13 

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. 

19 

20 n_scans : :obj:`int` 

21 Number of volumes in the functional image. 

22 

23 scrub : :obj:`int`, default=5 

24 Minimal segment length. 

25 

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 

62 

63 

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. 

67 

68 Parameters 

69 ---------- 

70 confounds : pandas.DataFrame 

71 DataFrame of confounds. 

72 

73 Returns 

74 ------- 

75 sample_mask : numpy.ndarray 

76 Index array of shape (n_samples) indicating the volumes 

77 that are not outliers. 

78 

79 confounds : pandas.DataFrame 

80 DataFrame of confounds without the one-hot encoders for 

81 outlier regressors. 

82 

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) 

94 

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 

104 

105 

106def _get_outlier_cols(confounds_columns): 

107 """Get outlier regressor column names. 

108 

109 Parameters 

110 ---------- 

111 confounds_columns : list 

112 List of confounds column names. 

113 

114 Returns 

115 ------- 

116 outlier_cols : list 

117 List of outlier regressor column names. 

118 

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) 

129 

130 

131def _outlier_to_sample_mask(outliers): 

132 """Generate sample mask from outlier regressors. 

133 

134 Parameters 

135 ---------- 

136 outliers : pandas.DataFrame 

137 DataFrame of outlier regressors. The shape should be 

138 (number of volumes, number of outlier regressors). 

139 

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]