Coverage for nilearn/plotting/surface/_utils.py: 0%

105 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1"""Utility functions used in nilearn.plotting.surface module.""" 

2 

3from collections.abc import Sequence 

4from warnings import warn 

5 

6import numpy as np 

7 

8from nilearn._utils import fill_doc 

9from nilearn._utils.helpers import is_matplotlib_installed, is_plotly_installed 

10from nilearn._utils.logger import find_stack_level 

11from nilearn.plotting._utils import DEFAULT_ENGINE 

12from nilearn.surface import ( 

13 PolyMesh, 

14 SurfaceImage, 

15 load_surf_data, 

16) 

17from nilearn.surface.surface import combine_hemispheres_meshes, get_data 

18from nilearn.surface.utils import check_polymesh_equal 

19 

20DEFAULT_HEMI = "left" 

21 

22VALID_VIEWS = ( 

23 "anterior", 

24 "posterior", 

25 "medial", 

26 "lateral", 

27 "dorsal", 

28 "ventral", 

29 "left", 

30 "right", 

31) 

32 

33VALID_HEMISPHERES = "left", "right", "both" 

34 

35 

36def get_surface_backend(engine=DEFAULT_ENGINE): 

37 """Instantiate and return the required backend engine. 

38 

39 Parameters 

40 ---------- 

41 engine: :obj:`str`, default='matplotlib' 

42 Name of the required backend engine. Can be ``matplotlib`` or 

43 ``plotly``. 

44 

45 Returns 

46 ------- 

47 backend : :class:`~nilearn.plotting.surface._matplotlib_backend` or 

48 :class:`~nilearn.plotting.surface._plotly_backend`. 

49 The backend module for the specified engine. 

50 """ 

51 if engine == "matplotlib": 

52 if is_matplotlib_installed(): 

53 import nilearn.plotting.surface._matplotlib_backend as backend 

54 else: 

55 raise ImportError( 

56 "Using engine='matplotlib' requires that ``matplotlib`` is " 

57 "installed." 

58 ) 

59 elif engine == "plotly": 

60 if is_plotly_installed(): 

61 import nilearn.plotting.surface._plotly_backend as backend 

62 else: 

63 raise ImportError( 

64 "Using engine='plotly' requires that ``plotly`` is installed." 

65 ) 

66 else: 

67 raise ValueError( 

68 f"Unknown plotting engine {engine}. " 

69 "Please use either 'matplotlib' or " 

70 "'plotly'." 

71 ) 

72 return backend 

73 

74 

75def check_engine_params(params, engine): 

76 """Check default values of the parameters that are not implemented for 

77 current engine and warn the user if the parameter has other value then 

78 None. 

79 

80 Parameters 

81 ---------- 

82 params: :obj:`dict` 

83 A dictionary where keys are the unimplemented parameter names for a 

84 specific engine and values are the assigned value for corresponding 

85 parameter. 

86 """ 

87 for parameter, value in params.items(): 

88 if value is not None: 

89 warn( 

90 f"'{parameter}' is not implemented " 

91 f"for the {engine} engine.\n" 

92 f"Got '{parameter} = {value}'.\n" 

93 f"Use '{parameter} = None' to silence this warning.", 

94 stacklevel=find_stack_level(), 

95 ) 

96 

97 

98def _check_hemisphere_is_valid(hemi): 

99 return hemi in VALID_HEMISPHERES 

100 

101 

102def check_hemispheres(hemispheres): 

103 """Check whether the hemispheres passed to in plot_img_on_surf are \ 

104 correct. 

105 

106 hemispheres : :obj:`list` 

107 Any combination of 'left' and 'right'. 

108 

109 """ 

110 invalid_hemis = [ 

111 not _check_hemisphere_is_valid(hemi) for hemi in hemispheres 

112 ] 

113 if any(invalid_hemis): 

114 raise ValueError( 

115 "Invalid hemispheres definition!\n" 

116 f"Got: {np.array(hemispheres)[invalid_hemis]!s}\n" 

117 f"Supported values are: {VALID_HEMISPHERES!s}" 

118 ) 

119 return hemispheres 

120 

121 

122def check_surf_map(surf_map, n_vertices): 

123 """Help for plot_surf. 

124 

125 This function checks the dimensions of provided surf_map. 

126 """ 

127 surf_map_data = load_surf_data(surf_map) 

128 if surf_map_data.ndim != 1: 

129 raise ValueError( 

130 "'surf_map' can only have one dimension " 

131 f"but has '{surf_map_data.ndim}' dimensions" 

132 ) 

133 if surf_map_data.shape[0] != n_vertices: 

134 raise ValueError( 

135 "The surf_map does not have the same number " 

136 "of vertices as the mesh." 

137 ) 

138 return surf_map_data 

139 

140 

141def _check_view_is_valid(view) -> bool: 

142 """Check whether a single view is one of two valid input types. 

143 

144 Parameters 

145 ---------- 

146 view : :obj:`str` in {"anterior", "posterior", "medial", "lateral", 

147 "dorsal", "ventral" or pair of floats (elev, azim). 

148 

149 Returns 

150 ------- 

151 valid : True if view is valid, False otherwise. 

152 """ 

153 if isinstance(view, str) and (view in VALID_VIEWS): 

154 return True 

155 return ( 

156 isinstance(view, Sequence) 

157 and len(view) == 2 

158 and all(isinstance(x, (int, float)) for x in view) 

159 ) 

160 

161 

162def check_views(views) -> list: 

163 """Check whether the views passed to in plot_img_on_surf are correct. 

164 

165 Parameters 

166 ---------- 

167 views : :obj:`list` 

168 Any combination of strings in {"anterior", "posterior", "medial", 

169 "lateral", "dorsal", "ventral"} and / or pair of floats (elev, azim). 

170 

171 Returns 

172 ------- 

173 views : :obj:`list` 

174 Views given as inputs. 

175 """ 

176 invalid_views = [not _check_view_is_valid(view) for view in views] 

177 

178 if any(invalid_views): 

179 raise ValueError( 

180 "Invalid view definition!\n" 

181 f"Got: {np.array(views)[invalid_views]!s}\n" 

182 f"Supported values are: {VALID_VIEWS!s}" 

183 " or a sequence of length 2" 

184 " setting the elevation and azimut of the camera." 

185 ) 

186 

187 return views 

188 

189 

190def _check_bg_map(bg_map, hemi): 

191 """Get the requested hemisphere if ``bg_map`` is a 

192 :obj:`~nilearn.surface.SurfaceImage`. If the hemisphere is not present, 

193 raise an error. If the hemisphere is `"both"`, concatenate the left and 

194 right hemispheres. 

195 

196 Parameters 

197 ---------- 

198 bg_map : Any 

199 

200 hemi : :obj:`str` 

201 

202 Returns 

203 ------- 

204 bg_map : :obj:`str` | :obj:`pathlib.Path` | :obj:`numpy.ndarray` | None 

205 """ 

206 if isinstance(bg_map, SurfaceImage): 

207 if len(bg_map.shape) > 1 and bg_map.shape[1] > 1: 

208 raise TypeError( 

209 "Input data has incompatible dimensionality. " 

210 f"Expected dimension is ({bg_map.shape[0]},) " 

211 f"or ({bg_map.shape[0]}, 1) " 

212 f"and you provided a {bg_map.shape} surface image." 

213 ) 

214 if hemi == "both": 

215 bg_map = get_data(bg_map) 

216 else: 

217 assert bg_map.data.parts[hemi] is not None 

218 bg_map = bg_map.data.parts[hemi] 

219 return bg_map 

220 

221 

222def _get_hemi(surf_mesh, hemi): 

223 """Check that a given hemisphere exists in a 

224 :obj:`~nilearn.surface.PolyMesh` and return the corresponding 

225 ``surf_mesh``. If "both" is requested, combine the left and right 

226 hemispheres. 

227 

228 Parameters 

229 ---------- 

230 surf_mesh: :obj:`~nilearn.surface.PolyMesh` 

231 The surface mesh object containing the left and/or right hemisphere 

232 meshes. 

233 hemi: {'left', 'right', 'both'} 

234 

235 Returns 

236 ------- 

237 surf_mesh : :obj:`numpy.ndarray`, :obj:`~nilearn.surface.InMemoryMesh` 

238 Surface mesh corresponding to the specified ``hemi``. 

239 

240 - If ``hemi='left'`` or ``hemi='right'``, returns 

241 :obj:`numpy.ndarray`. 

242 - If ``hemi='both'``, returns :obj:`~nilearn.surface.InMemoryMesh` 

243 """ 

244 if not isinstance(surf_mesh, PolyMesh): 

245 raise ValueError("mesh should be of type PolyMesh.") 

246 

247 if hemi == "both": 

248 return combine_hemispheres_meshes(surf_mesh) 

249 elif hemi in ["left", "right"]: 

250 if hemi in surf_mesh.parts: 

251 return surf_mesh.parts[hemi] 

252 else: 

253 raise ValueError( 

254 f"{hemi=} does not exist in mesh. Available hemispheres are:" 

255 f"{surf_mesh.parts.keys()}." 

256 ) 

257 else: 

258 raise ValueError("hemi must be one of 'left', 'right' or 'both'.") 

259 

260 

261@fill_doc 

262def check_surface_plotting_inputs( 

263 surf_map, 

264 surf_mesh, 

265 hemi=DEFAULT_HEMI, 

266 bg_map=None, 

267 map_var_name="surf_map", 

268 mesh_var_name="surf_mesh", 

269): 

270 """Check inputs for surface plotting. 

271 

272 Where possible this will 'convert' the inputs if 

273 :obj:`~nilearn.surface.SurfaceImage` or :obj:`~nilearn.surface.PolyMesh` 

274 objects are passed to be able to give them to the surface plotting 

275 functions. 

276 

277 - ``surf_mesh`` and ``surf_map`` cannot be `None` at the same time. 

278 - If ``surf_mesh=None``, then ``surf_map`` should be of type 

279 :obj:`~nilearn.surface.SurfaceImage`. 

280 - ``surf_mesh`` cannot be of type :obj:`~nilearn.surface.SurfaceImage`. 

281 - If ``surf_map`` and ``bg_map`` are of type 

282 :obj:`~nilearn.surface.SurfaceImage`, ``bg_map.mesh`` should be equal to 

283 ``surf_map.mesh``. 

284 

285 Parameters 

286 ---------- 

287 surf_map: :obj:`~nilearn.surface.SurfaceImage` | :obj:`numpy.ndarray` 

288 | None 

289 

290 %(surf_mesh)s 

291 If `None` is passed, then ``surf_map`` must be a 

292 :obj:`~nilearn.surface.SurfaceImage` instance and the mesh from that 

293 :obj:`~nilearn.surface.SurfaceImage` instance will be used. 

294 

295 %(hemi)s 

296 

297 %(bg_map)s 

298 

299 Returns 

300 ------- 

301 surf_map : :obj:`numpy.ndarray` 

302 

303 surf_mesh : :obj:`numpy.ndarray`, :obj:`~nilearn.surface.InMemoryMesh` 

304 Surface mesh corresponding to the specified ``hemi``. 

305 

306 - If ``hemi='left'`` or ``hemi='right'``, returns 

307 :obj:`numpy.ndarray`. 

308 - If ``hemi='both'``, returns :obj:`~nilearn.surface.InMemoryMesh` 

309 bg_map : :obj:`str` | :obj:`pathlib.Path` | :obj:`numpy.ndarray` | None 

310 

311 """ 

312 if surf_mesh is None and surf_map is None: 

313 raise TypeError( 

314 f"{mesh_var_name} and {map_var_name} cannot both be None." 

315 f"If you want to pass {mesh_var_name}=None, " 

316 f"then {mesh_var_name} must be a SurfaceImage instance." 

317 ) 

318 

319 if surf_mesh is None and not isinstance(surf_map, SurfaceImage): 

320 raise TypeError( 

321 f"If you want to pass {mesh_var_name}=None, " 

322 f"then {map_var_name} must be a SurfaceImage instance." 

323 ) 

324 

325 if isinstance(surf_mesh, SurfaceImage): 

326 raise TypeError( 

327 "'surf_mesh' cannot be a SurfaceImage instance. ", 

328 "Accepted types are: str, list of two numpy.ndarray, " 

329 "InMemoryMesh, PolyMesh, or None.", 

330 ) 

331 

332 if isinstance(surf_mesh, PolyMesh): 

333 surf_mesh = _get_hemi(surf_mesh, hemi) 

334 

335 if isinstance(surf_map, SurfaceImage): 

336 if len(surf_map.shape) > 1 and surf_map.shape[1] > 1: 

337 raise TypeError( 

338 "Input data has incompatible dimensionality. " 

339 f"Expected dimension is ({surf_map.shape[0]},) " 

340 f"or ({surf_map.shape[0]}, 1) " 

341 f"and you provided a {surf_map.shape} surface image." 

342 ) 

343 

344 if isinstance(bg_map, SurfaceImage): 

345 check_polymesh_equal(bg_map.mesh, surf_map.mesh) 

346 

347 if surf_mesh is None: 

348 surf_mesh = _get_hemi(surf_map.mesh, hemi) 

349 

350 # concatenate the left and right data if hemi is "both" 

351 if hemi == "both": 

352 surf_map = get_data(surf_map).T 

353 else: 

354 surf_map = surf_map.data.parts[hemi].T 

355 

356 bg_map = _check_bg_map(bg_map, hemi) 

357 

358 return surf_map, surf_mesh, bg_map 

359 

360 

361def get_faces_on_edge(faces, parc_idx): 

362 """Identify which faces lie on the outeredge of the parcellation defined by 

363 the indices in parc_idx. 

364 

365 Parameters 

366 ---------- 

367 faces : :obj:`numpy.ndarray` of shape (n, 3), indices of the mesh faces 

368 

369 parc_idx : :obj:`numpy.ndarray`, indices of the vertices of the region to 

370 be plotted 

371 

372 """ 

373 # count how many vertices belong to the given parcellation in each face 

374 verts_per_face = np.isin(faces, parc_idx).sum(axis=1) 

375 

376 # test if parcellation forms regions 

377 if np.all(verts_per_face < 2): 

378 raise ValueError("Vertices in parcellation do not form region.") 

379 

380 vertices_on_edge = np.intersect1d( 

381 np.unique(faces[verts_per_face == 2]), parc_idx 

382 ) 

383 faces_outside_edge = np.isin(faces, vertices_on_edge).sum(axis=1) 

384 

385 return np.logical_and(faces_outside_edge > 0, verts_per_face < 3) 

386 

387 

388def sanitize_hemi_view(hemi, view): 

389 """Check ``hemi`` and ``view``, if ``view`` is `None`, set value for 

390 ``view`` depending on the ``hemi`` value and return ``view``. 

391 """ 

392 check_hemispheres([hemi]) 

393 if view is None: 

394 view = "dorsal" if hemi == "both" else "lateral" 

395 check_views([view]) 

396 return view