Coverage for nilearn/_utils/niimg_conversions.py: 13%

127 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1"""Conversion utilities.""" 

2 

3import glob 

4import itertools 

5import warnings 

6from pathlib import Path 

7 

8import numpy as np 

9from joblib import Memory 

10from nibabel.spatialimages import SpatialImage 

11from numpy.testing import assert_array_equal 

12 

13import nilearn as ni 

14from nilearn._utils.cache_mixin import cache 

15from nilearn._utils.exceptions import DimensionError 

16from nilearn._utils.helpers import stringify_path 

17from nilearn._utils.logger import find_stack_level 

18from nilearn._utils.niimg import _get_data, load_niimg, safe_get_data 

19from nilearn._utils.path_finding import resolve_globbing 

20from nilearn.typing import NiimgLike 

21 

22 

23def _check_fov(img, affine, shape): 

24 """Return True if img's field of view correspond to given \ 

25 shape and affine, False elsewhere. 

26 """ 

27 img = check_niimg(img) 

28 return img.shape[:3] == shape and np.allclose(img.affine, affine) 

29 

30 

31def check_same_fov(*args, **kwargs) -> bool: 

32 """Return True if provided images have the same field of view (shape and \ 

33 affine) and return False or raise an error elsewhere, depending on the \ 

34 `raise_error` argument. 

35 

36 This function can take an unlimited number of 

37 images as arguments or keyword arguments and raise a user-friendly 

38 ValueError if asked. 

39 

40 Parameters 

41 ---------- 

42 args : images 

43 Images to be checked. Images passed without keywords will be labeled 

44 as img_#1 in the error message (replace 1 with the appropriate index). 

45 

46 kwargs : images 

47 Images to be checked. In case of error, images will be reference by 

48 their keyword name in the error message. 

49 

50 raise_error : boolean, optional 

51 If True, an error will be raised in case of error. 

52 

53 """ 

54 raise_error = kwargs.pop("raise_error", False) 

55 for i, arg in enumerate(args): 

56 kwargs[f"img_#{i}"] = arg 

57 errors = [] 

58 for (a_name, a_img), (b_name, b_img) in itertools.combinations( 

59 kwargs.items(), 2 

60 ): 

61 if a_img.shape[:3] != b_img.shape[:3]: 

62 errors.append((a_name, b_name, "shape")) 

63 if not np.allclose(a_img.affine, b_img.affine): 

64 errors.append((a_name, b_name, "affine")) 

65 if errors and raise_error: 

66 raise ValueError( 

67 "Following field of view errors were detected:\n" 

68 + "\n".join( 

69 [ 

70 f"- {e[0]} and {e[1]} do not have the same {e[2]}" 

71 for e in errors 

72 ] 

73 ) 

74 ) 

75 return not errors 

76 

77 

78def check_imgs_equal(img1, img2) -> bool: 

79 """Check if 2 NiftiImages have same fov and data.""" 

80 if not check_same_fov(img1, img2, raise_error=False): 

81 return False 

82 

83 data_img1 = safe_get_data(img1) 

84 data_img2 = safe_get_data(img2) 

85 

86 try: 

87 assert_array_equal(data_img1, data_img2) 

88 return True 

89 except AssertionError: 

90 return False 

91 except Exception as e: 

92 raise e 

93 

94 

95def _index_img(img, index): 

96 """Helper function for check_niimg_4d.""" # noqa: D401 

97 from ..image import new_img_like # avoid circular imports 

98 

99 return new_img_like( 

100 img, _get_data(img)[:, :, :, index], img.affine, copy_header=True 

101 ) 

102 

103 

104def iter_check_niimg( 

105 niimgs, 

106 ensure_ndim=None, 

107 atleast_4d=False, 

108 target_fov=None, 

109 dtype=None, 

110 memory=None, 

111 memory_level=0, 

112): 

113 """Iterate over a list of niimgs and do sanity checks and resampling. 

114 

115 Parameters 

116 ---------- 

117 niimgs : list of niimg or glob pattern 

118 Image to iterate over. 

119 

120 ensure_ndim : integer, optional 

121 If specified, an error is raised if the data does not have the 

122 required dimension. 

123 

124 atleast_4d : boolean, default=False 

125 If True, any 3D image is converted to a 4D single scan. 

126 

127 target_fov : tuple of affine and shape, optional 

128 If specified, images are resampled to this field of view. 

129 

130 %(dtype)s 

131 

132 memory : instance of joblib.Memory or string, default=None 

133 Used to cache the masking process. 

134 By default, no caching is done. 

135 If a string is given, it is the path to the caching directory. 

136 If ``None`` is passed will default to ``Memory(location=None)``. 

137 

138 memory_level : integer, default=0 

139 Rough estimator of the amount of memory used by caching. Higher value 

140 means more memory for caching. 

141 

142 See Also 

143 -------- 

144 check_niimg, check_niimg_3d, check_niimg_4d 

145 

146 """ 

147 if memory is None: 

148 memory = Memory(location=None) 

149 # If niimgs is a string, use glob to expand it to the matching filenames. 

150 niimgs = resolve_globbing(niimgs) 

151 

152 ref_fov = None 

153 resample_to_first_img = False 

154 ndim_minus_one = ensure_ndim - 1 if ensure_ndim is not None else None 

155 if target_fov is not None and target_fov != "first": 

156 ref_fov = target_fov 

157 i = -1 

158 for i, niimg in enumerate(niimgs): 

159 try: 

160 niimg = check_niimg( 

161 niimg, 

162 ensure_ndim=ndim_minus_one, 

163 atleast_4d=atleast_4d, 

164 dtype=dtype, 

165 ) 

166 if i == 0: 

167 ndim_minus_one = len(niimg.shape) 

168 if ref_fov is None: 

169 ref_fov = (niimg.affine, niimg.shape[:3]) 

170 resample_to_first_img = True 

171 

172 if not _check_fov(niimg, ref_fov[0], ref_fov[1]): 

173 if target_fov is None: 

174 raise ValueError( 

175 f"Field of view of image #{i} is different from " 

176 "reference FOV.\n" 

177 f"Reference affine:\n{ref_fov[0]!r}\n" 

178 f"Image affine:\n{niimg.affine!r}\n" 

179 f"Reference shape:\n{ref_fov[1]!r}\n" 

180 f"Image shape:\n{niimg.shape!r}\n" 

181 ) 

182 from nilearn import image # we avoid a circular import 

183 

184 if resample_to_first_img: 

185 warnings.warn( 

186 "Affine is different across subjects." 

187 " Realignment on first subject " 

188 "affine forced", 

189 stacklevel=find_stack_level(), 

190 ) 

191 niimg = cache( 

192 image.resample_img, 

193 memory, 

194 func_memory_level=2, 

195 memory_level=memory_level, 

196 )( 

197 niimg, 

198 target_affine=ref_fov[0], 

199 target_shape=ref_fov[1], 

200 copy_header=True, 

201 force_resample=False, # TODO update to True in 0.13.0 

202 ) 

203 yield niimg 

204 except DimensionError as exc: 

205 # Keep track of the additional dimension in the error 

206 exc.increment_stack_counter() 

207 raise 

208 except TypeError as exc: 

209 img_name = f" ({niimg}) " if isinstance(niimg, (str, Path)) else "" 

210 

211 exc.args = ( 

212 f"Error encountered while loading image #{i}{img_name}", 

213 *exc.args, 

214 ) 

215 raise 

216 

217 # Raising an error if input generator is empty. 

218 if i == -1: 

219 raise ValueError("Input niimgs list is empty.") 

220 

221 

222def check_niimg( 

223 niimg, 

224 ensure_ndim=None, 

225 atleast_4d=False, 

226 dtype=None, 

227 return_iterator=False, 

228 wildcards=True, 

229): 

230 """Check that niimg is a proper 3D/4D niimg. 

231 

232 Turn filenames into objects. 

233 

234 Parameters 

235 ---------- 

236 niimg : Niimg-like object 

237 See :ref:`extracting_data`. 

238 If niimg is a string or pathlib.Path, consider it as a path to 

239 Nifti image and call nibabel.load on it. The '~' symbol is expanded to 

240 the user home folder. 

241 If it is an object, check if the affine attribute present and that 

242 nilearn.image.get_data returns a result, raise TypeError otherwise. 

243 

244 ensure_ndim : integer {3, 4}, optional 

245 Indicate the dimensionality of the expected niimg. An 

246 error is raised if the niimg is of another dimensionality. 

247 

248 atleast_4d : boolean, default=False 

249 Indicates if a 3d image should be turned into a single-scan 4d niimg. 

250 

251 %(dtype)s 

252 If None, data will not be converted to a new data type. 

253 

254 return_iterator : boolean, default=False 

255 Returns an iterator on the content of the niimg file input. 

256 

257 wildcards : boolean, default=True 

258 Use niimg as a regular expression to get a list of matching input 

259 filenames. 

260 If multiple files match, the returned list is sorted using an ascending 

261 order. 

262 If no file matches the regular expression, a ValueError exception is 

263 raised. 

264 

265 Returns 

266 ------- 

267 result : 3D/4D Niimg-like object 

268 Result can be nibabel.Nifti1Image or the input, as-is. It is guaranteed 

269 that the returned object has an affine attribute and that its data can 

270 be retrieved with nilearn.image.get_data. 

271 

272 Notes 

273 ----- 

274 In nilearn, special care has been taken to make image manipulation easy. 

275 This method is a kind of pre-requisite for any data processing method in 

276 nilearn because it checks if data have a correct format and loads them if 

277 necessary. 

278 

279 Its application is idempotent. 

280 

281 See Also 

282 -------- 

283 iter_check_niimg, check_niimg_3d, check_niimg_4d 

284 

285 """ 

286 from ..image import new_img_like # avoid circular imports 

287 

288 if not ( 

289 isinstance(niimg, (NiimgLike, SpatialImage)) 

290 or (hasattr(niimg, "__iter__")) 

291 ): 

292 raise TypeError( 

293 "input should be a NiftiLike object " 

294 "or an iterable of NiftiLike object. " 

295 f"Got: {niimg.__class__.__name__}" 

296 ) 

297 

298 if hasattr(niimg, "__iter__"): 

299 for x in niimg: 

300 if not ( 

301 isinstance(x, (NiimgLike, SpatialImage)) 

302 or hasattr(x, "__iter__") 

303 ): 

304 raise TypeError( 

305 "iterable inputs should contain " 

306 "NiftiLike objects or iterables. " 

307 f"Got: {x.__class__.__name__}" 

308 ) 

309 

310 niimg = stringify_path(niimg) 

311 

312 if isinstance(niimg, str): 

313 if wildcards and ni.EXPAND_PATH_WILDCARDS: 

314 # Expand user path 

315 expanded_niimg = str(Path(niimg).expanduser()) 

316 # Ascending sorting 

317 filenames = sorted(glob.glob(expanded_niimg)) 

318 

319 # processing filenames matching globbing expression 

320 if len(filenames) >= 1 and glob.has_magic(niimg): 

321 niimg = filenames # iterable case 

322 # niimg is an existing filename 

323 elif [expanded_niimg] == filenames: 

324 niimg = filenames[0] 

325 # No files found by glob 

326 elif glob.has_magic(niimg): 

327 # No files matching the glob expression, warn the user 

328 message = ( 

329 "No files matching the entered niimg expression: " 

330 f"'{niimg}'.\n" 

331 "You may have left wildcards usage activated: " 

332 "please set the global constant " 

333 "'nilearn.EXPAND_PATH_WILDCARDS' to False " 

334 "to deactivate this behavior." 

335 ) 

336 raise ValueError(message) 

337 else: 

338 raise ValueError(f"File not found: '{niimg}'") 

339 elif not Path(niimg).exists(): 

340 raise ValueError(f"File not found: '{niimg}'") 

341 

342 # in case of an iterable 

343 if hasattr(niimg, "__iter__") and not isinstance(niimg, str): 

344 if return_iterator: 

345 return iter_check_niimg( 

346 niimg, ensure_ndim=ensure_ndim, dtype=dtype 

347 ) 

348 return ni.image.concat_imgs( 

349 niimg, ensure_ndim=ensure_ndim, dtype=dtype 

350 ) 

351 

352 # Otherwise, it should be a filename or a SpatialImage, we load it 

353 niimg = load_niimg(niimg, dtype=dtype) 

354 

355 if ensure_ndim == 3 and len(niimg.shape) == 4 and niimg.shape[3] == 1: 

356 # "squeeze" the image. 

357 data = safe_get_data(niimg) 

358 affine = niimg.affine 

359 niimg = new_img_like(niimg, data[:, :, :, 0], affine) 

360 if atleast_4d and len(niimg.shape) == 3: 

361 data = _get_data(niimg).view() 

362 data.shape = (*data.shape, 1) 

363 niimg = new_img_like(niimg, data, niimg.affine) 

364 

365 if ensure_ndim is not None and len(niimg.shape) != ensure_ndim: 

366 raise DimensionError(len(niimg.shape), ensure_ndim) 

367 

368 if return_iterator: 

369 return (_index_img(niimg, i) for i in range(niimg.shape[3])) 

370 

371 return niimg 

372 

373 

374def check_niimg_3d(niimg, dtype=None): 

375 """Check that niimg is a proper 3D niimg-like object and load it. 

376 

377 Parameters 

378 ---------- 

379 niimg : Niimg-like object 

380 See :ref:`extracting_data`. 

381 If niimg is a string, consider it as a path to Nifti image and 

382 call nibabel.load on it. 

383 If it is an object, check if the affine attribute present and that 

384 nilearn.image.get_data returns a result, raise TypeError otherwise. 

385 

386 %(dtype)s 

387 

388 Returns 

389 ------- 

390 result : 3D Niimg-like object 

391 Result can be nibabel.Nifti1Image or the input, as-is. It is guaranteed 

392 that the returned object has an affine attribute and that its data can 

393 be retrieved with nilearn.image.get_data. 

394 

395 Notes 

396 ----- 

397 In nilearn, special care has been taken to make image manipulation easy. 

398 This method is a kind of pre-requisite for any data processing method in 

399 nilearn because it checks if data have a correct format and loads them if 

400 necessary. 

401 

402 Its application is idempotent. 

403 

404 """ 

405 return check_niimg(niimg, ensure_ndim=3, dtype=dtype) 

406 

407 

408def check_niimg_4d(niimg, return_iterator=False, dtype=None): 

409 """Check that niimg is a proper 4D niimg-like object and load it. 

410 

411 Parameters 

412 ---------- 

413 niimg : 4D Niimg-like object 

414 See :ref:`extracting_data`. 

415 If niimgs is an iterable, checks if data is really 4D. Then, 

416 considering that it is a list of niimg and load them one by one. 

417 If niimg is a string, consider it as a path to Nifti image and 

418 call nibabel.load on it. 

419 If it is an object, check if the affine attribute present and that 

420 nilearn.image.get_data returns a result, raise TypeError otherwise. 

421 

422 dtype : {dtype, "auto"}, optional 

423 Data type toward which the data should be converted. If "auto", the 

424 data will be converted to int32 if dtype is discrete and float32 if it 

425 is continuous. 

426 

427 return_iterator : boolean, default=False 

428 If True, an iterator of 3D images is returned. This reduces the memory 

429 usage when `niimgs` contains 3D images. 

430 If False, a single 4D image is returned. When `niimgs` contains 3D 

431 images they are concatenated together. 

432 

433 Returns 

434 ------- 

435 niimg: 4D nibabel.Nifti1Image or iterator of 3D nibabel.Nifti1Image 

436 

437 Notes 

438 ----- 

439 This function is the equivalent to check_niimg_3d() for Niimg-like objects 

440 with a run level. 

441 

442 Its application is idempotent. 

443 

444 """ 

445 return check_niimg( 

446 niimg, ensure_ndim=4, return_iterator=return_iterator, dtype=dtype 

447 )