Coverage for nilearn/maskers/tests/test_nifti_labels_masker.py: 0%

346 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1"""Test the nifti_region module. 

2 

3Functions in this file only test features added by the NiftiLabelsMasker class, 

4not the underlying functions (clean(), img_to_signals_labels(), etc.). See 

5test_masking.py and test_signal.py for details. 

6""" 

7 

8import numpy as np 

9import pandas as pd 

10import pytest 

11from nibabel import Nifti1Image 

12from numpy.testing import assert_almost_equal, assert_array_equal 

13from sklearn.utils.estimator_checks import parametrize_with_checks 

14 

15from nilearn._utils.data_gen import ( 

16 generate_labeled_regions, 

17 generate_random_img, 

18) 

19from nilearn._utils.estimator_checks import ( 

20 check_estimator, 

21 nilearn_check_estimator, 

22 return_expected_failed_checks, 

23) 

24from nilearn._utils.tags import SKLEARN_LT_1_6 

25from nilearn.conftest import _img_labels 

26from nilearn.image import get_data 

27from nilearn.maskers import NiftiLabelsMasker, NiftiMasker 

28 

29ESTIMATORS_TO_CHECK = [NiftiLabelsMasker()] 

30 

31if SKLEARN_LT_1_6: 

32 

33 @pytest.mark.parametrize( 

34 "estimator, check, name", 

35 check_estimator(estimators=ESTIMATORS_TO_CHECK), 

36 ) 

37 def test_check_estimator_sklearn_valid(estimator, check, name): # noqa: ARG001 

38 """Check compliance with sklearn estimators.""" 

39 check(estimator) 

40 

41 @pytest.mark.xfail(reason="invalid checks should fail") 

42 @pytest.mark.parametrize( 

43 "estimator, check, name", 

44 check_estimator(estimators=ESTIMATORS_TO_CHECK, valid=False), 

45 ) 

46 def test_check_estimator_sklearn_invalid(estimator, check, name): # noqa: ARG001 

47 """Check compliance with sklearn estimators.""" 

48 check(estimator) 

49 

50else: 

51 

52 @parametrize_with_checks( 

53 estimators=ESTIMATORS_TO_CHECK, 

54 expected_failed_checks=return_expected_failed_checks, 

55 ) 

56 def test_check_estimator_sklearn(estimator, check): 

57 """Check compliance with sklearn estimators.""" 

58 check(estimator) 

59 

60 

61@pytest.mark.timeout(0) 

62@pytest.mark.parametrize( 

63 "estimator, check, name", 

64 nilearn_check_estimator( 

65 estimators=[NiftiLabelsMasker(labels_img=_img_labels())] 

66 ), 

67) 

68def test_check_estimator_nilearn(estimator, check, name): # noqa: ARG001 

69 """Check compliance with sklearn estimators.""" 

70 check(estimator) 

71 

72 

73def test_nifti_labels_masker( 

74 affine_eye, shape_3d_default, n_regions, length, img_labels 

75): 

76 """Check working of shape/affine checks.""" 

77 shape1 = (*shape_3d_default, length) 

78 

79 fmri_img, mask11_img = generate_random_img( 

80 shape1, 

81 affine=affine_eye, 

82 ) 

83 

84 # No exception raised here 

85 masker = NiftiLabelsMasker(img_labels, resampling_target=None) 

86 signals = masker.fit_transform(fmri_img) 

87 

88 assert signals.shape == (length, n_regions) 

89 

90 # No exception should be raised either 

91 masker = NiftiLabelsMasker(img_labels, resampling_target=None) 

92 

93 masker.fit() 

94 

95 # Check attributes defined at fit 

96 assert masker.n_elements_ == n_regions 

97 

98 # now with mask_img 

99 masker = NiftiLabelsMasker( 

100 img_labels, mask_img=mask11_img, resampling_target=None 

101 ) 

102 signals = masker.fit_transform(fmri_img) 

103 

104 assert signals.shape == (length, n_regions) 

105 

106 

107def test_nifti_labels_masker_errors( 

108 affine_eye, shape_3d_default, n_regions, length 

109): 

110 """Check working of shape/affine checks.""" 

111 masker = NiftiLabelsMasker() 

112 with pytest.raises(TypeError, match="input should be a NiftiLike object"): 

113 masker.fit() 

114 

115 shape1 = (*shape_3d_default, length) 

116 

117 shape2 = (12, 10, 14, length) 

118 affine2 = np.diag((1, 2, 3, 1)) 

119 

120 fmri12_img, mask12_img = generate_random_img( 

121 shape1, 

122 affine=affine2, 

123 ) 

124 fmri21_img, mask21_img = generate_random_img( 

125 shape2, 

126 affine=affine_eye, 

127 ) 

128 

129 labels11_img = generate_labeled_regions( 

130 shape1[:3], 

131 affine=affine_eye, 

132 n_regions=n_regions, 

133 ) 

134 

135 # check exception when transform() called without prior fit() 

136 masker11 = NiftiLabelsMasker(labels11_img, resampling_target=None) 

137 

138 # Test all kinds of mismatch between shapes and between affines 

139 masker11.fit() 

140 with pytest.raises( 

141 ValueError, match="Images have different affine matrices." 

142 ): 

143 masker11.transform(fmri12_img) 

144 with pytest.raises(ValueError, match="Images have incompatible shapes."): 

145 masker11.transform(fmri21_img) 

146 

147 masker11 = NiftiLabelsMasker( 

148 labels11_img, mask_img=mask12_img, resampling_target=None 

149 ) 

150 with pytest.raises( 

151 ValueError, match="Following field of view errors were detected" 

152 ): 

153 masker11.fit() 

154 

155 masker11 = NiftiLabelsMasker( 

156 labels11_img, mask_img=mask21_img, resampling_target=None 

157 ) 

158 with pytest.raises( 

159 ValueError, match="Following field of view errors were detected" 

160 ): 

161 masker11.fit() 

162 

163 

164def test_nifti_labels_masker_with_nans_and_infs( 

165 affine_eye, shape_3d_default, n_regions, length, img_labels 

166): 

167 """Deal with NaNs and infs in label image. 

168 

169 The masker should replace those NaNs and infs with zeros, 

170 while raising a warning. 

171 """ 

172 fmri_img, mask_img = generate_random_img( 

173 (*shape_3d_default, length), 

174 affine=affine_eye, 

175 ) 

176 

177 # Introduce nans with data type float 

178 # See issue: https://github.com/nilearn/nilearn/issues/2580 

179 data = get_data(img_labels).astype(np.float32) 

180 data[:, :, 7] = np.nan 

181 data[:, :, 4] = np.inf 

182 img_labels = Nifti1Image(data, affine_eye) 

183 

184 masker = NiftiLabelsMasker(img_labels, mask_img=mask_img) 

185 

186 with pytest.warns(UserWarning, match="Non-finite values detected."): 

187 sig = masker.fit_transform(fmri_img) 

188 

189 assert sig.shape == (length, n_regions) 

190 assert np.all(np.isfinite(sig)) 

191 

192 

193def test_nifti_labels_masker_with_nans_and_infs_in_data( 

194 affine_eye, shape_3d_default, n_regions, length, img_labels 

195): 

196 """Apply a NiftiLabelsMasker to 4D data containing NaNs and infs. 

197 

198 The masker should replace those NaNs and infs with zeros, 

199 while raising a warning. 

200 """ 

201 fmri_img, mask_img = generate_random_img( 

202 (*shape_3d_default, length), 

203 affine=affine_eye, 

204 ) 

205 

206 # Introduce nans with data type float 

207 # See issues: 

208 # - https://github.com/nilearn/nilearn/issues/2580 (why floats) 

209 # - https://github.com/nilearn/nilearn/issues/2711 (why test) 

210 fmri_data = get_data(fmri_img).astype(np.float32) 

211 fmri_data[:, :, 7, :] = np.nan 

212 fmri_data[:, :, 4, 0] = np.inf 

213 fmri_img = Nifti1Image(fmri_data, affine_eye) 

214 

215 masker = NiftiLabelsMasker(img_labels, mask_img=mask_img) 

216 

217 with pytest.warns(UserWarning, match="Non-finite values detected."): 

218 sig = masker.fit_transform(fmri_img) 

219 

220 assert sig.shape == (length, n_regions) 

221 assert np.all(np.isfinite(sig)) 

222 

223 

224@pytest.mark.parametrize( 

225 "strategy, function", 

226 [ 

227 ("mean", np.mean), 

228 ("median", np.median), 

229 ("sum", np.sum), 

230 ("minimum", np.min), 

231 ("maximum", np.max), 

232 ("standard_deviation", np.std), 

233 ("variance", np.var), 

234 ], 

235) 

236def test_nifti_labels_masker_reduction_strategies( 

237 affine_eye, strategy, function 

238): 

239 """Tests NiftiLabelsMasker strategies. 

240 

241 1. whether the usage of different reduction strategies work. 

242 2. whether unrecognized strategies raise a ValueError 

243 3. whether the default option is backwards compatible (calls "mean") 

244 """ 

245 test_values = [-2.0, -1.0, 0.0, 1.0, 2] 

246 

247 img_data = np.array([[test_values, test_values]]) 

248 

249 labels_data = np.array([[[0, 0, 0, 0, 0], [1, 1, 1, 1, 1]]], dtype=np.int8) 

250 

251 img = Nifti1Image(img_data, affine_eye) 

252 labels = Nifti1Image(labels_data, affine_eye) 

253 

254 # What NiftiLabelsMasker should return for each reduction strategy? 

255 expected_result = function(test_values) 

256 

257 masker = NiftiLabelsMasker(labels, strategy=strategy) 

258 # Here passing [img] within a list because it's a 3D object. 

259 result = masker.fit_transform([img]).squeeze() 

260 

261 assert result == expected_result 

262 

263 default_masker = NiftiLabelsMasker(labels) 

264 

265 assert default_masker.strategy == "mean" 

266 

267 

268def test_nifti_labels_masker_reduction_strategies_error(affine_eye): 

269 """Tests NiftiLabelsMasker invalid strategy.""" 

270 labels_data = np.array([[[0, 0, 0, 0, 0], [1, 1, 1, 1, 1]]], dtype=np.int8) 

271 

272 labels = Nifti1Image(labels_data, affine_eye) 

273 

274 with pytest.raises(ValueError, match="Invalid strategy 'TESTRAISE'"): 

275 masker = NiftiLabelsMasker(labels, strategy="TESTRAISE") 

276 masker.fit() 

277 

278 

279def test_nifti_labels_masker_resampling_errors(img_labels): 

280 """Test errors of resampling in NiftiLabelsMasker.""" 

281 with pytest.raises( 

282 ValueError, 

283 match="invalid value for 'resampling_target' parameter: mask", 

284 ): 

285 masker = NiftiLabelsMasker(img_labels, resampling_target="mask") 

286 masker.fit() 

287 

288 with pytest.raises( 

289 ValueError, 

290 match="invalid value for 'resampling_target' parameter: invalid", 

291 ): 

292 masker = NiftiLabelsMasker( 

293 img_labels, 

294 resampling_target="invalid", 

295 ) 

296 masker.fit() 

297 

298 

299def test_nifti_labels_masker_resampling_to_data(affine_eye, n_regions, length): 

300 """Test resampling to data in NiftiLabelsMasker.""" 

301 # mask 

302 shape2 = (8, 9, 10, length) 

303 # maps 

304 shape3 = (16, 18, 20) 

305 

306 _, mask_img = generate_random_img( 

307 shape2, 

308 affine=affine_eye, 

309 ) 

310 

311 labels_img = generate_labeled_regions(shape3, n_regions, affine=affine_eye) 

312 

313 # Test with data and atlas of different shape: 

314 # the atlas should be resampled to the data 

315 shape22 = (5, 5, 6, length) 

316 affine2 = 2 * affine_eye 

317 affine2[-1, -1] = 1 

318 

319 fmri_img, _ = generate_random_img( 

320 shape22, 

321 affine=affine2, 

322 ) 

323 

324 masker = NiftiLabelsMasker( 

325 labels_img, mask_img=mask_img, resampling_target="data" 

326 ) 

327 masker.fit_transform(fmri_img) 

328 

329 assert_array_equal(masker.labels_img_.affine, affine2) 

330 

331 

332@pytest.mark.parametrize("resampling_target", ["data", "labels"]) 

333def test_nifti_labels_masker_resampling( 

334 affine_eye, 

335 shape_3d_default, 

336 resampling_target, 

337 length, 

338 img_labels, 

339): 

340 """Test to return resampled labels having number of labels \ 

341 equal with transformed shape of 2nd dimension. 

342 

343 See https://github.com/nilearn/nilearn/issues/1673 

344 """ 

345 shape = (*shape_3d_default, length) 

346 affine = 2 * affine_eye 

347 

348 fmri_img, _ = generate_random_img(shape, affine=affine) 

349 

350 masker = NiftiLabelsMasker( 

351 labels_img=img_labels, resampling_target=resampling_target 

352 ) 

353 if resampling_target == "data": 

354 with pytest.warns( 

355 UserWarning, 

356 match="After resampling the label image " 

357 "to the data image, the following " 

358 "labels were removed", 

359 ): 

360 signals = masker.fit_transform(fmri_img) 

361 else: 

362 signals = masker.fit_transform(fmri_img) 

363 

364 resampled_labels_img = masker.labels_img_ 

365 n_resampled_labels = len(np.unique(get_data(resampled_labels_img))) 

366 

367 assert n_resampled_labels - 1 == signals.shape[1] 

368 

369 # inverse transform 

370 compressed_img = masker.inverse_transform(signals) 

371 

372 # Test that compressing the image a second time should yield an image 

373 # with the same data as compressed_img. 

374 signals2 = masker.fit_transform(fmri_img) 

375 # inverse transform again 

376 compressed_img2 = masker.inverse_transform(signals2) 

377 

378 assert_array_equal(get_data(compressed_img), get_data(compressed_img2)) 

379 

380 

381def test_nifti_labels_masker_resampling_to_labels( 

382 affine_eye, shape_3d_default, n_regions, length 

383): 

384 """Test resampling to labels in NiftiLabelsMasker.""" 

385 # fmri 

386 shape1 = (*shape_3d_default, length) 

387 # mask 

388 shape2 = (16, 17, 18, length) 

389 # labels 

390 shape3 = (13, 14, 15) 

391 

392 # With data of the same affine 

393 fmri_img, _ = generate_random_img( 

394 shape1, 

395 affine=affine_eye, 

396 ) 

397 _, mask_img = generate_random_img( 

398 shape2, 

399 affine=affine_eye, 

400 ) 

401 

402 labels_img = generate_labeled_regions( 

403 shape3, 

404 n_regions, 

405 affine=affine_eye, 

406 ) 

407 

408 masker = NiftiLabelsMasker( 

409 labels_img, mask_img=mask_img, resampling_target="labels" 

410 ) 

411 

412 signals = masker.fit_transform(fmri_img) 

413 

414 assert_almost_equal(masker.labels_img_.affine, labels_img.affine) 

415 assert masker.labels_img_.shape == labels_img.shape 

416 assert_almost_equal(masker.mask_img_.affine, masker.labels_img_.affine) 

417 assert masker.mask_img_.shape == masker.labels_img_.shape[:3] 

418 

419 assert signals.shape == (length, n_regions) 

420 

421 fmri11_img_r = masker.inverse_transform(signals) 

422 

423 assert_almost_equal(fmri11_img_r.affine, masker.labels_img_.affine) 

424 assert fmri11_img_r.shape == (masker.labels_img_.shape[:3] + (length,)) 

425 

426 

427def test_nifti_labels_masker_resampling_to_clipped_labels( 

428 affine_eye, shape_3d_default, n_regions, length 

429): 

430 """Test with clipped labels. 

431 

432 Mask does not contain all labels. 

433 

434 Shapes do matter in that case, 

435 because there is some resampling taking place. 

436 """ 

437 # fmri 

438 shape1 = (*shape_3d_default, length) 

439 # mask 

440 shape2 = (8, 9, 10, length) 

441 # maps 

442 shape3 = (16, 18, 20) 

443 

444 fmri11_img, _ = generate_random_img( 

445 shape1, 

446 affine=affine_eye, 

447 ) 

448 _, mask22_img = generate_random_img( 

449 shape2, 

450 affine=affine_eye, 

451 ) 

452 

453 labels33_img = generate_labeled_regions( 

454 shape3, n_regions, affine=affine_eye 

455 ) 

456 

457 masker = NiftiLabelsMasker( 

458 labels33_img, mask_img=mask22_img, resampling_target="labels" 

459 ) 

460 

461 signals = masker.fit_transform(fmri11_img) 

462 

463 assert_almost_equal(masker.labels_img_.affine, labels33_img.affine) 

464 

465 assert masker.labels_img_.shape == labels33_img.shape 

466 assert_almost_equal(masker.mask_img_.affine, masker.labels_img_.affine) 

467 assert masker.mask_img_.shape == masker.labels_img_.shape[:3] 

468 

469 uniq_labels = np.unique(get_data(masker.labels_img_)) 

470 assert uniq_labels[0] == 0 

471 assert len(uniq_labels) - 1 == n_regions 

472 

473 assert signals.shape == (length, n_regions) 

474 # Some regions have been clipped. Resulting signal must be zero 

475 assert (signals.var(axis=0) == 0).sum() < n_regions 

476 

477 fmri11_img_r = masker.inverse_transform(signals) 

478 

479 assert_almost_equal(fmri11_img_r.affine, masker.labels_img_.affine) 

480 assert fmri11_img_r.shape == (masker.labels_img_.shape[:3] + (length,)) 

481 

482 

483def test_nifti_labels_masker_resampling_to_none( 

484 affine_eye, length, shape_3d_default, img_labels 

485): 

486 """Test resampling to None in NiftiLabelsMasker. 

487 

488 All inputs must have same affine to avoid errors. 

489 """ 

490 fmri_img, mask_img = generate_random_img( 

491 shape=(*shape_3d_default, length), 

492 affine=affine_eye, 

493 ) 

494 

495 masker = NiftiLabelsMasker( 

496 img_labels, mask_img=mask_img, resampling_target=None 

497 ) 

498 masker.fit_transform(fmri_img) 

499 

500 fmri_img, _ = generate_random_img( 

501 (*shape_3d_default, length), 

502 affine=affine_eye * 2, 

503 ) 

504 masker = NiftiLabelsMasker( 

505 img_labels, mask_img=mask_img, resampling_target=None 

506 ) 

507 with pytest.raises( 

508 ValueError, match="Following field of view errors were detected" 

509 ): 

510 masker.fit_transform(fmri_img) 

511 

512 

513def test_standardization(rng, affine_eye, shape_3d_default, img_labels): 

514 """Check output properly standardized with 'standardize' parameter.""" 

515 n_samples = 400 

516 

517 signals = rng.standard_normal(size=(np.prod(shape_3d_default), n_samples)) 

518 means = ( 

519 rng.standard_normal(size=(np.prod(shape_3d_default), 1)) * 50 + 1000 

520 ) 

521 signals += means 

522 img = Nifti1Image( 

523 signals.reshape((*shape_3d_default, n_samples)), 

524 affine_eye, 

525 ) 

526 

527 # Unstandarized 

528 masker = NiftiLabelsMasker(img_labels, standardize=False) 

529 unstandarized_label_signals = masker.fit_transform(img) 

530 

531 # z-score 

532 masker = NiftiLabelsMasker(img_labels, standardize="zscore_sample") 

533 trans_signals = masker.fit_transform(img) 

534 

535 assert_almost_equal(trans_signals.mean(0), 0) 

536 assert_almost_equal(trans_signals.std(0), 1, decimal=3) 

537 

538 # psc 

539 masker = NiftiLabelsMasker(img_labels, standardize="psc") 

540 trans_signals = masker.fit_transform(img) 

541 

542 assert_almost_equal(trans_signals.mean(0), 0) 

543 assert_almost_equal( 

544 trans_signals, 

545 ( 

546 unstandarized_label_signals 

547 / unstandarized_label_signals.mean(0) 

548 * 100 

549 - 100 

550 ), 

551 ) 

552 

553 

554def test_nifti_labels_masker_with_mask( 

555 shape_3d_default, affine_eye, length, img_labels 

556): 

557 """Test NiftiLabelsMasker with a separate mask_img parameter.""" 

558 shape = (*shape_3d_default, length) 

559 fmri_img, mask_img = generate_random_img(shape, affine=affine_eye) 

560 

561 masker = NiftiLabelsMasker( 

562 img_labels, resampling_target=None, mask_img=mask_img 

563 ) 

564 signals = masker.fit_transform(fmri_img) 

565 

566 bg_masker = NiftiMasker(mask_img) 

567 tmp = bg_masker.fit_transform(img_labels) 

568 masked_labels = bg_masker.inverse_transform(tmp) 

569 

570 masked_masker = NiftiLabelsMasker( 

571 masked_labels, resampling_target=None, mask_img=mask_img 

572 ) 

573 masked_signals = masked_masker.fit_transform(fmri_img) 

574 

575 assert np.allclose(signals, masked_signals) 

576 

577 # masker.region_atlas_ should be the same as the masked_labels 

578 # masked_labels is a 3D image with shape (10,10,10) 

579 masked_labels_data = get_data(masked_labels)[:, :, :] 

580 assert np.allclose(get_data(masker.region_atlas_), masked_labels_data) 

581 

582 

583@pytest.mark.parametrize( 

584 "background", 

585 [ 

586 None, 

587 "background", 

588 "Background", 

589 ], 

590) 

591def test_warning_n_labels_not_equal_n_regions( 

592 shape_3d_default, affine_eye, background, n_regions 

593): 

594 """Check that n_labels provided match n_regions in image.""" 

595 labels_img = generate_labeled_regions( 

596 shape_3d_default[:3], 

597 affine=affine_eye, 

598 n_regions=n_regions, 

599 ) 

600 region_names = generate_labels(n_regions + 2, background=background) 

601 with pytest.warns( 

602 UserWarning, 

603 match="Too many names for the indices. Dropping excess names values.", 

604 ): 

605 masker = NiftiLabelsMasker( 

606 labels_img, 

607 labels=region_names, 

608 ) 

609 masker.fit() 

610 

611 

612def test_check_labels_errors(shape_3d_default, affine_eye): 

613 """Check that invalid types for labels are caught at fit time.""" 

614 labels_img = generate_labeled_regions( 

615 shape_3d_default, 

616 affine=affine_eye, 

617 n_regions=2, 

618 ) 

619 

620 with pytest.raises(TypeError, match="'labels' must be a list."): 

621 NiftiLabelsMasker( 

622 labels_img, 

623 labels={"foo", "bar", "baz"}, 

624 ).fit() 

625 

626 with pytest.raises( 

627 TypeError, match="All elements of 'labels' must be a string" 

628 ): 

629 masker = NiftiLabelsMasker( 

630 labels_img, 

631 labels=[1, 2, 3], 

632 ) 

633 masker.fit() 

634 

635 

636@pytest.mark.parametrize( 

637 "background", 

638 [ 

639 None, 

640 "background", 

641 "Background", 

642 ], # In case the list of labels includes one for background 

643) 

644@pytest.mark.parametrize( 

645 "dtype", 

646 ["int32", "float32"], # In case regions are labeled with floats 

647) 

648@pytest.mark.parametrize( 

649 "affine_data", 

650 [ 

651 None, # no resampling 

652 np.diag( 

653 (4, 4, 4, 4) # with resampling 

654 ), # region_names_ matches signals after resampling drops labels 

655 ], 

656) 

657def test_region_names( 

658 shape_3d_default, affine_eye, background, affine_data, dtype, n_regions 

659): 

660 """Test region_names_ attribute in NiftiLabelsMasker.""" 

661 resampling = True 

662 if affine_data is None: 

663 resampling = False 

664 affine_data = affine_eye 

665 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_data) 

666 labels_img = generate_labeled_regions( 

667 shape_3d_default, 

668 affine=affine_eye, 

669 n_regions=n_regions, 

670 dtype=dtype, 

671 ) 

672 

673 masker = NiftiLabelsMasker( 

674 labels_img, 

675 labels=generate_labels(n_regions, background=background), 

676 resampling_target="data", 

677 ) 

678 

679 signals = masker.fit_transform(fmri_img) 

680 

681 tmp = generate_labels(n_regions, background=background) 

682 if background is None: 

683 expected_lut = generate_expected_lut(["Background", *tmp]) 

684 else: 

685 expected_lut = generate_expected_lut(tmp) 

686 check_lut(masker, expected_lut) 

687 

688 region_names = generate_labels(n_regions, background=background) 

689 check_region_names_after_fit( 

690 masker, 

691 signals, 

692 region_names, 

693 background, 

694 resampling, 

695 ) 

696 

697 

698def generate_expected_lut(region_names): 

699 """Generate a look up table based on a list of regions names.""" 

700 if "background" in region_names: 

701 idx = region_names.index("background") 

702 region_names[idx] = "Background" 

703 return pd.DataFrame( 

704 {"name": region_names, "index": list(range(len(region_names)))} 

705 ) 

706 

707 

708def check_region_names_after_fit( 

709 masker, 

710 signals, 

711 region_names, 

712 background, 

713 resampling=False, 

714): 

715 """Perform several checks on the expected attributes of the masker. 

716 

717 - region_names_ does not include background 

718 should have same length as signals 

719 - region_ids_ does include background 

720 - region_names_ should be the same as the region names 

721 passed to the masker minus that for "background" 

722 """ 

723 n_regions = signals.shape[0] if signals.ndim == 1 else signals.shape[1] 

724 

725 assert len(masker.region_names_) == n_regions 

726 assert len(list(masker.region_ids_.items())) == n_regions + 1 

727 

728 # for coverage 

729 masker.labels_ # noqa: B018 

730 masker._region_id_name # noqa: B018 

731 

732 # resampling may drop some labels so we do not check the region names 

733 # in this case 

734 if not resampling: 

735 region_names_after_fit = [ 

736 masker.region_names_[i] for i in masker.region_names_ 

737 ] 

738 region_names_after_fit.sort() 

739 region_names.sort() 

740 if background: 

741 region_names.pop(region_names.index(background)) 

742 assert region_names_after_fit == region_names 

743 

744 

745def check_lut(masker, expected_lut): 

746 """Check content of the look up table.""" 

747 assert masker.background_label in masker.lut_["index"].to_list() 

748 assert "Background" in masker.lut_["name"].to_list() 

749 assert masker.lut_["name"].to_list() == expected_lut["name"].to_list() 

750 assert masker.lut_["index"].to_list() == expected_lut["index"].to_list() 

751 

752 

753@pytest.mark.parametrize( 

754 "background", 

755 [ 

756 None, 

757 "background", 

758 "Background", 

759 ], 

760) 

761@pytest.mark.parametrize( 

762 "affine_data", 

763 [ 

764 None, # no resampling 

765 np.diag( 

766 (4, 4, 4, 4) # with resampling 

767 ), # region_names_ matches signals after resampling drops labels 

768 ], 

769) 

770@pytest.mark.parametrize( 

771 "masking", 

772 [ 

773 False, # no masking 

774 True, # with masking 

775 ], 

776) 

777@pytest.mark.parametrize( 

778 "keep_masked_labels", 

779 [ 

780 False, 

781 True, 

782 ], 

783) 

784def test_region_names_ids_match_after_fit( 

785 shape_3d_default, 

786 affine_eye, 

787 background, 

788 affine_data, 

789 n_regions, 

790 masking, 

791 keep_masked_labels, 

792 img_labels, 

793): 

794 """Test that the same region names and ids correspond after fit.""" 

795 if affine_data is None: 

796 # no resampling 

797 affine_data = affine_eye 

798 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_data) 

799 

800 region_names = generate_labels(n_regions, background=background) 

801 region_ids = list(np.unique(get_data(img_labels))) 

802 

803 if masking: 

804 # create a mask_img with 3 regions 

805 labels_data = get_data(img_labels) 

806 mask_data = ( 

807 (labels_data == 1) + (labels_data == 2) + (labels_data == 5) 

808 ) 

809 mask_img = Nifti1Image(mask_data.astype(np.int8), img_labels.affine) 

810 else: 

811 mask_img = None 

812 

813 masker = NiftiLabelsMasker( 

814 img_labels, 

815 labels=region_names, 

816 resampling_target="data", 

817 mask_img=mask_img, 

818 keep_masked_labels=keep_masked_labels, 

819 ) 

820 

821 masker.fit_transform(fmri_img) 

822 

823 tmp = generate_labels(n_regions, background=background) 

824 if background is None: 

825 expected_lut = generate_expected_lut(["Background", *tmp]) 

826 else: 

827 expected_lut = generate_expected_lut(tmp) 

828 check_lut(masker, expected_lut) 

829 

830 check_region_names_ids_match_after_fit( 

831 masker, region_names, region_ids, background 

832 ) 

833 

834 

835def check_region_names_ids_match_after_fit( 

836 masker, region_names, region_ids, background 

837): 

838 """Check the region names and ids correspondence. 

839 

840 Check that the same region names and ids correspond to each other 

841 after fit by comparing with before fit. 

842 """ 

843 # region_ids includes background, so we make 

844 # sure that the region_names also include it 

845 if not background: 

846 region_names.insert(0, "background") 

847 # if they don't have the same length, we can't compare them 

848 if len(region_names) == len(region_ids): 

849 region_id_names = { 

850 region_id: region_names[i] 

851 for i, region_id in enumerate(region_ids) 

852 } 

853 for key, region_name in masker.region_names_.items(): 

854 assert region_id_names[masker.region_ids_[key]] == region_name 

855 

856 

857def generate_labels(n_regions, background=""): 

858 """Create list of strings to use as labels.""" 

859 labels = [] 

860 if background: 

861 labels.append(background) 

862 labels.extend([f"region_{i + 1!s}" for i in range(n_regions)]) 

863 return labels 

864 

865 

866@pytest.mark.parametrize("background", [None, "background", "Background"]) 

867def test_region_names_with_non_sequential_labels( 

868 shape_3d_default, affine_eye, background 

869): 

870 """Test for atlases with region id that are not consecutive. 

871 

872 See the AAL atlas for an example of this. 

873 """ 

874 labels = [2001, 2002, 2101, 2102, 9170] 

875 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_eye) 

876 labels_img = generate_labeled_regions( 

877 shape_3d_default[:3], 

878 affine=affine_eye, 

879 n_regions=len(labels), 

880 labels=[0, *labels], 

881 ) 

882 

883 masker = NiftiLabelsMasker( 

884 labels_img, 

885 labels=generate_labels(len(labels), background=background), 

886 resampling_target=None, 

887 ) 

888 

889 signals = masker.fit_transform(fmri_img) 

890 

891 expected_lut = pd.DataFrame( 

892 { 

893 "index": [0, *labels], 

894 "name": ["Background"] 

895 + [f"region_{i}" for i in range(1, len(labels) + 1)], 

896 } 

897 ) 

898 check_lut(masker, expected_lut) 

899 

900 region_names = generate_labels(len(labels), background=background) 

901 

902 check_region_names_after_fit(masker, signals, region_names, background) 

903 

904 

905@pytest.mark.parametrize("background", [None, "background", "Background"]) 

906def test_more_labels_than_actual_region_in_atlas( 

907 shape_3d_default, affine_eye, background, n_regions, img_labels 

908): 

909 """Test region_names_ property in NiftiLabelsMasker. 

910 

911 See fetch_atlas_destrieux_2009 for example. 

912 Some labels have no associated voxels. 

913 """ 

914 n_regions_in_labels = n_regions + 5 

915 

916 region_names = generate_labels(n_regions_in_labels, background=background) 

917 

918 masker = NiftiLabelsMasker( 

919 img_labels, 

920 labels=region_names, 

921 resampling_target="data", 

922 ) 

923 

924 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_eye) 

925 with pytest.warns( 

926 UserWarning, 

927 match="Too many names for the indices. Dropping excess names values.", 

928 ): 

929 masker.fit_transform(fmri_img) 

930 

931 

932@pytest.mark.parametrize("background", [None, "Background"]) 

933def test_pass_lut( 

934 shape_3d_default, affine_eye, n_regions, img_labels, tmp_path, background 

935): 

936 """Smoke test to pass LUT directly or as file.""" 

937 region_names = generate_labels(n_regions, background=background) 

938 if background: 

939 lut = pd.DataFrame( 

940 {"name": region_names, "index": list(range(n_regions + 1))} 

941 ) 

942 else: 

943 lut = pd.DataFrame( 

944 { 

945 "name": ["Background", *region_names], 

946 "index": list(range(n_regions + 1)), 

947 } 

948 ) 

949 

950 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_eye) 

951 

952 masker = NiftiLabelsMasker( 

953 img_labels, 

954 lut=lut, 

955 ) 

956 

957 masker.fit_transform(fmri_img) 

958 

959 assert masker.lut_["index"].to_list() == lut["index"].to_list() 

960 assert masker.lut_["name"].to_list() == lut["name"].to_list() 

961 

962 lut_file = tmp_path / "lut.csv" 

963 lut.to_csv(lut_file, index=False) 

964 masker = NiftiLabelsMasker( 

965 img_labels, 

966 lut=lut_file, 

967 ) 

968 

969 masker.fit_transform(fmri_img) 

970 

971 

972def test_pass_lut_error(shape_3d_default, affine_eye, n_regions, img_labels): 

973 """Cannot pass both LUT and labels.""" 

974 region_names = generate_labels(n_regions, background=None) 

975 lut = pd.DataFrame( 

976 { 

977 "name": ["Background", *region_names], 

978 "index": list(range(n_regions + 1)), 

979 } 

980 ) 

981 

982 fmri_img, _ = generate_random_img(shape_3d_default, affine=affine_eye) 

983 

984 with pytest.raises( 

985 ValueError, match="Pass either labels or a lookup table" 

986 ): 

987 NiftiLabelsMasker(img_labels, lut=lut, labels=region_names).fit()