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
« 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
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)
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
24 # identity affine
25 img = Nifti1Image(data, affine_eye)
26 mask_img = compute_epi_mask(img)
28 x, y, z = find_xyz_cut_coords(img, mask_img=mask_img)
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 )
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)
43 x, y, z = find_xyz_cut_coords(img, mask_img=mask_img)
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 )
54def test_no_data_exceeds_activation_threshold(affine_eye):
55 """Test when no data exceeds the activation threshold.
57 Cut coords should be the center of mass rather than
58 the center of the image (10, 10, 10).
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)
66 with pytest.warns(UserWarning, match="All voxels were masked."):
67 x, y, z = find_xyz_cut_coords(img, activation_threshold=1.1)
69 assert_array_equal([x, y, z], [17.5, 21.0, 17.5])
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)
77 cut_coords = find_xyz_cut_coords(img, mask_img=mask_img)
79 assert_array_equal(cut_coords, [9.0, 9.0, 9.0])
82def test_warning_all_voxels_masked(affine_eye):
83 """Warning when all values are masked.
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)
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)
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)
101 assert_array_equal(cut_coords, [4.5, 4.5, 4.5])
104def test_warning_all_voxels_masked_thresholding(affine_eye):
105 """Warn when all values are masked due to thresholding.
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)
113 mask_data = np.ones((20, 20, 20), dtype="uint8")
115 mask_img = Nifti1Image(mask_data, affine_eye)
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 )
128 assert_array_equal(cut_coords, [4.5, 4.5, 4.5])
131def test_pseudo_4d_image(rng, shape_3d_default, affine_eye):
132 """Check pseudo-4D images as input (i.e., X, Y, Z, 1).
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)
143 assert find_xyz_cut_coords(img_3d) == find_xyz_cut_coords(img_4d)
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)
151 assert cut_coords == [0.0, 0.0, 0.0]
154@pytest.mark.parametrize("direction", ["x", "z"])
155def test_find_cut_slices(affine_eye, direction):
156 """Test find_cut_slices.
158 Test that
160 - we are indeed getting the right number of cuts
162 - we are not getting cuts that are separated by
163 less than the minimum spacing that we asked for
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)
174 for n_cuts in (2, 4):
175 cuts = find_cut_slices(
176 img, direction=direction, n_cuts=n_cuts, spacing=2
177 )
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
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)
194def test_find_cut_slices_direction_z():
195 """Test find_cut_slices in the z direction.
197 Test that we are not getting cuts that are separated by
198 less than the minimum spacing that we asked for.
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
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)
219 cuts = find_cut_slices(img, direction="z")
221 assert np.diff(cuts).min() != 0.0
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)
233 cuts = find_cut_slices(img, direction="z")
235 assert np.diff(cuts).min() != 0.0
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)
245 cuts = find_cut_slices(img, direction="z")
247 assert np.diff(cuts).min() != 0.0
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"
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)
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))
274 assert_array_equal(cut1, cut2)
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)
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 )
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)
299 assert (
300 len(_transform_cut_coords(cut_coords, direction, affine_eye)) == n_cuts
301 )
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])
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
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))
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
349 return data
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)
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 )
370 # Number of labels
371 labels = np.unique(data)
372 labels = labels[labels != 0]
373 n_labels = len(labels)
375 # identity affine
376 img = Nifti1Image(data, affine_eye)
378 # find coordinates with return label names is True
379 coords, labels_list = find_parcellation_cut_coords(
380 img, return_label_names=True
381 )
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
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 )
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)
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 )
426 # Number of labels
427 labels = np.unique(data)
428 labels = labels[labels != 0]
429 n_labels = len(labels)
431 affine = np.diag([1 / 2.0, 1 / 3.0, 1 / 4.0, 1.0])
432 img = Nifti1Image(data, affine)
434 coords = find_parcellation_cut_coords(img)
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 )
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 )
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)
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]
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]
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
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
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))
509 return np.concatenate(
510 (arr1[..., np.newaxis], arr3[..., np.newaxis], arr2[..., np.newaxis]),
511 axis=3,
512 )
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
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 )
524 # Number of maps in time dimension
525 n_maps = data.shape[-1]
527 # run test on img with identity affine
528 img = Nifti1Image(data, affine_eye)
530 coords = find_probabilistic_atlas_cut_coords(img)
532 # Check outputs
533 assert (n_maps, 3) == coords.shape
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 )
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
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 )
556 # Number of maps in time dimension
557 n_maps = data.shape[-1]
559 # non-trivial affine
560 affine = np.diag([1 / 2.0, 1 / 3.0, 1 / 4.0, 1.0])
561 img = Nifti1Image(data, affine)
563 coords = find_probabilistic_atlas_cut_coords(img)
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 )