Coverage for nilearn/regions/tests/test_signal_extraction.py: 0%

309 statements  

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

1"""Test for "region" module.""" 

2 

3import warnings 

4 

5import numpy as np 

6import pytest 

7from nibabel import Nifti1Image 

8from numpy.testing import assert_almost_equal, assert_equal 

9 

10from nilearn._utils.data_gen import ( 

11 generate_fake_fmri, 

12 generate_labeled_regions, 

13 generate_maps, 

14 generate_timeseries, 

15) 

16from nilearn._utils.exceptions import DimensionError 

17from nilearn._utils.testing import write_imgs_to_path 

18from nilearn.conftest import _affine_eye, _shape_3d_default 

19from nilearn.image import get_data, new_img_like 

20from nilearn.maskers import NiftiLabelsMasker 

21from nilearn.regions.signal_extraction import ( 

22 _check_shape_and_affine_compatibility, 

23 _trim_maps, 

24 img_to_signals_labels, 

25 img_to_signals_maps, 

26 signals_to_img_labels, 

27 signals_to_img_maps, 

28) 

29 

30_3D_EXPECTED_ERROR_MSG = ( 

31 "Input data has incompatible dimensionality: " 

32 "Expected dimension is 3D and you provided " 

33 "a 4D image" 

34) 

35 

36_4D_EXPECTED_ERROR_MSG = ( 

37 "Input data has incompatible dimensionality: " 

38 "Expected dimension is 4D and you provided " 

39 "a 3D image" 

40) 

41 

42SHAPE_ERROR_MSG = "Images have incompatible shapes." 

43 

44AFFINE_ERROR_MSG = "Images have different affine matrices." 

45 

46EPS = np.finfo(np.float64).eps 

47 

48INF = 1000 * np.finfo(np.float32).eps 

49 

50N_REGIONS = 8 

51 

52N_TIMEPOINTS = 17 

53 

54 

55def _make_label_data(shape=None): 

56 if shape is None: 

57 shape = _shape_3d_default() 

58 labels_data = np.zeros(shape, dtype="int32") 

59 h0, h1, h2 = (s // 2 for s in shape) 

60 labels_data[:h0, :h1, :h2] = 1 

61 labels_data[:h0, :h1, h2:] = 2 

62 labels_data[:h0, h1:, :h2] = 3 

63 labels_data[:h0, h1:, h2:] = 4 

64 labels_data[h0:, :h1, :h2] = 5 

65 labels_data[h0:, :h1, h2:] = 6 

66 labels_data[h0:, h1:, :h2] = 7 

67 labels_data[h0:, h1:, h2:] = 8 

68 return labels_data 

69 

70 

71def _create_mask_with_3_regions_from_labels_data(labels_data, affine): 

72 """Create a mask containing only 3 regions.""" 

73 mask_data = (labels_data == 1) + (labels_data == 2) + (labels_data == 5) 

74 return Nifti1Image(mask_data.astype(np.int8), affine) 

75 

76 

77@pytest.fixture 

78def labels_data(): 

79 return _make_label_data() 

80 

81 

82@pytest.fixture 

83def labels_img(): 

84 return Nifti1Image(_make_label_data(_shape_3d_default()), _affine_eye()) 

85 

86 

87@pytest.fixture 

88def mask_img(): 

89 mask_data = np.zeros(_shape_3d_default()) 

90 mask_data[1:-1, 1:-1, 1:-1] = 1 

91 return Nifti1Image(mask_data, _affine_eye()) 

92 

93 

94@pytest.fixture 

95def signals(): 

96 return generate_timeseries(n_timepoints=N_TIMEPOINTS, n_features=N_REGIONS) 

97 

98 

99@pytest.fixture 

100def fmri_img(): 

101 return generate_fake_fmri(shape=_shape_3d_default(), affine=_affine_eye())[ 

102 0 

103 ] 

104 

105 

106@pytest.fixture 

107def labeled_regions(): 

108 labels = list(range(N_REGIONS + 1)) # 0 is background 

109 return generate_labeled_regions( 

110 shape=_shape_3d_default(), n_regions=N_REGIONS, labels=labels 

111 ) 

112 

113 

114def _all_voxel_of_each_region_have_same_values( 

115 data, labels_data, n_regions, signals 

116): 

117 for n in range(1, n_regions + 1): 

118 sigs = data[labels_data == n, :] 

119 assert_almost_equal(sigs[0, :], signals[:, n - 1]) 

120 assert abs(sigs - sigs[0, :]).max() < EPS 

121 

122 

123def test_check_shape_and_affine_compatibility_without_dim(img_3d_zeros_eye): 

124 """Ensure correct behavior for valid data without dim.""" 

125 with warnings.catch_warnings(): 

126 warnings.simplefilter("error") 

127 _check_shape_and_affine_compatibility( 

128 img1=img_3d_zeros_eye, img2=img_3d_zeros_eye 

129 ) 

130 

131 

132def test_check_shape_and_affine_compatibility_with_dim( 

133 img_3d_zeros_eye, img_4d_zeros_eye 

134): 

135 """Ensure correct behavior for valid data without dim.""" 

136 with warnings.catch_warnings(): 

137 warnings.simplefilter("error") 

138 _check_shape_and_affine_compatibility( 

139 img1=img_4d_zeros_eye, img2=img_3d_zeros_eye, dim=3 

140 ) 

141 

142 

143@pytest.mark.parametrize( 

144 "test_shape, test_affine, msg", 

145 [ 

146 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

147 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

148 ], 

149) 

150def test_check_shape_and_affine_compatibility_error( 

151 img_3d_zeros_eye, test_shape, test_affine, msg 

152): 

153 img2 = Nifti1Image(np.zeros(test_shape), test_affine) 

154 

155 with pytest.raises(ValueError, match=msg): 

156 _check_shape_and_affine_compatibility(img1=img_3d_zeros_eye, img2=img2) 

157 

158 

159def test_errors_3d(img_3d_zeros_eye, img_4d_zeros_eye): 

160 """Verify that 3D images are refused.""" 

161 wrong_dim_image = img_3d_zeros_eye 

162 

163 with pytest.raises(DimensionError, match=_4D_EXPECTED_ERROR_MSG): 

164 img_to_signals_labels( 

165 imgs=wrong_dim_image, labels_img=img_3d_zeros_eye 

166 ) 

167 

168 with pytest.raises(DimensionError, match=_4D_EXPECTED_ERROR_MSG): 

169 img_to_signals_maps(imgs=img_4d_zeros_eye, maps_img=wrong_dim_image) 

170 

171 

172def test_errors_4d_labels(img_4d_zeros_eye): 

173 """Verify that 4D images are refused.""" 

174 wrong_dim_label_img = img_4d_zeros_eye 

175 

176 with pytest.raises(DimensionError, match=_3D_EXPECTED_ERROR_MSG): 

177 img_to_signals_labels( 

178 imgs=img_4d_zeros_eye, labels_img=wrong_dim_label_img 

179 ) 

180 

181 with pytest.raises(DimensionError, match=_3D_EXPECTED_ERROR_MSG): 

182 signals_to_img_labels( 

183 signals=img_4d_zeros_eye, labels_img=wrong_dim_label_img 

184 ) 

185 

186 

187def test_errors_4d_masks(img_3d_zeros_eye, img_4d_zeros_eye): 

188 """Verify that 4D images are refused.""" 

189 wrong_dim_mask_img = img_4d_zeros_eye 

190 

191 with pytest.raises(DimensionError, match=_3D_EXPECTED_ERROR_MSG): 

192 img_to_signals_labels( 

193 imgs=img_4d_zeros_eye, 

194 labels_img=img_3d_zeros_eye, 

195 mask_img=wrong_dim_mask_img, 

196 ) 

197 

198 with pytest.raises(DimensionError, match=_3D_EXPECTED_ERROR_MSG): 

199 signals_to_img_labels( 

200 signals=img_4d_zeros_eye, 

201 labels_img=img_3d_zeros_eye, 

202 mask_img=wrong_dim_mask_img, 

203 ) 

204 

205 with pytest.raises(DimensionError, match=_3D_EXPECTED_ERROR_MSG): 

206 img_to_signals_maps( 

207 imgs=img_4d_zeros_eye, 

208 maps_img=img_4d_zeros_eye, 

209 mask_img=wrong_dim_mask_img, 

210 ) 

211 

212 

213@pytest.mark.parametrize( 

214 "shape, affine, error_msg", 

215 [ 

216 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

217 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

218 ], 

219) 

220def test_img_to_signals_labels_bad_labels_input( 

221 img_4d_zeros_eye, shape, affine, error_msg 

222): 

223 bad_img = Nifti1Image(np.zeros(shape), affine) 

224 

225 with pytest.raises(ValueError, match=error_msg): 

226 img_to_signals_labels(imgs=img_4d_zeros_eye, labels_img=bad_img) 

227 

228 

229@pytest.mark.parametrize( 

230 "shape, affine, error_msg", 

231 [ 

232 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

233 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

234 ], 

235) 

236def test_img_to_signals_labels_bad_mask_input( 

237 img_4d_zeros_eye, img_3d_zeros_eye, shape, affine, error_msg 

238): 

239 bad_img = Nifti1Image(np.zeros(shape), affine) 

240 

241 with pytest.raises(ValueError, match=error_msg): 

242 img_to_signals_labels( 

243 imgs=img_4d_zeros_eye, 

244 labels_img=img_3d_zeros_eye, 

245 mask_img=bad_img, 

246 ) 

247 

248 

249def test_img_to_signals_labels_error_strategy( 

250 img_4d_zeros_eye, img_3d_zeros_eye 

251): 

252 with pytest.raises(ValueError, match="Invalid strategy"): 

253 img_to_signals_labels( 

254 imgs=img_4d_zeros_eye, labels_img=img_3d_zeros_eye, strategy="foo" 

255 ) 

256 

257 

258@pytest.mark.parametrize( 

259 "shape, affine, error_msg", 

260 [ 

261 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

262 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

263 ], 

264) 

265def test_signals_to_img_labels_bad_label_input( 

266 img_4d_zeros_eye, img_3d_zeros_eye, shape, affine, error_msg 

267): 

268 bad_img = Nifti1Image(np.zeros(shape), affine) 

269 

270 with pytest.raises(ValueError, match=error_msg): 

271 signals_to_img_labels( 

272 signals=img_4d_zeros_eye, 

273 labels_img=bad_img, 

274 mask_img=img_3d_zeros_eye, 

275 ) 

276 

277 

278@pytest.mark.parametrize( 

279 "shape, affine, error_msg", 

280 [ 

281 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

282 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

283 ], 

284) 

285def test_signals_to_img_labels_bad_mask_input( 

286 img_4d_zeros_eye, img_3d_zeros_eye, shape, affine, error_msg 

287): 

288 bad_img = Nifti1Image(np.zeros(shape), affine) 

289 

290 with pytest.raises(ValueError, match=error_msg): 

291 signals_to_img_labels( 

292 signals=img_4d_zeros_eye, 

293 labels_img=img_3d_zeros_eye, 

294 mask_img=bad_img, 

295 ) 

296 

297 

298@pytest.mark.parametrize( 

299 "shape, affine, error_msg", 

300 [ 

301 ((8, 9, 11, 7), _affine_eye(), SHAPE_ERROR_MSG), 

302 ((*_shape_3d_default(), 7), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

303 ], 

304) 

305def test_img_to_signals_maps_bad_maps( 

306 img_4d_zeros_eye, shape, affine, error_msg 

307): 

308 bad_img = Nifti1Image(np.zeros(shape), affine) 

309 

310 with pytest.raises(ValueError, match=error_msg): 

311 img_to_signals_maps( 

312 imgs=img_4d_zeros_eye, 

313 maps_img=bad_img, 

314 ) 

315 

316 

317@pytest.mark.parametrize( 

318 "shape, affine, error_msg", 

319 [ 

320 ((8, 9, 11), _affine_eye(), SHAPE_ERROR_MSG), 

321 (_shape_3d_default(), 2 * _affine_eye(), AFFINE_ERROR_MSG), 

322 ], 

323) 

324def test_img_to_signals_maps_bad_masks( 

325 img_4d_zeros_eye, shape, affine, error_msg 

326): 

327 bad_img = Nifti1Image(np.zeros(shape), affine) 

328 

329 with pytest.raises(ValueError, match=error_msg): 

330 img_to_signals_maps( 

331 imgs=img_4d_zeros_eye, maps_img=img_4d_zeros_eye, mask_img=bad_img 

332 ) 

333 

334 

335def test_signals_extraction_with_labels_without_mask( 

336 signals, labels_data, labels_img, shape_3d_default, tmp_path 

337): 

338 """Test conversion between signals and images \ 

339 using regions defined by labels. 

340 """ 

341 data_img = signals_to_img_labels(signals=signals, labels_img=labels_img) 

342 

343 assert data_img.shape == (*shape_3d_default, N_TIMEPOINTS) 

344 data = get_data(data_img) 

345 assert np.all(data.std(axis=-1) > 0) 

346 # There must be non-zero data (safety net) 

347 assert abs(data).max() > 1e-9 

348 

349 _all_voxel_of_each_region_have_same_values( 

350 data, labels_data, N_REGIONS, signals 

351 ) 

352 

353 # and back 

354 signals_r, labels_r = img_to_signals_labels( 

355 imgs=data_img, labels_img=labels_img 

356 ) 

357 

358 assert_almost_equal(signals_r, signals) 

359 assert labels_r == list(range(1, 9)) 

360 

361 filenames = write_imgs_to_path(data_img, file_path=tmp_path) 

362 signals_r, labels_r = img_to_signals_labels( 

363 imgs=filenames, labels_img=labels_img 

364 ) 

365 

366 assert_almost_equal(signals_r, signals) 

367 assert labels_r == list(range(1, 9)) 

368 

369 

370def test_signals_extraction_with_labels_without_mask_return_masked_atlas( 

371 signals, labels_img 

372): 

373 """Test masked_atlas is correct in conversion between signals and images \ 

374 using regions defined by labels. 

375 """ 

376 data_img = signals_to_img_labels(signals=signals, labels_img=labels_img) 

377 

378 # test return_masked_atlas 

379 ( 

380 _, 

381 _, 

382 masked_atlas_r, 

383 ) = img_to_signals_labels( 

384 imgs=data_img, 

385 labels_img=labels_img, 

386 return_masked_atlas=True, 

387 ) 

388 

389 labels_data = get_data(labels_img) 

390 labels_data_r = get_data(masked_atlas_r) 

391 

392 # masked_atlas_r should be the same as labels_img 

393 assert_equal(labels_data_r, labels_data) 

394 

395 # labels should be the same as before 

396 # the labels_img does not contain background 

397 assert list(np.unique(labels_data_r)) == list(range(1, 9)) 

398 

399 

400def test_signals_extraction_with_labels_with_mask( 

401 signals, labels_img, labels_data, mask_img, shape_3d_default, tmp_path 

402): 

403 """Test conversion between signals and images \ 

404 using regions defined by labels with a mask. 

405 """ 

406 data_img = signals_to_img_labels( 

407 signals=signals, labels_img=labels_img, mask_img=mask_img 

408 ) 

409 

410 assert data_img.shape == (*shape_3d_default, N_TIMEPOINTS) 

411 # There must be non-zero data (safety net) 

412 data = get_data(data_img) 

413 assert abs(data).max() > 1e-9 

414 

415 # Zero outside of the mask 

416 assert np.all(data[np.logical_not(get_data(mask_img))].std(axis=-1) < EPS) 

417 

418 filenames = write_imgs_to_path(labels_img, mask_img, file_path=tmp_path) 

419 data_img = signals_to_img_labels( 

420 signals=signals, labels_img=filenames[0], mask_img=filenames[1] 

421 ) 

422 

423 assert data_img.shape == (*shape_3d_default, N_TIMEPOINTS) 

424 data = get_data(data_img) 

425 assert abs(data).max() > 1e-9 

426 # Zero outside of the mask 

427 assert np.all(data[np.logical_not(get_data(mask_img))].std(axis=-1) < EPS) 

428 

429 # mask labels before checking 

430 masked_labels_data = labels_data.copy() 

431 masked_labels_data[np.logical_not(get_data(mask_img))] = 0 

432 _all_voxel_of_each_region_have_same_values( 

433 data, masked_labels_data, N_REGIONS, signals 

434 ) 

435 

436 # and back 

437 signals_r, labels_r = img_to_signals_labels( 

438 imgs=data_img, labels_img=labels_img, mask_img=mask_img 

439 ) 

440 

441 assert_almost_equal(signals_r, signals) 

442 assert labels_r == list(range(1, 9)) 

443 

444 

445def test_signals_extraction_with_labels_with_mask_return_masked_atlas( 

446 signals, labels_img, mask_img 

447): 

448 """Test masked_atlas is correct in conversion between signals and images \ 

449 using regions defined by labels and a mask. 

450 """ 

451 data_img = signals_to_img_labels( 

452 signals=signals, labels_img=labels_img, mask_img=mask_img 

453 ) 

454 

455 # test return_masked_atlas 

456 # create a mask_img with only 3 regions 

457 mask_img = _create_mask_with_3_regions_from_labels_data( 

458 get_data(labels_img), labels_img.affine 

459 ) 

460 

461 ( 

462 _, 

463 _, 

464 masked_atlas_r, 

465 ) = img_to_signals_labels( 

466 imgs=data_img, 

467 labels_img=labels_img, 

468 mask_img=mask_img, 

469 return_masked_atlas=True, 

470 ) 

471 

472 labels_data_r = get_data(masked_atlas_r) 

473 

474 # labels should be masked and only contain 3 regions 

475 # and the background 

476 assert list(np.unique(labels_data_r)) == [0, 1, 2, 5] 

477 

478 

479def test_signal_extraction_with_maps(affine_eye, shape_3d_default, rng): 

480 # Generate signal imgs 

481 maps_img, mask_img = generate_maps(shape_3d_default, N_REGIONS) 

482 maps_data = get_data(maps_img) 

483 data = np.zeros((*shape_3d_default, N_TIMEPOINTS)) 

484 signals = np.zeros((N_TIMEPOINTS, maps_data.shape[-1])) 

485 for n in range(maps_data.shape[-1]): 

486 signals[:, n] = rng.standard_normal(size=N_TIMEPOINTS) 

487 data[maps_data[..., n] > 0, :] = signals[:, n] 

488 imgs = Nifti1Image(data, affine_eye) 

489 

490 # Get signals 

491 signals_r, _ = img_to_signals_maps( 

492 imgs=imgs, maps_img=maps_img, mask_img=mask_img 

493 ) 

494 assert_almost_equal(signals, signals_r) 

495 

496 # Recover image 

497 img_r = signals_to_img_maps(signals, maps_img, mask_img=mask_img) 

498 assert_almost_equal(get_data(img_r), get_data(imgs)) 

499 

500 # Same thing without mask 

501 signals_r, _ = img_to_signals_maps(imgs, maps_img) 

502 assert_almost_equal(signals, signals_r) 

503 img_r = signals_to_img_maps(signals, maps_img) 

504 assert_almost_equal(get_data(img_r), get_data(imgs)) 

505 

506 

507def test_signal_extraction_with_maps_and_labels( 

508 labeled_regions, fmri_img, shape_3d_default 

509): 

510 labels = list(range(N_REGIONS + 1)) 

511 labels_data = get_data(labeled_regions) 

512 # Convert to maps 

513 maps_data = np.zeros((*shape_3d_default, N_REGIONS)) 

514 for n, l in enumerate(labels): 

515 if n == 0: 

516 continue 

517 maps_data[labels_data == l, n - 1] = 1 

518 

519 maps_img = Nifti1Image(maps_data, labeled_regions.affine) 

520 

521 # Extract signals from maps and labels: results must be identical. 

522 maps_signals, maps_labels = img_to_signals_maps(fmri_img, maps_img) 

523 labels_signals, labels_labels = img_to_signals_labels( 

524 imgs=fmri_img, labels_img=labeled_regions 

525 ) 

526 assert_almost_equal(maps_signals, labels_signals) 

527 

528 # Same thing with a mask, containing only 3 regions. 

529 mask_img = _create_mask_with_3_regions_from_labels_data( 

530 labels_data, labeled_regions.affine 

531 ) 

532 labels_signals, labels_labels = img_to_signals_labels( 

533 imgs=fmri_img, labels_img=labeled_regions, mask_img=mask_img 

534 ) 

535 maps_signals, maps_labels = img_to_signals_maps( 

536 fmri_img, maps_img, mask_img=mask_img 

537 ) 

538 

539 assert_almost_equal(maps_signals, labels_signals) 

540 assert maps_signals.shape[1] == N_REGIONS 

541 assert maps_labels == list(range(len(maps_labels))) 

542 assert labels_signals.shape == (N_TIMEPOINTS, N_REGIONS) 

543 assert labels_labels == labels[1:] 

544 

545 # Inverse operation (mostly smoke test) 

546 labels_img_r = signals_to_img_labels( 

547 labels_signals, labeled_regions, mask_img=mask_img 

548 ) 

549 assert labels_img_r.shape == (*shape_3d_default, N_TIMEPOINTS) 

550 

551 maps_img_r = signals_to_img_maps(maps_signals, maps_img, mask_img=mask_img) 

552 assert maps_img_r.shape == (*shape_3d_default, N_TIMEPOINTS) 

553 

554 

555def test_img_to_signals_labels_warnings(labeled_regions, fmri_img): 

556 labels_data = get_data(labeled_regions) 

557 

558 mask_img = _create_mask_with_3_regions_from_labels_data( 

559 labels_data, labeled_regions.affine 

560 ) 

561 

562 # apply img_to_signals_labels with a masking, 

563 # containing only 3 regions, but 

564 # not keeping the masked labels 

565 with pytest.warns( 

566 UserWarning, 

567 match="After applying mask to the labels image, " 

568 "the following labels were " 

569 r"removed: \{3, 4, 6, 7, 8\}. " 

570 "Out of 9 labels, the " 

571 "masked labels image only contains " 

572 "4 labels " 

573 r"\(including background\).", 

574 ): 

575 labels_signals, labels_labels = img_to_signals_labels( 

576 imgs=fmri_img, 

577 labels_img=labeled_regions, 

578 mask_img=mask_img, 

579 keep_masked_labels=False, 

580 ) 

581 

582 # only 3 regions must be kept, others must be removed 

583 assert labels_signals.shape == (N_TIMEPOINTS, 3) 

584 assert len(labels_labels) == 3 

585 

586 # apply img_to_signals_labels with a masking, 

587 # containing only 3 regions, and 

588 # keeping the masked labels 

589 # test if the warning is raised 

590 

591 with pytest.warns( 

592 DeprecationWarning, 

593 match='Applying "mask_img" before ' 

594 "signal extraction may result in empty region signals in " 

595 "the output. These are currently kept. " 

596 "Starting from version 0.13, the default behavior will be " 

597 "changed to remove them by setting " 

598 '"keep_masked_labels=False". ' 

599 '"keep_masked_labels" parameter will be removed ' 

600 "in version 0.15.", 

601 ): 

602 labels_signals, labels_labels = img_to_signals_labels( 

603 imgs=fmri_img, 

604 labels_img=labeled_regions, 

605 mask_img=mask_img, 

606 keep_masked_labels=True, 

607 ) 

608 

609 # all regions must be kept 

610 assert labels_signals.shape == (N_TIMEPOINTS, 8) 

611 assert len(labels_labels) == 8 

612 

613 # test return_masked_atlas deprecation warning 

614 with pytest.warns( 

615 DeprecationWarning, 

616 match='After version 0.13. "img_to_signals_labels" will also return ' 

617 'the "masked_atlas". Meanwhile "return_masked_atlas" parameter can be ' 

618 "used to toggle this behavior. In version 0.15, " 

619 '"return_masked_atlas" parameter will be removed.', 

620 ): 

621 img_to_signals_labels( 

622 imgs=fmri_img, 

623 labels_img=labeled_regions, 

624 mask_img=mask_img, 

625 keep_masked_labels=False, 

626 return_masked_atlas=False, 

627 ) 

628 

629 

630def test_img_to_signals_maps_warnings( 

631 labeled_regions, fmri_img, shape_3d_default 

632): 

633 labels = list(range(N_REGIONS + 1)) 

634 labels_data = get_data(labeled_regions) 

635 # Convert to maps 

636 maps_data = np.zeros((*shape_3d_default, N_REGIONS)) 

637 for n, l in enumerate(labels): 

638 if n == 0: 

639 continue 

640 maps_data[labels_data == l, n - 1] = 1 

641 

642 maps_img = Nifti1Image(maps_data, labeled_regions.affine) 

643 

644 mask_img = _create_mask_with_3_regions_from_labels_data( 

645 labels_data, labeled_regions.affine 

646 ) 

647 

648 # apply img_to_signals_maps with a masking, 

649 # containing only 3 regions, but 

650 # not keeping the masked maps 

651 with pytest.warns( 

652 UserWarning, 

653 match="After applying mask to the maps image, " 

654 "maps with the following indices were " 

655 r"removed: \{2, 3, 5, 6, 7\}. " 

656 "Out of 8 maps, the " 

657 "masked map image only contains " 

658 "3 maps.", 

659 ): 

660 maps_signals, maps_labels = img_to_signals_maps( 

661 fmri_img, maps_img, mask_img=mask_img, keep_masked_maps=False 

662 ) 

663 

664 # only 3 regions must be kept, others must be removed 

665 assert maps_signals.shape == (N_TIMEPOINTS, 3) 

666 assert len(maps_labels) == 3 

667 

668 # apply img_to_signals_labels with a masking, 

669 # containing only 3 regions, and 

670 # keeping the masked labels 

671 # test if the warning is raised 

672 with pytest.warns( 

673 DeprecationWarning, 

674 match='Applying "mask_img" before ' 

675 "signal extraction may result in empty region signals in the " 

676 "output. These are currently kept. " 

677 "Starting from version 0.13, the default behavior will be " 

678 "changed to remove them by setting " 

679 '"keep_masked_maps=False". ' 

680 '"keep_masked_maps" parameter will be removed ' 

681 "in version 0.15.", 

682 ): 

683 maps_signals, maps_labels = img_to_signals_maps( 

684 fmri_img, maps_img, mask_img=mask_img, keep_masked_maps=True 

685 ) 

686 

687 # all regions must be kept 

688 assert maps_signals.shape == (N_TIMEPOINTS, 8) 

689 assert len(maps_labels) == 8 

690 

691 

692def test_signal_extraction_nans_in_regions_are_replaced_with_zeros(): 

693 shape = (4, 5, 6) 

694 labels = list(range(N_REGIONS + 1)) # 0 is background 

695 labels_img = generate_labeled_regions(shape, N_REGIONS, labels=labels) 

696 labels_data = get_data(labels_img) 

697 fmri_img, _ = generate_fake_fmri( 

698 shape=shape, affine=labels_img.affine, length=N_TIMEPOINTS 

699 ) 

700 

701 mask_img = _create_mask_with_3_regions_from_labels_data( 

702 labels_data, labels_img.affine 

703 ) 

704 

705 region1 = labels_data == 2 

706 indices = tuple(ind[:1] for ind in np.where(region1)) 

707 get_data(fmri_img)[indices] = np.nan 

708 

709 labels_signals, labels_labels = img_to_signals_labels( 

710 imgs=fmri_img, labels_img=labels_img, mask_img=mask_img 

711 ) 

712 

713 assert np.all(labels_signals[:, labels_labels.index(2)] == 0.0) 

714 

715 

716def test_trim_maps(shape_3d_default): 

717 # maps 

718 maps_data = np.zeros((*shape_3d_default, N_REGIONS), dtype=np.float32) 

719 h0, h1, h2 = (s // 2 for s in shape_3d_default) 

720 maps_data[:h0, :h1, :h2, 0] = 1 

721 maps_data[:h0, :h1, h2:, 1] = 1.1 

722 maps_data[:h0, h1:, :h2, 2] = 1 

723 maps_data[:h0, h1:, h2:, 3] = 0.5 

724 maps_data[h0:, :h1, :h2, 4] = 1 

725 maps_data[h0:, :h1, h2:, 5] = 1.4 

726 maps_data[h0:, h1:, :h2, 6] = 1 

727 maps_data[h0:, h1:, h2:, 7] = 1 

728 

729 # mask intersecting all regions 

730 mask_data = np.zeros(shape_3d_default, dtype=np.int8) 

731 mask_data[1:-1, 1:-1, 1:-1] = 1 

732 

733 maps_i, maps_i_mask, maps_i_indices = _trim_maps(maps_data, mask_data) 

734 

735 assert maps_i.flags["F_CONTIGUOUS"] 

736 assert len(maps_i_indices) == maps_i.shape[-1] 

737 assert maps_i.shape == maps_data.shape 

738 maps_i_correct = maps_data.copy() 

739 maps_i_correct[np.logical_not(mask_data), :] = 0 

740 assert_almost_equal(maps_i_correct, maps_i) 

741 assert_equal(mask_data, maps_i_mask) 

742 assert_equal(np.asarray(list(range(8))), maps_i_indices) 

743 

744 # mask intersecting half of the regions 

745 mask_data = np.zeros(shape_3d_default, dtype=np.int8) 

746 mask_data[1:2, 1:-1, 1:-1] = 1 

747 maps_data[1, 1, 1, 0] = 0 # remove one point inside mask 

748 

749 maps_i, maps_i_mask, maps_i_indices = _trim_maps(maps_data, mask_data) 

750 

751 assert maps_i.flags["F_CONTIGUOUS"] 

752 assert len(maps_i_indices) == maps_i.shape[-1] 

753 assert maps_i.shape == (maps_data.shape[:3] + (4,)) 

754 maps_i_correct = maps_data[..., :4].copy() 

755 maps_i_correct[np.logical_not(mask_data), :] = 0 

756 assert_almost_equal(maps_i_correct, maps_i) 

757 mask_data[1, 1, 1] = 0 # for test to succeed 

758 assert_equal(mask_data, maps_i_mask) 

759 mask_data[1, 1, 1] = 1 # reset, just in case. 

760 assert_equal(np.asarray(list(range(4))), maps_i_indices) 

761 

762 

763@pytest.mark.parametrize( 

764 "target_dtype", 

765 (float, np.float32, np.float64, int, np.uint), 

766) 

767def test_img_to_signals_labels_non_float_type(target_dtype, rng): 

768 fake_fmri_data = rng.uniform(size=(10, 10, 10, N_TIMEPOINTS)) > 0.5 

769 fake_affine = np.eye(4, 4).astype(np.float64) 

770 fake_fmri_img_orig = Nifti1Image( 

771 fake_fmri_data.astype(np.float64), fake_affine 

772 ) 

773 fake_fmri_img_target_dtype = new_img_like( 

774 fake_fmri_img_orig, fake_fmri_data.astype(target_dtype) 

775 ) 

776 

777 fake_mask_data = np.zeros((10, 10, 10), dtype=np.uint8) 

778 fake_mask_data[1:8, 1:8, 1:8] = 1 

779 fake_mask = Nifti1Image(fake_mask_data, fake_affine) 

780 

781 masker = NiftiLabelsMasker(fake_mask) 

782 masker.fit() 

783 

784 timeseries_int = masker.transform(fake_fmri_img_target_dtype) 

785 timeseries_float = masker.transform(fake_fmri_img_orig) 

786 

787 assert np.sum(timeseries_int) != 0 

788 assert np.allclose(timeseries_int, timeseries_float)