Coverage for nilearn/image/tests/test_image.py: 0%
797 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
1"""Test image pre-processing functions."""
3import platform
4import warnings
5from pathlib import Path
7import joblib
8import numpy as np
9import pandas as pd
10import pytest
11from nibabel import AnalyzeImage, Nifti1Image, Nifti2Image, load, spatialimages
12from nibabel.freesurfer import MGHImage
13from numpy.testing import (
14 assert_allclose,
15 assert_almost_equal,
16 assert_array_almost_equal,
17 assert_array_equal,
18 assert_equal,
19)
21from nilearn import signal
22from nilearn._utils import testing
23from nilearn._utils.data_gen import (
24 _basic_confounds,
25 generate_fake_fmri,
26 generate_labeled_regions,
27 generate_maps,
28)
29from nilearn._utils.exceptions import DimensionError
30from nilearn.conftest import _affine_eye, _img_3d_rand, _rng, _shape_4d_default
31from nilearn.image import (
32 binarize_img,
33 clean_img,
34 concat_imgs,
35 copy_img,
36 crop_img,
37 get_data,
38 high_variance_confounds,
39 index_img,
40 iter_img,
41 largest_connected_component_img,
42 math_img,
43 mean_img,
44 new_img_like,
45 smooth_img,
46 swap_img_hemispheres,
47 threshold_img,
48)
49from nilearn.image.image import (
50 _crop_img_to,
51 _fast_smooth_array,
52 smooth_array,
53)
54from nilearn.image.resampling import resample_img
55from nilearn.image.tests._testing import match_headers_keys
56from nilearn.surface.surface import SurfaceImage
57from nilearn.surface.surface import get_data as get_surface_data
58from nilearn.surface.utils import (
59 assert_polymesh_equal,
60 assert_surface_image_equal,
61)
63X64 = platform.architecture()[0] == "64bit"
66AFFINE_TO_TEST = [
67 _affine_eye(),
68 np.diag((1, 1, -1, 1)),
69 np.diag((0.6, 1, 0.6, 1)),
70]
72NON_EYE_AFFINE = np.array(
73 [
74 [1.0, 2.0, 3.0, 4.0],
75 [5.0, 6.0, 7.0, 8.0],
76 [9.0, 10.0, 11.0, 12.0],
77 [0.0, 0.0, 0.0, 1.0],
78 ]
79)
82def _new_data_for_smooth_array():
83 # Impulse in 3D
84 data = np.zeros((40, 41, 42))
85 data[20, 20, 20] = 1
86 return data
89def _make_largest_cc_img_test_data():
90 shapes = ((10, 11, 12), (13, 14, 15))
91 regions = [1, 3]
93 img1 = generate_labeled_regions(shape=shapes[0], n_regions=regions[0])
94 img2 = generate_labeled_regions(shape=shapes[1], n_regions=regions[1])
95 return img1, img2, shapes
98def _images_to_mean():
99 """Return a mixture of 4D and 3D images."""
100 rng = _rng()
102 data1 = np.zeros((5, 6, 7))
103 data2 = rng.uniform(size=(5, 6, 7))
104 data3 = rng.uniform(size=(5, 6, 7, 3))
106 affine = np.diag((4, 3, 2, 1))
108 img1 = Nifti1Image(data1, affine=affine)
109 img2 = Nifti1Image(data2, affine=affine)
110 img3 = Nifti1Image(data3, affine=affine)
112 imgs = (
113 [img1],
114 [img1, img2],
115 [img2, img1, img2],
116 [img3, img1, img2],
117 )
118 return imgs
121def _check_fwhm(data, affine, fwhm):
122 """Expect a full-width at half maximum of fwhm / voxel_size."""
123 vmax = data.max()
124 above_half_max = data > 0.5 * vmax
125 for axis in [0, 1, 2]:
126 proj = np.any(
127 np.any(np.rollaxis(above_half_max, axis=axis), axis=-1),
128 axis=-1,
129 )
130 assert_equal(proj.sum(), fwhm / np.abs(affine[axis, axis]))
133def _mean_ground_truth(imgs):
134 arrays = []
135 for img in imgs:
136 img = get_data(img)
137 if img.ndim == 4:
138 img = np.mean(img, axis=-1)
139 arrays.append(img)
140 return np.mean(arrays, axis=0)
143@pytest.fixture(scope="session")
144def smooth_array_data():
145 return _new_data_for_smooth_array()
148@pytest.fixture(scope="session")
149def stat_img_test_data():
150 shape = (20, 20, 30)
151 affine = _affine_eye()
152 data = np.zeros(shape, dtype="int32")
153 data[:2, :2, :2] = 4 # 8-voxel positive cluster
154 data[4:6, :2, :2] = -4 # 8-voxel negative cluster
155 data[8:11, 0, 0] = 5 # 3-voxel positive cluster
156 data[13:16, 0, 0] = -5 # 3-voxel positive cluster
157 data[:6, 4:10, :6] = 1 # 216-voxel positive cluster with low value
158 data[13:19, 4:10, :6] = -1 # 216-voxel negative cluster with low value
160 stat_img = Nifti1Image(data, affine)
162 return stat_img
165def test_get_data(tmp_path, shape_3d_default):
166 img, *_ = generate_fake_fmri(shape=shape_3d_default)
168 data = get_data(img)
170 assert data.shape == img.shape
171 assert data is img._data_cache
173 mask_img = new_img_like(img, data > 0)
174 data = get_data(mask_img)
176 assert data.dtype == np.dtype("uint8")
178 img_3d = index_img(img, 0)
180 filename = str(tmp_path / "img_{}.nii.gz")
181 img_3d.to_filename(filename.format("a"))
182 img_3d.to_filename(filename.format("b"))
184 data = get_data(filename.format("a"))
186 assert len(data.shape) == 3
188 data = get_data(filename.format("*"))
190 assert len(data.shape) == 4
193def test_high_variance_confounds(shape_3d_default):
194 """Check high_variance_confounds returns proper shape.
196 See also test_signals.test_high_variance_confounds()
197 There is only tests on what is added by high_variance_confounds()
198 compared to signal.high_variance_confounds()
199 """
200 length = 17
201 n_confounds = 10
203 img, mask_img = generate_fake_fmri(shape=shape_3d_default, length=length)
205 confounds1 = high_variance_confounds(
206 img, mask_img=mask_img, percentile=10.0, n_confounds=n_confounds
207 )
209 assert confounds1.shape == (length, n_confounds)
211 # No mask.
212 confounds2 = high_variance_confounds(
213 img, percentile=10.0, n_confounds=n_confounds
214 )
216 assert confounds2.shape == (length, n_confounds)
219def test_high_variance_confounds_surface(surf_mask_1d, surface_glm_data):
220 """Check high_variance_confounds returns proper shape from surface."""
221 length = 17
222 n_confounds = 10
224 img, _ = surface_glm_data(length)
226 confounds1 = high_variance_confounds(
227 img, mask_img=surf_mask_1d, percentile=10.0, n_confounds=n_confounds
228 )
230 assert confounds1.shape == (length, n_confounds)
232 # No mask.
233 confounds2 = high_variance_confounds(
234 img, percentile=10.0, n_confounds=n_confounds
235 )
237 assert confounds2.shape == (length, n_confounds)
240def test_fast_smooth_array():
241 N = 4
242 shape = (N, N, N)
243 # hardcoded in _fast_smooth_array
244 neighbor_weight = 0.2
245 # 6 neighbors in 3D if you are not on an edge
246 n_neighbors_max = 6
248 data = np.ones(shape)
249 smooth_data = _fast_smooth_array(data)
251 # this contains the number of neighbors for each cell in the array
252 n_neighbors_arr = np.empty(shape)
253 for (i, j, k), __ in np.ndenumerate(n_neighbors_arr):
254 n_neighbors_arr[i, j, k] = (
255 3 + (0 < i < N - 1) + (0 < j < N - 1) + (0 < k < N - 1)
256 )
258 expected = (1 + neighbor_weight * n_neighbors_arr) / (
259 1 + neighbor_weight * n_neighbors_max
260 )
261 assert_allclose(smooth_data, expected)
264@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
265def test_smooth_array_fwhm_is_odd_with_copy(smooth_array_data, affine):
266 """Test that fwhm divided by any affine is odd.
268 Otherwise assertion below will fail.
269 ( 9 / 0.6 = 15 is fine)
270 """
271 data = smooth_array_data
272 fwhm = 9
274 filtered = smooth_array(data, affine, fwhm=fwhm, copy=True)
276 assert not np.may_share_memory(filtered, data)
278 _check_fwhm(filtered, affine, fwhm)
281@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
282def test_smooth_array_fwhm_is_odd_no_copy(affine):
283 """Test that fwhm divided by any affine is odd.
285 Otherwise assertion below will fail.
286 ( 9 / 0.6 = 15 is fine)
287 """
288 data = _new_data_for_smooth_array()
289 fwhm = 9
291 smooth_array(data, affine, fwhm=fwhm, copy=False)
293 _check_fwhm(data, affine, fwhm)
296def test_smooth_array_nan_do_not_propagate():
297 data = _new_data_for_smooth_array()
298 data[10, 10, 10] = np.nan
299 fwhm = 9
300 affine = AFFINE_TO_TEST[2]
302 filtered = smooth_array(
303 data, affine, fwhm=fwhm, ensure_finite=True, copy=True
304 )
306 assert np.all(np.isfinite(filtered))
309def test_smooth_array_same_result_with_fwhm_none_or_zero(
310 smooth_array_data,
311):
312 affine = AFFINE_TO_TEST[2]
314 out_fwhm_none = smooth_array(smooth_array_data, affine, fwhm=None)
315 out_fwhm_zero = smooth_array(smooth_array_data, affine, fwhm=0.0)
317 assert_array_equal(out_fwhm_none, out_fwhm_zero)
320@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
321def test_fast_smooth_array_give_same_result_as_smooth_array(
322 smooth_array_data, affine
323):
324 assert_equal(
325 smooth_array(smooth_array_data, affine, fwhm="fast"),
326 _fast_smooth_array(smooth_array_data),
327 )
330def test_smooth_array_raise_warning_if_fwhm_is_zero(smooth_array_data):
331 """See https://github.com/nilearn/nilearn/issues/1537."""
332 affine = AFFINE_TO_TEST[2]
333 with pytest.warns(UserWarning):
334 smooth_array(smooth_array_data, affine, fwhm=0.0)
337def test_smooth_img(affine_eye, tmp_path):
338 """Checks added functionalities compared to image._smooth_array()."""
339 shapes = ((10, 11, 12), (13, 14, 15))
340 lengths = (17, 18)
341 fwhm = (1.0, 2.0, 3.0)
343 img1, _ = generate_fake_fmri(shape=shapes[0], length=lengths[0])
344 img2, _ = generate_fake_fmri(shape=shapes[1], length=lengths[1])
346 for create_files in (False, True):
347 imgs = testing.write_imgs_to_path(
348 img1, img2, file_path=tmp_path, create_files=create_files
349 )
350 # List of images as input
351 out = smooth_img(imgs, fwhm)
353 assert isinstance(out, list)
354 assert len(out) == 2
355 for o, s, l in zip(out, shapes, lengths):
356 assert o.shape == (*s, l)
358 # Single image as input
359 out = smooth_img(imgs[0], fwhm)
361 assert isinstance(out, Nifti1Image)
362 assert out.shape == (shapes[0] + (lengths[0],))
364 # Check corner case situations when fwhm=0, See issue #1537
365 # Test whether function smooth_img raises a warning when fwhm=0.
366 with pytest.warns(UserWarning):
367 smooth_img(img1, fwhm=0.0)
369 # Test output equal when fwhm=None and fwhm=0
370 out_fwhm_none = smooth_img(img1, fwhm=None)
371 out_fwhm_zero = smooth_img(img1, fwhm=0.0)
373 assert_array_equal(get_data(out_fwhm_none), get_data(out_fwhm_zero))
375 data1 = np.zeros((10, 11, 12))
376 data1[2:4, 1:5, 3:6] = 1
377 data2 = np.zeros((13, 14, 15))
378 data2[2:4, 1:5, 3:6] = 9
379 img1_nifti2 = Nifti2Image(data1, affine=affine_eye)
380 img2_nifti2 = Nifti2Image(data2, affine=affine_eye)
381 out = smooth_img([img1_nifti2, img2_nifti2], fwhm=1.0)
384def test_crop_img_to():
385 data = np.zeros((5, 6, 7))
386 data[2:4, 1:5, 3:6] = 1
387 affine = np.diag((4, 3, 2, 1))
388 img = Nifti1Image(data, affine=affine)
390 slices = [slice(2, 4), slice(1, 5), slice(3, 6)]
391 cropped_img = _crop_img_to(img, slices, copy=False)
393 new_origin = np.array((4, 3, 2)) * np.array((2, 1, 3))
395 # check that correct part was extracted:
396 assert (get_data(cropped_img) == 1).all()
397 assert cropped_img.shape == (2, 4, 3)
399 # check that affine was adjusted correctly
400 assert (cropped_img.affine[:3, 3] == new_origin).all()
402 # check that data was really not copied
403 data[2:4, 1:5, 3:6] = 2
405 assert (get_data(cropped_img) == 2).all()
407 # check that copying works
408 copied_cropped_img = _crop_img_to(img, slices)
410 data[2:4, 1:5, 3:6] = 1
411 assert (get_data(copied_cropped_img) == 2).all()
414def test_crop_img():
415 data = np.zeros((5, 6, 7))
416 data[2:4, 1:5, 3:6] = 1
417 affine = np.diag((4, 3, 2, 1))
418 img = Nifti1Image(data, affine=affine)
420 cropped_img = crop_img(img, copy_header=True)
422 # correction for padding with "-1"
423 # check that correct part was extracted:
424 # This also corrects for padding
425 assert (get_data(cropped_img)[1:-1, 1:-1, 1:-1] == 1).all()
426 assert cropped_img.shape == (2 + 2, 4 + 2, 3 + 2)
429def test_crop_img_copied_header(img_4d_mni_tr2):
430 # Test equality of header fields between input and output
431 # create zero padded data
432 data = np.zeros((10, 10, 10, 10))
433 data[0:4, 0:4, 0:4, :] = 1
434 # replace the img_4d_mni_tr2 values with data
435 img_4d_mni_tr2_zero_padded = new_img_like(
436 img_4d_mni_tr2,
437 data=data,
438 affine=img_4d_mni_tr2.affine,
439 copy_header=True,
440 )
441 cropped_img = crop_img(img_4d_mni_tr2_zero_padded, copy_header=True)
442 # only dim[1:4] should be different
443 assert (
444 cropped_img.header["dim"][1:4]
445 != img_4d_mni_tr2_zero_padded.header["dim"][1:4]
446 ).all()
447 # other dim indices should be the same
448 assert (
449 cropped_img.header["dim"][0]
450 == img_4d_mni_tr2_zero_padded.header["dim"][0]
451 )
452 assert (
453 cropped_img.header["dim"][4:]
454 == img_4d_mni_tr2_zero_padded.header["dim"][4:]
455 ).all()
456 # other header fields should also be same
457 match_headers_keys(
458 cropped_img, img_4d_mni_tr2_zero_padded, except_keys=["dim"]
459 )
462def test_crop_threshold_tolerance(affine_eye):
463 """Check if crop can skip values that are extremely close to zero.
465 In a relative sense and will crop them away
466 """
467 data = np.zeros([10, 14, 12])
468 data[3:7, 3:7, 5:9] = 1.0
469 active_shape = (4 + 2, 4 + 2, 4 + 2) # add padding
471 # add an infinitesimal outside this block
472 data[3, 3, 3] = 1e-12
473 img = Nifti1Image(data, affine=affine_eye)
475 cropped_img = crop_img(img, copy_header=True)
477 assert cropped_img.shape == active_shape
480@pytest.mark.parametrize("images_to_mean", _images_to_mean())
481def test_mean_img(images_to_mean, tmp_path):
482 affine = np.diag((4, 3, 2, 1))
484 truth = _mean_ground_truth(images_to_mean)
486 img = mean_img(images_to_mean, copy_header=True)
488 assert_array_equal(img.affine, affine)
489 assert_array_equal(get_data(img), truth)
491 # Test with files
492 imgs = testing.write_imgs_to_path(*images_to_mean, file_path=tmp_path)
493 img = mean_img(imgs, copy_header=True)
495 assert_array_equal(img.affine, affine)
496 if X64:
497 assert_array_equal(get_data(img), truth)
498 else:
499 # We don't really understand but arrays are not
500 # exactly equal on 32bit. Given that you can not do
501 # much real world data analysis with nilearn on a
502 # 32bit machine it is not worth investigating more
503 assert_allclose(
504 get_data(img),
505 truth,
506 rtol=np.finfo(truth.dtype).resolution,
507 atol=0,
508 )
511def test_mean_img_resample(rng):
512 # Test resampling in mean_img with a permutation of the axes
513 data = rng.uniform(size=(5, 6, 7, 40))
514 affine = np.diag((4, 3, 2, 1))
515 img = Nifti1Image(data, affine=affine)
516 mean_img_to_resample = Nifti1Image(data.mean(axis=-1), affine=affine)
518 target_affine = affine[:, [1, 0, 2, 3]] # permutation of axes
520 mean_img_with_resampling = mean_img(
521 img, target_affine=target_affine, copy_header=True
522 )
524 resampled_mean_image = resample_img(
525 mean_img_to_resample,
526 target_affine=target_affine,
527 copy_header=True,
528 force_resample=True,
529 )
531 assert_array_equal(
532 get_data(resampled_mean_image), get_data(mean_img_with_resampling)
533 )
534 assert_array_equal(
535 resampled_mean_image.affine, mean_img_with_resampling.affine
536 )
537 assert_array_equal(mean_img_with_resampling.affine, target_affine)
540def test_mean_img_copied_header(img_4d_mni_tr2):
541 # Test equality of header fields between input and output
542 result = mean_img(img_4d_mni_tr2, copy_header=True)
543 match_headers_keys(
544 result,
545 img_4d_mni_tr2,
546 except_keys=["dim", "pixdim", "cal_max", "cal_min"],
547 )
550def test_mean_img_surface(surf_img_1d, surf_img_2d):
551 """Check that mean is properly computed over 'time points'."""
552 # one 'time point' image returns same
553 img = mean_img(surf_img_1d)
555 assert_surface_image_equal(img, surf_img_1d)
557 # image with left hemisphere
558 # where timepoint 1 has all values == 0
559 # and timepoint 2 == 1
560 two_time_points_img = surf_img_2d(2)
561 two_time_points_img.data.parts["left"][:, 0] = np.zeros(shape=4)
562 two_time_points_img.data.parts["left"][:, 1] = np.ones(shape=4)
564 img = mean_img(two_time_points_img)
566 assert_array_equal(img.data.parts["left"], np.ones(shape=(4,)) * 0.5)
567 assert img.shape == (img.mesh.n_vertices,)
570def test_mean_img_surface_list(surf_img_2d):
571 """Check that mean_img computes mean of mean."""
572 surf_img_1 = surf_img_2d(2)
573 surf_img_2 = surf_img_2d(3)
575 mean_surf_img_1 = mean_img(surf_img_1)
576 mean_surf_img_2 = mean_img(surf_img_2)
578 direct_mean = mean_img([surf_img_1, surf_img_2])
580 assert_surface_image_equal(
581 direct_mean, mean_img([mean_surf_img_1, mean_surf_img_2])
582 )
585def test_swap_img_hemispheres(affine_eye, shape_3d_default, rng):
586 # make sure input image data is not overwritten inside function
587 data = rng.standard_normal(size=shape_3d_default)
588 data_img = Nifti1Image(data, affine_eye)
590 swap_img_hemispheres(data_img)
592 assert_array_equal(get_data(data_img), data)
593 # swapping operations work
594 assert_array_equal( # one turn
595 get_data(swap_img_hemispheres(data_img)), data[::-1]
596 )
597 assert_array_equal( # two turns -> back to original data
598 get_data(swap_img_hemispheres(swap_img_hemispheres(data_img))),
599 data,
600 )
603def test_index_img_error_3d(affine_eye):
604 img_3d = Nifti1Image(np.ones((3, 4, 5)), affine_eye)
605 expected_error_msg = (
606 "Input data has incompatible dimensionality: "
607 "Expected dimension is 4D and you provided "
608 "a 3D image."
609 )
610 with pytest.raises(TypeError, match=expected_error_msg):
611 index_img(img_3d, 0)
614def test_index_img():
615 img_4d, _ = generate_fake_fmri(affine=NON_EYE_AFFINE)
617 fourth_dim_size = img_4d.shape[3]
618 tested_indices = [
619 *range(fourth_dim_size),
620 slice(2, 8, 2),
621 [1, 2, 3, 2],
622 (np.arange(fourth_dim_size) % 3) == 1,
623 ]
624 for i in tested_indices:
625 this_img = index_img(img_4d, i)
627 expected_data_3d = get_data(img_4d)[..., i]
628 assert_array_equal(get_data(this_img), expected_data_3d)
629 assert_array_equal(this_img.affine, img_4d.affine)
632def test_index_img_error_4d(affine_eye):
633 img_4d, _ = generate_fake_fmri(affine=affine_eye)
634 fourth_dim_size = img_4d.shape[3]
635 for i in [
636 fourth_dim_size,
637 -fourth_dim_size - 1,
638 [0, fourth_dim_size],
639 np.repeat(True, fourth_dim_size + 1),
640 ]:
641 with pytest.raises(
642 IndexError,
643 match="out of bounds|invalid index|out of range|boolean index",
644 ):
645 index_img(img_4d, i)
648def test_pd_index_img(rng, img_4d_rand_eye):
649 # confirm indices from pandas dataframes are handled correctly
650 fourth_dim_size = img_4d_rand_eye.shape[3]
652 arr = rng.uniform(size=fourth_dim_size) > 0.5
654 np_index_img = index_img(img_4d_rand_eye, arr)
655 pd_index_img = index_img(img_4d_rand_eye, pd.DataFrame({"arr": arr}))
657 assert_array_equal(get_data(np_index_img), get_data(pd_index_img))
660def test_iter_img_3d_imag_error(affine_eye):
661 img_3d = Nifti1Image(np.ones((3, 4, 5)), affine_eye)
662 expected_error_msg = (
663 "Input data has incompatible dimensionality: "
664 "Expected dimension is 4D and you provided "
665 "a 3D image."
666 )
667 with pytest.raises(TypeError, match=expected_error_msg):
668 iter_img(img_3d)
671@pytest.mark.timeout(0)
672def test_iter_img(tmp_path):
673 img_4d, _ = generate_fake_fmri(affine=NON_EYE_AFFINE)
675 for i, img in enumerate(iter_img(img_4d)):
676 expected_data_3d = get_data(img_4d)[..., i]
678 assert_array_equal(get_data(img), expected_data_3d)
679 assert_array_equal(img.affine, img_4d.affine)
681 img_4d_filename = testing.write_imgs_to_path(img_4d, file_path=tmp_path)
682 for i, img in enumerate(iter_img(img_4d_filename)):
683 expected_data_3d = get_data(img_4d)[..., i]
685 assert_array_equal(get_data(img), expected_data_3d)
686 assert_array_equal(img.affine, img_4d.affine)
688 # enables to delete "img_4d_filename" on windows
689 del img
691 img_3d_list = list(iter_img(img_4d))
692 for i, img in enumerate(iter_img(img_3d_list)):
693 expected_data_3d = get_data(img_4d)[..., i]
695 assert_array_equal(get_data(img), expected_data_3d)
696 assert_array_equal(img.affine, img_4d.affine)
698 img_3d_filenames = testing.write_imgs_to_path(
699 *img_3d_list, file_path=tmp_path
700 )
701 for i, img in enumerate(iter_img(img_3d_filenames)):
702 expected_data_3d = get_data(img_4d)[..., i]
704 assert_array_equal(get_data(img), expected_data_3d)
705 assert_array_equal(img.affine, img_4d.affine)
707 # enables to delete "img_3d_filename" on windows
708 del img
711def test_iter_surface_img(surf_img_2d):
712 """Check iter_img returns list of SurfaceImage.
714 Each SurfaceImage must have same mesh as input
715 and data from one of the sample of the input SurfaceImage.
716 """
717 input = surf_img_2d(5)
718 output = list(iter_img(input))
720 assert isinstance(output, list)
721 assert len(output) == input.shape[1]
722 assert all(isinstance(x, SurfaceImage) for x in output)
723 for i in range(input.shape[1]):
724 assert_polymesh_equal(output[i].mesh, input.mesh)
725 assert_array_equal(
726 np.squeeze(output[i].data.parts["left"]),
727 input.data.parts["left"][..., i],
728 )
731def test_iter_img_surface_2d(surf_img_1d, surf_img_2d):
732 """Return as is if surface image is 2D."""
733 input = surf_img_2d(1)
734 output = list(iter_img(input))
736 assert_surface_image_equal(output[0], input)
738 output = list(iter_img(surf_img_1d))
740 assert_surface_image_equal(output[0], surf_img_1d)
743def test_new_img_like_mgz():
744 """Check that new images can be generated with bool MGZ type.
746 This is usually when computing masks using MGZ inputs, e.g.
747 when using plot_stap_map
748 """
749 img_filename = Path(__file__).parent / "data" / "test.mgz"
750 ref_img = load(img_filename)
751 data = np.ones(get_data(ref_img).shape, dtype=bool)
752 affine = ref_img.affine
753 new_img_like(ref_img, data, affine, copy_header=False)
756def test_new_img_like():
757 # Give a list to new_img_like
758 data = np.zeros((5, 6, 7))
759 data[2:4, 1:5, 3:6] = 1
760 affine = np.diag((4, 3, 2, 1))
761 img = Nifti1Image(data, affine=affine)
763 img2 = new_img_like([img], data)
765 assert_array_equal(get_data(img), get_data(img2))
767 # test_new_img_like_with_nifti2image_copy_header
768 img_nifti2 = Nifti2Image(data, affine=affine)
770 img2_nifti2 = new_img_like([img_nifti2], data, copy_header=True)
772 assert_array_equal(get_data(img_nifti2), get_data(img2_nifti2))
775def test_new_img_like_accepts_paths(affine_eye, tmp_path, rng):
776 """Check that new_img_like can accept instances of pathlib.Path."""
777 nifti_path = tmp_path / "sample.nii"
778 assert isinstance(nifti_path, Path)
780 data = rng.random((10, 10, 10))
781 img = Nifti1Image(data, affine_eye)
782 img.to_filename(nifti_path)
784 new_data = rng.random((10, 10, 10))
785 new_img = new_img_like(nifti_path, new_data)
786 assert new_img.shape == (10, 10, 10)
788 # Check that list of pathlib.Path also accepted
789 new_img = new_img_like([nifti_path], new_data)
790 assert new_img.shape == (10, 10, 10)
793def test_new_img_like_non_iterable_header(rng):
794 """Tests that when an niimg's header is not iterable \
795 and it is set to be copied, an error is not raised.
796 """
797 fake_fmri_data = rng.uniform(size=_shape_4d_default())
798 fake_affine = rng.uniform(size=(4, 4))
799 fake_spatial_image = spatialimages.SpatialImage(
800 fake_fmri_data, fake_affine
801 )
803 assert new_img_like(
804 fake_spatial_image, data=fake_fmri_data, copy_header=True
805 )
808@pytest.mark.parametrize("no_int64_nifti", ["allow for this test"])
809def test_new_img_like_int64(shape_3d_default):
810 img = generate_labeled_regions(shape=shape_3d_default, n_regions=2)
812 data = get_data(img).astype("int32")
814 with warnings.catch_warnings():
815 warnings.simplefilter("error")
816 new_img = new_img_like(img, data)
817 assert get_data(new_img).dtype == "int32"
819 data = data.astype("int64")
821 with pytest.warns(UserWarning, match=r".*array.*contains.*64.*"):
822 new_img = new_img_like(img, data)
823 assert get_data(new_img).dtype == "int32"
825 data[:] = 2**40
827 with pytest.warns(UserWarning, match=r".*64.*too large.*"):
828 new_img = new_img_like(img, data, copy_header=True)
829 assert get_data(new_img).dtype == "int64"
832def test_input_in_threshold_img(
833 shape_3d_default, surf_img_1d, surf_mask_1d, affine_eye
834):
835 """Check threshold_img only works with surface OR volume."""
836 threshold = 0.5
838 # setting copy_header to True to avoid warnings
839 # TODO remove when bumping to nilearn > 0.13
840 copy_header = True
842 vol_img, _ = generate_maps(shape_3d_default, n_regions=2)
843 vol_mask = Nifti1Image(np.ones(shape_3d_default), affine_eye)
845 # All of those should be OK
846 thr_img = threshold_img(
847 vol_img, threshold=threshold, mask_img=None, copy_header=copy_header
848 )
850 _check_thresholded_output(vol_img, thr_img, threshold)
852 thr_img = threshold_img(
853 surf_img_1d,
854 threshold=threshold,
855 mask_img=None,
856 copy_header=copy_header,
857 )
859 _check_thresholded_output(surf_img_1d, thr_img, threshold)
861 # same but with a mask
862 threshold_img(
863 vol_img,
864 threshold=threshold,
865 mask_img=vol_mask,
866 copy_header=copy_header,
867 )
868 threshold_img(
869 surf_img_1d,
870 threshold=threshold,
871 mask_img=surf_mask_1d,
872 copy_header=copy_header,
873 )
876def test_input_in_threshold_img_several_timepoints(
877 img_4d_rand_eye, surf_img_2d
878):
879 """Check threshold_img works with 2D surface OR 4D volume."""
880 threshold = 0.5
882 # setting copy_header to True to avoid warnings
883 # TODO remove when bumping to nilearn > 0.13
884 copy_header = True
885 thr_img = threshold_img(
886 img_4d_rand_eye, threshold=0.5, copy_header=copy_header
887 )
889 _check_thresholded_output(img_4d_rand_eye, thr_img, threshold)
891 original_image = surf_img_2d(5)
892 thr_img = threshold_img(original_image, threshold=0.5)
894 _check_thresholded_output(original_image, thr_img, threshold)
897def _check_thresholded_output(input, output, threshold):
898 """Check data was properly thresholed.
900 Assumes:
901 - a one-sided threshold that keeps data > threshold
902 - no extra mask used
903 """
904 if isinstance(input, Nifti1Image):
905 original_data = input.get_fdata()
906 data = output.get_fdata()
907 elif isinstance(input, SurfaceImage):
908 original_data = get_surface_data(input)
909 data = get_surface_data(output)
911 non_zero = data != 0
912 assert np.all(data[non_zero] > threshold)
914 data_to_mask = original_data < threshold
915 assert np.all(data[data_to_mask] == 0)
918def test_input_in_threshold_img_errors(
919 shape_3d_default, surf_img_1d, surf_mask_1d, affine_eye
920):
921 """Check invalid inputs to threshold_img ."""
922 vol_img, _ = generate_maps(shape_3d_default, n_regions=2)
923 vol_mask = Nifti1Image(np.ones(shape_3d_default), affine_eye)
925 # invalid input: img is an int
926 with pytest.raises(
927 TypeError,
928 match="'img' should be a 3D/4D Niimg-like object or a SurfaceImage.",
929 ):
930 threshold_img(img=1, threshold=1)
932 # incompatible inputs raise errors
933 with pytest.raises(
934 TypeError,
935 match="Mask and images to fit must be of compatible types.",
936 ):
937 threshold_img(vol_img, threshold=1, mask_img=surf_mask_1d)
938 with pytest.raises(
939 TypeError,
940 match="Mask and images to fit must be of compatible types.",
941 ):
942 threshold_img(surf_img_1d, threshold=1, mask_img=vol_mask)
945def test_threshold_img_warning(surf_img_1d):
946 """Check warnings thrown by threshold_img."""
947 with pytest.warns(
948 UserWarning,
949 match="Cluster thresholding not implemented for SurfaceImage.",
950 ):
951 threshold_img(surf_img_1d, threshold=1, cluster_threshold=10)
954@pytest.mark.parametrize("two_sided", [True, False])
955def test_validity_threshold_value_in_threshold_img(
956 shape_3d_default, two_sided
957):
958 """Check that invalid values to threshold_img's threshold parameter \
959 raise Exceptions.
960 """
961 # setting copy_header to True to avoid warnings
962 # TODO remove when bumping to nilearn > 0.13
963 copy_header = True
964 maps, _ = generate_maps(shape_3d_default, n_regions=2)
966 # testing to raise same error when threshold=None case
967 with pytest.raises(
968 TypeError,
969 match="threshold should be either a number or a string",
970 ):
971 threshold_img(maps, threshold=None, copy_header=copy_header)
973 threshold = object()
974 with pytest.raises(
975 TypeError, match="should be either a number or a string"
976 ):
977 threshold_img(
978 maps,
979 threshold=threshold,
980 two_sided=two_sided,
981 copy_header=copy_header,
982 )
984 invalid_threshold_values = ["90t%", "s%", "t", "0.1"]
985 name = "threshold"
986 for thr in invalid_threshold_values:
987 with pytest.raises(
988 ValueError,
989 match=f"{name}.+should be a number followed by the percent",
990 ):
991 threshold_img(
992 maps,
993 threshold=thr,
994 copy_header=copy_header,
995 two_sided=two_sided,
996 )
999def test_validity_negative_threshold_value_in_threshold_img(shape_3d_default):
1000 """Check that negative values to threshold_img's threshold parameter \
1001 raise Exceptions.
1002 """
1003 # setting copy_header to True to avoid warnings
1004 # TODO remove when bumping to nilearn > 0.13
1005 copy_header = True
1007 maps, _ = generate_maps(shape_3d_default, n_regions=2)
1009 # invalid threshold values when two_sided=True
1010 thresholds = [-10, "-10%"]
1011 for wrong_threshold in thresholds:
1012 with pytest.raises(ValueError, match="should not be a negative"):
1013 threshold_img(
1014 maps,
1015 threshold=wrong_threshold,
1016 two_sided=True,
1017 copy_header=copy_header,
1018 )
1020 with pytest.raises(ValueError, match="should not be a negative"):
1021 threshold_img(
1022 maps, threshold="-10%", two_sided=False, copy_header=copy_header
1023 )
1026def test_threshold_img(affine_eye):
1027 """Smoke test for threshold_img with valid threshold inputs."""
1028 # setting copy_header to True to avoid warnings
1029 # TODO remove when bumping to nilearn > 0.13
1030 copy_header = True
1032 shape = (10, 20, 30)
1033 maps, _ = generate_maps(shape, n_regions=4)
1034 mask_img = Nifti1Image(np.ones((shape), dtype=np.int8), affine_eye)
1036 for img in iter_img(maps):
1037 # when threshold is a float value
1038 threshold_img(img, threshold=0.8, copy_header=copy_header)
1040 # when we provide mask image
1041 threshold_img(
1042 img, threshold=1, mask_img=mask_img, copy_header=copy_header
1043 )
1045 # when threshold is a percentile
1046 threshold_img(img, threshold="2%", copy_header=copy_header)
1049@pytest.mark.parametrize(
1050 "threshold, expected_n_non_zero",
1051 [
1052 (1, {"left": 2, "right": 4}),
1053 (10, {"left": 0, "right": 3}),
1054 (50, {"left": 0, "right": 0}),
1055 ("50%", {"left": 0, "right": 4}),
1056 ],
1057)
1058def test_threshold_surf_img_1d(surf_img_1d, threshold, expected_n_non_zero):
1059 """Check number of elements surviving thresholding 1D surface image.
1061 For left hemisphere: 1 < values < 10
1062 For right hemisphere: 10 < values < 50
1063 """
1064 thr_img = threshold_img(surf_img_1d, threshold=threshold)
1065 for hemi in thr_img.data.parts:
1066 data = thr_img.data.parts[hemi]
1067 assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]
1070@pytest.mark.parametrize(
1071 "threshold, expected_n_non_zero",
1072 [
1073 (0.9, {"left": 2, "right": 3}),
1074 (9, {"left": 0, "right": 3}),
1075 (50, {"left": 0, "right": 0}),
1076 ("50%", {"left": 0, "right": 2}),
1077 ],
1078)
1079def test_threshold_surf_img_1d_with_mask(
1080 surf_img_1d, threshold, expected_n_non_zero, surf_mask_1d
1081):
1082 """Check number of elements surviving thresholding 1D surface image.
1084 Fewer elements survive thresholding when a mask is provided.
1086 For left hemisphere: 1 <= values < 10
1087 For right hemisphere: 10 <= values < 50
1088 """
1089 thr_img = threshold_img(
1090 surf_img_1d, threshold=threshold, mask_img=surf_mask_1d
1091 )
1092 for hemi in thr_img.data.parts:
1093 data = thr_img.data.parts[hemi]
1094 assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]
1097@pytest.mark.parametrize(
1098 "threshold, expected_n_non_zero, two_sided",
1099 [
1100 (1, {"left": 0, "right": 4}, False),
1101 (29, {"left": 0, "right": 2}, False),
1102 (50, {"left": 0, "right": 0}, False),
1103 ("50%", {"left": 0, "right": 3}, False),
1104 (1, {"left": 3, "right": 4}, True),
1105 (29, {"left": 1, "right": 2}, True),
1106 (50, {"left": 0, "right": 0}, True),
1107 ("50%", {"left": 1, "right": 2}, True),
1108 ],
1109)
1110def test_threshold_surf_img_1d_negative_values(
1111 surf_img_1d, threshold, expected_n_non_zero, two_sided
1112):
1113 """Check number of elements surviving thresholding 1D surface image.
1115 Data also includes negative values.
1117 Also test 2 sided thresholding.
1119 For left hemisphere: -41 < values < -9
1120 For right hemisphere: 9 < values < 50
1121 """
1122 surf_img_1d.data.parts["left"] *= -10
1124 thr_img = threshold_img(
1125 surf_img_1d, threshold=threshold, two_sided=two_sided
1126 )
1127 for hemi in thr_img.data.parts:
1128 data = thr_img.data.parts[hemi]
1129 assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]
1132@pytest.mark.parametrize(
1133 "threshold, two_sided, expected",
1134 [
1135 (3, True, 16),
1136 (3, False, 8),
1137 (4, True, 0),
1138 (4, False, 0),
1139 (0, True, 448),
1140 (0, False, 224),
1141 (-3, False, 8),
1142 (-0.5, False, 224),
1143 ("0%", True, 448),
1144 ("10%", False, 224),
1145 ("99%", False, 8),
1146 ("99%", True, 16),
1147 ("100%", True, 0),
1148 ],
1149)
1150def test_threshold_img_with_mask(
1151 stat_img_test_data, affine_eye, threshold, two_sided, expected
1152):
1153 """Tests `nilearn.image.threshold_img` for float and str values using
1154 mask.
1155 """
1156 temp_mask = np.ones((stat_img_test_data.shape), dtype=np.int8)
1158 temp_mask[8:11, 0, 0] = 0 # mask values 5
1159 temp_mask[13:16, 0, 0] = 0 # mask values -5
1160 temp_mask[19:, 10:, 6:] = 0
1161 temp_mask[:4, 10:, 6:] = 0
1162 temp_mask[13:19, 10:, 6:] = 0
1163 temp_mask[13:19, 0:4, 6:] = 0
1164 mask_img = Nifti1Image(temp_mask, affine_eye)
1166 thr_img = threshold_img(
1167 img=stat_img_test_data,
1168 mask_img=mask_img,
1169 threshold=threshold,
1170 two_sided=two_sided,
1171 copy_header=True,
1172 )
1174 img_data = thr_img.get_fdata()
1175 assert len(img_data[np.nonzero(img_data)]) == expected
1178@pytest.mark.parametrize(
1179 "threshold,two_sided,cluster_threshold,expected",
1180 [
1181 (2, False, 0, [0, 4, 5]), # cluster with values > 2
1182 (2, True, 0, [-5, -4, 0, 4, 5]), # cluster with |values| > 2
1183 (2, True, 5, [-4, 0, 4]), # cluster: |values| > 2 & size > 5
1184 (2, True, 5, [-4, 0, 4]), # cluster: |values| > 2 & size > 5
1185 (0.5, True, 5, [-4, -1, 0, 1, 4]), # cluster: |values| > 0.5 & size>5
1186 (0.5, False, 5, [0, 1, 4]), # cluster: values > 0.5 & size > 5
1187 ],
1188)
1189def test_threshold_img_with_cluster_threshold(
1190 stat_img_test_data, threshold, two_sided, cluster_threshold, expected
1191):
1192 """Check that passing specific threshold and cluster threshold values \
1193 only gives cluster the right number of voxels with the right values.
1194 """
1195 thr_img = threshold_img(
1196 img=stat_img_test_data,
1197 threshold=threshold,
1198 two_sided=two_sided,
1199 cluster_threshold=cluster_threshold,
1200 copy_header=True,
1201 )
1203 assert np.array_equal(np.unique(thr_img.get_fdata()), np.array(expected))
1206def test_threshold_img_threshold_n_clusters(stat_img_test_data):
1207 """With a cluster threshold of 5 we get 8 clusters with |values| > 2 \
1208 and cluster sizes > 5.
1209 """
1210 thr_img = threshold_img(
1211 img=stat_img_test_data,
1212 threshold=2,
1213 two_sided=True,
1214 cluster_threshold=5,
1215 copy_header=True,
1216 )
1218 assert np.sum(thr_img.get_fdata() == 4) == 8
1221def test_threshold_img_no_copy_surface(surf_img_1d):
1222 """Test copy=False on surface data.
1224 Check that not copying does mutate the original image.
1225 """
1226 threshold = 15
1227 input_img = surf_img_1d
1228 result = threshold_img(input_img, threshold=threshold, copy=False)
1229 assert_surface_image_equal(result, surf_img_1d)
1232def test_threshold_img_copy_surface(surf_img_1d):
1233 """Test copy=True on surface data.
1235 Check that copying does not mutate the original image.
1236 """
1237 threshold = 15
1238 input_img = surf_img_1d
1239 result = threshold_img(input_img, threshold=threshold, copy=True)
1240 with pytest.raises(ValueError):
1241 assert_surface_image_equal(result, surf_img_1d)
1244def test_threshold_img_copy_volume(img_4d_ones_eye):
1245 """Test the behavior of threshold_img's copy parameter."""
1246 threshold = 1
1247 # Check that copy does not mutate. It returns modified copy.
1248 thr_img = threshold_img(img_4d_ones_eye, threshold, copy_header=True)
1250 # Original img_ones should have all ones.
1251 assert_array_equal(get_data(img_4d_ones_eye), np.ones(_shape_4d_default()))
1252 # Thresholded should have all zeros.
1253 assert_array_equal(get_data(thr_img), np.zeros(_shape_4d_default()))
1255 # Check that not copying does mutate.
1256 img_to_mutate = img_4d_ones_eye
1258 thr_img = threshold_img(
1259 img_to_mutate, threshold, copy=False, copy_header=True
1260 )
1262 # Check that original mutates
1263 assert_array_equal(get_data(img_to_mutate), np.zeros(_shape_4d_default()))
1264 # And that returned value is also thresholded.
1265 assert_array_equal(get_data(img_to_mutate), get_data(thr_img))
1268def test_isnan_threshold_img_data(affine_eye, shape_3d_default):
1269 """Check threshold_img converges properly when input image has nans."""
1270 maps, _ = generate_maps(shape_3d_default, n_regions=2)
1271 data = get_data(maps)
1272 data[:, :, 0] = np.nan
1274 maps_img = Nifti1Image(data, affine_eye)
1276 threshold_img(maps_img, threshold=0.8, copy_header=True)
1279def test_threshold_img_copied_header(img_4d_mni_tr2):
1280 # Test equality of header fields between input and output
1281 thr_img = threshold_img(img_4d_mni_tr2, threshold=0.5, copy_header=True)
1282 # only the min value should be different
1283 match_headers_keys(
1284 thr_img,
1285 img_4d_mni_tr2,
1286 except_keys=["cal_min"],
1287 )
1288 # min value should be 0 in the result
1289 assert thr_img.header["cal_min"] == 0
1292def test_math_img_exceptions(affine_eye, img_4d_ones_eye, surf_img_2d):
1293 img1 = img_4d_ones_eye
1294 img2 = Nifti1Image(np.zeros((10, 20, 10, 10)), affine_eye)
1295 img3 = img_4d_ones_eye
1296 img4 = Nifti1Image(np.ones(_shape_4d_default()), affine_eye * 2)
1298 formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
1299 # Images with different shapes should raise a ValueError exception.
1300 with pytest.raises(ValueError, match="Input images cannot be compared"):
1301 math_img(formula, img1=img1, img2=img2)
1303 # Images with different affines should raise a ValueError exception.
1304 with pytest.raises(ValueError, match="Input images cannot be compared"):
1305 math_img(formula, img1=img1, img2=img4)
1307 bad_formula = "np.toto(img1, axis=-1) - np.mean(img3, axis=-1)"
1308 with pytest.raises(
1309 AttributeError, match="Input formula couldn't be processed"
1310 ):
1311 math_img(bad_formula, img1=img1, img3=img3)
1312 # Same but for surface data
1313 with pytest.raises(
1314 AttributeError, match="Input formula couldn't be processed"
1315 ):
1316 math_img(bad_formula, img1=surf_img_2d(2), img3=surf_img_2d(3))
1318 # Test copy_header_from parameter
1319 # Copying header from 4d image to a result that is 3d should raise a
1320 # ValueError
1321 formula = "np.mean(img1, axis=-1) - np.mean(img3, axis=-1)"
1322 with pytest.raises(ValueError, match="Cannot copy the header."):
1323 math_img(formula, img1=img1, img3=img3, copy_header_from="img1")
1325 # Passing an 'img*' variable (to copy_header_from) that is not in the
1326 # formula or an img* argument should raise a KeyError exception.
1327 with pytest.raises(KeyError):
1328 math_img(formula, img1=img1, img3=img3, copy_header_from="img2")
1331def test_math_img(
1332 affine_eye, img_4d_ones_eye, img_4d_zeros_eye, shape_3d_default, tmp_path
1333):
1334 img1 = img_4d_ones_eye
1335 img2 = img_4d_zeros_eye
1336 expected_result = Nifti1Image(np.ones(shape_3d_default), affine_eye)
1338 formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
1339 for create_files in (True, False):
1340 imgs = testing.write_imgs_to_path(
1341 img1, img2, file_path=tmp_path, create_files=create_files
1342 )
1343 result = math_img(formula, img1=imgs[0], img2=imgs[1])
1344 assert_array_equal(get_data(result), get_data(expected_result))
1345 assert_array_equal(result.affine, expected_result.affine)
1346 assert result.shape == expected_result.shape
1349def test_math_img_surface(surf_img_2d):
1350 """Test math_img on surface data."""
1351 img1 = surf_img_2d(1)
1352 img2 = surf_img_2d(3)
1354 tmp = {}
1355 for part in img1.data.parts:
1356 tmp[part] = np.mean(img1.data.parts[part], axis=-1) - np.mean(
1357 img2.data.parts[part], axis=-1
1358 )
1360 expected_result = SurfaceImage(mesh=img1.mesh, data=tmp)
1362 formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
1363 result = math_img(formula, img1=img1, img2=img2)
1365 assert isinstance(result, SurfaceImage)
1366 assert_surface_image_equal(result, expected_result)
1369def test_math_img_copy_default_header(
1370 img_4d_ones_eye_default_header, img_4d_ones_eye_tr2
1371):
1372 # case where data values are not changed and header values are not copied
1373 # the result should have default header values
1374 formula_no_change = "img * 1"
1375 # using img_4d_ones_eye_tr2 with edited header in the formula
1376 result = math_img(
1377 formula_no_change, img=img_4d_ones_eye_tr2, copy_header_from=None
1378 )
1379 # header values should instead match default header values
1380 assert result.header == img_4d_ones_eye_default_header.header
1383def test_math_img_copied_header_from_img(img_4d_mni_tr2):
1384 # case where data values are not changed but header values are copied
1385 # the result should have the same header values as the given image
1386 formula_no_change = "img * 1"
1387 result = math_img(
1388 formula_no_change, img=img_4d_mni_tr2, copy_header_from="img"
1389 )
1390 # all header values should be the same
1391 assert result.header == img_4d_mni_tr2.header
1394def test_math_img_copied_header_data_values_changed(
1395 img_4d_ones_eye_default_header, img_4d_ones_eye_tr2
1396):
1397 # case where data values are changed and header values are copied from one
1398 # of the input images
1399 # the result should have the same header values as img_4d_ones_eye_tr2,
1400 # except for cal_max and cal_min that should be different
1401 formula_change_min_max = "img1 - img2"
1402 result = math_img(
1403 formula_change_min_max,
1404 img1=img_4d_ones_eye_default_header,
1405 img2=img_4d_ones_eye_tr2,
1406 copy_header_from="img2",
1407 )
1408 for key in img_4d_ones_eye_tr2.header:
1409 # cal_max and cal_min should be different in result
1410 if key in ["cal_max", "cal_min"]:
1411 assert result.header[key] != img_4d_ones_eye_tr2.header[key]
1412 # other header values should be the same
1413 elif isinstance(result.header[key], np.ndarray):
1414 assert_array_equal(
1415 result.header[key], img_4d_ones_eye_tr2.header[key]
1416 )
1417 else:
1418 assert result.header[key] == img_4d_ones_eye_tr2.header[key]
1421def test_binarize_img(img_4d_rand_eye):
1422 # Test that all output values are 1.
1423 img1 = binarize_img(img_4d_rand_eye, copy_header=True)
1425 assert_array_equal(np.unique(img1.dataobj), np.array([1]))
1427 # Test that it works with threshold
1428 img2 = binarize_img(img_4d_rand_eye, threshold=0.5, copy_header=True)
1430 assert_array_equal(np.unique(img2.dataobj), np.array([0, 1]))
1431 # Test that manual binarization equals binarize_img results.
1432 img3 = copy_img(img_4d_rand_eye)
1433 img3.dataobj[img_4d_rand_eye.dataobj < 0.5] = 0
1434 img3.dataobj[img_4d_rand_eye.dataobj >= 0.5] = 1
1436 assert_array_equal(img2.dataobj, img3.dataobj)
1439def test_binarize_img_surface(surf_img_1d):
1440 """Test binarize_img on surface data."""
1441 img = surf_img_1d
1442 for k, v in img.data.parts.items():
1443 img.data.parts[k] = v + 1
1444 # Test that all output values are 1.
1445 img1 = binarize_img(surf_img_1d)
1447 assert_array_equal(np.unique(get_surface_data(img1)), np.array([1]))
1449 # Test that it works with threshold
1450 img2 = binarize_img(surf_img_1d, threshold=9)
1451 assert_array_equal(np.unique(get_surface_data(img2)), np.array([0, 1]))
1454def test_binarize_negative_img(img_4d_rand_eye, rng):
1455 # Test option to use original or absolute values
1456 img_data = img_4d_rand_eye.dataobj
1457 # Create a mask for half of the values and make them negative
1458 neg_mask = rng.choice(
1459 [True, False], size=img_4d_rand_eye.shape, p=[0.5, 0.5]
1460 )
1461 img_data[neg_mask] *= -1
1462 img = new_img_like(img_4d_rand_eye, img_data)
1463 # Binarize using original and absolute values
1464 img_original = binarize_img(
1465 img, threshold=0, two_sided=False, copy_header=True
1466 )
1467 img_absolute = binarize_img(
1468 img, threshold=0, two_sided=True, copy_header=True
1469 )
1470 # Check that all values are 1 for absolute valued threshold
1471 assert_array_equal(np.unique(img_absolute.dataobj), np.array([1]))
1472 # Check that binarized image contains 0 and 1 for original threshold
1473 assert_array_equal(np.unique(img_original.dataobj), np.array([0, 1]))
1476def test_binarize_img_copied_header(img_4d_mni_tr2):
1477 # Test equality of header fields between input and output
1478 result = binarize_img(img_4d_mni_tr2, threshold=0.5, copy_header=True)
1479 # only the min value should be different
1480 match_headers_keys(
1481 result,
1482 img_4d_mni_tr2,
1483 except_keys=["cal_min", "cal_max"],
1484 )
1485 # min value should be 0 in the result
1486 assert result.header["cal_min"] == 0
1487 # max value should be 1 in the result
1488 assert result.header["cal_max"] == 1
1491def test_binarize_img_no_userwarning(img_4d_rand_eye):
1492 # Test that a UserWarning is not thrown for a float64 img
1493 with warnings.catch_warnings():
1494 warnings.simplefilter("error", category=UserWarning)
1495 binarize_img(img_4d_rand_eye)
1498@pytest.mark.parametrize(
1499 "func, input_img",
1500 [
1501 (binarize_img, "img_4d_mni_tr2"),
1502 (crop_img, "img_4d_mni_tr2"),
1503 (mean_img, "img_4d_mni_tr2"),
1504 (threshold_img, "img_4d_mni_tr2"),
1505 ],
1506)
1507def test_warning_copy_header_false(request, func, input_img):
1508 # Use the request fixture to get the actual fixture value
1509 actual_input_img = request.getfixturevalue(input_img)
1510 with pytest.warns(FutureWarning, match="From release 0.13.0 onwards*"):
1511 if func is threshold_img:
1512 func(actual_input_img, threshold=0.5, copy_header=False)
1513 else:
1514 func(actual_input_img, copy_header=False)
1517def test_clean_img(affine_eye, shape_3d_default, rng):
1518 data = rng.standard_normal(size=(10, 10, 10, 100)) + 0.5
1519 data_flat = data.T.reshape(100, -1)
1520 data_img = Nifti1Image(data, affine_eye)
1522 with pytest.raises(ValueError, match="t_r.*must be specified"):
1523 clean_img(data_img, t_r=None, low_pass=0.1)
1525 data_img_ = clean_img(
1526 data_img, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
1527 )
1528 data_flat_ = signal.clean(
1529 data_flat, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
1530 )
1532 assert_almost_equal(get_data(data_img_).T.reshape(100, -1), data_flat_)
1533 # if NANs
1534 data[:, 9, 9] = np.nan
1535 # if infinity
1536 data[:, 5, 5] = np.inf
1537 nan_img = Nifti1Image(data, affine_eye)
1539 clean_im = clean_img(nan_img, ensure_finite=True)
1541 assert np.any(np.isfinite(get_data(clean_im)))
1543 # test_clean_img_passing_nifti2image
1544 data_img_nifti2 = Nifti2Image(data, affine_eye)
1546 clean_img(
1547 data_img_nifti2, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
1548 )
1550 # if mask_img
1551 img, mask_img = generate_fake_fmri(shape=shape_3d_default, length=10)
1553 data_img_mask_ = clean_img(img, mask_img=mask_img)
1555 # Checks that output with full mask and without is equal
1556 data_img_ = clean_img(img)
1558 assert_almost_equal(get_data(data_img_), get_data(data_img_mask_))
1561def test_clean_img_surface(surf_img_2d, surf_img_1d, surf_mask_1d) -> None:
1562 """Test clean on surface image.
1564 - check that clean returns image of same shape, geometry
1565 but different data
1566 - with mask should only clean the included vertices
1567 - 1D image should raise error.
1568 - check sample mask can be passed as a kwarg and used correctly
1569 """
1570 length = 50
1571 imgs = surf_img_2d(length)
1573 cleaned_img = clean_img(
1574 imgs, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
1575 )
1577 assert cleaned_img.shape == imgs.shape
1578 assert_polymesh_equal(cleaned_img.mesh, imgs.mesh)
1579 with pytest.raises(ValueError, match="not equal"):
1580 assert_surface_image_equal(cleaned_img, imgs)
1582 cleaned_img_with_mask = clean_img(
1583 imgs,
1584 detrend=True,
1585 standardize=False,
1586 low_pass=0.1,
1587 t_r=1.0,
1588 mask_img=surf_mask_1d,
1589 )
1590 with pytest.raises(ValueError, match="not equal"):
1591 assert_surface_image_equal(cleaned_img_with_mask, cleaned_img)
1593 # Checks that output with full mask and without is equal
1594 full_mask = new_img_like(
1595 surf_mask_1d,
1596 data={k: np.ones(v.shape) for k, v in surf_mask_1d.data.parts.items()},
1597 )
1598 cleaned_img_with_full_mask = clean_img(
1599 imgs,
1600 detrend=True,
1601 standardize=False,
1602 low_pass=0.1,
1603 t_r=1.0,
1604 mask_img=full_mask,
1605 )
1606 assert_surface_image_equal(cleaned_img, cleaned_img_with_full_mask)
1608 # 1D fails
1609 with pytest.raises(ValueError, match="should be 2D"):
1610 clean_img(surf_img_1d, detrend=True)
1612 sample_mask = np.arange(length - 1)
1614 # check sample mask can be passed as a kwarg and used correctly
1615 cleaned_img = clean_img(
1616 imgs,
1617 detrend=True,
1618 standardize=False,
1619 low_pass=0.1,
1620 t_r=1.0,
1621 clean__sample_mask=sample_mask,
1622 )
1623 assert cleaned_img.shape[-1] == length - 1
1626@pytest.mark.parametrize("create_files", [True, False])
1627def test_largest_cc_img(create_files, tmp_path):
1628 """Check the extraction of the largest connected component, for niftis.
1630 Similar to smooth_img tests for largest connected_component_img, here also
1631 only the added features for largest_connected_component are tested.
1632 """
1633 # Test whether dimension of 3Dimg and list of 3Dimgs are kept.
1634 img1, img2, shapes = _make_largest_cc_img_test_data()
1636 imgs = testing.write_imgs_to_path(
1637 img1, img2, file_path=tmp_path, create_files=create_files
1638 )
1639 # List of images as input
1640 out = largest_connected_component_img(imgs)
1642 assert isinstance(out, list)
1643 assert len(out) == 2
1644 for o, s in zip(out, shapes):
1645 assert o.shape == (s)
1647 # Single image as input
1648 out = largest_connected_component_img(imgs[0])
1650 assert isinstance(out, Nifti1Image)
1651 assert out.shape == (shapes[0])
1654@pytest.mark.parametrize("create_files", [True, False])
1655def test_largest_cc_img_non_native_endian_type(create_files, tmp_path):
1656 # Test whether dimension of 3Dimg and list of 3Dimgs are kept.
1657 img1, img2, shapes = _make_largest_cc_img_test_data()
1659 # tests adapted to non-native endian data dtype
1660 img1_change_dtype = Nifti1Image(
1661 get_data(img1).astype(">f8"), affine=img1.affine
1662 )
1663 img2_change_dtype = Nifti1Image(
1664 get_data(img2).astype(">f8"), affine=img2.affine
1665 )
1667 imgs = testing.write_imgs_to_path(
1668 img1_change_dtype,
1669 img2_change_dtype,
1670 file_path=tmp_path,
1671 create_files=create_files,
1672 )
1673 # List of images as input
1674 out = largest_connected_component_img(imgs)
1676 assert isinstance(out, list)
1677 assert len(out) == 2
1678 for o, s in zip(out, shapes):
1679 assert o.shape == (s)
1681 # Single image as input
1682 out = largest_connected_component_img(imgs[0])
1684 assert isinstance(out, Nifti1Image)
1685 assert out.shape == (shapes[0])
1687 # Test the output with native and without native
1688 out_native = largest_connected_component_img(img1)
1690 out_non_native = largest_connected_component_img(img1_change_dtype)
1691 assert_equal(get_data(out_native), get_data(out_non_native))
1694def test_largest_cc_img_error(shape_3d_default):
1695 # Test whether 4D Nifti throws the right error.
1696 img_4D = generate_fake_fmri(shape_3d_default)
1698 with pytest.raises(DimensionError, match="dimension"):
1699 largest_connected_component_img(img_4D)
1702def test_new_img_like_mgh_image(affine_eye, shape_3d_default):
1703 data = np.zeros(shape_3d_default, dtype=np.uint8)
1704 niimg = MGHImage(dataobj=data, affine=affine_eye)
1706 new_img_like(niimg, data.astype(float), niimg.affine, copy_header=True)
1709@pytest.mark.parametrize("image", [MGHImage, AnalyzeImage])
1710def test_new_img_like_boolean_data(affine_eye, image, shape_3d_default, rng):
1711 """Checks defaulting boolean input data to np.uint8 dtype is valid \
1712 forencoding with nibabel image classes MGHImage and AnalyzeImage.
1713 """
1714 data = rng.standard_normal(shape_3d_default).astype("uint8")
1715 in_img = image(dataobj=data, affine=affine_eye)
1717 out_img = new_img_like(in_img, data=in_img.get_fdata() > 0.5)
1719 assert get_data(out_img).dtype == "uint8"
1722def test_clean_img_sample_mask(img_4d_rand_eye, shape_4d_default):
1723 """Check sample mask can be passed as a kwarg and used correctly."""
1724 length = shape_4d_default[3]
1725 confounds = _basic_confounds(length)
1726 # exclude last time point
1727 sample_mask = np.arange(length - 1)
1729 img = clean_img(
1730 img_4d_rand_eye,
1731 confounds=confounds,
1732 clean__sample_mask=sample_mask,
1733 )
1734 assert img.shape == (*shape_4d_default[:3], length - 1)
1737def test_clean_img_sample_mask_mask_img(shape_3d_default):
1738 """Check sample_mask and mask_img can be correctly used together."""
1739 length = 10
1740 confounds = _basic_confounds(length)
1741 img_4d, mask_img = generate_fake_fmri(
1742 shape=shape_3d_default, length=length
1743 )
1745 # exclude last time point
1746 sample_mask = np.arange(length - 1)
1748 # test with sample mask
1749 img = clean_img(
1750 img_4d,
1751 confounds=confounds,
1752 mask_img=mask_img,
1753 clean__sample_mask=sample_mask,
1754 )
1755 assert img.shape == (*shape_3d_default, length - 1)
1758def test_concat_niimgs_errors(affine_eye, shape_3d_default):
1759 img1 = Nifti1Image(np.ones(shape_3d_default), affine_eye)
1760 img2 = Nifti1Image(np.ones(shape_3d_default), 2 * affine_eye)
1761 img4d = Nifti1Image(np.ones((*shape_3d_default, 2)), affine_eye)
1763 # check error for non-forced but necessary resampling
1764 with pytest.raises(ValueError, match="Field of view of image"):
1765 concat_imgs([img1, img2], auto_resample=False)
1767 # Regression test for #601.
1768 # Dimensionality of first image was not checked properly.
1769 _dimension_error_msg = (
1770 "Input data has incompatible dimensionality: "
1771 "Expected dimension is 4D and you provided "
1772 "a list of 4D images \\(5D\\)"
1773 )
1774 with pytest.raises(DimensionError, match=_dimension_error_msg):
1775 concat_imgs([img4d], ensure_ndim=4)
1777 with pytest.raises(DimensionError, match=_dimension_error_msg):
1778 concat_imgs([img1, img4d])
1780 img5d = Nifti1Image(np.ones((2, 2, 2, 2, 2)), affine_eye)
1781 with pytest.raises(
1782 TypeError,
1783 match="Concatenated images must be 3D or 4D. "
1784 "You gave a list of 5D images",
1785 ):
1786 concat_imgs([img5d, img5d])
1789def test_concat_niimgs(affine_eye, tmp_path):
1790 # create images different in affine and 3D/4D shape
1791 shape = (10, 11, 12)
1792 img1 = Nifti1Image(np.ones(shape), affine_eye)
1793 img2 = Nifti1Image(np.zeros(shape), affine_eye)
1795 shape2 = (12, 11, 10)
1796 img1b = Nifti1Image(np.ones(shape2), affine_eye)
1798 shape3 = (11, 22, 33)
1799 img1c = Nifti1Image(np.ones(shape3), affine_eye)
1801 # check basic concatenation with equal shape/affine
1802 # verbose for coverage
1803 concatenated = concat_imgs((img1, img2, img1), verbose=1)
1805 # smoke-test auto_resample
1806 concatenated = concat_imgs((img1, img1b, img1c), auto_resample=True)
1807 assert concatenated.shape == (*img1.shape, 3)
1809 # test list of 4D niimgs as input
1810 img1.to_filename(tmp_path / "1.nii")
1811 img2.to_filename(tmp_path / "2.nii")
1812 concatenated = concat_imgs(tmp_path / "*")
1813 assert_array_equal(get_data(concatenated)[..., 0], get_data(img1))
1814 assert_array_equal(get_data(concatenated)[..., 1], get_data(img2))
1817def test_concat_niimg_dtype(affine_eye):
1818 shape = [2, 3, 4]
1819 vols = [
1820 Nifti1Image(np.zeros([*shape, n_scans]).astype(np.int16), affine_eye)
1821 for n_scans in [1, 5]
1822 ]
1823 nimg = concat_imgs(vols)
1824 assert get_data(nimg).dtype == np.float32
1825 nimg = concat_imgs(vols, dtype=None)
1826 assert get_data(nimg).dtype == np.int16
1829def test_concat_imgs_surface(surf_img_2d):
1830 """Check concat_imgs returns a single SurfaceImage.
1832 Output must have as many samples as the sum of samples in the input.
1833 """
1834 img = concat_imgs([surf_img_2d(3), surf_img_2d(5)], dtype=np.float16)
1835 assert img.shape == (9, 8)
1836 for value in img.data.parts.values():
1837 assert value.ndim == 2
1838 assert value.dtype == np.float16
1841def nifti_generator(buffer):
1842 for _ in range(10):
1843 buffer.append(_img_3d_rand())
1844 yield buffer[-1]
1847def test_iterator_generator(img_3d_rand_eye):
1848 # Create a list of random images
1849 list_images = [img_3d_rand_eye for _ in range(10)]
1850 cc = concat_imgs(list_images)
1851 assert cc.shape[-1] == 10
1852 assert_array_almost_equal(get_data(cc)[..., 0], get_data(list_images[0]))
1854 # Same with iteration
1855 i = iter_img(list_images)
1856 cc = concat_imgs(i)
1857 assert cc.shape[-1] == 10
1858 assert_array_almost_equal(get_data(cc)[..., 0], get_data(list_images[0]))
1860 # Now, a generator
1861 b = []
1862 g = nifti_generator(b)
1863 cc = concat_imgs(g)
1864 assert cc.shape[-1] == 10
1865 assert len(b) == 10
1868def test_copy_img():
1869 with pytest.raises(ValueError, match="Input value is not an image"):
1870 copy_img(3)
1873def test_copy_img_side_effect(img_4d_ones_eye):
1874 hash1 = joblib.hash(img_4d_ones_eye)
1875 copy_img(img_4d_ones_eye)
1876 hash2 = joblib.hash(img_4d_ones_eye)
1877 assert hash1 == hash2