10000 XFit-style dipole fitting GUI by wmvanvliet · Pull Request #13074 · mne-tools/mne-python · GitHub
[go: up one dir, main page]

Skip to content

XFit-style dipole fitting GUI #13074

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

Draft
wants to merge 117 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
752854d
Add EvokedField.plotter
wmvanvliet Sep 20, 2023
d9b2e07
Merge branch 'main' of github.com:mne-tools/mne-python
wmvanvliet Sep 27, 2023
55edc31
Merge branch 'main' of github.com:mne-tools/mne-python
wmvanvliet Oct 2, 2023
bb7e229
Merge branch 'main' of github.com:mne-tools/mne-python
wmvanvliet Oct 4, 2023
1178ebc
BUG: Fix bug with sensor_colors
larsoner Oct 4, 2023
7556823
Update devel.rst
larsoner Oct 4, 2023
cb5a520
Add foreground/background parameters to EvokedField plot
wmvanvliet Oct 4, 2023
d4015d4
Set foreground automatically
wmvanvliet Oct 4, 2023
2c307fe
First working version of lasso select in plot_evoked_topo
wmvanvliet Oct 4, 2023
3b37ea3
Add slider to control contour line thickness
wmvanvliet Oct 4, 2023
6245c0a
Merge branch 'main' into evoked-field-colors
wmvanvliet Oct 4, 2023
4f43e50
Merge branch 'colors' into xfit
wmvanvliet Oct 4, 2023
9779843
Merge branch 'evoked-field-colors' into xfit
wmvanvliet Oct 4, 2023
9747338
Merge branch 'sensorselect' into xfit
wmvanvliet Oct 4, 2023
8268315
fix
wmvanvliet Oct 5, 2023
7dd77d9
Some initial stuff
wmvanvliet Oct 6, 2023
10c2b2a
Merge branch 'main' into xfit
wmvanvliet Oct 13, 2023
f0a92da
Merge branch 'main' of github.com:mne-tools/mne-python into xfit
wmvanvliet Oct 13, 2023
8ef1179
Small steps
wmvanvliet Oct 18, 2023
ca46e1a
Merge remote-tracking branch 'upstream/main' into xfit
wmvanvliet Oct 18, 2023
458c509
Continue work on the "fit dipole" button
wmvanvliet Oct 19, 2023
3c91d05
More progress
wmvanvliet Oct 31, 2023
6d2496c
Merge branch 'main' into xfit
wmvanvliet Nov 4, 2023
8fc904d
Merge branch 'main' of github.com:mne-tools/mne-python into xfit
wmvanvliet Nov 11, 2023
6170c63
Merge branch 'main' of github.com:mne-tools/mne-python into sensorselect
wmvanvliet Nov 11, 2023
41087bb
Fix divide by zero
wmvanvliet Nov 14, 2023
568560b
Fix sensor picking
wmvanvliet Nov 14, 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
76aebb3
Merge branch 'sensorselect' into xfit
wmvanvliet Nov 27, 2023
10000
1cbdb96
Merge branch 'main' into xfit
wmvanvliet Dec 11, 2023
b0e6cb2
Enable sensor selection again
wmvanvliet Dec 11, 2023
474f184
Merge branch 'main' of github.com:mne-tools/mne-python into xfit
wmvanvliet Dec 18, 2023
bc54c3a
Merge branch 'main' into xfit
wmvanvliet Apr 19, 2024
aa25982
Merge branch 'main' into xfit
wmvanvliet May 17, 2024
bb4fd2c
Implement toggling fixed orientation with MNE solution
wmvanvliet May 17, 2024
b498c15
small fixes
wmvanvliet Jun 4, 2024
c2eed61
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jun 4, 2024
f7005a5
Merge branch 'main' into xfit
wmvanvliet Jun 7, 2024
b47792e
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jun 7, 2024
fda59e8
Merge branch 'main' into xfit
wmvanvliet Jul 12, 2024
ca1b99e
Remove features not for v1
wmvanvliet Jul 14, 2024
1b877df
Work more on xfit
wmvanvliet Jul 18, 2024
f7bd8cc
Fixes
wmvanvliet Jul 18, 2024
5f594dd
more fixes
wmvanvliet Jul 19, 2024
263b99f
Merge branch 'main' into xfit
wmvanvliet Jul 22, 2024
eaa081e
fix multi-dipole model
wmvanvliet Jul 24, 2024
ffb2fe1
Add save option
wmvanvliet Jul 24, 2024
2f801a4
style tweaks
wmvanvliet Jul 24, 2024
56ebda7
Merge branch 'main' into sensorselect
wmvanvliet Jul 24, 2024
d913fff
fix bugs (thanks vulture!)
wmvanvliet Jul 24, 2024
13877b4
Add rank parameter
wmvanvliet Jul 29, 2024
74e80d6
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jul 29, 2024
4039603
Merge branch 'main' into xfit
wmvanvliet Jul 29, 2024
43e145f
Merge branch 'main' into xfit
wmvanvliet Aug 9, 2024
b2bdbe2
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Aug 9, 2024
e342e43
start work on dipole deletion
wmvanvliet Aug 21, 2024
5c8e337
Merge branch 'main' into xfit
wmvanvliet Oct 2, 2024
5bdb9e1
Finish dipole deletion, add occlusion mesh
wmvanvliet Oct 2, 2024
2aa51d6
set rank
wmvanvliet Oct 8, 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
8ebd885
Merge branch 'main' into xfit
wmvanvliet Nov 15, 2024
5f5666a
also show field strength input fields when not plotting density
wmvanvliet Jan 7, 2025
0277981
Update unit tests for lasso select
wmvanvliet Jan 7, 2025
bea101d
Update unit tests for lasso select
wmvanvliet Jan 7, 2025
2ae07bf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2025
f55028d
Merge branch 'main' into sensorselect
wmvanvliet Jan 20, 2025
49c9d0b
Merge branch 'sensorselect' of github.com:wmvanvliet/mne-python into …
wmvanvliet Jan 20, 2025
e857a2e
Move large lasso test to test_utils.py and have smaller tests in test…
wmvanvliet Jan 20, 2025
822f761
select from proper list of channels
wmvanvliet Jan 20, 2025
51efe6c
fix version
wmvanvliet Jan 20, 2025
e806634
more versionadded annotations
wmvanvliet Jan 20, 2025
23b5c23
Merge branch 'sensorselect' into xfit
wmvanvliet Jan 20, 2025
8b1a014
Save and load dipoles Xfit style. Fix legend when hiding dipoles.
wmvanvliet Jan 21, 2025
0066f0b
cleanup
wmvanvliet Jan 21, 2025
f1a0364
more cleanup
wmvanvliet Jan 21, 2025
727fa29
Fix single dipole fitting with loose orientation
wmvanvliet Jan 21, 2025
710b77d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 21, 2025
4b71df0
fixes proposed by vulture
wmvanvliet Jan 21, 2025
dbabf05
Properly implement and test single channel picking
wmvanvliet Jan 22, 2025
e90887d
Add logging message
wmvanvliet Jan 22, 2025
df0737f
Merge branch 'sensorselect' into xfit
wmvanvliet Jan 22, 2025
1e67acc
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jan 22, 2025
622ff54
small fix
wmvanvliet Jan 22, 2025
64b0ed1
Merge branch 'main' into xfit
wmvanvliet Jan 22, 2025
0841c88
Merge branch 'main' into xfit
wmvanvliet Jan 24, 2025
0f3239a
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jan 24, 2025
17e28b0
Merge branch 'main' into xfit
wmvanvliet Jan 26, 2025
b4467c9
Merge branch 'main' into xfit
wmvanvliet Jan 29, 2025
d878f60
fix to dipole
wmvanvliet Jan 31, 2025
b1ee63a
Add possibility to show an stc with the fieldmap
wmvanvliet Feb 4, 2025
15519eb
Take units (m or mm) into account when showing fieldmaps on top of br…
wmvanvliet Feb 4, 2025
5108607
Add unit test and towncrier
wmvanvliet Feb 4, 2025
e243310
Don't make unnecessary copy
wmvanvliet Feb 4, 2025
a1f5328
Fix
wmvanvliet Feb 4, 2025
4e7270a
Merge branch 'evoked-field-units' into xfit
wmvanvliet Feb 4, 2025
c703e32
some checks on inputs and doc
wmvanvliet Feb 5, 2025
08e28e6
Merge branch 'main' into xfit
wmvanvliet Feb 24, 2025
b01cfdf
Merge branch 'main' of github.com:mne-tools/mne-python into xfit
wmvanvliet Apr 7, 2025
35a2b64
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Apr 7, 2025
8f26350
Merge branch 'main' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jun 30, 2025
a92aead
Merge branch 'xfit' of github.com:wmvanvliet/mne-python into xfit
wmvanvliet Jun 30, 2025
f4d879d
work on dipolefitui
wmvanvliet Jul 18, 2025
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
32 changes: 23 additions & 9 deletions mne/viz/topo.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
_setup_ax_spines,
_check_cov,
_plot_masked_image,
SelectFromCollection,
)


Expand Down Expand Up @@ -195,8 +196,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 @@ -237,15 +241,22 @@ def format_coord_multiaxis(x, y, ch_name=None):
],
[1, 0, 2],
)
if not img:
under_ax.add_collection(
collections.PolyCollection(
verts,
facecolor=axis_facecolor,
edgecolor=axis_spinecolor,
linewidth=1.0,
)
) # Not needed for image plots.
if not img: # Not needed for image plots.
collection = collections.PolyCollection(
verts,
facecolor=axis_facecolor,
edgecolor=axis_spinecolor,
)
under_ax.add_collection(collection)
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,
)
for ax in axs:
yield ax, ax._mne_ch_idx

Expand Down Expand Up @@ -344,6 +355,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
20 changes: 20 additions & 0 deletions mne/viz/ui_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,26 @@ class Contours(UIEvent):
line_width: Optional[float]


@dataclass
@fill_doc
class ChannelsSelect(UIEvent):
"""Indicates that the user has selected one or more channels.

Parameters
----------
ch_names : list of str
The names of the channels that were selected.

Attributes
----------
%(ui_event_name_source)s
ch_names : list of str
The names of the channels that were selected.
"""

ch_names: List[str]


def _get_event_channel(fig):
"""Get the event channel associated with a figure.

Expand Down
139 changes: 82 additions & 57 deletions mne/viz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
_check_decim,
)
from ..transforms import apply_trans
from .ui_events import publish, subscribe, ChannelsSelect


_channel_type_prettyprint = {
Expand Down Expand Up @@ -1044,7 +1045,7 @@ def plot_sensors(
Whether to plot the sensors as 3d, topomap or as an interactive
sensor selection dialog. Available options ``'topomap'``, ``'3d'``,
``'select'``. If ``'select'``, a set of channels can be selected
interactively by using lasso selector or clicking while holding control
interactively by using lasso selector or clicking while holding the shift
key. The selected channels are returned along with the figure instance.
Defaults to ``'topomap'``.
ch_type : None | str
Expand Down Expand Up @@ -1255,7 +1256,7 @@ def _onpick_sensor(event, fig, ax, pos, ch_names, show_names):
if event.mouseevent.inaxes != ax:
return

if event.mouseevent.key == "control" and fig.lasso is not None:
if event.mouseevent.key in ["shift", "alt"] and fig.lasso is not None:
for ind in event.ind:
fig.lasso.select_one(ind)

Expand Down Expand Up @@ -1360,7 +1361,7 @@ def _plot_sensors(
lw=linewidth,
)
if kind == "select":
fig.lasso = SelectFromCollection(ax, pts, ch_names)
fig.lasso = SelectFromCollection(ax, pts, names=ch_names)
else:
fig.lasso = None

Expand Down Expand Up @@ -1693,72 +1694,95 @@ def _draw_without_rendering(cbar):


class SelectFromCollection:
"""Select channels from a matplotlib collection using ``LassoSelector``.
"""Select objects from a matplotlib collection using ``LassoSelector``.

Selected channels are saved in the ``selection`` attribute. This tool
highlights selected points by fading other points out (i.e., reducing their
alpha values).
The names of the selected objects are saved in the ``selection`` attribute.
This tool highlights selected objects by fading other objects out (i.e.,
reducing their alpha values).

Parameters
----------
ax : instance of Axes
Axes to interact with.
collection : instance of matplotlib collection
Collection you want to select from.
alpha_other : 0 <= float <= 1
To highlight a selection, this tool sets all selected points to an
alpha value of 1 and non-selected points to ``alpha_other``.
Defaults to 0.3.
linewidth_other : float
Linewidth to use for non-selected sensors. Default is 1.
names : list of str
The names of the object. The selection is returned as a subset of these names.
alpha_selected : float
Alpha for selected objects (0=tranparant, 1=opaque).
alpha_nonselected : float
Alpha for non-selected objects (0=tranparant, 1=opaque).
linewidth_selected : float
Linewidth for the borders of selected objects.
linewidth_nonselected : float
Linewidth for the borders of non-selected objects.

Notes
-----
This tool selects collection objects based on their *origins*
(i.e., ``offsets``). Calls all callbacks in self.callbacks when selection
is ready.
This tool selects collection objects which bounding boxes intersect with a lasso
path. Calls all callbacks in self.callbacks when selection is ready.
"""

def __init__(
self,
ax,
collection,
ch_names,
alpha_other=0.5,
linewidth_other=0.5,
*,
names,
alpha_selected=1,
alpha_nonselected=0.5,
linewidth_selected=1,
linewidth_nonselected=0.5,
):
from matplotlib.widgets import LassoSelector

self.fig = ax.figure
self.canvas = ax.figure.canvas
self.collection = collection
self.ch_names = ch_names
self.alpha_other = alpha_other
self.linewidth_other = linewidth_other
self.names = names
self.alpha_selected = alpha_selected
self.alpha_nonselected = alpha_nonselected
self.linewidth_selected = linewidth_selected
self.linewidth_nonselected = linewidth_nonselected

from matplotlib.collections import PolyCollection
from matplotlib.path import Path

self.xys = collection.get_offsets()
self.Npts = len(self.xys)
if isinstance(collection, PolyCollection):
self.paths = collection.get_paths()
else:
self.paths = [Path([point]) for point in collection.get_offsets()]
self.Npts = len(self.paths)
if self.Npts != len(names):
raise ValueError(
f"Number of names ({len(names)}) does not match the number of objects "
f"in the collection ({self.Npts})."
)

# Ensure that we have separate colors for each object
# Ensure that we have colors for each object.
self.fc = collection.get_facecolors()
self.ec = collection.get_edgecolors()
self.lw = collection.get_linewidths()
if len(self.fc) == 0:
raise ValueError("Collection must have a facecolor")
elif len(self.fc) == 1:
self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1)
if len(self.ec) == 0:
self.ec = np.zeros((self.Npts, 4)) # all black
elif len(self.ec) == 1:
self.ec = np.tile(self.ec, self.Npts).reshape(self.Npts, -1)
self.fc[:, -1] = self.alpha_other # deselect in the beginning
self.ec[:, -1] = self.alpha_other
self.lw = np.full(self.Npts, self.linewidth_other)
self.lw = np.full(self.Npts, float(self.linewidth_nonselected))

# Initialize the lasso selector
line_kw = _prop_kw("line", dict(color="red", linewidth=0.5))
self.lasso = LassoSelector(ax, onselect=self.on_select, **line_kw)
self.selection = list()
self.callbacks = list()
self.selection_inds = np.array([], dtype="int")

# Deselect everything in the beginning.
self.style_objects([])

# Respond to UI-Events
subscribe(self.fig, "channels_select", self._on_channels_select)

def on_select(self, verts):
"""Select a subset from the collection."""
Expand All @@ -1768,44 +1792,45 @@ def on_select(self, verts):
return

path = Path(verts)
inds = np.nonzero([path.contains_point(xy) for xy in self.xys])[0]
if self.canvas._key == "control": # Appending selection.
sels = [np.where(self.ch_names == c)[0][0] for c in self.selection]
inters = set(inds) - set(sels)
inds = list(inters.union(set(sels) - set(inds)))
inds = np.nonzero([path.intersects_path(p) for p in self.paths])[0]
if self.canvas._key == "shift": # Appending selection.
self.selection_inds = np.union1d(self.selection_inds, inds)
elif self.canvas._key == "alt": # Removing selection.
self.selection_inds = np.setdiff1d(self.selection_inds, inds)
else:
self.selection_inds = inds
ch_names = [self.names[i] for i in self.selection_inds]
publish(self.fig, ChannelsSelect(ch_names=ch_names))

self.selection[:] = np.array(self.ch_names)[inds].tolist()
self.style_sensors(inds)
self.notify()
def _on_channels_select(self, event):
ch_inds = {name: i for i, name in enumerate(self.names)}
self.selection = [name for name in event.ch_names if name in ch_inds]
self.selection_inds = [ch_inds[name] for name in self.selection]
self.style_objects(self.selection_inds)

def select_one(self, ind):
"""Select or deselect one sensor."""
ch_name = self.ch_names[ind]
if ch_name in self.selection:
sel_ind = self.selection.index(ch_name)
self.selection.pop(sel_ind)
if self.canvas._key == "shift":
self.selection_inds = np.union1d(self.selection_inds, [ind])
elif self.canvas._key == "alt":
self.selection_inds = np.setdiff1d(self.selection_inds, [ind])
else:
self.selection.append(ch_name)
inds = np.isin(self.ch_names, self.selection).nonzero()[0]
self.style_sensors(inds)
self.notify()

def notify(self):
"""Notify listeners that a selection has been made."""
for callback in self.callbacks:
callback()
return # don't notify()
ch_names = [self.names[i] for i in self.selection_inds]
publish(self.fig, ChannelsSelect(ch_names=ch_names))

def select_many(self, inds):
"""Select many sensors using indices (for predefined selections)."""
self.selection[:] = np.array(self.ch_names)[inds].tolist()
self.style_sensors(inds)
self.selected_inds = inds
ch_names = [self.names[i] for i in self.selection_inds]
publish(self.fig, ChannelsSelect(ch_names=ch_names))

def style_sensors(self, inds):
def style_objects(self, inds):
"""Style selected sensors as "active"."""
# reset
self.fc[:, -1] = self.alpha_other
self.ec[:, -1] = self.alpha_other / 2
self.lw[:] = self.linewidth_other
self.fc[:, -1] = self.alpha_nonselected
self.ec[:, -1] = self.alpha_nonselected / 2
self.lw[:] = self.linewidth_nonselected
# style sensors at `inds`
self.fc[inds, -1] = self.alpha_selected
self.ec[inds, -1] = self.alpha_selected
Expand Down
0