Coverage for nilearn/tests/test_masking.py: 0%

376 statements  

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

1"""Test the mask-extracting utilities.""" 

2 

3import warnings 

4 

5import numpy as np 

6import pytest 

7from nibabel import Nifti1Image 

8from numpy.testing import assert_array_equal, assert_equal 

9from sklearn.preprocessing import StandardScaler 

10 

11from nilearn._utils import data_gen 

12from nilearn._utils.exceptions import DimensionError 

13from nilearn._utils.testing import write_imgs_to_path 

14from nilearn.conftest import _affine_eye, _rng 

15from nilearn.image import get_data, high_variance_confounds 

16from nilearn.maskers import NiftiMasker 

17from nilearn.masking import ( 

18 _MaskWarning, 

19 _unmask_3d, 

20 _unmask_4d, 

21 apply_mask, 

22 compute_background_mask, 

23 compute_brain_mask, 

24 compute_epi_mask, 

25 compute_multi_brain_mask, 

26 compute_multi_epi_mask, 

27 extrapolate_out_mask, 

28 intersect_masks, 

29 load_mask_img, 

30 unmask, 

31 unmask_from_to_3d_array, 

32) 

33from nilearn.surface.surface import SurfaceImage 

34 

35np_version = ( 

36 np.version.full_version 

37 if hasattr(np.version, "full_version") 

38 else np.version.short_version 

39) 

40 

41_TEST_DIM_ERROR_MSG = ( 

42 "Input data has incompatible dimensionality: " 

43 "Expected dimension is 3D and you provided " 

44 "a %s image" 

45) 

46 

47 

48def _simu_img(): 

49 # Random confounds 

50 rng = _rng() 

51 conf = 2 + rng.standard_normal((100, 6)) 

52 # Random 4D volume 

53 vol = 100 + 10 * rng.standard_normal((5, 5, 2, 100)) 

54 img = Nifti1Image(vol, np.eye(4)) 

55 # Create an nifti image with the data, and corresponding mask 

56 mask = Nifti1Image(np.ones([5, 5, 2]), np.eye(4)) 

57 return img, mask, conf 

58 

59 

60def _cov_conf(tseries, conf): 

61 conf_n = StandardScaler().fit_transform(conf) 

62 _ = StandardScaler().fit_transform(tseries) 

63 cov_mat = np.dot(tseries.T, conf_n) 

64 return cov_mat 

65 

66 

67def test_load_mask_img_error_inputs(surf_img_2d, img_4d_ones_eye): 

68 """Check input validation of load_mask_img.""" 

69 with pytest.raises( 

70 TypeError, match="a 3D/4D Niimg-like object or a SurfaceImage" 

71 ): 

72 load_mask_img(1) 

73 

74 with pytest.raises( 

75 TypeError, 

76 match="Expected dimension is 3D and you provided a 4D image.", 

77 ): 

78 load_mask_img(img_4d_ones_eye) 

79 

80 with pytest.raises( 

81 ValueError, match="Data for each part of .* should be 1D." 

82 ): 

83 load_mask_img(surf_img_2d()) 

84 

85 

86def test_load_mask_img_surface(surf_mask_1d): 

87 """Check load_mask_img returns a boolean surface image \ 

88 when SurfaceImage is used as input. 

89 """ 

90 mask, _ = load_mask_img(surf_mask_1d) 

91 assert isinstance(mask, SurfaceImage) 

92 for hemi in mask.data.parts.values(): 

93 assert hemi.dtype == "bool" 

94 

95 

96def test_high_variance_confounds(): 

97 """Test high_variance_confounds.""" 

98 img, mask, conf = _simu_img() 

99 

100 hv_confounds = high_variance_confounds(img) 

101 

102 masker1 = NiftiMasker( 

103 standardize="zscore_sample", 

104 detrend=False, 

105 high_variance_confounds=False, 

106 mask_img=mask, 

107 ).fit() 

108 tseries1 = masker1.transform(img, confounds=[hv_confounds, conf]) 

109 

110 masker2 = NiftiMasker( 

111 standardize="zscore_sample", 

112 detrend=False, 

113 high_variance_confounds=True, 

114 mask_img=mask, 

115 ).fit() 

116 tseries2 = masker2.transform(img, confounds=conf) 

117 

118 assert_array_equal(tseries1, tseries2) 

119 

120 

121def _confounds_regression( 

122 standardize_signal="zscore_sample", standardize_confounds=True 

123): 

124 img, mask, conf = _simu_img() 

125 

126 masker = NiftiMasker( 

127 standardize=standardize_signal, 

128 standardize_confounds=standardize_confounds, 

129 detrend=False, 

130 mask_img=mask, 

131 ).fit() 

132 

133 tseries = masker.transform(img, confounds=conf) 

134 

135 if standardize_confounds: 

136 conf = StandardScaler(with_std=False).fit_transform(conf) 

137 

138 cov_mat = _cov_conf(tseries, conf) 

139 

140 return np.sum(np.abs(cov_mat)) 

141 

142 

143@pytest.mark.parametrize( 

144 "standardize_signal, standardize_confounds, expected", 

145 [ 

146 # Signal is not standardized 

147 (False, True, 10.0 * 10e-10), 

148 # Signal is z-scored with string arg 

149 ("zscore_sample", True, 10e-10), 

150 # Signal is psc standardized 

151 ("psc", True, 10.0 * 10e-10), 

152 ], 

153) 

154def test_confounds_standardization( 

155 standardize_signal, standardize_confounds, expected 

156): 

157 """Tests for confounds standardization. 

158 

159 Explicit standardization of confounds 

160 

161 See Issue #2584 

162 Code from @pbellec 

163 """ 

164 assert ( 

165 _confounds_regression( 

166 standardize_signal=standardize_signal, 

167 standardize_confounds=standardize_confounds, 

168 ) 

169 < expected 

170 ) 

171 

172 

173@pytest.mark.parametrize( 

174 "standardize_signal", 

175 [ 

176 # Signal is not standardized 

177 False, 

178 # Signal is z-scored with string arg 

179 "zscore_sample", 

180 # Signal is psc standardized 

181 "psc", 

182 ], 

183) 

184def test_confounds_not_standardized(standardize_signal): 

185 """Tests for confounds standardization. 

186 

187 Confounds are not standardized 

188 In this case, the regression should fail... 

189 

190 See Issue #2584 

191 Code from @pbellec 

192 """ 

193 # Signal is not standardized 

194 assert ( 

195 _confounds_regression( 

196 standardize_signal=standardize_signal, 

197 standardize_confounds=False, 

198 ) 

199 > 100 

200 ) 

201 

202 

203@pytest.mark.parametrize( 

204 "fn", 

205 [ 

206 compute_background_mask, 

207 compute_brain_mask, 

208 compute_epi_mask, 

209 ], 

210) 

211def test_compute_mask_error(fn): 

212 """Check that an empty list of images creates a meaningful error.""" 

213 with pytest.raises(TypeError, match="Cannot concatenate empty objects"): 

214 fn([]) 

215 

216 

217def test_compute_epi_mask(affine_eye): 

218 """Test compute_epi_mask.""" 

219 mean_image = np.ones((9, 9, 3)) 

220 mean_image[3:-2, 3:-2, :] = 10 

221 mean_image[5, 5, :] = 11 

222 mean_image = Nifti1Image(mean_image, affine_eye) 

223 

224 mask1 = compute_epi_mask(mean_image, opening=False, verbose=1) 

225 mask2 = compute_epi_mask(mean_image, exclude_zeros=True, opening=False) 

226 

227 # With an array with no zeros, exclude_zeros should not make 

228 # any difference 

229 assert_array_equal(get_data(mask1), get_data(mask2)) 

230 

231 # Check that padding with zeros does not change the extracted mask 

232 mean_image2 = np.zeros((30, 30, 3)) 

233 mean_image2[3:12, 3:12, :] = get_data(mean_image) 

234 mean_image2 = Nifti1Image(mean_image2, affine_eye) 

235 

236 mask3 = compute_epi_mask(mean_image2, exclude_zeros=True, opening=False) 

237 

238 assert_array_equal(get_data(mask1), get_data(mask3)[3:12, 3:12]) 

239 

240 # However, without exclude_zeros, it does 

241 mask3 = compute_epi_mask(mean_image2, opening=False) 

242 assert not np.allclose(get_data(mask1), get_data(mask3)[3:12, 3:12]) 

243 

244 

245def test_compute_epi_mask_errors_warnings(affine_eye): 

246 """Check that we get a ValueError for incorrect shape.""" 

247 mean_image = np.ones((9, 9)) 

248 mean_image[3:-3, 3:-3] = 10 

249 mean_image[5, 5] = 100 

250 mean_image = Nifti1Image(mean_image, affine_eye) 

251 

252 with pytest.raises( 

253 ValueError, 

254 match=( 

255 "Computation expects 3D or 4D images, but 2 dimensions were given" 

256 ), 

257 ): 

258 compute_epi_mask(mean_image) 

259 

260 # Check that we get a useful warning for empty masks 

261 mean_image = np.zeros((9, 9, 9)) 

262 mean_image[0, 0, 1] = -1 

263 mean_image[0, 0, 0] = 1.2 

264 mean_image[0, 0, 2] = 1.1 

265 mean_image = Nifti1Image(mean_image, affine_eye) 

266 

267 with pytest.warns(_MaskWarning, match="Computed an empty mask"): 

268 compute_epi_mask(mean_image, exclude_zeros=True) 

269 

270 

271@pytest.mark.parametrize("value", (0, np.nan)) 

272def test_compute_background_mask(affine_eye, value): 

273 """Test compute_background_mask.""" 

274 mean_image = value * np.ones((9, 9, 9)) 

275 mean_image[3:-3, 3:-3, 3:-3] = 1 

276 mask = mean_image == 1 

277 mean_image = Nifti1Image(mean_image, affine_eye) 

278 

279 mask1 = compute_background_mask(mean_image, opening=False, verbose=1) 

280 

281 assert_array_equal(get_data(mask1), mask.astype(np.int8)) 

282 

283 

284def test_compute_background_mask_errors_warnings(affine_eye): 

285 """Check that we get a ValueError for incorrect shape.""" 

286 mean_image = np.ones((9, 9)) 

287 mean_image[3:-3, 3:-3] = 10 

288 mean_image[5, 5] = 100 

289 mean_image = Nifti1Image(mean_image, affine_eye) 

290 

291 with pytest.raises(ValueError): 

292 compute_background_mask(mean_image) 

293 

294 # Check that we get a useful warning for empty masks 

295 mean_image = np.zeros((9, 9, 9)) 

296 mean_image = Nifti1Image(mean_image, affine_eye) 

297 

298 with pytest.warns(_MaskWarning, match="Computed an empty mask"): 

299 compute_background_mask(mean_image) 

300 

301 

302def test_compute_brain_mask(): 

303 """Test compute_brain_mask.""" 

304 img, _ = data_gen.generate_mni_space_img(res=8, random_state=0) 

305 

306 brain_mask = compute_brain_mask(img, threshold=0.2, verbose=1) 

307 gm_mask = compute_brain_mask(img, threshold=0.2, mask_type="gm") 

308 wm_mask = compute_brain_mask(img, threshold=0.2, mask_type="wm") 

309 

310 brain_data, gm_data, wm_data = map( 

311 get_data, (brain_mask, gm_mask, wm_mask) 

312 ) 

313 

314 # Check that whole-brain mask is non-empty 

315 assert (brain_data != 0).any() 

316 for subset in gm_data, wm_data: 

317 # Test that gm and wm masks are included in the whole-brain mask 

318 assert ( 

319 np.logical_and(brain_data, subset) == subset.astype(bool) 

320 ).all() 

321 # Test that gm and wm masks are non-empty 

322 assert (subset != 0).any() 

323 

324 # Test that gm and wm masks have empty intersection 

325 assert (np.logical_and(gm_data, wm_data) == 0).all() 

326 

327 # Check that we get a useful warning for empty masks 

328 with pytest.warns(_MaskWarning): 

329 compute_brain_mask(img, threshold=1) 

330 

331 # Check that masks obtained from same FOV are the same 

332 img1, _ = data_gen.generate_mni_space_img(res=8, random_state=1) 

333 mask_img1 = compute_brain_mask(img1, verbose=1, threshold=0.2) 

334 

335 assert (brain_data == get_data(mask_img1)).all() 

336 

337 # Check that error is raised if mask type is unknown 

338 with pytest.raises(ValueError, match="Unknown mask type foo."): 

339 compute_brain_mask(img, verbose=1, mask_type="foo") 

340 

341 

342@pytest.mark.parametrize( 

343 "affine", 

344 [_affine_eye(), np.diag((1, 1, -1, 1)), np.diag((0.5, 1, 0.5, 1))], 

345) 

346@pytest.mark.parametrize("create_files", (False, True)) 

347def test_apply_mask(tmp_path, create_files, affine): 

348 """Test smoothing of timeseries extraction.""" 

349 # A delta in 3D 

350 # Standard masking 

351 data = np.zeros((40, 40, 40, 2)) 

352 data[20, 20, 20] = 1 

353 data_img = Nifti1Image(data, affine) 

354 

355 mask = np.ones((40, 40, 40)) 

356 mask_img = Nifti1Image(mask, affine) 

357 

358 filenames = write_imgs_to_path( 

359 data_img, 

360 mask_img, 

361 file_path=tmp_path, 

362 create_files=create_files, 

363 ) 

364 

365 series = apply_mask(filenames[0], filenames[1], smoothing_fwhm=9) 

366 

367 series = np.reshape(series[0, :], (40, 40, 40)) 

368 vmax = series.max() 

369 # We are expecting a full-width at half maximum of 

370 # 9mm/voxel_size: 

371 above_half_max = series > 0.5 * vmax 

372 for axis in (0, 1, 2): 

373 proj = np.any( 

374 np.any(np.rollaxis(above_half_max, axis=axis), axis=-1), 

375 axis=-1, 

376 ) 

377 

378 assert_equal(proj.sum(), 9 / np.abs(affine[axis, axis])) 

379 

380 

381def test_apply_mask_surface(surf_img_2d, surf_mask_1d): 

382 """Test apply_mask on surface.""" 

383 length = 5 

384 series = apply_mask(surf_img_2d(length), surf_mask_1d) 

385 

386 assert isinstance(series, np.ndarray) 

387 assert series.shape[0] == length 

388 

389 

390def test_apply_mask_nan(affine_eye): 

391 """Check that NaNs in the data do not propagate.""" 

392 data = np.zeros((40, 40, 40, 2)) 

393 data[20, 20, 20] = 1 

394 data[10, 10, 10] = np.nan 

395 data_img = Nifti1Image(data, affine_eye) 

396 

397 mask = np.ones((40, 40, 40)) 

398 mask_img = Nifti1Image(mask, affine_eye) 

399 

400 series = apply_mask(data_img, mask_img, smoothing_fwhm=9) 

401 

402 assert np.all(np.isfinite(series)) 

403 

404 

405def test_apply_mask_errors(affine_eye): 

406 """Check errors for dimension.""" 

407 data = np.zeros((40, 40, 40, 2)) 

408 data[20, 20, 20] = 1 

409 data_img = Nifti1Image(data, affine_eye) 

410 

411 mask = np.ones((40, 40, 40)) 

412 mask_img = Nifti1Image(mask, affine_eye) 

413 

414 full_mask = np.zeros((40, 40, 40)) 

415 full_mask_img = Nifti1Image(full_mask, affine_eye) 

416 

417 # veriy that 4D masks are rejected 

418 mask_img_4d = Nifti1Image(np.ones((40, 40, 40, 2)), affine_eye) 

419 

420 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"): 

421 apply_mask(data_img, mask_img_4d) 

422 

423 # Check that 3D data is accepted 

424 data_3d = Nifti1Image( 

425 np.arange(27, dtype="int32").reshape((3, 3, 3)), affine_eye 

426 ) 

427 mask_data_3d = np.zeros((3, 3, 3)) 

428 mask_data_3d[1, 1, 0] = True 

429 mask_data_3d[0, 1, 0] = True 

430 mask_data_3d[0, 1, 1] = True 

431 

432 data_3d = apply_mask(data_3d, Nifti1Image(mask_data_3d, affine_eye)) 

433 

434 assert sorted(data_3d.tolist()) == [3.0, 4.0, 12.0] 

435 

436 # Check data shape and affine 

437 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "2D"): 

438 apply_mask(data_img, Nifti1Image(mask[20, ...], affine_eye)) 

439 

440 with pytest.raises(ValueError, match="is different from img affine"): 

441 apply_mask(data_img, Nifti1Image(mask, affine_eye / 2.0)) 

442 

443 # Check that full masking raises error 

444 with pytest.raises( 

445 ValueError, 

446 match="The mask is invalid as it is empty: it masks all data.", 

447 ): 

448 apply_mask(data_img, full_mask_img) 

449 

450 # Check weird values in data 

451 mask[10, 10, 10] = 2 

452 with pytest.raises( 

453 ValueError, 

454 match="Background of the mask must be represented with 0.", 

455 ): 

456 apply_mask(data_img, Nifti1Image(mask, affine_eye)) 

457 

458 mask[15, 15, 15] = 3 

459 with pytest.raises( 

460 ValueError, match="Given mask is not made of 2 values.*" 

461 ): 

462 apply_mask(Nifti1Image(data, affine_eye), mask_img) 

463 

464 

465def test_unmask_4d(rng, affine_eye, shape_4d_default): 

466 """Test unmask on 4D images.""" 

467 data4D = rng.uniform(size=shape_4d_default) 

468 mask = rng.integers(2, size=shape_4d_default[:3], dtype="int32") 

469 mask_img = Nifti1Image(mask, affine_eye) 

470 mask = mask.astype(bool) 

471 

472 masked4D = data4D[mask, :].T 

473 unmasked4D = data4D.copy() 

474 unmasked4D[np.logical_not(mask), :] = 0 

475 

476 # 4D Test, test value ordering at the same time. 

477 t = get_data(unmask(masked4D, mask_img, order="C")) 

478 

479 assert t.ndim == 4 

480 assert t.flags["C_CONTIGUOUS"] 

481 assert not t.flags["F_CONTIGUOUS"] 

482 assert_array_equal(t, unmasked4D) 

483 

484 t = unmask([masked4D], mask_img, order="F") 

485 t = [get_data(t_) for t_ in t] 

486 

487 assert isinstance(t, list) 

488 assert t[0].ndim == 4 

489 assert not t[0].flags["C_CONTIGUOUS"] 

490 assert t[0].flags["F_CONTIGUOUS"] 

491 assert_array_equal(t[0], unmasked4D) 

492 

493 

494@pytest.mark.parametrize("create_files", [False, True]) 

495def test_unmask_3d_with_files( 

496 rng, affine_eye, tmp_path, create_files, shape_3d_default 

497): 

498 """Test unmask on 3D images. 

499 

500 Check both with Nifti1Image and file. 

501 """ 

502 data3D = rng.uniform(size=shape_3d_default) 

503 mask = rng.integers(2, size=shape_3d_default, dtype="int32") 

504 mask_img = Nifti1Image(mask, affine_eye) 

505 mask = mask.astype(bool) 

506 

507 masked3D = data3D[mask] 

508 unmasked3D = data3D.copy() 

509 unmasked3D[np.logical_not(mask)] = 0 

510 

511 filename = write_imgs_to_path( 

512 mask_img, 

513 file_path=tmp_path, 

514 create_files=create_files, 

515 ) 

516 t = get_data(unmask(masked3D, filename, order="C")) 

517 

518 assert t.ndim == 3 

519 assert t.flags["C_CONTIGUOUS"] 

520 assert not t.flags["F_CONTIGUOUS"] 

521 assert_array_equal(t, unmasked3D) 

522 

523 t = unmask([masked3D], filename, order="F") 

524 t = [get_data(t_) for t_ in t] 

525 

526 assert isinstance(t, list) 

527 assert t[0].ndim == 3 

528 assert not t[0].flags["C_CONTIGUOUS"] 

529 assert t[0].flags["F_CONTIGUOUS"] 

530 assert_array_equal(t[0], unmasked3D) 

531 

532 

533def test_unmask_errors(rng, affine_eye, shape_3d_default): 

534 """Test unmask errors.""" 

535 # A delta in 3D 

536 mask = rng.integers(2, size=shape_3d_default, dtype="int32") 

537 mask_img = Nifti1Image(mask, affine_eye) 

538 mask = mask.astype(bool) 

539 

540 # Error test: shape 

541 vec_1D = np.empty((500,), dtype=int) 

542 

543 msg = "X must be of shape" 

544 with pytest.raises(TypeError, match=msg): 

545 unmask(vec_1D, mask_img) 

546 with pytest.raises(TypeError, match=msg): 

547 unmask([vec_1D], mask_img) 

548 

549 vec_2D = np.empty((500, 500), dtype=np.float64) 

550 

551 with pytest.raises(TypeError, match=msg): 

552 unmask(vec_2D, mask_img) 

553 

554 with pytest.raises(TypeError, match=msg): 

555 unmask([vec_2D], mask_img) 

556 

557 # Error test: mask type 

558 msg = "mask must be a boolean array" 

559 with pytest.raises(TypeError, match=msg): 

560 _unmask_3d(vec_1D, mask.astype(int)) 

561 

562 with pytest.raises(TypeError, match=msg): 

563 _unmask_4d(vec_2D, mask.astype(np.float64)) 

564 

565 # Transposed vector 

566 transposed_vector = np.ones((np.sum(mask), 1), dtype=bool) 

567 with pytest.raises(TypeError, match="X must be of shape"): 

568 unmask(transposed_vector, mask_img) 

569 

570 

571def test_unmask_error_shape(rng, affine_eye, shape_4d_default): 

572 """Test unmask errors shape between mask and image.""" 

573 X = rng.standard_normal() 

574 mask_img = np.zeros(shape_4d_default, dtype=np.uint8) 

575 mask_img[rng.standard_normal(size=shape_4d_default) > 0.4] = 1 

576 n_features = (mask_img > 0).sum() 

577 mask_img = Nifti1Image(mask_img, affine_eye) 

578 n_samples = shape_4d_default[0] 

579 

580 X = rng.standard_normal(size=(n_samples, n_features, 2)) 

581 

582 # 3D X (unmask should raise a DimensionError) 

583 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"): 

584 unmask(X, mask_img) 

585 

586 X = rng.standard_normal(size=(n_samples, n_features)) 

587 

588 # Raises an error because the mask is 4D 

589 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"): 

590 unmask(X, mask_img) 

591 

592 

593@pytest.fixture 

594def img_2d_mask_bottom_right(affine_eye): 

595 """Return 3D nifti binary mask image with bottom right filled. 

596 

597 +---+---+---+---+ 

598 | | | | | 

599 +---+---+---+---+ 

600 | | | | | 

601 +---+---+---+---+ 

602 | | | X | X | 

603 +---+---+---+---+ 

604 | | | X | X | 

605 +---+---+---+---+ 

606 

607 """ 

608 mask_a = np.zeros((4, 4, 1), dtype=bool) 

609 mask_a[2:4, 2:4] = 1 

610 return Nifti1Image(mask_a.astype("int32"), affine_eye) 

611 

612 

613@pytest.fixture 

614def img_2d_mask_center(affine_eye): 

615 """Return 3D nifti binary mask image with center filled. 

616 

617 +---+---+---+---+ 

618 | | | | | 

619 +---+---+---+---+ 

620 | | X | X | | 

621 +---+---+---+---+ 

622 | | X | X | | 

623 +---+---+---+---+ 

624 | | | | | 

625 +---+---+---+---+ 

626 

627 """ 

628 mask_b = np.zeros((4, 4, 1), dtype=bool) 

629 mask_b[1:3, 1:3] = 1 

630 return Nifti1Image(mask_b.astype("int32"), affine_eye) 

631 

632 

633@pytest.mark.parametrize("create_files", (False, True)) 

634def test_intersect_masks_filename( 

635 tmp_path, img_2d_mask_bottom_right, img_2d_mask_center, create_files 

636): 

637 """Test the intersect_masks function on files.""" 

638 filenames = write_imgs_to_path( 

639 img_2d_mask_bottom_right, 

640 img_2d_mask_center, 

641 file_path=tmp_path, 

642 create_files=create_files, 

643 ) 

644 mask_ab = np.zeros((4, 4, 1), dtype=bool) 

645 mask_ab[2, 2] = 1 

646 mask_ab_ = intersect_masks(filenames, threshold=1.0) 

647 

648 assert_array_equal(mask_ab, get_data(mask_ab_)) 

649 

650 

651def test_intersect_masks_f8(img_2d_mask_bottom_right, img_2d_mask_center): 

652 """Test intersect mask images with '>f8'. 

653 

654 This function uses largest_connected_component 

655 to check if intersect_masks passes 

656 with connected=True (which is by default) 

657 """ 

658 mask_a_img_change_dtype = Nifti1Image( 

659 get_data(img_2d_mask_bottom_right).astype(">f8"), 

660 affine=img_2d_mask_bottom_right.affine, 

661 ) 

662 mask_b_img_change_dtype = Nifti1Image( 

663 get_data(img_2d_mask_center).astype(">f8"), 

664 affine=img_2d_mask_center.affine, 

665 ) 

666 mask_ab_change_type = intersect_masks( 

667 [mask_a_img_change_dtype, mask_b_img_change_dtype], threshold=1.0 

668 ) 

669 

670 mask_ab = np.zeros((4, 4, 1), dtype=bool) 

671 mask_ab[2, 2] = 1 

672 assert_array_equal(mask_ab, get_data(mask_ab_change_type)) 

673 

674 

675def test_intersect_masks( 

676 affine_eye, img_2d_mask_bottom_right, img_2d_mask_center 

677): 

678 """Test the intersect_masks function.""" 

679 mask_c = np.zeros((4, 4, 1), dtype=bool) 

680 mask_c[:, 2] = 1 

681 mask_c[0, 0] = 1 

682 mask_c_img = Nifti1Image(mask_c.astype("int32"), affine_eye) 

683 

684 # +---+---+---+---+ 

685 # | X | | X | | 

686 # +---+---+---+---+ 

687 # | | | X | | 

688 # +---+---+---+---+ 

689 # | | | X | | 

690 # +---+---+---+---+ 

691 # | | | X | | 

692 # +---+---+---+---+ 

693 

694 mask_a = np.zeros((4, 4, 1), dtype=bool) 

695 mask_a[2:4, 2:4] = 1 

696 

697 mask_b = np.zeros((4, 4, 1), dtype=bool) 

698 mask_b[1:3, 1:3] = 1 

699 

700 mask_abc = mask_a + mask_b + mask_c 

701 mask_abc_ = intersect_masks( 

702 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img], 

703 threshold=0.0, 

704 connected=False, 

705 ) 

706 

707 assert_array_equal(mask_abc, get_data(mask_abc_)) 

708 

709 mask_abc[0, 0] = 0 

710 mask_abc_ = intersect_masks( 

711 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img], 

712 threshold=0.0, 

713 ) 

714 

715 assert_array_equal(mask_abc, get_data(mask_abc_)) 

716 

717 mask_abc = np.zeros((4, 4, 1), dtype=bool) 

718 mask_abc[2, 2] = 1 

719 mask_abc_ = intersect_masks( 

720 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img], 

721 threshold=1.0, 

722 ) 

723 

724 assert_array_equal(mask_abc, get_data(mask_abc_)) 

725 

726 mask_abc[1, 2] = 1 

727 mask_abc[3, 2] = 1 

728 mask_abc_ = intersect_masks( 

729 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img] 

730 ) 

731 

732 assert_array_equal(mask_abc, get_data(mask_abc_)) 

733 

734 

735def test_compute_multi_epi_mask(affine_eye): 

736 """Test resampling done with compute_multi_epi_mask.""" 

737 mask_a = np.zeros((4, 4, 1), dtype=bool) 

738 mask_a[2:4, 2:4] = 1 

739 mask_a_img = Nifti1Image(mask_a.astype("uint8"), affine_eye) 

740 

741 mask_b = np.zeros((8, 8, 1), dtype=bool) 

742 mask_b[2:6, 2:6] = 1 

743 mask_b_img = Nifti1Image(mask_b.astype("uint8"), affine_eye / 2.0) 

744 

745 with warnings.catch_warnings(): 

746 warnings.simplefilter("ignore", _MaskWarning) 

747 with pytest.raises( 

748 ValueError, match="cannot convert float NaN to integer" 

749 ): 

750 compute_multi_epi_mask([mask_a_img, mask_b_img]) 

751 

752 mask_ab = np.zeros((4, 4, 1), dtype=bool) 

753 mask_ab[2, 2] = 1 

754 mask_ab_ = compute_multi_epi_mask( 

755 [mask_a_img, mask_b_img], 

756 threshold=1.0, 

757 opening=0, 

758 target_affine=affine_eye, 

759 target_shape=(4, 4, 1), 

760 verbose=1, 

761 ) 

762 

763 assert_array_equal(mask_ab, get_data(mask_ab_)) 

764 

765 

766def test_compute_multi_brain_mask_error(): 

767 """Check error raised if images with different shapes given as input.""" 

768 imgs = [ 

769 data_gen.generate_mni_space_img(res=8, random_state=0)[0], 

770 data_gen.generate_mni_space_img(res=12, random_state=0)[0], 

771 ] 

772 with pytest.raises( 

773 ValueError, 

774 match="Field of view of image #1 is different from reference FOV.", 

775 ): 

776 compute_multi_brain_mask(imgs) 

777 

778 

779def test_compute_multi_brain_mask(): 

780 """Check results are the same if affine is the same.""" 

781 imgs1 = [ 

782 data_gen.generate_mni_space_img(res=9, random_state=0)[0], 

783 data_gen.generate_mni_space_img(res=9, random_state=1)[0], 

784 ] 

785 imgs2 = [ 

786 data_gen.generate_mni_space_img(res=9, random_state=2)[0], 

787 data_gen.generate_mni_space_img(res=9, random_state=3)[0], 

788 ] 

789 mask1 = compute_multi_brain_mask(imgs1, threshold=0.2, verbose=1) 

790 mask2 = compute_multi_brain_mask(imgs2, threshold=0.2) 

791 

792 assert_array_equal(get_data(mask1), get_data(mask2)) 

793 

794 

795def test_nifti_masker_empty_mask_warning(affine_eye): 

796 """Check error is raised when mask_strategy="epi" masks all data.""" 

797 X = Nifti1Image(np.ones((2, 2, 2, 5)), affine_eye) 

798 with pytest.raises( 

799 ValueError, 

800 match="The mask is invalid as it is empty: it masks all data", 

801 ): 

802 NiftiMasker(mask_strategy="epi").fit_transform(X) 

803 

804 

805def test_unmask_list(rng, shape_3d_default, affine_eye): 

806 """Test unmask on list input. 

807 

808 Results should be equivalent to array input. 

809 """ 

810 mask_data = rng.uniform(size=shape_3d_default) < 0.5 

811 mask_img = Nifti1Image(mask_data.astype(np.uint8), affine_eye) 

812 

813 a = unmask(mask_data[mask_data], mask_img) 

814 b = unmask(mask_data[mask_data].tolist(), mask_img) # shouldn't crash 

815 

816 assert_array_equal(get_data(a), get_data(b)) 

817 

818 

819def test_extrapolate_out_mask(): 

820 """Test extrapolate_out_mask.""" 

821 # Input data: 

822 initial_data = np.zeros((5, 5, 5)) 

823 initial_data[1, 2, 2] = 1 

824 initial_data[2, 1, 2] = 2 

825 initial_data[2, 2, 1] = 3 

826 initial_data[3, 2, 2] = 4 

827 initial_data[2, 3, 2] = 5 

828 initial_data[2, 2, 3] = 6 

829 initial_mask = initial_data.copy() != 0 

830 

831 # Expected result 

832 target_data = np.array( 

833 [ 

834 [ 

835 [0.0, 0.0, 0.0, 0.0, 0.0], 

836 [0.0, 0.0, 0.0, 0.0, 0.0], 

837 [0.0, 0.0, 1.0, 0.0, 0.0], 

838 [0.0, 0.0, 0.0, 0.0, 0.0], 

839 [0.0, 0.0, 0.0, 0.0, 0.0], 

840 ], 

841 [ 

842 [0.0, 0.0, 0.0, 0.0, 0.0], 

843 [0.0, 0.0, 1.5, 0.0, 0.0], 

844 [0.0, 2.0, 1.0, 3.5, 0.0], 

845 [0.0, 0.0, 3.0, 0.0, 0.0], 

846 [0.0, 0.0, 0.0, 0.0, 0.0], 

847 ], 

848 [ 

849 [0.0, 0.0, 2.0, 0.0, 0.0], 

850 [0.0, 2.5, 2.0, 4.0, 0.0], 

851 [3.0, 3.0, 3.5, 6.0, 6.0], 

852 [0.0, 4.0, 5.0, 5.5, 0.0], 

853 [0.0, 0.0, 5.0, 0.0, 0.0], 

854 ], 

855 [ 

856 [0.0, 0.0, 0.0, 0.0, 0.0], 

857 [0.0, 0.0, 3.0, 0.0, 0.0], 

858 [0.0, 3.5, 4.0, 5.0, 0.0], 

859 [0.0, 0.0, 4.5, 0.0, 0.0], 

860 [0.0, 0.0, 0.0, 0.0, 0.0], 

861 ], 

862 [ 

863 [0.0, 0.0, 0.0, 0.0, 0.0], 

864 [0.0, 0.0, 0.0, 0.0, 0.0], 

865 [0.0, 0.0, 4.0, 0.0, 0.0], 

866 [0.0, 0.0, 0.0, 0.0, 0.0], 

867 [0.0, 0.0, 0.0, 0.0, 0.0], 

868 ], 

869 ] 

870 ) 

871 target_mask = np.array( 

872 [ 

873 [ 

874 [False, False, False, False, False], 

875 [False, False, False, False, False], 

876 [False, False, True, False, False], 

877 [False, False, False, False, False], 

878 [False, False, False, False, False], 

879 ], 

880 [ 

881 [False, False, False, False, False], 

882 [False, False, True, False, False], 

883 [False, True, True, True, False], 

884 [False, False, True, False, False], 

885 [False, False, False, False, False], 

886 ], 

887 [ 

888 [False, False, True, False, False], 

889 [False, True, True, True, False], 

890 [True, True, True, True, True], 

891 [False, True, True, True, False], 

892 [False, False, True, False, False], 

893 ], 

894 [ 

895 [False, False, False, False, False], 

896 [False, False, True, False, False], 

897 [False, True, True, True, False], 

898 [False, False, True, False, False], 

899 [False, False, False, False, False], 

900 ], 

901 [ 

902 [False, False, False, False, False], 

903 [False, False, False, False, False], 

904 [False, False, True, False, False], 

905 [False, False, False, False, False], 

906 [False, False, False, False, False], 

907 ], 

908 ] 

909 ) 

910 

911 # Test: 

912 extrapolated_data, extrapolated_mask = extrapolate_out_mask( 

913 initial_data, initial_mask, iterations=1 

914 ) 

915 

916 assert_array_equal(extrapolated_data, target_data) 

917 assert_array_equal(extrapolated_mask, target_mask) 

918 

919 

920@pytest.mark.parametrize("ndim", range(4)) 

921def test_unmask_from_to_3d_array(rng, ndim): 

922 """Test unmask_from_to_3d_array.""" 

923 size = 5 

924 shape = [size] * ndim 

925 mask = np.zeros(shape).astype(bool) 

926 mask[rng.uniform(size=shape) > 0.8] = 1 

927 support = rng.standard_normal(size=mask.sum()) 

928 

929 full = unmask_from_to_3d_array(support, mask) 

930 

931 assert_array_equal(full.shape, shape) 

932 assert_array_equal(full[mask], support)