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

1"""Neuroimaging file input and output.""" 

2 

3import collections.abc 

4import gc 

5from copy import deepcopy 

6from pathlib import Path 

7from warnings import warn 

8 

9import numpy as np 

10from nibabel import is_proxy, load, spatialimages 

11 

12from nilearn._utils.logger import find_stack_level 

13 

14from .helpers import stringify_path 

15 

16 

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 

28 

29 

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. 

33 

34 Parameters 

35 ---------- 

36 img : Nifti image/object 

37 Image to get data. 

38 

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. 

42 

43 copy_data : bool, default=False 

44 If true, the returned data is a copy of the img data. 

45 

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) 

53 

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() 

57 

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 

68 

69 return data 

70 

71 

72def _get_target_dtype(dtype, target_dtype): 

73 """Return a new dtype if conversion is needed. 

74 

75 Parameters 

76 ---------- 

77 dtype : dtype 

78 Data type of the original data 

79 

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. 

85 

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 

96 

97 

98def load_niimg(niimg, dtype=None): 

99 """Load a niimg, check if it is a nibabel SpatialImage and cast if needed. 

100 

101 Parameters 

102 ---------- 

103 niimg : Niimg-like object 

104 See :ref:`extracting_data`. 

105 Image to load. 

106 

107 %(dtype)s 

108 

109 Returns 

110 ------- 

111 img : image 

112 A loaded image object. 

113 """ 

114 from ..image import new_img_like # avoid circular imports 

115 

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 ) 

126 

127 img_data = _get_data(niimg) 

128 target_dtype = _get_target_dtype(img_data.dtype, dtype) 

129 

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) 

140 

141 return niimg 

142 

143 

144def is_binary_niimg(niimg): 

145 """Return whether a given niimg is binary or not. 

146 

147 Parameters 

148 ---------- 

149 niimg : Niimg-like object 

150 See :ref:`extracting_data`. 

151 Image to test. 

152 

153 Returns 

154 ------- 

155 is_binary : Boolean 

156 True if binary, False otherwise. 

157 

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 ) 

165 

166 

167def repr_niimgs(niimgs, shorten=True): 

168 """Pretty printing of niimg or niimgs. 

169 

170 Parameters 

171 ---------- 

172 niimgs : image or collection of images 

173 nibabel SpatialImage to repr. 

174 

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. 

179 

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) 

225 

226 

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 

249 

250 

251def img_data_dtype(niimg): 

252 """Determine type of data contained in image. 

253 

254 Based on the information contained in ``niimg.dataobj``, determine the 

255 dtype of ``np.array(niimg.dataobj).dtype``. 

256 """ 

257 dataobj = niimg.dataobj 

258 

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 

265 

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 )