|
83 | 83 | ) |
84 | 84 | from ._dipole import _check_concat_dipoles, _plot_dipole_3d, _plot_dipole_mri_outlines |
85 | 85 | from .evoked_field import EvokedField |
| 86 | +from .ui_events import subscribe |
86 | 87 | from .utils import ( |
87 | 88 | _check_time_unit, |
88 | 89 | _get_cmap, |
@@ -4301,3 +4302,158 @@ def _get_3d_option(key): |
4301 | 4302 | else: |
4302 | 4303 | opt = opt.lower() == "true" |
4303 | 4304 | return opt |
| 4305 | + |
| 4306 | + |
| 4307 | +def plot_stat_cluster(cluster, src, brain, time="max-extent", color="magenta", width=1): |
| 4308 | + """Plot the spatial extent of a cluster on top of a brain. |
| 4309 | +
|
| 4310 | + Parameters |
| 4311 | + ---------- |
| 4312 | + cluster : tuple (time_idx, vertex_idx) |
| 4313 | + The cluster to plot. |
| 4314 | + src : SourceSpaces |
| 4315 | + The source space that was used for the inverse computation. |
| 4316 | + brain : Brain |
| 4317 | + The brain figure on which to plot the cluster. |
| 4318 | + time : float | "interactive" | "max-extent" |
| 4319 | + The time (in seconds) at which to plot the spatial extent of the cluster. |
| 4320 | + If set to ``"interactive"`` the time will follow the selected time in the brain |
| 4321 | + figure. |
| 4322 | + By default, ``"max-extent"``, the time of maximal spatial extent is chosen. |
| 4323 | + color : str |
| 4324 | + A maplotlib-style color specification indicating the color to use when plotting |
| 4325 | + the spatial extent of the cluster. |
| 4326 | + width : int |
| 4327 | + The width of the lines used to draw the outlines. |
| 4328 | +
|
| 4329 | + Returns |
| 4330 | + ------- |
| 4331 | + brain : Brain |
| 4332 | + The brain figure, now with the cluster plotted on top of it. |
| 4333 | + """ |
| 4334 | + # Here due to circular import |
| 4335 | + from ..label import Label |
| 4336 | + |
| 4337 | + # args check |
| 4338 | + if isinstance(cluster, tuple): |
| 4339 | + if len(cluster) != 2: |
| 4340 | + raise ValueError( |
| 4341 | + "A cluster is a tuple of two elements, a list time " |
| 4342 | + "indices and list of vertex indices" |
| 4343 | + ) |
| 4344 | + else: |
| 4345 | + raise TypeError(f"Tuple expected, got {type(cluster)} instead.") |
| 4346 | + |
| 4347 | + cluster_time_idx, cluster_vertex_index = cluster |
| 4348 | + |
| 4349 | + # A cluster is defined both in space and time. If we want to plot the boundaries of |
| 4350 | + # the cluster in space, we must choose a specific time for which to show the |
| 4351 | + # boundaries (as they change over time). |
| 4352 | + if time == "max-extent": |
| 4353 | + time_idx, n_vertices = np.unique(cluster_time_idx, return_counts=True) |
| 4354 | + time_idx = time_idx[np.argmax(n_vertices)] |
| 4355 | + elif time == "interactive": |
| 4356 | + time_idx = brain._data["time_idx"] |
| 4357 | + elif isinstance(time, float): |
| 4358 | + time_idx = np.searchsorted(brain._times[:-1], time) |
| 4359 | + else: |
| 4360 | + raise ValueError( |
| 4361 | + "Time should be 'max-extent', 'interactive', or floating point" |
| 4362 | + f" value, got '{time}' instead." |
| 4363 | + ) |
| 4364 | + |
| 4365 | + # Select only the vertex indices at the chosen time |
| 4366 | + draw_vertex_index = [ |
| 4367 | + v for v, t in zip(cluster_vertex_index, cluster_time_idx) if t == time_idx |
| 4368 | + ] |
| 4369 | + |
| 4370 | + # Let's create an anatomical label containing these vertex indices. |
| 4371 | + # Problem 1): a label must be defined for either the left or right hemisphere. It |
| 4372 | + # cannot span both hemispheres. So we must filter the vertices based on their |
| 4373 | + # hemisphere. |
| 4374 | + # Problem 2): we have vertex *indices* that need to be transformed into proper |
| 4375 | + # vertex numbers. Not every vertex in the original high-resolution brain mesh is a |
| 4376 | + # source point in the source estimate. Do draw nice smooth curves, we need to |
| 4377 | + # interpolate the vertex indices. |
| 4378 | + |
| 4379 | + # Both problems can be solved by accessing the vertices defined in the source space |
| 4380 | + # object. The source space object is actually a list of two source spaces. |
| 4381 | + src_lh, src_rh = src |
| 4382 | + |
| 4383 | + # Split the vertices based on the hemisphere in which they are located. |
| 4384 | + lh_verts, rh_verts = src_lh["vertno"], src_rh["vertno"] |
| 4385 | + n_lh_verts = len(lh_verts) |
| 4386 | + draw_lh_verts = [lh_verts[v] for v in draw_vertex_index if v < n_lh_verts] |
| 4387 | + draw_rh_verts = [ |
| 4388 | + rh_verts[v - n_lh_verts] for v in draw_vertex_index if v >= n_lh_verts |
| 4389 | + ] |
| 4390 | + |
| 4391 | + # Vertices in a label must be unique and in increasing order |
| 4392 | + draw_lh_verts = np.unique(draw_lh_verts) |
| 4393 | + draw_rh_verts = np.unique(draw_rh_verts) |
| 4394 | + |
| 4395 | + # We are now ready to create the anatomical label objects |
| 4396 | + cluster_index = 0 |
| 4397 | + for label in brain.labels["lh"] + brain.labels["rh"]: |
| 4398 | + if label.name.startswith("cluster-"): |
| 4399 | + try: |
| 4400 | + cluster_index = max(cluster_index, int(label.name.split("-", 1)[1])) |
| 4401 | + except ValueError: |
| 4402 | + pass |
| 4403 | + lh_label = Label(draw_lh_verts, hemi="lh", name=f"cluster-{cluster_index}") |
| 4404 | + rh_label = Label(draw_rh_verts, hemi="rh", name=f"cluster-{cluster_index}") |
| 4405 | + |
| 4406 | + # Interpolate the vertices in each label to the full resolution mesh |
| 4407 | + if len(lh_label) > 0: |
| 4408 | + lh_label = lh_label.smooth( |
| 4409 | + smooth=3, subject=brain._subject, subjects_dir=brain._subjects_dir |
| 4410 | + ) |
| 4411 | + brain.add_label(lh_label, borders=width, color=color) |
| 4412 | + if len(rh_label) > 0: |
| 4413 | + rh_label = rh_label.smooth( |
| 4414 | + smooth=3, subject=brain._subject, subjects_dir=brain._subjects_dir |
| 4415 | + ) |
| 4416 | + brain.add_label(rh_label, borders=width, color=color) |
| 4417 | + |
| 4418 | + def on_time_change(event): |
| 4419 | + time_idx = np.searchsorted(brain._times, event.time) |
| 4420 | + for hemi in brain._hemis: |
| 4421 | + mesh = brain._layered_meshes[hemi] |
| 4422 | + for i, label in enumerate(brain.labels[hemi]): |
| 4423 | + if label.name == f"cluster-{cluster_index}": |
| 4424 | + del brain.labels[hemi][i] |
| 4425 | + mesh.remove_overlay(label.name) |
| 4426 | + |
| 4427 | + # Select only the vertex indices at the chosen time |
| 4428 | + draw_vertex_index = [ |
| 4429 | + v for v, t in zip(cluster_vertex_index, cluster_time_idx) if t == time_idx |
| 4430 | + ] |
| 4431 | + draw_lh_verts = [lh_verts[v] for v in draw_vertex_index if v < n_lh_verts] |
| 4432 | + draw_rh_verts = [ |
| 4433 | + rh_verts[v - n_lh_verts] for v in draw_vertex_index if v >= n_lh_verts |
| 4434 | + ] |
| 4435 | + |
| 4436 | + # Vertices in a label must be unique and in increasing order |
| 4437 | + draw_lh_verts = np.unique(draw_lh_verts) |
| 4438 | + draw_rh_verts = np.unique(draw_rh_verts) |
| 4439 | + lh_label = Label(draw_lh_verts, hemi="lh", name=f"cluster-{cluster_index}") |
| 4440 | + rh_label = Label(draw_rh_verts, hemi="rh", name=f"cluster-{cluster_index}") |
| 4441 | + if len(lh_label) > 0: |
| 4442 | + lh_label = lh_label.smooth( |
| 4443 | + smooth=3, |
| 4444 | + subject=brain._subject, |
| 4445 | + subjects_dir=brain._subjects_dir, |
| 4446 | + verbose=False, |
| 4447 | + ) |
| 4448 | + brain.add_label(lh_label, borders=width, color=color) |
| 4449 | + if len(rh_label) > 0: |
| 4450 | + rh_label = rh_label.smooth( |
| 4451 | + smooth=3, |
| 4452 | + subject=brain._subject, |
| 4453 | + subjects_dir=brain._subjects_dir, |
| 4454 | + verbose=False, |
| 4455 | + ) |
| 4456 | + brain.add_label(rh_label, borders=width, color=color) |
| 4457 | + |
| 4458 | + if time == "interactive": |
| 4459 | + subscribe(brain, "time_change", on_time_change) |
0 commit comments