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
« 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."""
3from collections.abc import Sequence
4from warnings import warn
6import numpy as np
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
20DEFAULT_HEMI = "left"
22VALID_VIEWS = (
23 "anterior",
24 "posterior",
25 "medial",
26 "lateral",
27 "dorsal",
28 "ventral",
29 "left",
30 "right",
31)
33VALID_HEMISPHERES = "left", "right", "both"
36def get_surface_backend(engine=DEFAULT_ENGINE):
37 """Instantiate and return the required backend engine.
39 Parameters
40 ----------
41 engine: :obj:`str`, default='matplotlib'
42 Name of the required backend engine. Can be ``matplotlib`` or
43 ``plotly``.
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
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.
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 )
98def _check_hemisphere_is_valid(hemi):
99 return hemi in VALID_HEMISPHERES
102def check_hemispheres(hemispheres):
103 """Check whether the hemispheres passed to in plot_img_on_surf are \
104 correct.
106 hemispheres : :obj:`list`
107 Any combination of 'left' and 'right'.
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
122def check_surf_map(surf_map, n_vertices):
123 """Help for plot_surf.
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
141def _check_view_is_valid(view) -> bool:
142 """Check whether a single view is one of two valid input types.
144 Parameters
145 ----------
146 view : :obj:`str` in {"anterior", "posterior", "medial", "lateral",
147 "dorsal", "ventral" or pair of floats (elev, azim).
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 )
162def check_views(views) -> list:
163 """Check whether the views passed to in plot_img_on_surf are correct.
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).
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]
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 )
187 return views
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.
196 Parameters
197 ----------
198 bg_map : Any
200 hemi : :obj:`str`
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
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.
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'}
235 Returns
236 -------
237 surf_mesh : :obj:`numpy.ndarray`, :obj:`~nilearn.surface.InMemoryMesh`
238 Surface mesh corresponding to the specified ``hemi``.
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.")
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'.")
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.
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.
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``.
285 Parameters
286 ----------
287 surf_map: :obj:`~nilearn.surface.SurfaceImage` | :obj:`numpy.ndarray`
288 | None
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.
295 %(hemi)s
297 %(bg_map)s
299 Returns
300 -------
301 surf_map : :obj:`numpy.ndarray`
303 surf_mesh : :obj:`numpy.ndarray`, :obj:`~nilearn.surface.InMemoryMesh`
304 Surface mesh corresponding to the specified ``hemi``.
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
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 )
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 )
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 )
332 if isinstance(surf_mesh, PolyMesh):
333 surf_mesh = _get_hemi(surf_mesh, hemi)
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 )
344 if isinstance(bg_map, SurfaceImage):
345 check_polymesh_equal(bg_map.mesh, surf_map.mesh)
347 if surf_mesh is None:
348 surf_mesh = _get_hemi(surf_map.mesh, hemi)
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
356 bg_map = _check_bg_map(bg_map, hemi)
358 return surf_map, surf_mesh, bg_map
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.
365 Parameters
366 ----------
367 faces : :obj:`numpy.ndarray` of shape (n, 3), indices of the mesh faces
369 parc_idx : :obj:`numpy.ndarray`, indices of the vertices of the region to
370 be plotted
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)
376 # test if parcellation forms regions
377 if np.all(verts_per_face < 2):
378 raise ValueError("Vertices in parcellation do not form region.")
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)
385 return np.logical_and(faces_outside_edge > 0, verts_per_face < 3)
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