Coverage for nilearn/_utils/niimg.py: 13%
93 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"""Neuroimaging file input and output."""
3import collections.abc
4import gc
5from copy import deepcopy
6from pathlib import Path
7from warnings import warn
9import numpy as np
10from nibabel import is_proxy, load, spatialimages
12from nilearn._utils.logger import find_stack_level
14from .helpers import stringify_path
17def _get_data(img):
18 # copy-pasted from
19 # https://github.com/nipy/nibabel/blob/de44a10/nibabel/dataobj_images.py#L204
20 #
21 # get_data is removed from nibabel because:
22 # see https://github.com/nipy/nibabel/wiki/BIAP8
23 if img._data_cache is not None:
24 return img._data_cache
25 data = np.asanyarray(img._dataobj)
26 img._data_cache = data
27 return data
30def safe_get_data(img, ensure_finite=False, copy_data=False) -> np.ndarray:
31 """Get the data in the image without having a side effect \
32 on the Nifti1Image object.
34 Parameters
35 ----------
36 img : Nifti image/object
37 Image to get data.
39 ensure_finite : bool
40 If True, non-finite values such as (NaNs and infs) found in the
41 image will be replaced by zeros.
43 copy_data : bool, default=False
44 If true, the returned data is a copy of the img data.
46 Returns
47 -------
48 data : numpy array
49 nilearn.image.get_data return from Nifti image.
50 """
51 if copy_data:
52 img = deepcopy(img)
54 # typically the line below can double memory usage
55 # that's why we invoke a forced call to the garbage collector
56 gc.collect()
58 data = _get_data(img)
59 if ensure_finite:
60 non_finite_mask = np.logical_not(np.isfinite(data))
61 if non_finite_mask.sum() > 0: # any non_finite_mask values?
62 warn(
63 "Non-finite values detected. "
64 "These values will be replaced with zeros.",
65 stacklevel=find_stack_level(),
66 )
67 data[non_finite_mask] = 0
69 return data
72def _get_target_dtype(dtype, target_dtype):
73 """Return a new dtype if conversion is needed.
75 Parameters
76 ----------
77 dtype : dtype
78 Data type of the original data
80 target_dtype : {None, dtype, "auto"}
81 If None, no conversion is required. If a type is provided, the
82 function will check if a conversion is needed. The "auto" mode will
83 automatically convert to int32 if dtype is discrete and float32 if it
84 is continuous.
86 Returns
87 -------
88 dtype : dtype
89 The data type toward which the original data should be converted.
90 """
91 if target_dtype is None:
92 return None
93 if target_dtype == "auto":
94 target_dtype = np.int32 if dtype.kind == "i" else np.float32
95 return None if target_dtype == dtype else target_dtype
98def load_niimg(niimg, dtype=None):
99 """Load a niimg, check if it is a nibabel SpatialImage and cast if needed.
101 Parameters
102 ----------
103 niimg : Niimg-like object
104 See :ref:`extracting_data`.
105 Image to load.
107 %(dtype)s
109 Returns
110 -------
111 img : image
112 A loaded image object.
113 """
114 from ..image import new_img_like # avoid circular imports
116 niimg = stringify_path(niimg)
117 if isinstance(niimg, str):
118 # data is a filename, we load it
119 niimg = load(niimg)
120 elif not isinstance(niimg, spatialimages.SpatialImage):
121 raise TypeError(
122 "Data given cannot be loaded because it is"
123 " not compatible with nibabel format:\n"
124 + repr_niimgs(niimg, shorten=True)
125 )
127 img_data = _get_data(niimg)
128 target_dtype = _get_target_dtype(img_data.dtype, dtype)
130 if target_dtype is not None:
131 copy_header = niimg.header is not None
132 niimg = new_img_like(
133 niimg,
134 img_data.astype(target_dtype),
135 niimg.affine,
136 copy_header=copy_header,
137 )
138 if copy_header:
139 niimg.header.set_data_dtype(target_dtype)
141 return niimg
144def is_binary_niimg(niimg):
145 """Return whether a given niimg is binary or not.
147 Parameters
148 ----------
149 niimg : Niimg-like object
150 See :ref:`extracting_data`.
151 Image to test.
153 Returns
154 -------
155 is_binary : Boolean
156 True if binary, False otherwise.
158 """
159 niimg = load_niimg(niimg)
160 data = safe_get_data(niimg, ensure_finite=True)
161 unique_values = np.unique(data)
162 return (
163 False if len(unique_values) != 2 else sorted(unique_values) == [0, 1]
164 )
167def repr_niimgs(niimgs, shorten=True):
168 """Pretty printing of niimg or niimgs.
170 Parameters
171 ----------
172 niimgs : image or collection of images
173 nibabel SpatialImage to repr.
175 shorten : boolean, default=True
176 If True, filenames with more than 20 characters will be
177 truncated, and lists of more than 3 file names will be
178 printed with only first and last element.
180 Returns
181 -------
182 repr : str
183 String representation of the image.
184 """
185 # Simple string case
186 if isinstance(niimgs, (str, Path)):
187 return _short_repr(niimgs, shorten=shorten)
188 # Collection case
189 if isinstance(niimgs, collections.abc.Iterable):
190 # Maximum number of elements to be displayed
191 # Note: should be >= 3 to make sense...
192 list_max_display = 3
193 if shorten and len(niimgs) > list_max_display:
194 tmp = ",\n ...\n ".join(
195 repr_niimgs(niimg, shorten=shorten)
196 for niimg in [niimgs[0], niimgs[-1]]
197 )
198 return f"[{tmp}]"
199 elif len(niimgs) > list_max_display:
200 tmp = ",\n ".join(
201 repr_niimgs(niimg, shorten=shorten) for niimg in niimgs
202 )
203 return f"[{tmp}]"
204 else:
205 tmp = [repr_niimgs(niimg, shorten=shorten) for niimg in niimgs]
206 return f"[{', '.join(tmp)}]"
207 # Nibabel objects have a 'get_filename'
208 try:
209 filename = niimgs.get_filename()
210 if filename is not None:
211 return (
212 f"{niimgs.__class__.__name__}"
213 f"('{_short_repr(filename, shorten=shorten)}')"
214 )
215 else:
216 # No shortening in this case
217 return (
218 f"{niimgs.__class__.__name__}"
219 f"(\nshape={niimgs.shape!r},"
220 f"\naffine={niimgs.affine!r}\n)"
221 )
222 except Exception:
223 pass
224 return _short_repr(repr(niimgs), shorten=shorten)
227def _short_repr(niimg_rep, shorten=True, truncate=20):
228 """Give a shorter version of niimg representation."""
229 # Make sure truncate has a reasonable value
230 truncate = max(truncate, 10)
231 path_to_niimg = Path(niimg_rep)
232 if not shorten:
233 return str(path_to_niimg)
234 # If the name of the file itself
235 # is larger than truncate,
236 # then shorten the name only
237 # else add some folder structure if available
238 if len(path_to_niimg.name) > truncate:
239 return f"{path_to_niimg.name[: (truncate - 2)]}..."
240 rep = path_to_niimg.name
241 if len(path_to_niimg.parts) > 1:
242 for p in path_to_niimg.parts[::-1][1:]:
243 if len(rep) + len(p) < truncate - 3:
244 rep = str(Path(p, rep))
245 else:
246 rep = str(Path("...", rep))
247 break
248 return rep
251def img_data_dtype(niimg):
252 """Determine type of data contained in image.
254 Based on the information contained in ``niimg.dataobj``, determine the
255 dtype of ``np.array(niimg.dataobj).dtype``.
256 """
257 dataobj = niimg.dataobj
259 # Neuroimages that scale data should be interpreted as floating point
260 if is_proxy(dataobj) and (dataobj.slope, dataobj.inter) != (
261 1.0,
262 0.0,
263 ):
264 return np.float64
266 # ArrayProxy gained the dtype attribute in nibabel 2.2
267 return (
268 dataobj.dtype if hasattr(dataobj, "dtype") else niimg.get_data_dtype()
269 )