Coverage for nilearn/tests/test_masking.py: 0%
376 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1"""Test the mask-extracting utilities."""
3import warnings
5import numpy as np
6import pytest
7from nibabel import Nifti1Image
8from numpy.testing import assert_array_equal, assert_equal
9from sklearn.preprocessing import StandardScaler
11from nilearn._utils import data_gen
12from nilearn._utils.exceptions import DimensionError
13from nilearn._utils.testing import write_imgs_to_path
14from nilearn.conftest import _affine_eye, _rng
15from nilearn.image import get_data, high_variance_confounds
16from nilearn.maskers import NiftiMasker
17from nilearn.masking import (
18 _MaskWarning,
19 _unmask_3d,
20 _unmask_4d,
21 apply_mask,
22 compute_background_mask,
23 compute_brain_mask,
24 compute_epi_mask,
25 compute_multi_brain_mask,
26 compute_multi_epi_mask,
27 extrapolate_out_mask,
28 intersect_masks,
29 load_mask_img,
30 unmask,
31 unmask_from_to_3d_array,
32)
33from nilearn.surface.surface import SurfaceImage
35np_version = (
36 np.version.full_version
37 if hasattr(np.version, "full_version")
38 else np.version.short_version
39)
41_TEST_DIM_ERROR_MSG = (
42 "Input data has incompatible dimensionality: "
43 "Expected dimension is 3D and you provided "
44 "a %s image"
45)
48def _simu_img():
49 # Random confounds
50 rng = _rng()
51 conf = 2 + rng.standard_normal((100, 6))
52 # Random 4D volume
53 vol = 100 + 10 * rng.standard_normal((5, 5, 2, 100))
54 img = Nifti1Image(vol, np.eye(4))
55 # Create an nifti image with the data, and corresponding mask
56 mask = Nifti1Image(np.ones([5, 5, 2]), np.eye(4))
57 return img, mask, conf
60def _cov_conf(tseries, conf):
61 conf_n = StandardScaler().fit_transform(conf)
62 _ = StandardScaler().fit_transform(tseries)
63 cov_mat = np.dot(tseries.T, conf_n)
64 return cov_mat
67def test_load_mask_img_error_inputs(surf_img_2d, img_4d_ones_eye):
68 """Check input validation of load_mask_img."""
69 with pytest.raises(
70 TypeError, match="a 3D/4D Niimg-like object or a SurfaceImage"
71 ):
72 load_mask_img(1)
74 with pytest.raises(
75 TypeError,
76 match="Expected dimension is 3D and you provided a 4D image.",
77 ):
78 load_mask_img(img_4d_ones_eye)
80 with pytest.raises(
81 ValueError, match="Data for each part of .* should be 1D."
82 ):
83 load_mask_img(surf_img_2d())
86def test_load_mask_img_surface(surf_mask_1d):
87 """Check load_mask_img returns a boolean surface image \
88 when SurfaceImage is used as input.
89 """
90 mask, _ = load_mask_img(surf_mask_1d)
91 assert isinstance(mask, SurfaceImage)
92 for hemi in mask.data.parts.values():
93 assert hemi.dtype == "bool"
96def test_high_variance_confounds():
97 """Test high_variance_confounds."""
98 img, mask, conf = _simu_img()
100 hv_confounds = high_variance_confounds(img)
102 masker1 = NiftiMasker(
103 standardize="zscore_sample",
104 detrend=False,
105 high_variance_confounds=False,
106 mask_img=mask,
107 ).fit()
108 tseries1 = masker1.transform(img, confounds=[hv_confounds, conf])
110 masker2 = NiftiMasker(
111 standardize="zscore_sample",
112 detrend=False,
113 high_variance_confounds=True,
114 mask_img=mask,
115 ).fit()
116 tseries2 = masker2.transform(img, confounds=conf)
118 assert_array_equal(tseries1, tseries2)
121def _confounds_regression(
122 standardize_signal="zscore_sample", standardize_confounds=True
123):
124 img, mask, conf = _simu_img()
126 masker = NiftiMasker(
127 standardize=standardize_signal,
128 standardize_confounds=standardize_confounds,
129 detrend=False,
130 mask_img=mask,
131 ).fit()
133 tseries = masker.transform(img, confounds=conf)
135 if standardize_confounds:
136 conf = StandardScaler(with_std=False).fit_transform(conf)
138 cov_mat = _cov_conf(tseries, conf)
140 return np.sum(np.abs(cov_mat))
143@pytest.mark.parametrize(
144 "standardize_signal, standardize_confounds, expected",
145 [
146 # Signal is not standardized
147 (False, True, 10.0 * 10e-10),
148 # Signal is z-scored with string arg
149 ("zscore_sample", True, 10e-10),
150 # Signal is psc standardized
151 ("psc", True, 10.0 * 10e-10),
152 ],
153)
154def test_confounds_standardization(
155 standardize_signal, standardize_confounds, expected
156):
157 """Tests for confounds standardization.
159 Explicit standardization of confounds
161 See Issue #2584
162 Code from @pbellec
163 """
164 assert (
165 _confounds_regression(
166 standardize_signal=standardize_signal,
167 standardize_confounds=standardize_confounds,
168 )
169 < expected
170 )
173@pytest.mark.parametrize(
174 "standardize_signal",
175 [
176 # Signal is not standardized
177 False,
178 # Signal is z-scored with string arg
179 "zscore_sample",
180 # Signal is psc standardized
181 "psc",
182 ],
183)
184def test_confounds_not_standardized(standardize_signal):
185 """Tests for confounds standardization.
187 Confounds are not standardized
188 In this case, the regression should fail...
190 See Issue #2584
191 Code from @pbellec
192 """
193 # Signal is not standardized
194 assert (
195 _confounds_regression(
196 standardize_signal=standardize_signal,
197 standardize_confounds=False,
198 )
199 > 100
200 )
203@pytest.mark.parametrize(
204 "fn",
205 [
206 compute_background_mask,
207 compute_brain_mask,
208 compute_epi_mask,
209 ],
210)
211def test_compute_mask_error(fn):
212 """Check that an empty list of images creates a meaningful error."""
213 with pytest.raises(TypeError, match="Cannot concatenate empty objects"):
214 fn([])
217def test_compute_epi_mask(affine_eye):
218 """Test compute_epi_mask."""
219 mean_image = np.ones((9, 9, 3))
220 mean_image[3:-2, 3:-2, :] = 10
221 mean_image[5, 5, :] = 11
222 mean_image = Nifti1Image(mean_image, affine_eye)
224 mask1 = compute_epi_mask(mean_image, opening=False, verbose=1)
225 mask2 = compute_epi_mask(mean_image, exclude_zeros=True, opening=False)
227 # With an array with no zeros, exclude_zeros should not make
228 # any difference
229 assert_array_equal(get_data(mask1), get_data(mask2))
231 # Check that padding with zeros does not change the extracted mask
232 mean_image2 = np.zeros((30, 30, 3))
233 mean_image2[3:12, 3:12, :] = get_data(mean_image)
234 mean_image2 = Nifti1Image(mean_image2, affine_eye)
236 mask3 = compute_epi_mask(mean_image2, exclude_zeros=True, opening=False)
238 assert_array_equal(get_data(mask1), get_data(mask3)[3:12, 3:12])
240 # However, without exclude_zeros, it does
241 mask3 = compute_epi_mask(mean_image2, opening=False)
242 assert not np.allclose(get_data(mask1), get_data(mask3)[3:12, 3:12])
245def test_compute_epi_mask_errors_warnings(affine_eye):
246 """Check that we get a ValueError for incorrect shape."""
247 mean_image = np.ones((9, 9))
248 mean_image[3:-3, 3:-3] = 10
249 mean_image[5, 5] = 100
250 mean_image = Nifti1Image(mean_image, affine_eye)
252 with pytest.raises(
253 ValueError,
254 match=(
255 "Computation expects 3D or 4D images, but 2 dimensions were given"
256 ),
257 ):
258 compute_epi_mask(mean_image)
260 # Check that we get a useful warning for empty masks
261 mean_image = np.zeros((9, 9, 9))
262 mean_image[0, 0, 1] = -1
263 mean_image[0, 0, 0] = 1.2
264 mean_image[0, 0, 2] = 1.1
265 mean_image = Nifti1Image(mean_image, affine_eye)
267 with pytest.warns(_MaskWarning, match="Computed an empty mask"):
268 compute_epi_mask(mean_image, exclude_zeros=True)
271@pytest.mark.parametrize("value", (0, np.nan))
272def test_compute_background_mask(affine_eye, value):
273 """Test compute_background_mask."""
274 mean_image = value * np.ones((9, 9, 9))
275 mean_image[3:-3, 3:-3, 3:-3] = 1
276 mask = mean_image == 1
277 mean_image = Nifti1Image(mean_image, affine_eye)
279 mask1 = compute_background_mask(mean_image, opening=False, verbose=1)
281 assert_array_equal(get_data(mask1), mask.astype(np.int8))
284def test_compute_background_mask_errors_warnings(affine_eye):
285 """Check that we get a ValueError for incorrect shape."""
286 mean_image = np.ones((9, 9))
287 mean_image[3:-3, 3:-3] = 10
288 mean_image[5, 5] = 100
289 mean_image = Nifti1Image(mean_image, affine_eye)
291 with pytest.raises(ValueError):
292 compute_background_mask(mean_image)
294 # Check that we get a useful warning for empty masks
295 mean_image = np.zeros((9, 9, 9))
296 mean_image = Nifti1Image(mean_image, affine_eye)
298 with pytest.warns(_MaskWarning, match="Computed an empty mask"):
299 compute_background_mask(mean_image)
302def test_compute_brain_mask():
303 """Test compute_brain_mask."""
304 img, _ = data_gen.generate_mni_space_img(res=8, random_state=0)
306 brain_mask = compute_brain_mask(img, threshold=0.2, verbose=1)
307 gm_mask = compute_brain_mask(img, threshold=0.2, mask_type="gm")
308 wm_mask = compute_brain_mask(img, threshold=0.2, mask_type="wm")
310 brain_data, gm_data, wm_data = map(
311 get_data, (brain_mask, gm_mask, wm_mask)
312 )
314 # Check that whole-brain mask is non-empty
315 assert (brain_data != 0).any()
316 for subset in gm_data, wm_data:
317 # Test that gm and wm masks are included in the whole-brain mask
318 assert (
319 np.logical_and(brain_data, subset) == subset.astype(bool)
320 ).all()
321 # Test that gm and wm masks are non-empty
322 assert (subset != 0).any()
324 # Test that gm and wm masks have empty intersection
325 assert (np.logical_and(gm_data, wm_data) == 0).all()
327 # Check that we get a useful warning for empty masks
328 with pytest.warns(_MaskWarning):
329 compute_brain_mask(img, threshold=1)
331 # Check that masks obtained from same FOV are the same
332 img1, _ = data_gen.generate_mni_space_img(res=8, random_state=1)
333 mask_img1 = compute_brain_mask(img1, verbose=1, threshold=0.2)
335 assert (brain_data == get_data(mask_img1)).all()
337 # Check that error is raised if mask type is unknown
338 with pytest.raises(ValueError, match="Unknown mask type foo."):
339 compute_brain_mask(img, verbose=1, mask_type="foo")
342@pytest.mark.parametrize(
343 "affine",
344 [_affine_eye(), np.diag((1, 1, -1, 1)), np.diag((0.5, 1, 0.5, 1))],
345)
346@pytest.mark.parametrize("create_files", (False, True))
347def test_apply_mask(tmp_path, create_files, affine):
348 """Test smoothing of timeseries extraction."""
349 # A delta in 3D
350 # Standard masking
351 data = np.zeros((40, 40, 40, 2))
352 data[20, 20, 20] = 1
353 data_img = Nifti1Image(data, affine)
355 mask = np.ones((40, 40, 40))
356 mask_img = Nifti1Image(mask, affine)
358 filenames = write_imgs_to_path(
359 data_img,
360 mask_img,
361 file_path=tmp_path,
362 create_files=create_files,
363 )
365 series = apply_mask(filenames[0], filenames[1], smoothing_fwhm=9)
367 series = np.reshape(series[0, :], (40, 40, 40))
368 vmax = series.max()
369 # We are expecting a full-width at half maximum of
370 # 9mm/voxel_size:
371 above_half_max = series > 0.5 * vmax
372 for axis in (0, 1, 2):
373 proj = np.any(
374 np.any(np.rollaxis(above_half_max, axis=axis), axis=-1),
375 axis=-1,
376 )
378 assert_equal(proj.sum(), 9 / np.abs(affine[axis, axis]))
381def test_apply_mask_surface(surf_img_2d, surf_mask_1d):
382 """Test apply_mask on surface."""
383 length = 5
384 series = apply_mask(surf_img_2d(length), surf_mask_1d)
386 assert isinstance(series, np.ndarray)
387 assert series.shape[0] == length
390def test_apply_mask_nan(affine_eye):
391 """Check that NaNs in the data do not propagate."""
392 data = np.zeros((40, 40, 40, 2))
393 data[20, 20, 20] = 1
394 data[10, 10, 10] = np.nan
395 data_img = Nifti1Image(data, affine_eye)
397 mask = np.ones((40, 40, 40))
398 mask_img = Nifti1Image(mask, affine_eye)
400 series = apply_mask(data_img, mask_img, smoothing_fwhm=9)
402 assert np.all(np.isfinite(series))
405def test_apply_mask_errors(affine_eye):
406 """Check errors for dimension."""
407 data = np.zeros((40, 40, 40, 2))
408 data[20, 20, 20] = 1
409 data_img = Nifti1Image(data, affine_eye)
411 mask = np.ones((40, 40, 40))
412 mask_img = Nifti1Image(mask, affine_eye)
414 full_mask = np.zeros((40, 40, 40))
415 full_mask_img = Nifti1Image(full_mask, affine_eye)
417 # veriy that 4D masks are rejected
418 mask_img_4d = Nifti1Image(np.ones((40, 40, 40, 2)), affine_eye)
420 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"):
421 apply_mask(data_img, mask_img_4d)
423 # Check that 3D data is accepted
424 data_3d = Nifti1Image(
425 np.arange(27, dtype="int32").reshape((3, 3, 3)), affine_eye
426 )
427 mask_data_3d = np.zeros((3, 3, 3))
428 mask_data_3d[1, 1, 0] = True
429 mask_data_3d[0, 1, 0] = True
430 mask_data_3d[0, 1, 1] = True
432 data_3d = apply_mask(data_3d, Nifti1Image(mask_data_3d, affine_eye))
434 assert sorted(data_3d.tolist()) == [3.0, 4.0, 12.0]
436 # Check data shape and affine
437 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "2D"):
438 apply_mask(data_img, Nifti1Image(mask[20, ...], affine_eye))
440 with pytest.raises(ValueError, match="is different from img affine"):
441 apply_mask(data_img, Nifti1Image(mask, affine_eye / 2.0))
443 # Check that full masking raises error
444 with pytest.raises(
445 ValueError,
446 match="The mask is invalid as it is empty: it masks all data.",
447 ):
448 apply_mask(data_img, full_mask_img)
450 # Check weird values in data
451 mask[10, 10, 10] = 2
452 with pytest.raises(
453 ValueError,
454 match="Background of the mask must be represented with 0.",
455 ):
456 apply_mask(data_img, Nifti1Image(mask, affine_eye))
458 mask[15, 15, 15] = 3
459 with pytest.raises(
460 ValueError, match="Given mask is not made of 2 values.*"
461 ):
462 apply_mask(Nifti1Image(data, affine_eye), mask_img)
465def test_unmask_4d(rng, affine_eye, shape_4d_default):
466 """Test unmask on 4D images."""
467 data4D = rng.uniform(size=shape_4d_default)
468 mask = rng.integers(2, size=shape_4d_default[:3], dtype="int32")
469 mask_img = Nifti1Image(mask, affine_eye)
470 mask = mask.astype(bool)
472 masked4D = data4D[mask, :].T
473 unmasked4D = data4D.copy()
474 unmasked4D[np.logical_not(mask), :] = 0
476 # 4D Test, test value ordering at the same time.
477 t = get_data(unmask(masked4D, mask_img, order="C"))
479 assert t.ndim == 4
480 assert t.flags["C_CONTIGUOUS"]
481 assert not t.flags["F_CONTIGUOUS"]
482 assert_array_equal(t, unmasked4D)
484 t = unmask([masked4D], mask_img, order="F")
485 t = [get_data(t_) for t_ in t]
487 assert isinstance(t, list)
488 assert t[0].ndim == 4
489 assert not t[0].flags["C_CONTIGUOUS"]
490 assert t[0].flags["F_CONTIGUOUS"]
491 assert_array_equal(t[0], unmasked4D)
494@pytest.mark.parametrize("create_files", [False, True])
495def test_unmask_3d_with_files(
496 rng, affine_eye, tmp_path, create_files, shape_3d_default
497):
498 """Test unmask on 3D images.
500 Check both with Nifti1Image and file.
501 """
502 data3D = rng.uniform(size=shape_3d_default)
503 mask = rng.integers(2, size=shape_3d_default, dtype="int32")
504 mask_img = Nifti1Image(mask, affine_eye)
505 mask = mask.astype(bool)
507 masked3D = data3D[mask]
508 unmasked3D = data3D.copy()
509 unmasked3D[np.logical_not(mask)] = 0
511 filename = write_imgs_to_path(
512 mask_img,
513 file_path=tmp_path,
514 create_files=create_files,
515 )
516 t = get_data(unmask(masked3D, filename, order="C"))
518 assert t.ndim == 3
519 assert t.flags["C_CONTIGUOUS"]
520 assert not t.flags["F_CONTIGUOUS"]
521 assert_array_equal(t, unmasked3D)
523 t = unmask([masked3D], filename, order="F")
524 t = [get_data(t_) for t_ in t]
526 assert isinstance(t, list)
527 assert t[0].ndim == 3
528 assert not t[0].flags["C_CONTIGUOUS"]
529 assert t[0].flags["F_CONTIGUOUS"]
530 assert_array_equal(t[0], unmasked3D)
533def test_unmask_errors(rng, affine_eye, shape_3d_default):
534 """Test unmask errors."""
535 # A delta in 3D
536 mask = rng.integers(2, size=shape_3d_default, dtype="int32")
537 mask_img = Nifti1Image(mask, affine_eye)
538 mask = mask.astype(bool)
540 # Error test: shape
541 vec_1D = np.empty((500,), dtype=int)
543 msg = "X must be of shape"
544 with pytest.raises(TypeError, match=msg):
545 unmask(vec_1D, mask_img)
546 with pytest.raises(TypeError, match=msg):
547 unmask([vec_1D], mask_img)
549 vec_2D = np.empty((500, 500), dtype=np.float64)
551 with pytest.raises(TypeError, match=msg):
552 unmask(vec_2D, mask_img)
554 with pytest.raises(TypeError, match=msg):
555 unmask([vec_2D], mask_img)
557 # Error test: mask type
558 msg = "mask must be a boolean array"
559 with pytest.raises(TypeError, match=msg):
560 _unmask_3d(vec_1D, mask.astype(int))
562 with pytest.raises(TypeError, match=msg):
563 _unmask_4d(vec_2D, mask.astype(np.float64))
565 # Transposed vector
566 transposed_vector = np.ones((np.sum(mask), 1), dtype=bool)
567 with pytest.raises(TypeError, match="X must be of shape"):
568 unmask(transposed_vector, mask_img)
571def test_unmask_error_shape(rng, affine_eye, shape_4d_default):
572 """Test unmask errors shape between mask and image."""
573 X = rng.standard_normal()
574 mask_img = np.zeros(shape_4d_default, dtype=np.uint8)
575 mask_img[rng.standard_normal(size=shape_4d_default) > 0.4] = 1
576 n_features = (mask_img > 0).sum()
577 mask_img = Nifti1Image(mask_img, affine_eye)
578 n_samples = shape_4d_default[0]
580 X = rng.standard_normal(size=(n_samples, n_features, 2))
582 # 3D X (unmask should raise a DimensionError)
583 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"):
584 unmask(X, mask_img)
586 X = rng.standard_normal(size=(n_samples, n_features))
588 # Raises an error because the mask is 4D
589 with pytest.raises(DimensionError, match=_TEST_DIM_ERROR_MSG % "4D"):
590 unmask(X, mask_img)
593@pytest.fixture
594def img_2d_mask_bottom_right(affine_eye):
595 """Return 3D nifti binary mask image with bottom right filled.
597 +---+---+---+---+
598 | | | | |
599 +---+---+---+---+
600 | | | | |
601 +---+---+---+---+
602 | | | X | X |
603 +---+---+---+---+
604 | | | X | X |
605 +---+---+---+---+
607 """
608 mask_a = np.zeros((4, 4, 1), dtype=bool)
609 mask_a[2:4, 2:4] = 1
610 return Nifti1Image(mask_a.astype("int32"), affine_eye)
613@pytest.fixture
614def img_2d_mask_center(affine_eye):
615 """Return 3D nifti binary mask image with center filled.
617 +---+---+---+---+
618 | | | | |
619 +---+---+---+---+
620 | | X | X | |
621 +---+---+---+---+
622 | | X | X | |
623 +---+---+---+---+
624 | | | | |
625 +---+---+---+---+
627 """
628 mask_b = np.zeros((4, 4, 1), dtype=bool)
629 mask_b[1:3, 1:3] = 1
630 return Nifti1Image(mask_b.astype("int32"), affine_eye)
633@pytest.mark.parametrize("create_files", (False, True))
634def test_intersect_masks_filename(
635 tmp_path, img_2d_mask_bottom_right, img_2d_mask_center, create_files
636):
637 """Test the intersect_masks function on files."""
638 filenames = write_imgs_to_path(
639 img_2d_mask_bottom_right,
640 img_2d_mask_center,
641 file_path=tmp_path,
642 create_files=create_files,
643 )
644 mask_ab = np.zeros((4, 4, 1), dtype=bool)
645 mask_ab[2, 2] = 1
646 mask_ab_ = intersect_masks(filenames, threshold=1.0)
648 assert_array_equal(mask_ab, get_data(mask_ab_))
651def test_intersect_masks_f8(img_2d_mask_bottom_right, img_2d_mask_center):
652 """Test intersect mask images with '>f8'.
654 This function uses largest_connected_component
655 to check if intersect_masks passes
656 with connected=True (which is by default)
657 """
658 mask_a_img_change_dtype = Nifti1Image(
659 get_data(img_2d_mask_bottom_right).astype(">f8"),
660 affine=img_2d_mask_bottom_right.affine,
661 )
662 mask_b_img_change_dtype = Nifti1Image(
663 get_data(img_2d_mask_center).astype(">f8"),
664 affine=img_2d_mask_center.affine,
665 )
666 mask_ab_change_type = intersect_masks(
667 [mask_a_img_change_dtype, mask_b_img_change_dtype], threshold=1.0
668 )
670 mask_ab = np.zeros((4, 4, 1), dtype=bool)
671 mask_ab[2, 2] = 1
672 assert_array_equal(mask_ab, get_data(mask_ab_change_type))
675def test_intersect_masks(
676 affine_eye, img_2d_mask_bottom_right, img_2d_mask_center
677):
678 """Test the intersect_masks function."""
679 mask_c = np.zeros((4, 4, 1), dtype=bool)
680 mask_c[:, 2] = 1
681 mask_c[0, 0] = 1
682 mask_c_img = Nifti1Image(mask_c.astype("int32"), affine_eye)
684 # +---+---+---+---+
685 # | X | | X | |
686 # +---+---+---+---+
687 # | | | X | |
688 # +---+---+---+---+
689 # | | | X | |
690 # +---+---+---+---+
691 # | | | X | |
692 # +---+---+---+---+
694 mask_a = np.zeros((4, 4, 1), dtype=bool)
695 mask_a[2:4, 2:4] = 1
697 mask_b = np.zeros((4, 4, 1), dtype=bool)
698 mask_b[1:3, 1:3] = 1
700 mask_abc = mask_a + mask_b + mask_c
701 mask_abc_ = intersect_masks(
702 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img],
703 threshold=0.0,
704 connected=False,
705 )
707 assert_array_equal(mask_abc, get_data(mask_abc_))
709 mask_abc[0, 0] = 0
710 mask_abc_ = intersect_masks(
711 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img],
712 threshold=0.0,
713 )
715 assert_array_equal(mask_abc, get_data(mask_abc_))
717 mask_abc = np.zeros((4, 4, 1), dtype=bool)
718 mask_abc[2, 2] = 1
719 mask_abc_ = intersect_masks(
720 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img],
721 threshold=1.0,
722 )
724 assert_array_equal(mask_abc, get_data(mask_abc_))
726 mask_abc[1, 2] = 1
727 mask_abc[3, 2] = 1
728 mask_abc_ = intersect_masks(
729 [img_2d_mask_bottom_right, img_2d_mask_center, mask_c_img]
730 )
732 assert_array_equal(mask_abc, get_data(mask_abc_))
735def test_compute_multi_epi_mask(affine_eye):
736 """Test resampling done with compute_multi_epi_mask."""
737 mask_a = np.zeros((4, 4, 1), dtype=bool)
738 mask_a[2:4, 2:4] = 1
739 mask_a_img = Nifti1Image(mask_a.astype("uint8"), affine_eye)
741 mask_b = np.zeros((8, 8, 1), dtype=bool)
742 mask_b[2:6, 2:6] = 1
743 mask_b_img = Nifti1Image(mask_b.astype("uint8"), affine_eye / 2.0)
745 with warnings.catch_warnings():
746 warnings.simplefilter("ignore", _MaskWarning)
747 with pytest.raises(
748 ValueError, match="cannot convert float NaN to integer"
749 ):
750 compute_multi_epi_mask([mask_a_img, mask_b_img])
752 mask_ab = np.zeros((4, 4, 1), dtype=bool)
753 mask_ab[2, 2] = 1
754 mask_ab_ = compute_multi_epi_mask(
755 [mask_a_img, mask_b_img],
756 threshold=1.0,
757 opening=0,
758 target_affine=affine_eye,
759 target_shape=(4, 4, 1),
760 verbose=1,
761 )
763 assert_array_equal(mask_ab, get_data(mask_ab_))
766def test_compute_multi_brain_mask_error():
767 """Check error raised if images with different shapes given as input."""
768 imgs = [
769 data_gen.generate_mni_space_img(res=8, random_state=0)[0],
770 data_gen.generate_mni_space_img(res=12, random_state=0)[0],
771 ]
772 with pytest.raises(
773 ValueError,
774 match="Field of view of image #1 is different from reference FOV.",
775 ):
776 compute_multi_brain_mask(imgs)
779def test_compute_multi_brain_mask():
780 """Check results are the same if affine is the same."""
781 imgs1 = [
782 data_gen.generate_mni_space_img(res=9, random_state=0)[0],
783 data_gen.generate_mni_space_img(res=9, random_state=1)[0],
784 ]
785 imgs2 = [
786 data_gen.generate_mni_space_img(res=9, random_state=2)[0],
787 data_gen.generate_mni_space_img(res=9, random_state=3)[0],
788 ]
789 mask1 = compute_multi_brain_mask(imgs1, threshold=0.2, verbose=1)
790 mask2 = compute_multi_brain_mask(imgs2, threshold=0.2)
792 assert_array_equal(get_data(mask1), get_data(mask2))
795def test_nifti_masker_empty_mask_warning(affine_eye):
796 """Check error is raised when mask_strategy="epi" masks all data."""
797 X = Nifti1Image(np.ones((2, 2, 2, 5)), affine_eye)
798 with pytest.raises(
799 ValueError,
800 match="The mask is invalid as it is empty: it masks all data",
801 ):
802 NiftiMasker(mask_strategy="epi").fit_transform(X)
805def test_unmask_list(rng, shape_3d_default, affine_eye):
806 """Test unmask on list input.
808 Results should be equivalent to array input.
809 """
810 mask_data = rng.uniform(size=shape_3d_default) < 0.5
811 mask_img = Nifti1Image(mask_data.astype(np.uint8), affine_eye)
813 a = unmask(mask_data[mask_data], mask_img)
814 b = unmask(mask_data[mask_data].tolist(), mask_img) # shouldn't crash
816 assert_array_equal(get_data(a), get_data(b))
819def test_extrapolate_out_mask():
820 """Test extrapolate_out_mask."""
821 # Input data:
822 initial_data = np.zeros((5, 5, 5))
823 initial_data[1, 2, 2] = 1
824 initial_data[2, 1, 2] = 2
825 initial_data[2, 2, 1] = 3
826 initial_data[3, 2, 2] = 4
827 initial_data[2, 3, 2] = 5
828 initial_data[2, 2, 3] = 6
829 initial_mask = initial_data.copy() != 0
831 # Expected result
832 target_data = np.array(
833 [
834 [
835 [0.0, 0.0, 0.0, 0.0, 0.0],
836 [0.0, 0.0, 0.0, 0.0, 0.0],
837 [0.0, 0.0, 1.0, 0.0, 0.0],
838 [0.0, 0.0, 0.0, 0.0, 0.0],
839 [0.0, 0.0, 0.0, 0.0, 0.0],
840 ],
841 [
842 [0.0, 0.0, 0.0, 0.0, 0.0],
843 [0.0, 0.0, 1.5, 0.0, 0.0],
844 [0.0, 2.0, 1.0, 3.5, 0.0],
845 [0.0, 0.0, 3.0, 0.0, 0.0],
846 [0.0, 0.0, 0.0, 0.0, 0.0],
847 ],
848 [
849 [0.0, 0.0, 2.0, 0.0, 0.0],
850 [0.0, 2.5, 2.0, 4.0, 0.0],
851 [3.0, 3.0, 3.5, 6.0, 6.0],
852 [0.0, 4.0, 5.0, 5.5, 0.0],
853 [0.0, 0.0, 5.0, 0.0, 0.0],
854 ],
855 [
856 [0.0, 0.0, 0.0, 0.0, 0.0],
857 [0.0, 0.0, 3.0, 0.0, 0.0],
858 [0.0, 3.5, 4.0, 5.0, 0.0],
859 [0.0, 0.0, 4.5, 0.0, 0.0],
860 [0.0, 0.0, 0.0, 0.0, 0.0],
861 ],
862 [
863 [0.0, 0.0, 0.0, 0.0, 0.0],
864 [0.0, 0.0, 0.0, 0.0, 0.0],
865 [0.0, 0.0, 4.0, 0.0, 0.0],
866 [0.0, 0.0, 0.0, 0.0, 0.0],
867 [0.0, 0.0, 0.0, 0.0, 0.0],
868 ],
869 ]
870 )
871 target_mask = np.array(
872 [
873 [
874 [False, False, False, False, False],
875 [False, False, False, False, False],
876 [False, False, True, False, False],
877 [False, False, False, False, False],
878 [False, False, False, False, False],
879 ],
880 [
881 [False, False, False, False, False],
882 [False, False, True, False, False],
883 [False, True, True, True, False],
884 [False, False, True, False, False],
885 [False, False, False, False, False],
886 ],
887 [
888 [False, False, True, False, False],
889 [False, True, True, True, False],
890 [True, True, True, True, True],
891 [False, True, True, True, False],
892 [False, False, True, False, False],
893 ],
894 [
895 [False, False, False, False, False],
896 [False, False, True, False, False],
897 [False, True, True, True, False],
898 [False, False, True, False, False],
899 [False, False, False, False, False],
900 ],
901 [
902 [False, False, False, False, False],
903 [False, False, False, False, False],
904 [False, False, True, False, False],
905 [False, False, False, False, False],
906 [False, False, False, False, False],
907 ],
908 ]
909 )
911 # Test:
912 extrapolated_data, extrapolated_mask = extrapolate_out_mask(
913 initial_data, initial_mask, iterations=1
914 )
916 assert_array_equal(extrapolated_data, target_data)
917 assert_array_equal(extrapolated_mask, target_mask)
920@pytest.mark.parametrize("ndim", range(4))
921def test_unmask_from_to_3d_array(rng, ndim):
922 """Test unmask_from_to_3d_array."""
923 size = 5
924 shape = [size] * ndim
925 mask = np.zeros(shape).astype(bool)
926 mask[rng.uniform(size=shape) > 0.8] = 1
927 support = rng.standard_normal(size=mask.sum())
929 full = unmask_from_to_3d_array(support, mask)
931 assert_array_equal(full.shape, shape)
932 assert_array_equal(full[mask], support)