Coverage for nilearn/plotting/tests/test_find_cuts.py: 0%

211 statements  

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

1import numpy as np 

2import pytest 

3from nibabel import Nifti1Image 

4from numpy.testing import assert_allclose, assert_array_equal 

5 

6from nilearn.masking import compute_epi_mask 

7from nilearn.plotting.find_cuts import ( 

8 _transform_cut_coords, 

9 find_cut_slices, 

10 find_parcellation_cut_coords, 

11 find_probabilistic_atlas_cut_coords, 

12 find_xyz_cut_coords, 

13) 

14 

15 

16def test_find_cut_coords(affine_eye): 

17 """Test find_xyz_cut_coords.""" 

18 data = np.zeros((100, 100, 100)) 

19 x_map, y_map, z_map = 50, 10, 40 

20 data[ 

21 x_map - 30 : x_map + 30, y_map - 3 : y_map + 3, z_map - 10 : z_map + 10 

22 ] = 1 

23 

24 # identity affine 

25 img = Nifti1Image(data, affine_eye) 

26 mask_img = compute_epi_mask(img) 

27 

28 x, y, z = find_xyz_cut_coords(img, mask_img=mask_img) 

29 

30 assert_allclose( 

31 (x, y, z), 

32 (x_map, y_map, z_map), 

33 # Need such a high tolerance for the test to 

34 # pass. x, y, z = [49.5, 9.5, 39.5] 

35 rtol=6e-2, 

36 ) 

37 

38 # non-trivial affine 

39 affine = np.diag([1.0 / 2, 1 / 3.0, 1 / 4.0, 1.0]) 

40 img = Nifti1Image(data, affine) 

41 mask_img = compute_epi_mask(img) 

42 

43 x, y, z = find_xyz_cut_coords(img, mask_img=mask_img) 

44 

45 assert_allclose( 

46 (x, y, z), 

47 (x_map / 2.0, y_map / 3.0, z_map / 4.0), 

48 # Need such a high tolerance for the test to 

49 # pass. x, y, z = [24.75, 3.17, 9.875] 

50 rtol=6e-2, 

51 ) 

52 

53 

54def test_no_data_exceeds_activation_threshold(affine_eye): 

55 """Test when no data exceeds the activation threshold. 

56 

57 Cut coords should be the center of mass rather than 

58 the center of the image (10, 10, 10). 

59 

60 regression test 

61 https://github.com/nilearn/nilearn/issues/473 

62 """ 

63 data = np.ones((36, 43, 36)) 

64 img = Nifti1Image(data, affine_eye) 

65 

66 with pytest.warns(UserWarning, match="All voxels were masked."): 

67 x, y, z = find_xyz_cut_coords(img, activation_threshold=1.1) 

68 

69 assert_array_equal([x, y, z], [17.5, 21.0, 17.5]) 

70 

71 data = np.zeros((20, 20, 20)) 

72 data[4:6, 4:6, 4:6] = 1000 

73 img = Nifti1Image(data, 2 * affine_eye) 

74 mask_data = np.ones((20, 20, 20), dtype="uint8") 

75 mask_img = Nifti1Image(mask_data, 2 * affine_eye) 

76 

77 cut_coords = find_xyz_cut_coords(img, mask_img=mask_img) 

78 

79 assert_array_equal(cut_coords, [9.0, 9.0, 9.0]) 

80 

81 

82def test_warning_all_voxels_masked(affine_eye): 

83 """Warning when all values are masked. 

84 

85 And that the center of mass is returned. 

86 """ 

87 data = np.zeros((20, 20, 20)) 

88 data[4:6, 4:6, 4:6] = 1000 

89 img = Nifti1Image(data, affine_eye) 

90 

91 mask_data = np.ones((20, 20, 20), dtype="uint8") 

92 mask_data[np.argwhere(data == 1000)] = 0 

93 mask_img = Nifti1Image(mask_data, affine_eye) 

94 

95 with pytest.warns( 

96 UserWarning, 

97 match=("Could not determine cut coords: All values were masked."), 

98 ): 

99 cut_coords = find_xyz_cut_coords(img, mask_img=mask_img) 

100 

101 assert_array_equal(cut_coords, [4.5, 4.5, 4.5]) 

102 

103 

104def test_warning_all_voxels_masked_thresholding(affine_eye): 

105 """Warn when all values are masked due to thresholding. 

106 

107 Also return the center of mass is returned. 

108 """ 

109 data = np.zeros((20, 20, 20)) 

110 data[4:6, 4:6, 4:6] = 1000 

111 img = Nifti1Image(data, affine_eye) 

112 

113 mask_data = np.ones((20, 20, 20), dtype="uint8") 

114 

115 mask_img = Nifti1Image(mask_data, affine_eye) 

116 

117 with pytest.warns( 

118 UserWarning, 

119 match=( 

120 "Could not determine cut coords: " 

121 "All voxels were masked by the thresholding." 

122 ), 

123 ): 

124 cut_coords = find_xyz_cut_coords( 

125 img, mask_img=mask_img, activation_threshold=10**3 

126 ) 

127 

128 assert_array_equal(cut_coords, [4.5, 4.5, 4.5]) 

129 

130 

131def test_pseudo_4d_image(rng, shape_3d_default, affine_eye): 

132 """Check pseudo-4D images as input (i.e., X, Y, Z, 1). 

133 

134 Previously raised "ValueError: too many values to unpack" 

135 regression test 

136 https://github.com/nilearn/nilearn/issues/922 

137 """ 

138 data_3d = rng.standard_normal(size=shape_3d_default) 

139 data_4d = data_3d[..., np.newaxis] 

140 img_3d = Nifti1Image(data_3d, affine_eye) 

141 img_4d = Nifti1Image(data_4d, affine_eye) 

142 

143 assert find_xyz_cut_coords(img_3d) == find_xyz_cut_coords(img_4d) 

144 

145 

146def test_empty_image_ac_pc_line(img_3d_zeros_eye): 

147 """Pass empty image returns coordinates pointing to AC-PC line.""" 

148 with pytest.warns(UserWarning, match="Given img is empty."): 

149 cut_coords = find_xyz_cut_coords(img_3d_zeros_eye) 

150 

151 assert cut_coords == [0.0, 0.0, 0.0] 

152 

153 

154@pytest.mark.parametrize("direction", ["x", "z"]) 

155def test_find_cut_slices(affine_eye, direction): 

156 """Test find_cut_slices. 

157 

158 Test that 

159 

160 - we are indeed getting the right number of cuts 

161 

162 - we are not getting cuts that are separated by 

163 less than the minimum spacing that we asked for 

164 

165 - the cuts indeed go through the 'activated' part of the data 

166 """ 

167 data = np.zeros((50, 50, 50)) 

168 x_map, y_map, z_map = 25, 5, 20 

169 data[ 

170 x_map - 15 : x_map + 15, y_map - 3 : y_map + 3, z_map - 10 : z_map + 10 

171 ] = 1 

172 img = Nifti1Image(data, affine_eye) 

173 

174 for n_cuts in (2, 4): 

175 cuts = find_cut_slices( 

176 img, direction=direction, n_cuts=n_cuts, spacing=2 

177 ) 

178 

179 assert len(cuts) == n_cuts 

180 assert np.diff(cuts).min() == 2 

181 for cut in cuts: 

182 if direction == "x": 

183 cut_value = data[int(cut)] 

184 elif direction == "z": 

185 cut_value = data[..., int(cut)] 

186 assert cut_value.max() == 1 

187 

188 # Now ask more cuts than it is possible to have with a given spacing 

189 n_cuts = 15 

190 # Only a smoke test 

191 cuts = find_cut_slices(img, direction=direction, n_cuts=n_cuts, spacing=2) 

192 

193 

194def test_find_cut_slices_direction_z(): 

195 """Test find_cut_slices in the z direction. 

196 

197 Test that we are not getting cuts that are separated by 

198 less than the minimum spacing that we asked for. 

199 

200 Done with several affines, voxel size... 

201 """ 

202 data = np.zeros((50, 50, 50)) 

203 x_map, y_map, z_map = 25, 5, 20 

204 data[ 

205 x_map - 15 : x_map + 15, y_map - 3 : y_map + 3, z_map - 10 : z_map + 10 

206 ] = 1 

207 

208 # non-diagonal affines 

209 affine = np.array( 

210 [ 

211 [-1.0, 0.0, 0.0, 123.46980286], 

212 [0.0, 0.0, 1.0, -94.11079407], 

213 [0.0, -1.0, 0.0, 160.694], 

214 [0.0, 0.0, 0.0, 1.0], 

215 ] 

216 ) 

217 img = Nifti1Image(data, affine) 

218 

219 cuts = find_cut_slices(img, direction="z") 

220 

221 assert np.diff(cuts).min() != 0.0 

222 

223 affine = np.array( 

224 [ 

225 [-2.0, 0.0, 0.0, 123.46980286], 

226 [0.0, 0.0, 2.0, -94.11079407], 

227 [0.0, -2.0, 0.0, 160.694], 

228 [0.0, 0.0, 0.0, 1.0], 

229 ] 

230 ) 

231 img = Nifti1Image(data, affine) 

232 

233 cuts = find_cut_slices(img, direction="z") 

234 

235 assert np.diff(cuts).min() != 0.0 

236 

237 # Rotate it slightly 

238 angle = np.pi / 180 * 15 

239 rotation_matrix = np.array( 

240 [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]] 

241 ) 

242 affine[:2, :2] = rotation_matrix * 2.0 

243 img = Nifti1Image(data, affine) 

244 

245 cuts = find_cut_slices(img, direction="z") 

246 

247 assert np.diff(cuts).min() != 0.0 

248 

249 

250@pytest.mark.parametrize( 

251 "n_cuts", (0, -2, -10.00034, 0.999999, 0.4, 0.11111111) 

252) 

253def test_validity_of_ncuts_error_in_find_cut_slices(n_cuts, img_3d_rand_eye): 

254 """Throw error for invalid cut numbers.""" 

255 direction = "z" 

256 

257 message = ( 

258 f"Image has {img_3d_rand_eye.shape[2]} " 

259 f"slices in direction {direction}. " 

260 "Therefore, the number of cuts " 

261 f"must be between 1 and {img_3d_rand_eye.shape[2]}. " 

262 f"You provided n_cuts={n_cuts}." 

263 ) 

264 with pytest.raises(ValueError, match=message): 

265 find_cut_slices(img_3d_rand_eye, n_cuts=n_cuts) 

266 

267 

268@pytest.mark.parametrize("n_cuts", (1, 5.0, 0.9999999, 2.000000004)) 

269def test_passing_of_ncuts_in_find_cut_slices(n_cuts, img_mask_mni): 

270 """Test valid cut numbers: check if it rounds the floating point inputs.""" 

271 cut1 = find_cut_slices(img_mask_mni, direction="x", n_cuts=n_cuts) 

272 cut2 = find_cut_slices(img_mask_mni, direction="x", n_cuts=round(n_cuts)) 

273 

274 assert_array_equal(cut1, cut2) 

275 

276 

277def test_singleton_ax_dim(affine_eye): 

278 for axis, direction in enumerate("xyz"): 

279 shape = [5, 6, 7] 

280 shape[axis] = 1 

281 img = Nifti1Image(np.ones(shape), affine_eye) 

282 find_cut_slices(img, direction=direction) 

283 

284 

285@pytest.mark.parametrize("direction", ["x", "y", "z"]) 

286def test_tranform_cut_coords_return_iterable(affine_eye, direction): 

287 """Test that when n_cuts is 1 we do get an iterable.""" 

288 assert hasattr( 

289 _transform_cut_coords([4], direction, affine_eye), "__iter__" 

290 ) 

291 

292 

293@pytest.mark.parametrize("direction", ["x", "y", "z"]) 

294def test_tranform_cut_coords_n_cuts(affine_eye, direction): 

295 """Test that n_cuts after as before function call.""" 

296 n_cuts = 5 

297 cut_coords = np.arange(n_cuts) 

298 

299 assert ( 

300 len(_transform_cut_coords(cut_coords, direction, affine_eye)) == n_cuts 

301 ) 

302 

303 

304def test_find_cuts_empty_mask_no_crash(affine_eye): 

305 img = Nifti1Image(np.ones((2, 2, 2)), affine_eye) 

306 mask_img = compute_epi_mask(img) 

307 with pytest.warns(UserWarning): 

308 cut_coords = find_xyz_cut_coords(img, mask_img=mask_img) 

309 assert_array_equal(cut_coords, [0.5, 0.5, 0.5]) 

310 

311 

312def test_fast_abs_percentile_no_index_error_find_cuts(affine_eye): 

313 # check that find_cuts functions are safe 

314 data = np.array([[[1.0, 2.0], [3.0, 4.0]], [[0.0, 0.0], [0.0, 0.0]]]) 

315 img = Nifti1Image(data, affine_eye) 

316 assert len(find_xyz_cut_coords(img)) == 3 

317 

318 

319def _parcellation_3_roi( 

320 x_map_a, 

321 y_map_a, 

322 z_map_a, 

323 x_map_b, 

324 y_map_b, 

325 z_map_b, 

326 x_map_c, 

327 y_map_c, 

328 z_map_c, 

329): 

330 """Return data defining 3 parcellations.""" 

331 data = np.zeros((100, 100, 100)) 

332 

333 data[ 

334 x_map_a - 10 : x_map_a + 10, 

335 y_map_a - 10 : y_map_a + 10, 

336 z_map_a - 10 : z_map_a + 10, 

337 ] = 2301 

338 data[ 

339 x_map_b - 10 : x_map_b + 10, 

340 y_map_b - 10 : y_map_b + 10, 

341 z_map_b - 10 : z_map_b + 10, 

342 ] = 4001 

343 data[ 

344 x_map_c - 10 : x_map_c + 10, 

345 y_map_c - 10 : y_map_c + 10, 

346 z_map_c - 10 : z_map_c + 10, 

347 ] = 6201 

348 

349 return data 

350 

351 

352def test_find_parcellation_cut_coords(affine_eye): 

353 """Test find_parcellation_cut_coords on simple affine.""" 

354 x_map_a, y_map_a, z_map_a = (10, 10, 10) 

355 x_map_b, y_map_b, z_map_b = (30, 30, 30) 

356 x_map_c, y_map_c, z_map_c = (50, 50, 50) 

357 

358 data = _parcellation_3_roi( 

359 x_map_a, 

360 y_map_a, 

361 z_map_a, 

362 x_map_b, 

363 y_map_b, 

364 z_map_b, 

365 x_map_c, 

366 y_map_c, 

367 z_map_c, 

368 ) 

369 

370 # Number of labels 

371 labels = np.unique(data) 

372 labels = labels[labels != 0] 

373 n_labels = len(labels) 

374 

375 # identity affine 

376 img = Nifti1Image(data, affine_eye) 

377 

378 # find coordinates with return label names is True 

379 coords, labels_list = find_parcellation_cut_coords( 

380 img, return_label_names=True 

381 ) 

382 

383 # Check outputs 

384 assert (n_labels, 3) == coords.shape 

385 # number of labels in data should equal number of labels list returned 

386 assert n_labels == len(labels_list) 

387 # Labels numbered should match the numbers in returned labels list 

388 assert list(labels) == labels_list 

389 

390 # Match with the number of non-overlapping labels 

391 assert_allclose( 

392 (coords[0][0], coords[0][1], coords[0][2]), 

393 (x_map_a, y_map_a, z_map_a), 

394 rtol=6e-2, 

395 ) 

396 assert_allclose( 

397 (coords[1][0], coords[1][1], coords[1][2]), 

398 (x_map_b, y_map_b, z_map_b), 

399 rtol=6e-2, 

400 ) 

401 assert_allclose( 

402 (coords[2][0], coords[2][1], coords[2][2]), 

403 (x_map_c, y_map_c, z_map_c), 

404 rtol=6e-2, 

405 ) 

406 

407 

408def test_find_parcellation_cut_coords_non_trivial_affine(): 

409 """Test find_parcellation_cut_coords with non-trivial affine.""" 

410 x_map_a, y_map_a, z_map_a = (10, 10, 10) 

411 x_map_b, y_map_b, z_map_b = (30, 30, 30) 

412 x_map_c, y_map_c, z_map_c = (50, 50, 50) 

413 

414 data = _parcellation_3_roi( 

415 x_map_a, 

416 y_map_a, 

417 z_map_a, 

418 x_map_b, 

419 y_map_b, 

420 z_map_b, 

421 x_map_c, 

422 y_map_c, 

423 z_map_c, 

424 ) 

425 

426 # Number of labels 

427 labels = np.unique(data) 

428 labels = labels[labels != 0] 

429 n_labels = len(labels) 

430 

431 affine = np.diag([1 / 2.0, 1 / 3.0, 1 / 4.0, 1.0]) 

432 img = Nifti1Image(data, affine) 

433 

434 coords = find_parcellation_cut_coords(img) 

435 

436 assert (n_labels, 3) == coords.shape 

437 assert_allclose( 

438 (coords[0][0], coords[0][1], coords[0][2]), 

439 (x_map_a / 2.0, y_map_a / 3.0, z_map_a / 4.0), 

440 rtol=6e-2, 

441 ) 

442 assert_allclose( 

443 (coords[1][0], coords[1][1], coords[1][2]), 

444 (x_map_b / 2.0, y_map_b / 3.0, z_map_b / 4.0), 

445 rtol=6e-2, 

446 ) 

447 assert_allclose( 

448 (coords[2][0], coords[2][1], coords[2][2]), 

449 (x_map_c / 2.0, y_map_c / 3.0, z_map_c / 4.0), 

450 rtol=6e-2, 

451 ) 

452 

453 

454def test_find_parcellation_cut_coords_error(img_3d_mni): 

455 """Test error with wrong label_hemisphere name with 'lft'.""" 

456 error_msg = ( 

457 "Invalid label_hemisphere name:lft.\nShould be one of " 

458 "these 'left' or 'right'." 

459 ) 

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

461 find_parcellation_cut_coords( 

462 labels_img=img_3d_mni, label_hemisphere="lft" 

463 ) 

464 

465 

466def test_find_parcellation_cut_coords_hemispheres(affine_mni): 

467 # Create a mock labels_img object 

468 data = np.zeros((10, 10, 10)) 

469 data[2:5, 2:5, 2:5] = 1 # left hemisphere 

470 labels_img = Nifti1Image(data, affine_mni) 

471 

472 # Test when label_hemisphere is "left" 

473 coords, labels = find_parcellation_cut_coords( 

474 labels_img, return_label_names=True, label_hemisphere="left" 

475 ) 

476 assert len(coords) == 1 

477 assert labels == [1] 

478 

479 # Test when label_hemisphere is "right" 

480 coords, labels = find_parcellation_cut_coords( 

481 labels_img, return_label_names=True, label_hemisphere="right" 

482 ) 

483 assert len(coords) == 1 

484 assert labels == [1] 

485 

486 

487def _proba_parcellation_2_roi( 

488 x_map_a, y_map_a, z_map_a, x_map_b, y_map_b, z_map_b 

489): 

490 """Return data defining probabilistic atlas with 2 rois.""" 

491 arr1 = np.zeros((100, 100, 100)) 

492 arr1[ 

493 x_map_a - 10 : x_map_a + 10, 

494 y_map_a - 20 : y_map_a + 20, 

495 z_map_a - 30 : z_map_a + 30, 

496 ] = 1 

497 

498 arr2 = np.zeros((100, 100, 100)) 

499 arr2[ 

500 x_map_b - 10 : x_map_b + 10, 

501 y_map_b - 20 : y_map_b + 20, 

502 z_map_b - 30 : z_map_b + 30, 

503 ] = 1 

504 

505 # make data with empty in between non-empty maps to make sure that 

506 # code does not crash 

507 arr3 = np.zeros((100, 100, 100)) 

508 

509 return np.concatenate( 

510 (arr1[..., np.newaxis], arr3[..., np.newaxis], arr2[..., np.newaxis]), 

511 axis=3, 

512 ) 

513 

514 

515def test_find_probabilistic_atlas_cut_coords(affine_eye): 

516 """Test find_probabilistic_atlas_cut_coords with simple affine.""" 

517 x_map_a, y_map_a, z_map_a = 30, 40, 50 

518 x_map_b, y_map_b, z_map_b = 40, 50, 60 

519 

520 data = _proba_parcellation_2_roi( 

521 x_map_a, y_map_a, z_map_a, x_map_b, y_map_b, z_map_b 

522 ) 

523 

524 # Number of maps in time dimension 

525 n_maps = data.shape[-1] 

526 

527 # run test on img with identity affine 

528 img = Nifti1Image(data, affine_eye) 

529 

530 coords = find_probabilistic_atlas_cut_coords(img) 

531 

532 # Check outputs 

533 assert (n_maps, 3) == coords.shape 

534 

535 assert_allclose( 

536 (coords[0][0], coords[0][1], coords[0][2]), 

537 (x_map_a, y_map_a, z_map_a), 

538 rtol=6e-2, 

539 ) 

540 assert_allclose( 

541 (coords[2][0], coords[2][1], coords[2][2]), 

542 (x_map_b - 0.5, y_map_b - 0.5, z_map_b - 0.5), 

543 rtol=6e-2, 

544 ) 

545 

546 

547def test_find_probabilistic_atlas_cut_coords_non_trivial_affine(): 

548 """Test find_probabilistic_atlas_cut_coords with non trivial affine.""" 

549 x_map_a, y_map_a, z_map_a = 30, 40, 50 

550 x_map_b, y_map_b, z_map_b = 40, 50, 60 

551 

552 data = _proba_parcellation_2_roi( 

553 x_map_a, y_map_a, z_map_a, x_map_b, y_map_b, z_map_b 

554 ) 

555 

556 # Number of maps in time dimension 

557 n_maps = data.shape[-1] 

558 

559 # non-trivial affine 

560 affine = np.diag([1 / 2.0, 1 / 3.0, 1 / 4.0, 1.0]) 

561 img = Nifti1Image(data, affine) 

562 

563 coords = find_probabilistic_atlas_cut_coords(img) 

564 

565 # Check outputs 

566 assert (n_maps, 3) == coords.shape 

567 assert_allclose( 

568 (coords[0][0], coords[0][1], coords[0][2]), 

569 (x_map_a / 2.0, y_map_a / 3.0, z_map_a / 4.0), 

570 rtol=6e-2, 

571 ) 

572 assert_allclose( 

573 (coords[2][0], coords[2][1], coords[2][2]), 

574 (x_map_b / 2.0, y_map_b / 3.0, z_map_b / 4.0), 

575 rtol=6e-2, 

576 )