Coverage for nilearn/_utils/tests/test_niimg_conversions.py: 0%
248 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Test the niimg_conversions.
3This test file is in nilearn/tests because Nosetest,
4which we historically used,
5ignores modules whose name starts with an underscore.
6"""
8import os
9import re
10from pathlib import Path
11from tempfile import mkstemp
13import numpy as np
14import pytest
15from nibabel import Nifti1Image, spatialimages
16from numpy.testing import assert_array_equal
18import nilearn as ni
19from nilearn._utils import (
20 check_niimg,
21 check_niimg_3d,
22 check_niimg_4d,
23 repr_niimgs,
24)
25from nilearn._utils.exceptions import DimensionError
26from nilearn._utils.niimg_conversions import check_same_fov, iter_check_niimg
27from nilearn._utils.testing import (
28 assert_memory_less_than,
29 with_memory_profiler,
30 write_imgs_to_path,
31)
32from nilearn.image import get_data
35class PhonyNiimage(spatialimages.SpatialImage):
36 def __init__(self):
37 self.data = np.ones((9, 9, 9, 9))
38 self.my_affine = np.ones((4, 4))
40 def get_data(self):
41 return self.data
43 def get_affine(self):
44 return self.my_affine
46 @property
47 def shape(self):
48 return self.data.shape
50 @property
51 def _data_cache(self):
52 return self.data
54 @property
55 def _dataobj(self):
56 return self.data
59def test_check_same_fov(affine_eye):
60 affine_b = affine_eye * 2
62 shape_a = (2, 2, 2)
63 shape_b = (3, 3, 3)
65 shape_a_affine_a = Nifti1Image(np.empty(shape_a), affine_eye)
66 shape_a_affine_a_2 = Nifti1Image(np.empty(shape_a), affine_eye)
67 shape_a_affine_b = Nifti1Image(np.empty(shape_a), affine_b)
68 shape_b_affine_a = Nifti1Image(np.empty(shape_b), affine_eye)
69 shape_b_affine_b = Nifti1Image(np.empty(shape_b), affine_b)
71 check_same_fov(a=shape_a_affine_a, b=shape_a_affine_a_2, raise_error=True)
73 with pytest.raises(
74 ValueError, match="[ac] and [ac] do not have the same affine"
75 ):
76 check_same_fov(
77 a=shape_a_affine_a,
78 b=shape_a_affine_a_2,
79 c=shape_a_affine_b,
80 raise_error=True,
81 )
82 with pytest.raises(
83 ValueError, match="[ab] and [ab] do not have the same shape"
84 ):
85 check_same_fov(
86 a=shape_a_affine_a, b=shape_b_affine_a, raise_error=True
87 )
88 with pytest.raises(
89 ValueError, match="[ab] and [ab] do not have the same affine"
90 ):
91 check_same_fov(
92 a=shape_b_affine_b, b=shape_a_affine_a, raise_error=True
93 )
95 with pytest.raises(
96 ValueError, match="[ab] and [ab] do not have the same shape"
97 ):
98 check_same_fov(
99 a=shape_b_affine_b, b=shape_a_affine_a, raise_error=True
100 )
103def test_check_niimg_3d(affine_eye, img_3d_zeros_eye, tmp_path):
104 # check error for non-forced but necessary resampling
105 with pytest.raises(TypeError, match="input should be a NiftiLike object"):
106 check_niimg(0)
108 # check error for non-forced but necessary resampling
109 with pytest.raises(TypeError, match="empty object"):
110 check_niimg([])
112 # Test dimensionality error
113 with pytest.raises(
114 TypeError,
115 match="Input data has incompatible dimensionality: "
116 "Expected dimension is 3D and you provided a list "
117 "of 3D images \\(4D\\).",
118 ):
119 check_niimg_3d([img_3d_zeros_eye, img_3d_zeros_eye])
121 # Check that a filename does not raise an error
122 data = np.zeros((40, 40, 40, 1))
123 data[20, 20, 20] = 1
124 data_img = Nifti1Image(data, affine_eye)
126 filename = write_imgs_to_path(
127 data_img, file_path=tmp_path, create_files=True
128 )
129 check_niimg_3d(filename)
131 # check data dtype equal with dtype='auto'
132 img_check = check_niimg_3d(img_3d_zeros_eye, dtype="auto")
133 assert (
134 get_data(img_3d_zeros_eye).dtype.kind == get_data(img_check).dtype.kind
135 )
138def test_check_niimg_4d_errors(affine_eye, img_3d_zeros_eye, shape_3d_default):
139 with pytest.raises(TypeError, match="input should be a NiftiLike object"):
140 check_niimg_4d(0)
142 with pytest.raises(TypeError, match="empty object"):
143 check_niimg_4d([])
145 # This should raise an error: a 3D img is given and we want a 4D
146 with pytest.raises(
147 DimensionError,
148 match="Input data has incompatible dimensionality: "
149 "Expected dimension is 4D and you provided a 3D image.",
150 ):
151 check_niimg_4d(img_3d_zeros_eye)
153 a = img_3d_zeros_eye
154 b = np.zeros(shape_3d_default)
155 c = check_niimg_4d([a, b], return_iterator=True)
156 with pytest.raises(
157 TypeError, match="Error encountered while loading image #1"
158 ):
159 list(c)
161 b = Nifti1Image(np.zeros((10, 20, 10)), affine_eye)
162 c = check_niimg_4d([a, b], return_iterator=True)
163 with pytest.raises(
164 ValueError,
165 match="Field of view of image #1 is different from reference FOV",
166 ):
167 list(c)
170def test_check_niimg_4d(affine_eye, img_3d_zeros_eye, shape_3d_default):
171 # Tests with return_iterator=False
172 img_4d_1 = check_niimg_4d([img_3d_zeros_eye, img_3d_zeros_eye])
173 assert get_data(img_4d_1).shape == (*shape_3d_default, 2)
174 assert_array_equal(img_4d_1.affine, affine_eye)
176 img_4d_2 = check_niimg_4d(img_4d_1)
177 assert_array_equal(get_data(img_4d_2), get_data(img_4d_2))
178 assert_array_equal(img_4d_2.affine, img_4d_2.affine)
180 # Tests with return_iterator=True
181 img_3d_iterator = check_niimg_4d(
182 [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
183 )
184 img_3d_iterator_length = sum(1 for _ in img_3d_iterator)
185 assert img_3d_iterator_length == 2
187 img_3d_iterator_1 = check_niimg_4d(
188 [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
189 )
190 img_3d_iterator_2 = check_niimg_4d(img_3d_iterator_1, return_iterator=True)
191 for img_1, img_2 in zip(img_3d_iterator_1, img_3d_iterator_2):
192 assert get_data(img_1).shape == shape_3d_default
193 assert_array_equal(get_data(img_1), get_data(img_2))
194 assert_array_equal(img_1.affine, img_2.affine)
196 img_3d_iterator_1 = check_niimg_4d(
197 [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
198 )
199 img_3d_iterator_2 = check_niimg_4d(img_4d_1, return_iterator=True)
200 for img_1, img_2 in zip(img_3d_iterator_1, img_3d_iterator_2):
201 assert get_data(img_1).shape == shape_3d_default
202 assert_array_equal(get_data(img_1), get_data(img_2))
203 assert_array_equal(img_1.affine, img_2.affine)
205 # Test a Niimg-like object that does not hold a shape attribute
206 phony_img = PhonyNiimage()
207 check_niimg_4d(phony_img)
210def test_check_niimg(img_3d_zeros_eye, img_4d_zeros_eye):
211 img_3_3d = [[[img_3d_zeros_eye, img_3d_zeros_eye]]]
212 img_2_4d = [[img_4d_zeros_eye, img_4d_zeros_eye]]
214 with pytest.raises(
215 DimensionError,
216 match="Input data has incompatible dimensionality: "
217 "Expected dimension is 2D and you provided "
218 "a list of list of list of 3D images \\(6D\\)",
219 ):
220 check_niimg(img_3_3d, ensure_ndim=2)
222 with pytest.raises(
223 DimensionError,
224 match="Input data has incompatible dimensionality: "
225 "Expected dimension is 4D and you provided "
226 "a list of list of 4D images \\(6D\\)",
227 ):
228 check_niimg(img_2_4d, ensure_ndim=4)
230 # check data dtype equal with dtype='auto'
231 img_3d_check = check_niimg(img_3d_zeros_eye, dtype="auto")
232 assert (
233 get_data(img_3d_zeros_eye).dtype.kind
234 == get_data(img_3d_check).dtype.kind
235 )
237 img_4d_check = check_niimg(img_4d_zeros_eye, dtype="auto")
238 assert (
239 get_data(img_4d_zeros_eye).dtype.kind
240 == get_data(img_4d_check).dtype.kind
241 )
244def test_check_niimg_pathlike(img_3d_zeros_eye, tmp_path):
245 filename = write_imgs_to_path(
246 img_3d_zeros_eye, file_path=tmp_path, create_files=True
247 )
248 filename = Path(filename)
249 check_niimg_3d(filename)
252def test_check_niimg_wildcards_errors():
253 # Check bad filename
254 # Non existing file (with no magic) raise a ValueError exception
255 nofile_path = "/tmp/nofile"
256 file_not_found_msg = "File not found: '%s'"
257 with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
258 check_niimg(nofile_path)
260 # Non matching wildcard raises a ValueError exception
261 nofile_path_wildcards = "/tmp/no*file"
262 with pytest.raises(
263 ValueError, match="You may have left wildcards usage activated"
264 ):
265 check_niimg(nofile_path_wildcards)
268@pytest.mark.parametrize("shape", [(10, 10, 10), (10, 10, 10, 3)])
269@pytest.mark.parametrize(
270 "wildcards", [True, False]
271) # (With globbing behavior or not)
272def test_check_niimg_wildcards(affine_eye, shape, wildcards, tmp_path):
273 # First create some testing data
274 img = Nifti1Image(np.zeros(shape), affine_eye)
276 filename = write_imgs_to_path(img, file_path=tmp_path, create_files=True)
277 assert_array_equal(
278 get_data(check_niimg(filename, wildcards=wildcards)),
279 get_data(img),
280 )
283@pytest.fixture
284def img_in_home_folder(img_3d_mni):
285 """Create a test file in the home folder.
287 Teardown: use yield instead of return to make sure the file
288 is deleted after the test,
289 even if the test fails.
290 https://docs.pytest.org/en/stable/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization
291 """
292 created_file = Path("~/test.nii")
293 img_3d_mni.to_filename(created_file.expanduser())
294 assert created_file.expanduser().exists()
296 yield img_3d_mni
298 created_file.expanduser().unlink()
301@pytest.mark.parametrize(
302 "filename", ["~/test.nii", r"~/test.nii", Path("~/test.nii")]
303)
304def test_check_niimg_user_expand(img_in_home_folder, filename):
305 """Check that user path are expanded."""
306 found_file = check_niimg(filename)
308 assert_array_equal(
309 get_data(found_file),
310 get_data(img_in_home_folder),
311 )
314@pytest.mark.parametrize(
315 "filename",
316 [
317 "~/*.nii",
318 r"~/*.nii",
319 ["~/test.nii"],
320 [r"~/test.nii"],
321 [Path("~/test.nii")],
322 ],
323)
324def test_check_niimg_user_expand_4d(img_in_home_folder, filename):
325 """Check that user path are expanded.
327 Wildcards and lists should expected 4D data to be returned.
328 """
329 found_file = check_niimg(filename)
331 assert_array_equal(
332 get_data(found_file),
333 get_data(check_niimg(img_in_home_folder, atleast_4d=True)),
334 )
337def test_check_niimg_wildcards_one_file_name(img_3d_zeros_eye, tmp_path):
338 file_not_found_msg = "File not found: '%s'"
340 # Testing with a glob matching exactly one filename
341 # Using a glob matching one file containing a 3d image returns a 4d image
342 # with 1 as last dimension.
343 globs = write_imgs_to_path(
344 img_3d_zeros_eye,
345 file_path=tmp_path,
346 create_files=True,
347 use_wildcards=True,
348 )
349 assert_array_equal(
350 get_data(check_niimg(globs))[..., 0],
351 get_data(img_3d_zeros_eye),
352 )
353 # Disabled globbing behavior should raise an ValueError exception
354 with pytest.raises(
355 ValueError, match=file_not_found_msg % re.escape(globs)
356 ):
357 check_niimg(globs, wildcards=False)
359 # Testing with a glob matching multiple filenames
360 img_4d = check_niimg_4d((img_3d_zeros_eye, img_3d_zeros_eye))
361 globs = write_imgs_to_path(
362 img_3d_zeros_eye,
363 img_3d_zeros_eye,
364 file_path=tmp_path,
365 create_files=True,
366 use_wildcards=True,
367 )
368 assert_array_equal(get_data(check_niimg(globs)), get_data(img_4d))
371def test_check_niimg_wildcards_no_expand_wildcards(
372 img_3d_zeros_eye, img_4d_zeros_eye, tmp_path
373):
374 nofile_path = "/tmp/nofile"
376 file_not_found_msg = "File not found: '%s'"
378 #######
379 # Test when global variable is set to False => no globbing allowed
380 ni.EXPAND_PATH_WILDCARDS = False
382 # Non existing filename (/tmp/nofile) could match an existing one through
383 # globbing but global wildcards variable overrides this feature => raises
384 # a ValueError
385 with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
386 check_niimg(nofile_path)
388 # Verify wildcards function parameter has no effect
389 with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
390 check_niimg(nofile_path, wildcards=False)
392 # Testing with an exact filename matching (3d case)
393 filename = write_imgs_to_path(
394 img_3d_zeros_eye, file_path=tmp_path, create_files=True
395 )
396 assert_array_equal(
397 get_data(check_niimg(filename)), get_data(img_3d_zeros_eye)
398 )
400 # Testing with an exact filename matching (4d case)
401 filename = write_imgs_to_path(
402 img_4d_zeros_eye, file_path=tmp_path, create_files=True
403 )
404 assert_array_equal(
405 get_data(check_niimg(filename)), get_data(img_4d_zeros_eye)
406 )
408 # Reverting to default behavior
409 ni.EXPAND_PATH_WILDCARDS = True
412def test_iter_check_niimgs_error():
413 no_file_matching = "No files matching path: %s"
415 for empty in ((), [], iter(())):
416 with pytest.raises(ValueError, match="Input niimgs list is empty."):
417 list(iter_check_niimg(empty))
419 nofile_path = "/tmp/nofile"
420 with pytest.raises(ValueError, match=no_file_matching % nofile_path):
421 list(iter_check_niimg(nofile_path))
424def test_iter_check_niimgs(tmp_path, img_4d_zeros_eye):
425 img_2_4d = [[img_4d_zeros_eye, img_4d_zeros_eye]]
427 # Create a test file
428 filename = tmp_path / "nilearn_test.nii"
429 img_4d_zeros_eye.to_filename(filename)
430 niimgs = list(iter_check_niimg([filename]))
431 assert_array_equal(
432 get_data(niimgs[0]), get_data(check_niimg(img_4d_zeros_eye))
433 )
434 del niimgs
436 # Regular case
437 niimgs = list(iter_check_niimg(img_2_4d))
438 assert_array_equal(get_data(niimgs[0]), get_data(check_niimg(img_2_4d)))
441def _check_memory(list_img_3d):
442 # We intentionally add an offset of memory usage to avoid non trustable
443 # measures with memory_profiler.
444 mem_offset = b"a" * 100 * 1024**2
445 list(iter_check_niimg(list_img_3d))
446 return mem_offset
449@with_memory_profiler
450def test_iter_check_niimgs_memory(affine_eye):
451 # Verify that iterating over a list of images doesn't consume extra
452 # memory.
453 assert_memory_less_than(
454 100,
455 0.1,
456 _check_memory,
457 [Nifti1Image(np.ones((100, 100, 200)), affine_eye) for _ in range(10)],
458 )
461def test_repr_niimgs():
462 # Tests with file path
463 assert repr_niimgs("test") == "test"
464 assert repr_niimgs("test", shorten=False) == "test"
466 # Shortening long names by default
467 long_name = "this-is-a-very-long-name-for-a-nifti-file.nii"
468 short_name = "this-is-a-very-lon..."
469 assert repr_niimgs(long_name) == short_name
470 # Explicit shortening of long names
471 assert repr_niimgs(long_name, shorten=True) == short_name
473 # Lists of long names up to length 3
474 list_of_size_3 = [
475 "this-is-a-very-long-name-for-a-nifti-file.nii",
476 "this-is-another-very-long-name-for-a-nifti-file.nii",
477 "this-is-again-another-very-long-name-for-a-nifti-file.nii",
478 ]
479 # Explicit shortening, all 3 names are displayed, but shortened
480 shortened_rep_list_of_size_3 = (
481 "[this-is-a-very-lon..., this-is-another-ve..., this-is-again-anot...]"
482 )
484 assert (
485 repr_niimgs(list_of_size_3, shorten=True)
486 == shortened_rep_list_of_size_3
487 )
489 # Lists longer than 3
490 # Small names - Explicit shortening
491 long_list_small_names = ["test", "retest", "reretest", "rereretest"]
492 shortened_rep_long_list_small_names = "[test,\n ...\n rereretest]"
494 assert (
495 repr_niimgs(long_list_small_names, shorten=True)
496 == shortened_rep_long_list_small_names
497 )
499 # Long names - Explicit shortening
500 list_of_size_4 = [
501 *list_of_size_3,
502 "this-is-again-another-super-very-long-name-for-a-nifti-file.nii",
503 ]
504 shortened_rep_long_list_long_names = (
505 "[this-is-a-very-lon...,\n ...\n this-is-again-anot...]"
506 )
508 assert (
509 repr_niimgs(list_of_size_4, shorten=True)
510 == shortened_rep_long_list_long_names
511 )
514def test_repr_niimgs_force_long_names():
515 long_name = "this-is-a-very-long-name-for-a-nifti-file.nii"
516 # Force long display of long names
517 assert repr_niimgs(long_name, shorten=False) == long_name
519 # Tests with list of file paths
520 assert repr_niimgs(["test", "retest"]) == "[test, retest]"
521 assert repr_niimgs(["test", "retest"], shorten=False) == "[test, retest]"
523 # Force display, all 3 names are displayed
524 list_of_size_3 = [
525 "this-is-a-very-long-name-for-a-nifti-file.nii",
526 "this-is-another-very-long-name-for-a-nifti-file.nii",
527 "this-is-again-another-very-long-name-for-a-nifti-file.nii",
528 ]
529 long_rep_list_of_size_3 = (
530 "[this-is-a-very-long-name-for-a-nifti-file.nii,"
531 " this-is-another-very-long-name-for-a-nifti-file.nii,"
532 " this-is-again-another-very-long-name-for-a-nifti-file.nii]"
533 )
534 assert (
535 repr_niimgs(list_of_size_3, shorten=False) == long_rep_list_of_size_3
536 )
538 long_list_small_names = ["test", "retest", "reretest", "rereretest"]
539 long_rep_long_list_small_names = (
540 "[test,\n retest,\n reretest,\n rereretest]"
541 )
543 assert (
544 repr_niimgs(long_list_small_names, shorten=False)
545 == long_rep_long_list_small_names
546 )
548 # Long names - Force full display in pretty print style for readability
549 list_of_size_4 = [
550 *list_of_size_3,
551 "this-is-again-another-super-very-long-name-for-a-nifti-file.nii",
552 ]
553 long_rep_long_list_long_names = (
554 long_rep_list_of_size_3[:-1].replace(",", ",\n")
555 + ",\n "
556 + "this-is-again-another-super-very-long-name-for-a-nifti-file.nii]"
557 )
559 assert (
560 repr_niimgs(list_of_size_4, shorten=False)
561 == long_rep_long_list_long_names
562 )
565def test_repr_niimgs_with_niimg_pathlib():
566 # Tests with pathlib
567 # Case with very long path and small filename
568 long_path = Path("/this/is/a/fake/long/path/to/file.nii")
569 short_path = Path(".../path/to/file.nii")
570 assert repr_niimgs(long_path, shorten=True) == str(short_path)
571 assert repr_niimgs(long_path, shorten=False) == str(long_path)
573 # Case with very long path but very long filename
574 long_path_long_name = Path(
575 "/this/is/a/fake/long/path/to/my_file_with_a_very_long_name.nii"
576 )
577 short_name = "my_file_with_a_ver..."
578 assert repr_niimgs(long_path_long_name, shorten=True) == short_name
579 assert repr_niimgs(long_path_long_name, shorten=False) == str(
580 long_path_long_name
581 )
583 # Case with lists
584 list_of_paths = [
585 Path("/this/is/a/fake/long/path/to/file.nii"),
586 Path("/this/is/a/fake/long/path/to/another/file2.nii"),
587 Path("/again/another/fake/long/path/to/file3.nii"),
588 Path("/this/is/a/fake/long/path/to/a-very-long-file-name.nii"),
589 ]
591 shortened_list_of_paths = (
592 f"[...{Path('/path/to/file.nii')!s},\n"
593 f" ...\n"
594 f" a-very-long-file-n...]"
595 )
597 assert repr_niimgs(list_of_paths, shorten=True) == shortened_list_of_paths
598 long_list_of_paths = ",\n ".join([str(_) for _ in list_of_paths])
599 long_list_of_paths = f"[{long_list_of_paths}]"
600 assert repr_niimgs(list_of_paths, shorten=False) == long_list_of_paths
603@pytest.mark.parametrize("shorten", [True, False])
604def test_repr_niimgs_with_niimg(
605 shorten, tmp_path, affine_eye, img_3d_ones_eye, shape_3d_default
606):
607 # Shorten has no effect in this case
608 assert repr_niimgs(img_3d_ones_eye, shorten=shorten).replace(
609 "10L", "10"
610 ) == (
611 f"{img_3d_ones_eye.__class__.__name__}(\nshape={shape_3d_default!r},\naffine={affine_eye!r}\n)"
612 )
614 # Add filename long enough to qualify for shortening
615 fd, tmpimg1 = mkstemp(suffix="_very_long.nii", dir=str(tmp_path))
616 os.close(fd)
617 img_3d_ones_eye.to_filename(tmpimg1)
618 class_name = img_3d_ones_eye.__class__.__name__
619 filename = Path(img_3d_ones_eye.get_filename())
620 assert (
621 repr_niimgs(img_3d_ones_eye, shorten=False)
622 == f"{class_name}('{filename}')"
623 )
624 assert (
625 repr_niimgs(img_3d_ones_eye, shorten=True)
626 == f"{class_name}('{Path(filename).name[:18]}...')"
627 )