Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow lasso selection sensors in a plot_evoked_topo #12071

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2c307fe
First working version of lasso select in plot_evoked_topo
wmvanvliet Oct 4, 2023
6170c63
Merge branch 'main' of github.com:mne-tools/mne-python into sensorselect
wmvanvliet Nov 11, 2023
f6738e7
Fix bug
wmvanvliet Nov 14, 2023
a83b8fd
Fix more renames
wmvanvliet Nov 14, 2023
3bfe2a5
Don't draw patches for channels that do not exist
wmvanvliet Nov 14, 2023
2e94752
Move the ChannelsSelect ui-event one abstraction layer higher
wmvanvliet Nov 14, 2023
3c6b73c
Some more fixes
wmvanvliet Nov 14, 2023
9b6bd60
select_many should not notify()
wmvanvliet Nov 14, 2023
8796836
Merge branch 'main' into sensorselect
wmvanvliet Nov 14, 2023
573cb40
Add "select" parameter to enable/disable the lasso selection tool
wmvanvliet Nov 14, 2023
facd394
Add select parameter to relevant methods
wmvanvliet Nov 14, 2023
a0069d8
Update test
wmvanvliet Nov 15, 2023
56ebda7
Merge branch 'main' into sensorselect
wmvanvliet Jul 24, 2024
d913fff
fix bugs (thanks vulture!)
wmvanvliet Jul 24, 2024
65fb86b
Merge branch 'main' of github.com:mne-tools/mne-python into sensorselect
wmvanvliet Oct 8, 2024
9b6b06a
Merge branch 'sensorselect' of github.com:wmvanvliet/mne-python into …
wmvanvliet Oct 8, 2024
bcef66e
attempt to fix tests
wmvanvliet Oct 8, 2024
8efcb8c
further attempts to fix tests
wmvanvliet Oct 22, 2024
87f72e2
Add what's new entry
wmvanvliet Oct 22, 2024
6601235
Merge branch 'main' into sensorselect
wmvanvliet Oct 22, 2024
871e15a
Merge branch 'sensorselect' of github.com:wmvanvliet/mne-python into …
wmvanvliet Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/devel/12071.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new ``select`` parameter to :func:`mne.viz.plot_evoked_topo` and :meth:`mne.Evoked.plot_topo` to toggle lasso selection of sensors, by `Marijn van Vliet`_.
2 changes: 2 additions & 0 deletions mne/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ def plot_topo_image(
fig_facecolor="k",
fig_background=None,
font_color="w",
select=False,
show=True,
):
return plot_topo_image_epochs(
Expand All @@ -1371,6 +1372,7 @@ def plot_topo_image(
fig_facecolor=fig_facecolor,
fig_background=fig_background,
font_color=font_color,
select=select,
show=show,
)

Expand Down
2 changes: 2 additions & 0 deletions mne/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ def plot_topo(
background_color="w",
noise_cov=None,
exclude="bads",
select=False,
show=True,
):
"""
Expand All @@ -638,6 +639,7 @@ def plot_topo(
background_color=background_color,
noise_cov=noise_cov,
exclude=exclude,
select=select,
show=show,
)

Expand Down
6 changes: 3 additions & 3 deletions mne/viz/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,11 +500,11 @@ def _create_ch_location_fig(self, pick):
show=False,
)
# highlight desired channel & disable interactivity
inds = np.isin(fig.lasso.ch_names, [ch_name])
fig.lasso.selection_inds = np.isin(fig.lasso.names, [ch_name])
fig.lasso.disconnect()
fig.lasso.alpha_other = 0.3
fig.lasso.alpha_nonselected = 0.3
fig.lasso.linewidth_selected = 3
fig.lasso.style_sensors(inds)
fig.lasso.style_objects()

return fig

Expand Down
2 changes: 1 addition & 1 deletion mne/viz/_mpl_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@ def _update_selection(self):
def _update_highlighted_sensors(self):
"""Update the sensor plot to show what is selected."""
inds = np.isin(
self.mne.fig_selection.lasso.ch_names, self.mne.ch_names[self.mne.picks]
self.mne.fig_selection.lasso.names, self.mne.ch_names[self.mne.picks]
).nonzero()[0]
self.mne.fig_selection.lasso.select_many(inds)

Expand Down
13 changes: 12 additions & 1 deletion mne/viz/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ def plot_evoked_topo(
background_color="w",
noise_cov=None,
exclude="bads",
select=False,
show=True,
):
"""Plot 2D topography of evoked responses.
Expand Down Expand Up @@ -1216,6 +1217,15 @@ def plot_evoked_topo(
exclude : list of str | ``'bads'``
Channels names to exclude from the plot. If ``'bads'``, the
bad channels are excluded. By default, exclude is set to ``'bads'``.
select : bool
Whether to enable the lasso-selection tool to enable the user to select
channels. The selected channels will be available in
``fig.lasso.selection``.

.. versionadded:: 1.9.0
exclude : list of str | ``'bads'``
Channels names to exclude from the plot. If ``'bads'``, the
bad channels are excluded. By default, exclude is set to ``'bads'``.
show : bool
Show figure if True.

Expand Down Expand Up @@ -1272,10 +1282,11 @@ def plot_evoked_topo(
font_color=font_color,
merge_channels=merge_grads,
legend=legend,
noise_cov=noise_cov,
axes=axes,
exclude=exclude,
select=select,
show=show,
noise_cov=noise_cov,
)


Expand Down
54 changes: 35 additions & 19 deletions mne/viz/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,32 +1094,48 @@ def test_plot_sensors(raw):
ax = fig.axes[0]

# Click with no sensors
_fake_click(fig, ax, (0.0, 0.0), xform="data")
_fake_click(fig, ax, (0, 0.0), xform="data", kind="release")
_fake_click(fig, ax, (-0.14, 0.14), xform="data")
_fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion")
_fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="motion")
_fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="release")
assert fig.lasso.selection == []

# Lasso with 1 sensor (upper left)
_fake_click(fig, ax, (0, 1), xform="ax")
fig.canvas.draw()
assert fig.lasso.selection == []
_fake_click(fig, ax, (0.65, 1), xform="ax", kind="motion")
_fake_click(fig, ax, (0.65, 0.7), xform="ax", kind="motion")
_fake_keypress(fig, "control")
_fake_click(fig, ax, (0, 0.7), xform="ax", kind="release", key="control")
_fake_click(fig, ax, (-0.13, 0.13), xform="data")
_fake_click(fig, ax, (-0.11, 0.13), xform="data", kind="motion")
_fake_click(fig, ax, (-0.11, 0.06), xform="data", kind="motion")
_fake_click(fig, ax, (-0.13, 0.06), xform="data", kind="motion")
_fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion")
_fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="release")
assert fig.lasso.selection == ["MEG 0121"]

# check that point appearance changes
# Use SHIFT key to lasso an additional sensor.
_fake_keypress(fig, "shift")
_fake_click(fig, ax, (-0.17, 0.07), xform="data")
_fake_click(fig, ax, (-0.17, 0.05), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.05), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="release")
_fake_keypress(fig, "shift", kind="release")
assert fig.lasso.selection == ["MEG 0111", "MEG 0121"]

# Check that the two selected sensors have a different appearance.
fc = fig.lasso.collection.get_facecolors()
ec = fig.lasso.collection.get_edgecolors()
assert (fc[:, -1] == [0.5, 1.0, 0.5]).all()
assert (ec[:, -1] == [0.25, 1.0, 0.25]).all()

_fake_click(fig, ax, (0.7, 1), xform="ax", kind="motion", key="control")
xy = ax.collections[0].get_offsets()
_fake_click(fig, ax, xy[2], xform="data", key="control") # single sel
assert fig.lasso.selection == ["MEG 0121", "MEG 0131"]
_fake_click(fig, ax, xy[2], xform="data", key="control") # deselect
assert fig.lasso.selection == ["MEG 0121"]
assert (fc[2:, -1] == 0.5).all()
assert (ec[2:, -1] == 0.25).all()
assert (fc[:2, -1] == 1.0).all()
assert (ec[:2:, -1] == 1.0).all()

# Use ALT key to remove a sensor from the lasso.
_fake_keypress(fig, "alt")
_fake_click(fig, ax, (-0.17, 0.07), xform="data")
_fake_click(fig, ax, (-0.17, 0.05), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.05), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="motion")
_fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="release")
_fake_keypress(fig, "alt", kind="release")

plt.close("all")

raw.info["dev_head_t"] = None # like empty room
Expand Down
83 changes: 69 additions & 14 deletions mne/viz/topo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
from .._fiff.pick import _picks_to_idx, channel_type, pick_types
from ..defaults import _handle_default
from ..utils import Bunch, _check_option, _clean_names, _is_numeric, _to_rgb, fill_doc
from .ui_events import ChannelsSelect, publish, subscribe
from .utils import (
DraggableColorbar,
SelectFromCollection,
_check_cov,
_check_delayed_ssp,
_draw_proj_checkbox,
Expand All @@ -37,6 +39,7 @@ def iter_topography(
axis_spinecolor="k",
layout_scale=None,
legend=False,
select=False,
):
"""Create iterator over channel positions.

Expand Down Expand Up @@ -72,6 +75,10 @@ def iter_topography(
If True, an additional axis is created in the bottom right corner
that can be used to, e.g., construct a legend. The index of this
axis will be -1.
select : bool
Whether to enable the lasso-selection tool to enable the user to select
channels. The selected channels will be available in
``fig.lasso.selection``.

Returns
-------
Expand All @@ -93,6 +100,7 @@ def iter_topography(
axis_spinecolor,
layout_scale,
legend=legend,
select=select,
)


Expand Down Expand Up @@ -128,6 +136,7 @@ def _iter_topography(
img=False,
axes=None,
legend=False,
select=False,
):
"""Iterate over topography.

Expand Down Expand Up @@ -193,8 +202,11 @@ def format_coord_multiaxis(x, y, ch_name=None):
under_ax.set(xlim=[0, 1], ylim=[0, 1])

axs = list()

shown_ch_names = []
for idx, name in iter_ch:
ch_idx = ch_names.index(name)
shown_ch_names.append(name)
if not unified: # old, slow way
ax = plt.axes(pos[idx])
ax.patch.set_facecolor(axis_facecolor)
Expand Down Expand Up @@ -226,24 +238,47 @@ def format_coord_multiaxis(x, y, ch_name=None):
if unified:
under_ax._mne_axs = axs
# Create a PolyCollection for the axis backgrounds
sel_pos = pos[[i[0] for i in iter_ch]]
verts = np.transpose(
[
pos[:, :2],
pos[:, :2] + pos[:, 2:] * [1, 0],
pos[:, :2] + pos[:, 2:],
pos[:, :2] + pos[:, 2:] * [0, 1],
sel_pos[:, :2],
sel_pos[:, :2] + sel_pos[:, 2:] * [1, 0],
sel_pos[:, :2] + sel_pos[:, 2:],
sel_pos[:, :2] + sel_pos[:, 2:] * [0, 1],
],
[1, 0, 2],
)
if not img:
under_ax.add_collection(
collections.PolyCollection(
verts,
facecolor=axis_facecolor,
edgecolor=axis_spinecolor,
linewidth=1.0,
if not img: # Not needed for image plots.
collection = collections.PolyCollection(
verts,
facecolor=axis_facecolor,
edgecolor=axis_spinecolor,
linewidth=1.0,
)
under_ax.add_collection(collection)

if select:
# Configure the lasso-selection tool
fig.lasso = SelectFromCollection(
ax=under_ax,
collection=collection,
names=shown_ch_names,
alpha_nonselected=0,
alpha_selected=1,
linewidth_nonselected=0,
linewidth_selected=0.7,
)
) # Not needed for image plots.

def on_select():
publish(fig, ChannelsSelect(ch_names=fig.lasso.selection))

def on_channels_select(event):
ch_inds = {name: i for i, name in enumerate(ch_names)}
selection_inds = [ch_inds[name] for name in event.ch_names]
fig.lasso.select_many(selection_inds)

fig.lasso.callbacks.append(on_select)
subscribe(fig, "channels_select", on_channels_select)
for ax in axs:
yield ax, ax._mne_ch_idx

Expand All @@ -270,6 +305,7 @@ def _plot_topo(
unified=False,
img=False,
axes=None,
select=False,
):
"""Plot on sensor layout."""
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -322,6 +358,7 @@ def _plot_topo(
unified=unified,
img=img,
axes=axes,
select=select,
)

for ax, ch_idx in my_topo_plot:
Expand All @@ -342,6 +379,9 @@ def _plot_topo_onpick(event, show_func):
"""Onpick callback that shows a single channel in a new figure."""
# make sure that the swipe gesture in OS-X doesn't open many figures
orig_ax = event.inaxes
if orig_ax.figure.canvas._key in ["shift", "alt"]:
return

import matplotlib.pyplot as plt

try:
Expand Down Expand Up @@ -838,9 +878,10 @@ def _plot_evoked_topo(
merge_channels=False,
legend=True,
axes=None,
noise_cov=None,
exclude="bads",
select=False,
show=True,
noise_cov=None,
):
"""Plot 2D topography of evoked responses.

Expand Down Expand Up @@ -912,6 +953,10 @@ def _plot_evoked_topo(
exclude : list of str | 'bads'
Channels names to exclude from being shown. If 'bads', the
bad channels are excluded. By default, exclude is set to 'bads'.
select : bool
Whether to enable the lasso-selection tool to enable the user to select
channels. The selected channels will be available in
``fig.lasso.selection``.
show : bool
Show figure if True.

Expand Down Expand Up @@ -1091,14 +1136,18 @@ def _plot_evoked_topo(
y_label=y_label,
unified=True,
axes=axes,
select=select,
)

add_background_image(fig, fig_background)

if legend is not False:
legend_loc = 0 if legend is True else legend
labels = [e.comment if e.comment else "Unknown" for e in evoked]
handles = fig.axes[0].lines[: len(evoked)]
if select:
handles = fig.axes[0].lines[1 : len(evoked) + 1]
else:
handles = fig.axes[0].lines[: len(evoked)]
legend = plt.legend(
labels=labels, handles=handles, loc=legend_loc, prop={"size": 10}
)
Expand Down Expand Up @@ -1157,6 +1206,7 @@ def plot_topo_image_epochs(
fig_facecolor="k",
fig_background=None,
font_color="w",
select=False,
show=True,
):
"""Plot Event Related Potential / Fields image on topographies.
Expand Down Expand Up @@ -1204,6 +1254,10 @@ def plot_topo_image_epochs(
:func:`matplotlib.pyplot.imshow`. Defaults to ``None``.
font_color : color
The color of tick labels in the colorbar. Defaults to white.
select : bool
Whether to enable the lasso-selection tool to enable the user to select
channels. The selected channels will be available in
``fig.lasso.selection``.
show : bool
Whether to show the figure. Defaults to ``True``.

Expand Down Expand Up @@ -1293,6 +1347,7 @@ def plot_topo_image_epochs(
y_label="Epoch",
unified=True,
img=True,
select=select,
)
add_background_image(fig, fig_background)
plt_show(show)
Expand Down
Loading
Loading