Coverage for nilearn/interfaces/fmriprep/load_confounds.py: 12%

74 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-18 13:00 +0200

1"""Flexible method to load confounds generated by fMRIprep.""" 

2 

3import warnings 

4 

5import pandas as pd 

6 

7from nilearn._utils.logger import find_stack_level 

8from nilearn.interfaces.fmriprep import load_confounds_components as components 

9from nilearn.interfaces.fmriprep.load_confounds_utils import ( 

10 MissingConfoundError, 

11 get_confounds_file, 

12 get_json, 

13 load_confounds_file_as_dataframe, 

14 load_confounds_json, 

15 prepare_output, 

16 sanitize_confounds, 

17) 

18 

19# Global variables listing the admissible types of noise components 

20all_confounds = [ 

21 "motion", 

22 "high_pass", 

23 "wm_csf", 

24 "global_signal", 

25 "compcor", 

26 "ica_aroma", 

27 "scrub", 

28 "non_steady_state", 

29] 

30 

31# extra parameters needed for each noise component 

32component_parameters = { 

33 "motion": ["motion"], 

34 "wm_csf": ["wm_csf"], 

35 "global_signal": ["global_signal"], 

36 "compcor": ["meta_json", "compcor", "n_compcor"], 

37 "ica_aroma": ["ica_aroma"], 

38 "scrub": ["scrub", "fd_threshold", "std_dvars_threshold"], 

39} 

40 

41 

42def _check_strategy(strategy): 

43 """Ensure the denoising strategies combinations are valid. 

44 

45 Parameters 

46 ---------- 

47 strategy : :obj:`tuple` or :obj:`list` of :obj:`str`. 

48 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

49 

50 Raises 

51 ------ 

52 ValueError 

53 If any of the confounds specified in the strategy are not supported, 

54 or the combination of the strategies are not valid. 

55 """ 

56 if (not isinstance(strategy, tuple)) and (not isinstance(strategy, list)): 

57 raise ValueError( 

58 "strategy needs to be a tuple or list of strings" 

59 f" A {type(strategy)} was provided instead." 

60 ) 

61 

62 if len(strategy) == 0: 

63 warnings.warn( 

64 "strategy is empty, confounds will return None.", 

65 stacklevel=find_stack_level(), 

66 ) 

67 

68 for conf in strategy: 

69 if conf == "non_steady_state": 

70 warnings.warn( 

71 "Non-steady state volumes are always detected. It " 

72 "doesn't need to be supplied as part of the " 

73 "strategy. Supplying non_steady_state in strategy " 

74 "will not have additional effect.", 

75 stacklevel=find_stack_level(), 

76 ) 

77 if conf not in all_confounds: 

78 raise ValueError(f"{conf} is not a supported type of confounds.") 

79 

80 # high pass filtering must be present if using fmriprep compcor outputs 

81 if ("compcor" in strategy) and ("high_pass" not in strategy): 

82 raise ValueError( 

83 "When using compcor, `high_pass` must be included in " 

84 f"strategy. Current strategy: '{strategy}'" 

85 ) 

86 

87 

88def _check_error(missing): 

89 """Consolidate a single error message across multiple missing confounds.""" 

90 if missing["confounds"] or missing["keywords"]: 

91 error_msg = ( 

92 "The following keywords or parameters are missing: " 

93 f" {missing['confounds']}" 

94 f" {missing['keywords']}" 

95 ". You may want to try a different denoising strategy." 

96 ) 

97 raise ValueError(error_msg) 

98 

99 

100def load_confounds( 

101 img_files, 

102 strategy=("motion", "high_pass", "wm_csf"), 

103 motion="full", 

104 scrub=5, 

105 fd_threshold=0.2, 

106 std_dvars_threshold=3, 

107 wm_csf="basic", 

108 global_signal="basic", 

109 compcor="anat_combined", 

110 n_compcor="all", 

111 ica_aroma="full", 

112 demean=True, 

113): 

114 """ 

115 Use confounds from :term:`fMRIPrep`. 

116 

117 To enable easy confound variables loading from :term:`fMRIPrep` outputs, 

118 `load_confounds` provides an interface that groups subsets of confound 

119 variables into noise components and their parameters. It is possible to 

120 fine-tune a subset of noise components and their parameters through this 

121 function. 

122 

123 The implementation will only support :term:`fMRIPrep` functional derivative 

124 directory from the 1.2.x series. The `compcor` noise component requires 

125 1.4.x series or above. 

126 

127 .. versionadded:: 0.9.0 

128 

129 Parameters 

130 ---------- 

131 img_files : :obj:`str` or :obj:`list` of :obj:`str` 

132 Path of processed nii.gz/dtseries.nii/func.gii file reside in a 

133 :term:`fMRIPrep` generated functional derivative directory (i.e.The 

134 associated confound files should be in the same directory as the image 

135 file). As long as the image file, confound related tsv and json are in 

136 the same directory with BIDS-complied names, `load_confounds` can 

137 retrieve the relevant files correctly. 

138 

139 - `nii.gz` or `dtseries.nii`: path to files, optionally as a list. 

140 - `func.gii`: list of a pair of paths to files, optionally as a list 

141 of lists. 

142 

143 strategy : :obj:`tuple` or :obj:`list` of :obj:`str`, \ 

144 default=("motion", "high_pass", "wm_csf") 

145 The type of noise components to include. 

146 

147 - "motion": head motion estimates. Associated parameter: `motion` 

148 - "wm_csf" confounds derived from white matter and cerebrospinal fluid. 

149 Associated parameter: `wm_csf` 

150 - "global_signal" confounds derived from the global signal. 

151 Associated parameter: `global_signal` 

152 - "compcor" confounds derived from CompCor (:footcite:t:`Behzadi2007`). 

153 When using this noise component, "high_pass" must also be applied. 

154 Associated parameter: `compcor`, `n_compcor` 

155 - "ica_aroma" confounds derived 

156 from ICA-AROMA (:footcite:t:`Pruim2015`). 

157 Associated parameter: `ica_aroma` 

158 - "scrub" regressors for :footcite:t:`Power2014` scrubbing approach. 

159 Associated parameter: `scrub`, `fd_threshold`, `std_dvars_threshold` 

160 

161 For each component above, associated parameters will be applied if 

162 specified. If associated parameters are not specified, any values 

163 supplied to the parameters are ignored. 

164 For example, `strategy=('motion', 'global_signal')` will allow users 

165 to supply input to associated parameter `motion` and `global_signal`; 

166 if users pass `wm_csf` parameter, it will not be applied as it is not 

167 part of the `strategy`. 

168 

169 There are two additional noise components with no optional parameters. 

170 

171 - "non_steady_state" denotes volumes collected before 

172 the :term:`fMRI` scanner has reached a stable state. 

173 - "high_pass" adds discrete cosines transformation 

174 basis regressors to handle low-frequency signal drifts. 

175 

176 Non-steady-state volumes will always be checked. There's no need to 

177 supply this component to the strategy. 

178 

179 motion : :obj:`str`, default="full" 

180 Type of confounds extracted from head motion estimates. 

181 

182 - "basic" translation/rotation (6 parameters) 

183 - "power2" translation/rotation + quadratic terms (12 parameters) 

184 - "derivatives" translation/rotation + derivatives (12 parameters) 

185 - "full" translation/rotation + derivatives + quadratic terms + power2d 

186 derivatives (24 parameters) 

187 

188 wm_csf : :obj:`str`, default="basic" 

189 Type of confounds extracted from masks of white matter and 

190 cerebrospinal fluids. 

191 

192 - "basic" the averages in each mask (2 parameters) 

193 - "power2" averages and quadratic terms (4 parameters) 

194 - "derivatives" averages and derivatives (4 parameters) 

195 - "full" averages + derivatives + quadratic terms + power2d derivatives 

196 (8 parameters) 

197 

198 global_signal : :obj:`str`, default="basic" 

199 Type of confounds extracted from the global signal. 

200 

201 - "basic" just the global signal (1 parameter) 

202 - "power2" global signal and quadratic term (2 parameters) 

203 - "derivatives" global signal and derivative (2 parameters) 

204 - "full" global signal + derivatives + quadratic terms + power2d 

205 derivatives (4 parameters) 

206 

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

208 After accounting for time frames with excessive motion, further remove 

209 segments shorter than the given number. The default value is referred 

210 as full scrubbing in :footcite:t:`Power2014`. When the value is 0, 

211 remove time frames based on excessive framewise displacement and 

212 DVARS only. 

213 

214 fd_threshold : :obj:`float`, default=0.2 

215 

216 .. deprecated:: 0.10.3 

217 The default value will be changed to 0.5 in 0.13.0 

218 

219 Framewise displacement threshold for scrub in mm. 

220 

221 std_dvars_threshold : :obj:`float`, default=3 

222 

223 .. deprecated:: 0.10.3 

224 The default value will be changed to 1.5 in 0.13.0 

225 

226 Standardized DVARS threshold for scrub. 

227 The default threshold matching :term:`fMRIPrep`. 

228 DVARs is defined as root mean squared intensity difference of volume N 

229 to volume N+1 :footcite:t:`Power2012`. 

230 D referring to temporal derivative of timecourses, 

231 VARS referring to root mean squared variance over voxels. 

232 

233 compcor : :obj:`str`, default="anat_combined" 

234 

235 .. warning:: 

236 Require :term:`fMRIPrep` >= v:1.4.0. 

237 

238 Type of confounds extracted from a component based noise correction 

239 method :footcite:t:`Behzadi2007`. 

240 

241 - "anat_combined" noise components calculated using a white matter and 

242 CSF combined anatomical mask 

243 - "anat_separated" noise components calculated using white matter mask 

244 and CSF mask compcor separately; two sets of scores are concatenated 

245 - "temporal" noise components calculated using temporal compcor 

246 - "temporal_anat_combined" components of "temporal" and "anat_combined" 

247 - "temporal_anat_separated" components of "temporal" and 

248 "anat_separated" 

249 

250 n_compcor : :obj:`str` or :obj:`int`, default="all" 

251 The number of noise components to be extracted. 

252 For acompcor_combined=False, and/or compcor="full", this is the number 

253 of components per mask. 

254 "all": select all components (50% variance explained by 

255 :term:`fMRIPrep` defaults) 

256 

257 ica_aroma : :obj:`str`, default="full" 

258 

259 - "full": use :term:`fMRIPrep` output 

260 `~desc-smoothAROMAnonaggr_bold.nii.gz`. 

261 - "basic": use noise independent components only. 

262 

263 demean : :obj:`bool`, default=True 

264 If True, the confounds are standardized to a zero mean (over time). 

265 When using :class:`nilearn.maskers.NiftiMasker` with default 

266 parameters, the recommended option is True. 

267 When using :func:`nilearn.signal.clean` with default parameters, the 

268 recommended option is False. 

269 When `sample_mask` is not None, the mean is calculated on retained 

270 volumes. 

271 

272 Returns 

273 ------- 

274 confounds : :class:`pandas.DataFrame`, or :obj:`list` of \ 

275 :class:`pandas.DataFrame` 

276 A reduced version of :term:`fMRIPrep` confounds based on selected 

277 strategy and flags. 

278 The columns contains the labels of the regressors. 

279 

280 sample_mask : None, :class:`numpy.ndarray` or, :obj:`list` of \ 

281 :class:`numpy.ndarray` or None 

282 When no volumns require removal, the value is None. 

283 Otherwise, shape: (number of scans - number of volumes removed, ) 

284 The index of the niimgs along time/fourth dimension for valid volumes 

285 for subsequent analysis. 

286 This attribute should be passed to parameter `sample_mask` of 

287 :class:`nilearn.maskers.NiftiMasker` or 

288 :func:`nilearn.signal.clean`. 

289 Volumns are removed if flagged as following: 

290 

291 - Non-steady-state volumes (if present) 

292 - Motion outliers detected by scrubbing 

293 

294 Notes 

295 ----- 

296 The noise components implemented in this class are adapted from 

297 :footcite:t:`Ciric2017`. Band-pass filter is replaced by high-pass filter. 

298 Low-pass filters can be implemented, e.g., through `NifitMaskers`. 

299 Other aspects of the preprocessing listed 

300 in :footcite:t:`Ciric2017` are controlled 

301 through :term:`fMRIPrep`, e.g. distortion correction. 

302 

303 See Also 

304 -------- 

305 :func:`nilearn.interfaces.fmriprep.load_confounds_strategy` 

306 

307 References 

308 ---------- 

309 .. footbibliography:: 

310 

311 """ 

312 _check_strategy(strategy) 

313 if "scrub" in strategy and fd_threshold == 0.2: 

314 fd_threshold_default = ( 

315 "The default parameter for fd_threshold is currently 0.2 " 

316 "which is inconsistent with the fMRIPrep default of 0.5. " 

317 "In release 0.13.0, " 

318 "the default strategy will be replaced by 0.5." 

319 ) 

320 warnings.warn( 

321 category=DeprecationWarning, 

322 message=fd_threshold_default, 

323 stacklevel=find_stack_level(), 

324 ) 

325 if "scrub" in strategy and std_dvars_threshold == 3: 

326 std_dvars_threshold_default = ( 

327 "The default parameter for std_dvars_threshold is currently 3 " 

328 "which is inconsistent with the fMRIPrep default of 1.5. " 

329 "In release 0.13.0, " 

330 "the default strategy will be replaced by 1.5." 

331 ) 

332 warnings.warn( 

333 category=DeprecationWarning, 

334 message=std_dvars_threshold_default, 

335 stacklevel=find_stack_level(), 

336 ) 

337 # load confounds per image provided 

338 img_files, flag_single = sanitize_confounds(img_files) 

339 confounds_out = [] 

340 sample_mask_out = [] 

341 for file in img_files: 

342 sample_mask, conf = _load_confounds_for_single_image_file( 

343 file, 

344 strategy, 

345 demean, 

346 motion=motion, 

347 scrub=scrub, 

348 fd_threshold=fd_threshold, 

349 std_dvars_threshold=std_dvars_threshold, 

350 wm_csf=wm_csf, 

351 global_signal=global_signal, 

352 compcor=compcor, 

353 n_compcor=n_compcor, 

354 ica_aroma=ica_aroma, 

355 ) 

356 confounds_out.append(conf) 

357 sample_mask_out.append(sample_mask) 

358 

359 # If a single input was provided, 

360 # send back a single output instead of a list 

361 if flag_single: 

362 confounds_out = confounds_out[0] 

363 sample_mask_out = sample_mask_out[0] 

364 

365 # If no strategy was provided, return None for confounds 

366 if len(strategy) == 0: 

367 confounds_out = None 

368 

369 return confounds_out, sample_mask_out 

370 

371 

372def _load_confounds_for_single_image_file( 

373 image_file, strategy, demean, **kwargs 

374): 

375 """Load confounds for a single image file. 

376 

377 Parameters 

378 ---------- 

379 image_file : :obj:`str` 

380 Path to processed image file. 

381 

382 strategy : :obj:`tuple` or :obj:`list` of :obj:`str`. 

383 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

384 

385 demean : :obj:`bool`, default=True 

386 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

387 

388 kwargs : :obj:`dict` 

389 Extra relevant parameters for the given `strategy`. 

390 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

391 

392 Returns 

393 ------- 

394 sample_mask : None, :class:`numpy.ndarray` or, :obj:`list` of \ 

395 :class:`numpy.ndarray` or None 

396 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

397 

398 confounds : :class:`pandas.DataFrame`, or :obj:`list` of \ 

399 :class:`pandas.DataFrame` 

400 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

401 """ 

402 # Check for ica_aroma in strategy, this will change the required image_file 

403 flag_full_aroma = ("ica_aroma" in strategy) and ( 

404 kwargs.get("ica_aroma") == "full" 

405 ) 

406 

407 confounds_file = get_confounds_file( 

408 image_file, flag_full_aroma=flag_full_aroma 

409 ) 

410 confounds_json_file = get_json(confounds_file) 

411 

412 return _load_single_confounds_file( 

413 confounds_file=confounds_file, 

414 strategy=strategy, 

415 demean=demean, 

416 confounds_json_file=confounds_json_file, 

417 **kwargs, 

418 ) 

419 

420 

421def _load_single_confounds_file( 

422 confounds_file, strategy, demean=True, confounds_json_file=None, **kwargs 

423): 

424 """Load and extract specified confounds from the confounds file. 

425 

426 Parameters 

427 ---------- 

428 confounds_file : :obj:`str` 

429 Path to confounds file. 

430 

431 strategy : :obj:`tuple` or :obj:`list` of :obj:`str`. 

432 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

433 

434 demean : :obj:`bool`, default=True 

435 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

436 

437 confounds_json_file : :obj:`str`, default=None 

438 Path to confounds json file. 

439 

440 kwargs : :obj:`dict` 

441 Extra relevant parameters for the given `strategy`. 

442 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

443 

444 Returns 

445 ------- 

446 confounds : :class:`pandas.DataFrame` 

447 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

448 

449 Raises 

450 ------ 

451 ValueError 

452 If any of the confounds specified in the strategy are not found in the 

453 confounds file or confounds json file. 

454 """ 

455 flag_acompcor = ("compcor" in strategy) and ( 

456 "anat" in kwargs.get("compcor") 

457 ) 

458 # Convert tsv file to pandas dataframe 

459 confounds_all = load_confounds_file_as_dataframe(confounds_file) 

460 

461 if confounds_json_file is None: 

462 confounds_json_file = get_json(confounds_file) 

463 

464 # Read the associated json file 

465 meta_json = load_confounds_json( 

466 confounds_json_file, flag_acompcor=flag_acompcor 

467 ) 

468 

469 missing = {"confounds": [], "keywords": []} 

470 # always check non steady state volumes are loaded 

471 confounds_select, missing = _load_noise_component( 

472 confounds_all, 

473 "non_steady_state", 

474 missing, 

475 meta_json=meta_json, 

476 **kwargs, 

477 ) 

478 for component in strategy: 

479 loaded_confounds, missing = _load_noise_component( 

480 confounds_all, component, missing, meta_json=meta_json, **kwargs 

481 ) 

482 confounds_select = pd.concat( 

483 [confounds_select, loaded_confounds], axis=1 

484 ) 

485 

486 _check_error(missing) # raise any missing 

487 return prepare_output(confounds_select, demean) 

488 

489 

490def _load_noise_component(confounds_raw, component, missing, **kargs): 

491 """Load confound of a single noise component. 

492 

493 Parameters 

494 ---------- 

495 confounds_raw : :class:`pandas.DataFrame` 

496 The confounds loaded from the confounds file. 

497 

498 component : :obj:`str` 

499 The noise component to be loaded. The item from the strategy list. 

500 

501 missing : :obj:`dict` 

502 A dictionary of missing confounds and noise component keywords. 

503 

504 kargs : :obj:`dict` 

505 Extra relevant parameters for the given `component`. 

506 See :func:`nilearn.interfaces.fmriprep.load_confounds` for details. 

507 

508 Returns 

509 ------- 

510 loaded_confounds : :class:`pandas.DataFrame` 

511 The confounds loaded from the confounds file for the given component. 

512 

513 missing : :obj:`dict` 

514 A dictionary of missing confounds and noise component keywords. 

515 

516 Raises 

517 ------ 

518 MissingConfoundError 

519 If any of the confounds specified in the strategy are not found in the 

520 confounds file or confounds json file. 

521 """ 

522 try: 

523 need_params = component_parameters.get(component) 

524 if need_params: 

525 params = {param: kargs.get(param) for param in need_params} 

526 loaded_confounds = getattr(components, f"_load_{component}")( 

527 confounds_raw, **params 

528 ) 

529 else: 

530 loaded_confounds = getattr(components, f"_load_{component}")( 

531 confounds_raw 

532 ) 

533 except MissingConfoundError as exception: 

534 missing["confounds"] += exception.params 

535 missing["keywords"] += exception.keywords 

536 loaded_confounds = pd.DataFrame() 

537 return loaded_confounds, missing